diff --git a/.agents/rules/bzl.md b/.agents/rules/bzl.md new file mode 100644 index 0000000000..10aca72184 --- /dev/null +++ b/.agents/rules/bzl.md @@ -0,0 +1,10 @@ +--- +trigger: glob +description: Starlark / Bazel .bzl file coding style rules +globs: *.bzl +--- + +# Starlark Rules + +* Use triple-quoted strings for multi-line rule doc args. +* Don't use backslash line continuation in rule doc args. diff --git a/.agents/rules/cc.md b/.agents/rules/cc.md new file mode 100644 index 0000000000..5f2c2b43c2 --- /dev/null +++ b/.agents/rules/cc.md @@ -0,0 +1,22 @@ +# C/C++ Rules Design & Bazel rules_cc Integration + +## Concise `cc_shared_library` Interface & Export Rules +* In `cc_shared_library`: + * Targets directly placed in `deps` are **automatically exported** by Bazel; + they do not need to be listed in `exports_filter`. + * `exports_filter` is defined in `rules_cc` as an `attr.string_list()`, **not** + a `Label` attribute. Passing Starlark `Label` objects directly causes an + immediate Bazel analysis type error. + * **Citation**: [rules_cc `cc_shared_library.bzl`](https://github.com/bazelbuild/rules_cc/blob/main/cc/private/rules_impl/cc_shared_library.bzl) + (*"exports_filter is a list of strings attribute"*). + +## macOS Dynamic Lookup Link Flag +* Apple's `ld64`/`lld` linkers require `-undefined dynamic_lookup` in + `user_link_flags` on macOS so CPython C-API symbols remain unresolved at + link time and resolve dynamically at runtime. + +## Linux Platform Tag Composition +* Linux platform tags are formatted as `{platform_machine}-linux-{libc}` (e.g., + `x86_64-linux-gnu` or `aarch64-linux-musl`). +* **Citation**: [PEP 600 — Perennial manylinux](https://peps.python.org/pep-0600/) + & [PEP 656 — musllinux](https://peps.python.org/pep-0656/). diff --git a/.agents/rules/docs.md b/.agents/rules/docs.md new file mode 100644 index 0000000000..99ce5608ac --- /dev/null +++ b/.agents/rules/docs.md @@ -0,0 +1,15 @@ +--- +trigger: glob +description: Documentation formatting, Sphinx MyST style rules, and documentation build correctness +globs: docs/*.md +--- + +# Documentation Rules + +* Act as an expert in tech writing, Sphinx, MyST, and markdown. +* Wrap lines at 80 columns. +* Use hyphens (`-`) in file names instead of underscores (`_`). +* In Sphinx MyST markup, outer directives must have more colons than inner directives. +* When adding `{versionadded}` or `{versionchanged}` sections, add them at the end of the documentation text. +* For unreleased features or attributes, use `VERSION_NEXT_FEATURE` in `{versionadded}` / `{versionchanged}` directives. +* **Documentation Build Correctness**: Ensure new `.bzl` files or user-facing APIs are properly registered in documentation build targets (e.g. `//docs:docs` or relevant Starlark API reference generation configs). diff --git a/.agents/rules/github_actions_workflows.md b/.agents/rules/github_actions_workflows.md new file mode 100644 index 0000000000..525d2a2699 --- /dev/null +++ b/.agents/rules/github_actions_workflows.md @@ -0,0 +1,12 @@ +--- +name: github-actions-workflows +trigger: glob +globs: [".github/workflows/*.yml", ".github/workflows/*.yaml", ".github/*.yaml"] +--- + +# GitHub Actions Workflows Rule + +* Use the latest version of referenced actions in `.github/workflows/`. +* Preserve `suppress-no-jobs-ran-error` fallback jobs in conditional workflows. +* Pass inputs (e.g. `${{ inputs.issue }}`) directly to commands without + redundant shell parameter stripping or conversions. diff --git a/.agents/rules/news.md b/.agents/rules/news.md new file mode 100644 index 0000000000..ce8a844439 --- /dev/null +++ b/.agents/rules/news.md @@ -0,0 +1,20 @@ +--- +trigger: news/*.md +description: Apply when drafting news entries. +--- + +@CONTRIBUTING.md + +# News Entry Conventions + +Before drafting any news entry, strictly adhere to the rules in `CONTRIBUTING.md` +above. + +## Sphinx MyST Cross-Reference Syntax (`{obj}`) +* Use `{obj}\`\`` in news entries for rules, macros, targets, providers, + attributes, args, and any other cross-referencable Starlark or Python + objects. + +## GitHub Issue Link Formatting +* Append GitHub issue cross-references at the end of news entries in markdown + link format: `([#3283](https://github.com/bazel-contrib/rules_python/issues/3283))`. diff --git a/.agents/rules/no_gh_auth_token_in_cmdline.md b/.agents/rules/no_gh_auth_token_in_cmdline.md new file mode 100644 index 0000000000..e8cb7794fb --- /dev/null +++ b/.agents/rules/no_gh_auth_token_in_cmdline.md @@ -0,0 +1,11 @@ +--- +trigger: always_on +--- + +# No Auth Token in Cmdline Rule + +* NEVER pass `gh auth token` or embed authentication tokens in command line + arguments or URLs. Doing so can leak credentials in process tables (`ps`), + shell history, or logs. +* Use configured SSH keys, standard `git push`, or `gh` commands natively + instead. diff --git a/.agents/rules/pr.md b/.agents/rules/pr.md new file mode 100644 index 0000000000..caacb07519 --- /dev/null +++ b/.agents/rules/pr.md @@ -0,0 +1,24 @@ +--- +trigger: model_decision +description: rules to apply to pull request descriptions +--- + +@CONTRIBUTING.md + +# Pull Request Conventions + +Before drafting any pull request description, strictly adhere to the rules in +`CONTRIBUTING.md` above. + +## Prevent Agent Oversight of `CONTRIBUTING.md` +* Always include `@CONTRIBUTING.md` in rule definitions so that PR formatting + rules are injected into context whenever PR workflows run. + +## PR Commit Workflow Invariant +* Once a Pull Request is created, always make new commits or merge commits. +* **NEVER** amend or rebase commits on an active PR branch to avoid breaking + code review threads. +* **NEVER** include a list of per-file edits or changelog bullet points of + individual file modifications in PR descriptions or commit messages. +* High-level overview only: state *why* the change is made and *how* at a + conceptual level. Link related issues (e.g. `Work towards #`). diff --git a/.agents/rules/python.md b/.agents/rules/python.md new file mode 100644 index 0000000000..21f6671b93 --- /dev/null +++ b/.agents/rules/python.md @@ -0,0 +1,42 @@ +# Python Conventions + +## pytest +* Register helper fixtures using `pytest_plugins = [""]`. +* Name fixture functions with `fixture_` prefix and pass public name via + `@pytest.fixture(name="foo")`. + +## CLI & Arguments +* Use direct attribute access (e.g. `args.foo`) on `argparse.Namespace` with + well-defined shapes. Avoid defensive `getattr()`. + +## TypedDict +* **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. +* **Ignore comments**: When adding `# pyrefly: ignore[...]` or type + suppressions, add an explanatory comment indicating why it is suppressed. +* **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`. +* **Union syntax (`X | None`)**: Use `X | None` instead of `typing.Optional[X]`. + Add `from __future__ import annotations` if necessary. +* **Collections generics**: Use `collections.abc` (e.g., `Sequence`, `Iterable`, + `Iterator`, `Callable`, `Mapping`) and builtin generics (`list`, `dict`, + `tuple`, `set`) instead of `typing.XXX` collection types. + +## Runfiles +* **Fail-fast creation**: Prefer `runfiles.CreateOrRaise()` over + `runfiles.Create()` followed by manual `assert` when initializing runfiles in + tests and runtime scripts. + +## 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/rules/starlark.md b/.agents/rules/starlark.md new file mode 100644 index 0000000000..4517b06aec --- /dev/null +++ b/.agents/rules/starlark.md @@ -0,0 +1,60 @@ +--- +trigger: glob +description: Starlark Language, Macro, and Testing Invariants +globs: "*.bzl,BUILD,BUILD.bazel,*.bazel" +--- + +# Starlark Language & Macro Invariants + +## Macro Target Canonicalization +* In macro implementations, internal repository target references **MUST** be + canonicalized using `str(Label("//path/to:target"))` so they resolve in the + macro's module context rather than the caller's repository context. +* Note that `python/private/common_labels.bzl` defines `labels`, a struct + containing common canonicalized label strings used across the project. + +## Manual Tag on Internal Macro Helper Targets +* When macros instantiate internal helper targets (such as private rule targets + for artifact extraction or linking support), always include `tags = ["manual"]`. +* **Why**: This prevents internal helper targets from being implicitly built when + wildcard target patterns (e.g., `//...`) are expanded. + +## Private Alias Pattern when Appending to User `deps` +* When macros append internal helper targets to user-provided dependency lists + (`deps`), use private alias targets (e.g. + `//python/private/cc:current_py_cc_headers_private_alias`) to prevent + "duplicate dependency label" analysis errors if the user also explicitly + passes the public target label in their `deps` (including in `select()` + expressions). + +## Attribute Normalization at Function Entry +* Always normalize input list attributes at the start of macro definitions + (e.g., `deps = deps or []`, `copts = copts or []`). +* **Why**: This avoids inlining the normalization as part of a complex + expression later in the macro expansion. + +## Control Flow & Algorithmic Restrictions +* **Iterative Algorithms Only (No Recursion)**: Starlark does not support + recursive function calls; always implement iterative algorithms using bounded + loops. +* **Iterable `for` Loops Only (No `while` Loops)**: Starlark does not support + `while` loops; iterate over fixed-size ranges or explicit collections. + +## Code Style & Conventions +* **Docstring Formatting Invariants**: Use triple-quoted strings for multi-line + docstrings without trailing backslashes (`\`) for line continuation. +* **No Bazel Copyright Headers**: Do not add Bazel copyright headers to new or + existing files unless explicitly directed by the user. +* **Line Length & Wrapping**: Wrap Markdown and Starlark lines to 80 columns in + accordance with `.editorconfig`. + +## Starlark Testing (`rules_testing`) +* **`rules_testing` over `bazel_skylib`**: Always use `@rules_testing` (analysis + tests with `env.expect.that_...`) rather than `bazel_skylib` for Starlark + rule analysis tests. +* **Analysis Test Two-Part Structure**: Separate tests into a setup target + function `def _test_foo(name)` calling `analysis_test` and an implementation + function `def _test_foo_impl(env, target)`. +* **Test Suite Registration**: Collect test setup functions in a private + `_tests` list and register them cleanly via `test_suite(name = name, tests = + _tests)`. diff --git a/.agents/rules/testing.md b/.agents/rules/testing.md new file mode 100644 index 0000000000..769197f968 --- /dev/null +++ b/.agents/rules/testing.md @@ -0,0 +1,24 @@ +# Testing & Validation Guardrails + +## `--config=fast-tests` for Test Targets +* Always pass `--config=fast-tests` when running or building test targets to + avoid running expensive, flaky integration tests. + +## CRITICAL: Never use `--config=fast-tests` on Non-Test Targets +* `--config=fast-tests` sets `--build_tests_only=true`, which silently ignores + non-test targets (such as `//docs:docs` or package libraries), resulting in 0 + targets built! + +## Lockfile Testing (`MODULE.bazel.lock`) +* Changes to transitive module extension dependencies or `.bzl` files loaded by + extensions update Bazel 9 lockfile hashes, requiring `bazel mod deps + --lockfile_mode=update` in integration test workspaces. +* When requirements files (e.g., in `//tools/publish` or root pip parses) are + modified or bumped by Dependabot, update the integration lockfile by running + `bazel mod deps --lockfile_mode=update` in `tests/integration/bzlmod_lockfile` + and verify with + `bazel test //tests/integration:bzlmod_lockfile_test_bazel_9.1.0`. + +## Documentation Flake Handling +* When building `//docs:docs` fails with exit code 2, treat it as a known + Sphinx/Bazel flake and retry the build. diff --git a/.agents/rules/when-to-use-copyright.md b/.agents/rules/when-to-use-copyright.md new file mode 100644 index 0000000000..a73cbbfd70 --- /dev/null +++ b/.agents/rules/when-to-use-copyright.md @@ -0,0 +1,8 @@ +--- +trigger: always_on +--- + +# When to Use Copyright + +Unless directed by the user otherwise, do not add Bazel copyright to new or +existing files. diff --git a/.agents/rules/windows.md b/.agents/rules/windows.md new file mode 100644 index 0000000000..2eea9b2df5 --- /dev/null +++ b/.agents/rules/windows.md @@ -0,0 +1,32 @@ +# Windows MSVC Toolchains & Linking + +## MSVC `#pragma comment(lib, ...)` Embedded References +* CPython's MSVC headers embed `#pragma comment(lib, "python3xx.lib")` into + generated `.obj` files. MSVC `link.exe` automatically searches for + `python3xx.lib` in library search paths during linking. +* **Citation**: [Microsoft Learn `comment` pragma](https://learn.microsoft.com/en-us/cpp/preprocessor/comment-c-cpp) + (*"Places a library-search record in the object file... The linker searches for + this library the same way as if you had named it on the command line"*). + +## PEP 3149 ABI Tag Prefix (`cp` vs. `cpython-`) +* Windows CPython ABI tags use the `cp` prefix (e.g., `cp311`, `cp312t`) + whereas POSIX platforms use `cpython-` (e.g., `cpython-311`). +* **Citation**: [PEP 3149 — ABI version tagged .so files](https://peps.python.org/pep-3149/) + (*"The tag starts with `cpython-` followed by the Python major and minor version + without dots... followed by build flags"*). + +## Windows Platform Tag Derivation +* Windows platform tags for Python C extensions evaluate to `win_amd64` + (x86_64/amd64), `win_arm64` (aarch64/arm64), or `win32` (32-bit x86). +* **`platform.machine()` Normalization**: On Windows, + `platform.machine()` returns uppercase (`"AMD64"`, `"ARM64"`). Always + normalize with `.lower()` when deriving PEP 508 markers or resolving platform + tags. +* **Citation**: [PEP 425 — Compatibility Tags for Built Distributions](https://peps.python.org/pep-0425/). + +## Windows CPython SOABI & ABI Infix Support +* CPython on Windows natively supports loading ABI-tagged `.pyd` files + containing platform tags (e.g., `foo.cp314-win_amd64.pyd`, + `foo.cp311-win_amd64.pyd`). +* SOABI on Windows includes both the ABI prefix and platform tag (e.g., + `cp311-win_amd64`). diff --git a/.agents/rules/workspace.md b/.agents/rules/workspace.md new file mode 100644 index 0000000000..bc64d4779c --- /dev/null +++ b/.agents/rules/workspace.md @@ -0,0 +1,20 @@ +--- +trigger: always_on +--- + +# Workspace Rules + +To avoid confusion from using outdated code states, when starting a new +conversation/session or when first starting a new branch or worktree, unless +explicitly instructed otherwise, ensure the latest upstream code is used as the +basis: +* Fetch `upstream/main` (`git fetch upstream main`). +* Base any new branch or worktree upon `upstream/main` (e.g., + `git checkout -b upstream/main`). +* Run the workspace helper script to configure upstream tracking and safe + pushing: + ```bash + .agents/scripts/setup_triangle_branch.sh + ``` +* Never use `git push -u` or `--set-upstream` (it breaks upstream tracking). + diff --git a/.agents/scripts/setup_triangle_branch.sh b/.agents/scripts/setup_triangle_branch.sh new file mode 100755 index 0000000000..b6bdc2bb20 --- /dev/null +++ b/.agents/scripts/setup_triangle_branch.sh @@ -0,0 +1,25 @@ +#!/bin/bash +# Helper script to set up upstream tracking for fork-and-PR workflow. +set -e + +# Detect current branch if not provided as argument +BRANCH_NAME="${1:-$(git symbolic-ref --short HEAD)}" + +if [ -z "$BRANCH_NAME" ]; then + echo "Error: Could not detect current branch name." >&2 + exit 1 +fi + +echo "Setting up upstream tracking for branch: $BRANCH_NAME" + +# 1. Set the pull/fetch behavior to track the canonical repo's main branch +git config --local "branch.${BRANCH_NAME}.remote" upstream +git config --local "branch.${BRANCH_NAME}.merge" refs/heads/main + +# 2. Override the destination remote for pushing +git config --local "branch.${BRANCH_NAME}.pushRemote" origin + +# 3. Defend against global push.default settings (ensure it pushes to current branch name) +git config --local push.default current + +echo "Successfully configured upstream/main tracking and origin pushRemote for $BRANCH_NAME!" diff --git a/.agents/skills/analyze-ci-failure/SKILL.md b/.agents/skills/analyze-ci-failure/SKILL.md new file mode 100644 index 0000000000..0af8a92fb1 --- /dev/null +++ b/.agents/skills/analyze-ci-failure/SKILL.md @@ -0,0 +1,25 @@ +--- +name: analyze-ci-failure +description: Download and analyze a CI failure log to construct an actionable + suggested fix plan and report back +--- + +When a CI monitoring workflow alerts you to a failed Buildkite job or GitHub +check, invoke this skill by running: +```bash +./.agents/skills/analyze-ci-failure/scripts/analyze_ci_failure.py \ + "" "" "" "" +``` + +### ✨ What this Skill Does +1. **Resolves Log**: Automatically resolves the Buildkite job download URL or + fetches GitHub Actions logs via `gh` CLI. +2. **Downloads & Ingests**: Fetches the full raw CI log file and saves it + locally to `.agents/skills/analyze-ci-failure/scratch/`. +3. **Smart Error Extraction**: Scans log lines for critical failure signatures + (`Traceback`, `ERROR:`, `FAILED:`, missing packages, compiler aborts, linter + errors). +4. **Fix Plan Synthesis**: Constructs a beautifully structured Markdown + suggested plan on how to resolve the root cause. +5. **Natively Notifies**: Dispatches a high-priority summary notification + message back to your active agent conversation via `agentapi send-message`. diff --git a/.agents/skills/analyze-ci-failure/scripts/analyze_ci_failure.py b/.agents/skills/analyze-ci-failure/scripts/analyze_ci_failure.py new file mode 100755 index 0000000000..31d8dd7b6e --- /dev/null +++ b/.agents/skills/analyze-ci-failure/scripts/analyze_ci_failure.py @@ -0,0 +1,254 @@ +#!/usr/bin/env python3 + +import argparse +import os +import re +import subprocess +import sys +import urllib.request + + +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"): + log_url = job_id + else: + 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) + 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...") + cmd = ["gh", "run", "view", "--job", gh_job_id, "--log"] + try: + res = subprocess.run(cmd, capture_output=True, text=True, check=True) + with open(output_path, "w") as f: + f.write(res.stdout) + return True + except Exception as e: + print( + f"⚠️ Failed to fetch GitHub log via gh CLI for job {gh_job_id}: {e}", + file=sys.stderr, + ) + + if not log_url.endswith("/download.txt") and "buildkite.com" in log_url: + log_url = re.sub(r"/log$", "/download.txt", log_url) + + print(f"📥 Downloading CI failure log from {log_url}...") + req = urllib.request.Request(log_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 log from {log_url}: {e}", file=sys.stderr) + with open(output_path, "w") as f: + f.write(f"Failed to download log from {log_url}: {e}\n") + 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}"] + + with open(log_path, errors="replace") as f: + lines = f.readlines() + + 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.lower() in clean_line.lower() + for keyword in [ + "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 be reformatted", + "error]", + "error waiting for container", + "error during connect:", + "user command error:", + "curl: (", + "connection reset by peer", + "input/output error", + "errno 5", + ] + ): + if clean_line: + errors.append(clean_line) + + return errors[:30] + + +def create_plan(job_name, log_path, errors): + err_str = ( + "\n".join(errors) + if 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." + elif any( + "curl:" in e.lower() + or "recv failure: connection reset" in e.lower() + or "connection reset by peer" in e.lower() + for e in errors + ): + is_flake = True + flake_reason = "Network connection reset during runner bootstrap or artifact download (curl recv failure / connection reset). This is an infrastructure network flake." + elif any( + "input/output error" in e.lower() or "errno 5" in e.lower() for e in errors + ): + is_flake = True + flake_reason = "Transient runner host disk / Darwin sandbox I/O error (OSError: [Errno 5] Input/output error). This is an infrastructure flake, not a codebase defect." + + 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}` + +## 🔥 Extracted Failure Snippets +```text +{err_str} +``` + +## 🛠️ Suggested Plan to Fix +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 + + +def main(): + parser = argparse.ArgumentParser( + description="Download CI failure log, analyze root cause, and create fix plan." + ) + parser.add_argument("job_name", help="Name of the failed job") + parser.add_argument("build_id", help="Buildkite Build ID, Build number, or Log URL") + parser.add_argument("job_id", help="Buildkite Job ID or link") + parser.add_argument("conv_id", help="Conversation ID to report back to") + args = parser.parse_args() + + skill_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) + scratch_dir = os.path.join(skill_dir, "scratch") + os.makedirs(scratch_dir, exist_ok=True) + + 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.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) + plan = create_plan(args.job_name, log_path, errors) + + plan_file = os.path.join(scratch_dir, f"ci_plan_{safe_jname}.md") + with open(plan_file, "w") as f: + f.write(plan) + + print( + f"📄 Plan generated at '{plan_file}'. Dispatching notification to conversation {args.conv_id}..." + ) + + msg = ( + f"⚠️ Remote CI Job '{args.job_name}' Analysis Complete!\n\n" + f"I downloaded and analyzed the failure log. Findings and suggested fix plan compiled at artifact file: `{plan_file}`.\n\n" + f"Raw CI Log Path: `{log_path}`" + ) + + res = subprocess.run( + [ + "agentapi", + "send-message", + "--title=CI Failure Analysis Plan", + args.conv_id, + msg, + ] + ) + if res.returncode != 0: + print(f"❌ Failed to send agentapi message. Printing plan directly:\n{plan}") + + +if __name__ == "__main__": + main() diff --git a/.agents/skills/buildkite-get-results/SKILL.md b/.agents/skills/buildkite-get-results/SKILL.md index a2a936513e..801e51c02d 100644 --- a/.agents/skills/buildkite-get-results/SKILL.md +++ b/.agents/skills/buildkite-get-results/SKILL.md @@ -3,7 +3,7 @@ name: buildkite-get-results description: Gets buildkite build results --- -Pass the PR number to the `scripts/get_buildkite_results.py` script. +Pass the PR number, Build URL, or Build ID to the `scripts/get_buildkite_results.py` script. The `--jobs` flag can do glob-style filtering of jobs. diff --git a/.agents/skills/buildkite-get-results/scripts/get_buildkite_results.py b/.agents/skills/buildkite-get-results/scripts/get_buildkite_results.py index e1fc635cf6..1af829a8e6 100755 --- a/.agents/skills/buildkite-get-results/scripts/get_buildkite_results.py +++ b/.agents/skills/buildkite-get-results/scripts/get_buildkite_results.py @@ -1,246 +1,94 @@ #!/usr/bin/env python3 + import argparse import json import re import subprocess import sys -import urllib.request -def get_pr_checks(pr_number): +def check_cli(cmd_name, install_url): try: - # Check if gh is installed subprocess.run( - ["gh", "--version"], + [cmd_name, "--version"], check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, ) - except FileNotFoundError: + except Exception: print( - "Error: 'gh' (GitHub CLI) is not installed or not in PATH.", file=sys.stderr + f"❌ Error: '{cmd_name}' CLI is not installed or not in PATH.", + file=sys.stderr, ) + print(f"Please install it from {install_url}", file=sys.stderr) sys.exit(1) - except subprocess.CalledProcessError: - print("Error: 'gh' command failed. Is it installed?", file=sys.stderr) - sys.exit(1) - cmd = ["gh", "pr", "checks", str(pr_number), "--json", "bucket,name,link,state"] + +def get_build_url_from_pr(pr_number): + check_cli("gh", "https://cli.github.com/") + cmd = ["gh", "pr", "checks", str(pr_number), "--json", "name,link"] try: - result = subprocess.run(cmd, capture_output=True, text=True, check=True) - return json.loads(result.stdout) + res = subprocess.run(cmd, capture_output=True, text=True, check=True) + checks = json.loads(res.stdout) + for c in checks: + link = c.get("link", "") + if "buildkite.com" in link: + return link.split("#")[0] + print(f"❌ No Buildkite checks found for PR #{pr_number}.", file=sys.stderr) + sys.exit(1) except subprocess.CalledProcessError as e: - print(f"Error fetching PR checks: {e.stderr}", file=sys.stderr) + print(f"❌ Error fetching PR checks: {e.stderr}", file=sys.stderr) sys.exit(1) -def get_buildkite_build_url(checks): - for check in checks: - # Looking for Buildkite check. The name usually contains "buildkite" - if "buildkite" in check.get("name", "").lower(): - return check.get("link") - return None - - -def fetch_buildkite_data(build_url): - # Convert https://buildkite.com/org/pipeline/builds/number - # to https://buildkite.com/org/pipeline/builds/number.json - if not build_url.endswith(".json"): - json_url = build_url + ".json" - else: - json_url = build_url - - try: - with urllib.request.urlopen(json_url) as response: - if response.status != 200: - print( - f"Error fetching data from {json_url}: Status {response.status}", - file=sys.stderr, - ) - return None - data = json.loads(response.read().decode()) - except Exception as e: - print(f"Error fetching data from {json_url}: {e}", file=sys.stderr) - return None - - # If jobs list is truncated or empty but statistics says there are more jobs, - # try to fetch from /data/jobs.json - jobs = data.get("jobs", []) - jobs_count = data.get("statistics", {}).get("jobs_count", 0) - - if len(jobs) < jobs_count: - # Try fetching from /data/jobs.json - # Build URL might have .json already from the check above - base_url = build_url - if base_url.endswith(".json"): - base_url = base_url[:-5] - - jobs_url = f"{base_url}/data/jobs.json" - try: - with urllib.request.urlopen(jobs_url) as response: - if response.status == 200: - jobs_data = json.loads(response.read().decode()) - if isinstance(jobs_data, list): - data["jobs"] = jobs_data - elif isinstance(jobs_data, dict) and "records" in jobs_data: - data["jobs"] = jobs_data["records"] - except Exception as e: - print( - f"Warning: Could not fetch detailed jobs from {jobs_url}: {e}", - file=sys.stderr, - ) - - return data - - -def download_log(job_url, output_path): - # Construct raw log URL: job_url + "/raw" (Buildkite convention) - # job_url e.g. https://buildkite.com/org/pipeline/builds/14394#job-id - # Wait, the job['path'] gives /org/pipeline/builds/14394#job-id - # We want /org/pipeline/builds/14394/jobs/job-id/raw? No - # The clean URL for a job is https://buildkite.com/org/pipeline/builds/14394/jobs/job-id - # And raw log is https://buildkite.com/org/pipeline/builds/14394/jobs/job-id/raw - - # We have full_url e.g. https://buildkite.com/bazel/rules-python-python/builds/14394#019c5cf9-e3cf-468f-a7b1-8f9f5ad4b08c - # We need to transform it. - - if "#" in job_url: - base, job_id = job_url.split("#") - # Ensure base doesn't end with / - if base.endswith("/"): - base = base[:-1] - - # Build raw URL - raw_url = f"{base}/jobs/{job_id}/raw" - else: - print(f"Could not parse job URL for download: {job_url}", file=sys.stderr) - return False - - try: - with urllib.request.urlopen(raw_url) as response: - if response.status != 200: - print( - f"Error downloading log from {raw_url}: Status {response.status}", - file=sys.stderr, - ) - return False - with open(output_path, "wb") as f: - f.write(response.read()) - return True - except Exception as e: - print(f"Error downloading log from {raw_url}: {e}", file=sys.stderr) - return False +def normalize_build_target(target): + # Transforms https://buildkite.com/bazel/rules-python-python/builds/15707 + # into bazel/rules-python-python/15707 + m = re.search(r"buildkite\.com/([^/]+)/([^/]+)/builds/(\d+)", target) + if m: + return f"{m.group(1)}/{m.group(2)}/{m.group(3)}" + return target def main(): - parser = argparse.ArgumentParser(description="Get Buildkite CI results for a PR.") - parser.add_argument("pr_number", help="The PR number.") + parser = argparse.ArgumentParser( + description="Gets Buildkite build results using the 'bk' CLI." + ) parser.add_argument( - "--jobs", - action="append", - help="Filter by job name (regex match). Can be specified multiple times.", + "pr", help="PR number, Build URL, or Build ID (org/pipeline/build)" ) parser.add_argument( - "--download", - action="store_true", - help="If exactly one job is matched, download its log to a local file.", + "--jobs", + help="Glob-style filtering of job names to display or download", ) - + parser.add_argument("--download", action="store_true", help="Download job logs") args = parser.parse_args() - pr_display = args.pr_number - if "pull/" in pr_display: - pr_display = pr_display.split("pull/")[1].split("#")[0].split("/")[0] + check_cli("bk", "https://github.com/buildkite/cli") - print(f"Fetching checks for PR #{pr_display}...", file=sys.stderr) - checks = get_pr_checks(args.pr_number) + target = args.pr + if target.isdigit() and len(target) < 10: + print(f"🔍 Inspecting PR #{target} via gh to find Buildkite URL...") + target = get_build_url_from_pr(target) - build_url = get_buildkite_build_url(checks) - if not build_url: - print("No Buildkite check found for this PR.", file=sys.stderr) - sys.exit(1) + build_id = normalize_build_target(target) + print(f"🚀 Querying Buildkite for build: {build_id}\n") - print(f"Found Buildkite URL: {build_url}", file=sys.stderr) - - data = fetch_buildkite_data(build_url) - if not data: - sys.exit(1) - - build_state = data.get("state", "Unknown") - print(f"Build State: {build_state}") - - jobs = data.get("jobs", []) - jobs_count = data.get("statistics", {}).get("jobs_count", 0) - - print(f"Total jobs reported: {jobs_count}") - print(f"Jobs found in data: {len(jobs)}") - - if jobs_count != len(jobs): + # Run bk build view + res = subprocess.run(["bk", "build", "view", build_id]) + if res.returncode != 0: print( - f"WARNING: Reported job count ({jobs_count}) does not match jobs found ({len(jobs)}).", + f"❌ Failed to view build '{build_id}' via 'bk' CLI.", file=sys.stderr, ) - - print("-" * 40) - - filtered_jobs = [] - if args.jobs: - for job in jobs: - job_name = job.get("name") - if not job_name: - continue - for pattern in args.jobs: - if re.search(pattern, job_name, re.IGNORECASE): - filtered_jobs.append(job) - break - else: - filtered_jobs = jobs - - for job in filtered_jobs: - name = job.get("name", "Unknown") - state = job.get("state", "Unknown") - path = job.get("path") - full_url = f"https://buildkite.com{path}" if path else "N/A" - - passed = job.get("passed", False) - outcome = job.get("outcome") - - if passed: - result_str = "PASSED" - elif outcome: - result_str = outcome.upper() - else: - result_str = state.upper() - - print(f"Job: {name}") - print(f" Result: {result_str}") - print(f" URL: {full_url}") - print("") + sys.exit(res.returncode) if args.download: - if len(filtered_jobs) == 1: - job = filtered_jobs[0] - name = job.get("name", "unknown_job") - # Sanitize name for filename - safe_name = re.sub(r"[^a-zA-Z0-9_\-]", "_", name) - output_path = f"{safe_name}.log" - - path = job.get("path") - if path: - full_url = f"https://buildkite.com{path}" - print(f"Downloading log for '{name}'...", file=sys.stderr) - if download_log(full_url, output_path): - print(f"Downloaded log to: {output_path}") - else: - print("Failed to download log.", file=sys.stderr) - else: - print("Job has no URL path, cannot download.", file=sys.stderr) - elif len(filtered_jobs) == 0: - print("No jobs matched to download.", file=sys.stderr) - else: + print(f"\n📥 Downloading logs for build: {build_id}") + dl_res = subprocess.run(["bk", "build", "download", build_id]) + if dl_res.returncode != 0: print( - f"Matched {len(filtered_jobs)} jobs. Please filter to exactly one job to download.", - file=sys.stderr, + "⚠️ 'bk build download' failed or not supported. Try using 'bk job log ' for specific jobs." ) diff --git a/.agents/skills/buildkite-retry-job/SKILL.md b/.agents/skills/buildkite-retry-job/SKILL.md index e8e3bcd491..4b822e6222 100644 --- a/.agents/skills/buildkite-retry-job/SKILL.md +++ b/.agents/skills/buildkite-retry-job/SKILL.md @@ -1,15 +1,27 @@ --- name: buildkite-retry-job -description: Retry a failed build kite job +description: Retry failed Buildkite jobs individually or rebuild full builds --- -Use `scripts/retry_buildkite_jobs.py` to retry a job. This is best used -when there are network failures. +Use `scripts/retry_buildkite_jobs.py` to retry individual jobs or rebuild full +Buildkite builds. -example: +### Retrying Modes -``` -retry_buildkite_jobs.py org pipeline build -``` +1. **Job-Level Retry (Default for failed jobs)**: + Retries only specific failing jobs via `bk job retry `: + ```bash + ./.agents/skills/buildkite-retry-job/scripts/retry_buildkite_jobs.py + ``` -The `--jobs` flag can be used to retry specific jobs. +2. **Retry Specific Jobs by Name or ID**: + ```bash + ./.agents/skills/buildkite-retry-job/scripts/retry_buildkite_jobs.py --jobs "compile_pip_requirements" + ./.agents/skills/buildkite-retry-job/scripts/retry_buildkite_jobs.py --job-id + ``` + +3. **Rebuild Full Build**: + Re-runs the entire pipeline build from scratch via `bk build rebuild`: + ```bash + ./.agents/skills/buildkite-retry-job/scripts/retry_buildkite_jobs.py --rebuild + ``` diff --git a/.agents/skills/buildkite-retry-job/scripts/retry_buildkite_jobs.py b/.agents/skills/buildkite-retry-job/scripts/retry_buildkite_jobs.py index 67385fb8fd..b0a154ac0c 100755 --- a/.agents/skills/buildkite-retry-job/scripts/retry_buildkite_jobs.py +++ b/.agents/skills/buildkite-retry-job/scripts/retry_buildkite_jobs.py @@ -1,90 +1,214 @@ #!/usr/bin/env python3 + import argparse import json -import os +import re +import subprocess import sys -import urllib.request -from urllib.error import HTTPError -def make_request(url, method="GET", data=None, token=None): - headers = { - "Authorization": f"Bearer {token}", - "Accept": "application/json", - } - if data: - data = json.dumps(data).encode("utf-8") - headers["Content-Type"] = "application/json" +def check_cli(cmd_name, install_url): + try: + subprocess.run( + [cmd_name, "--version"], + check=True, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + ) + except Exception: + print( + f"❌ Error: '{cmd_name}' CLI is not installed or not in PATH.", + file=sys.stderr, + ) + print(f"Please install it from {install_url}", file=sys.stderr) + sys.exit(1) + - req = urllib.request.Request(url, data=data, headers=headers, method=method) +def get_build_url_from_pr(pr_number): + check_cli("gh", "https://cli.github.com/") + cmd = ["gh", "pr", "checks", str(pr_number), "--json", "name,link"] try: - with urllib.request.urlopen(req) as response: - return json.loads(response.read().decode()) - except HTTPError as e: - print(f"HTTP Error: {e.code} - {e.reason}", file=sys.stderr) - if e.fp: - print(e.fp.read().decode(), file=sys.stderr) - return None + res = subprocess.run(cmd, capture_output=True, text=True, check=True) + checks = json.loads(res.stdout) + for c in checks: + link = c.get("link", "") + if "buildkite.com" in link: + return link.split("#")[0] + print(f"❌ No Buildkite checks found for PR #{pr_number}.", file=sys.stderr) + sys.exit(1) + except subprocess.CalledProcessError as e: + print(f"❌ Error fetching PR checks: {e.stderr}", file=sys.stderr) + sys.exit(1) + + +def normalize_build_target(target): + # Transforms https://buildkite.com/bazel/rules-python-python/builds/15707 + # into (org/pipeline, 15707) + m = re.search(r"buildkite\.com/([^/]+/[^/]+)/builds/(\d+)", target) + if m: + return m.group(1), m.group(2) + parts = target.split("/") + if len(parts) == 3: + return f"{parts[0]}/{parts[1]}", parts[2] + elif len(parts) == 2: + return parts[0], parts[1] + return "bazel/rules-python-python", target + + +def retry_job_by_uuid(job_id): + check_cli("bk", "https://github.com/buildkite/cli") + print(f"🚀 Retrying individual job UUID: {job_id}") + return subprocess.run(["bk", "job", "retry", job_id]) + + +def retry_jobs_by_name(pipeline, build_number, job_pattern): + check_cli("bk", "https://github.com/buildkite/cli") + cmd = ["bk", "build", "view", str(build_number), "-p", pipeline, "--json"] + try: + res = subprocess.run(cmd, capture_output=True, text=True, check=True) + data = json.loads(res.stdout) + jobs = data.get("jobs", []) + matched = [] + for j in jobs: + name = j.get("name", "") or j.get("label", "") + state = j.get("state", "") + jid = j.get("id") + if jid and ( + re.search(job_pattern, name, re.IGNORECASE) + or job_pattern.lower() in name.lower() + ): + matched.append((jid, name, state)) + + if not matched: + print( + f"⚠️ No jobs found matching pattern '{job_pattern}' in build #{build_number}", + file=sys.stderr, + ) + sys.exit(1) + + for jid, name, state in matched: + print(f"🚀 Retrying job '{name}' (ID: {jid}, status: {state})...") + subprocess.run(["bk", "job", "retry", jid]) except Exception as e: - print(f"Error: {e}", file=sys.stderr) - return None + print(f"❌ Error fetching build jobs: {e}", file=sys.stderr) + sys.exit(1) def main(): parser = argparse.ArgumentParser( - description="Retry failed jobs in a Buildkite build." + description="Retry Buildkite jobs individually using 'bk job retry' or rebuild the whole build using 'bk build rebuild'." ) - parser.add_argument("org", help="Organization slug") - parser.add_argument("pipeline", help="Pipeline slug") - parser.add_argument("build", help="Build number") parser.add_argument( + "args", + nargs="+", + help="Target build (PR#, Buildkite URL, or org/pipeline/build)", + ) + parser.add_argument( + "--jobs", "--job-name", - help="Specific job name to retry (if failed). Regex/substring allowed.", + dest="job_name", + help="Specific job name or regex pattern to retry individually via 'bk job retry'", + ) + parser.add_argument( + "--job-id", + dest="job_id", + help="Specific job UUID to retry individually via 'bk job retry'", + ) + parser.add_argument( + "--rebuild", + action="store_true", + help="Rebuild the entire build afresh via 'bk build rebuild'", ) - args = parser.parse_args() - token = os.environ.get("BUILDKITE_API_TOKEN") - if not token: - print( - "Please set the BUILDKITE_API_TOKEN environment variable.", file=sys.stderr - ) - sys.exit(1) + check_cli("bk", "https://github.com/buildkite/cli") - url = f"https://api.buildkite.com/v2/organizations/{args.org}/pipelines/{args.pipeline}/builds/{args.build}" - print(f"Fetching build details from {url}...") - build_data = make_request(url, token=token) + if args.job_id: + res = retry_job_by_uuid(args.job_id) + sys.exit(res.returncode) - if not build_data: - print("Failed to fetch build details.", file=sys.stderr) + if len(args.args) == 3: + target = f"{args.args[0]}/{args.args[1]}/{args.args[2]}" + elif len(args.args) == 1: + target = args.args[0] + else: + print( + "❌ Error: Invalid arguments. Provide a single target (PR#, URL, or org/pipeline/build).", + file=sys.stderr, + ) sys.exit(1) - jobs = build_data.get("jobs", []) - failed_jobs = [j for j in jobs if j.get("state") == "failed"] + if target.isdigit() and len(target) < 10: + print(f"🔍 Inspecting PR #{target} via gh to find Buildkite URL...") + target = get_build_url_from_pr(target) - if not failed_jobs: - print("No failed jobs found in this build.") - sys.exit(0) + pipeline, build_number = normalize_build_target(target) - for job in failed_jobs: - job_id = job.get("id") - job_name = job.get("name", "Unknown") - - if ( - args.job_name - and args.job_name.lower() not in job_name.lower() - and args.job_name.lower() not in job.get("step_key", "").lower() - ): - continue - - print(f"Retrying job: {job_name} ({job_id})") - retry_url = f"https://api.buildkite.com/v2/organizations/{args.org}/pipelines/{args.pipeline}/builds/{args.build}/jobs/{job_id}/retry" - - result = make_request(retry_url, method="PUT", token=token) - if result: - print(f" Successfully triggered retry for {job_name}") - else: - print(f" Failed to trigger retry for {job_name}") + if args.job_name: + retry_jobs_by_name(pipeline, build_number, args.job_name) + elif args.rebuild: + print(f"🚀 Rebuilding entire build #{build_number} for pipeline: {pipeline}") + res = subprocess.run( + ["bk", "build", "rebuild", build_number, "-p", pipeline, "--yes"] + ) + sys.exit(res.returncode) + else: + # Default behavior: try job-level retries of failed jobs if any, otherwise rebuild + cmd = [ + "bk", + "build", + "view", + str(build_number), + "-p", + pipeline, + "--json", + ] + try: + res = subprocess.run(cmd, capture_output=True, text=True, check=True) + data = json.loads(res.stdout) + failed_jobs = [ + j + for j in data.get("jobs", []) + if j.get("state") in ("failed", "broken", "timed_out") and j.get("id") + ] + if failed_jobs: + for j in failed_jobs: + jid = j["id"] + jname = j.get("name") or j.get("label") or jid + print(f"🚀 Retrying failed job '{jname}' (ID: {jid})...") + subprocess.run(["bk", "job", "retry", jid]) + else: + print( + f"🚀 Rebuilding entire build #{build_number} for pipeline: {pipeline}" + ) + res = subprocess.run( + [ + "bk", + "build", + "rebuild", + build_number, + "-p", + pipeline, + "--yes", + ] + ) + sys.exit(res.returncode) + except Exception: + print( + f"🚀 Rebuilding entire build #{build_number} for pipeline: {pipeline}" + ) + res = subprocess.run( + [ + "bk", + "build", + "rebuild", + build_number, + "-p", + pipeline, + "--yes", + ] + ) + sys.exit(res.returncode) if __name__ == "__main__": diff --git a/.agents/skills/create-pr/SKILL.md b/.agents/skills/create-pr/SKILL.md new file mode 100644 index 0000000000..1c73c66424 --- /dev/null +++ b/.agents/skills/create-pr/SKILL.md @@ -0,0 +1,43 @@ +--- +name: create-pr +description: Propose, draft, or create a pull request by delegating to a + subagent +--- + +When proposing, drafting, or creating a Pull Request, you MUST ALWAYS delegate +to a subagent. NEVER create or draft PRs directly in the main conversation. + +### Instructions + +1. **Code Review Audit**: Launch a subagent with `review-code` to verify + `git diff` conforms to all rules (news entry in `news/`, line wrapping, + copyright, conventions, Starlark formatting). Fix any issues before + proceeding. +2. **Launch Subagent**: Use `invoke_subagent` (`TypeName: "self"`). +3. **Subagent Prompt Instructions**: + - Follow `CONTRIBUTING.md` and `@/.agents/rules/pr.md`. + - **PR Title**: Conventional commits format (`agents:` prefix for agent + rules/skills). + - **PR Body**: Explain *why* and conceptual *how*. Wrap strictly at 72 + columns max. Omit TAG/CONV. + - **Artifact Requirements**: Create `pr_info.md` with: + - **User-facing**: Published directly in the user interface. + - **Interactive feedback enabled**: Allows selecting lines and leaving + inline comments on the draft (`RequestFeedback: true`). + - **User decision choices**: Present choices: + 1. Create a regular PR + 2. Create a draft PR + 3. Provide feedback on the draft text + 4. Discard the draft text + - **Propose vs. Create**: If proposing or drafting a PR, do NOT run + `gh pr create`—only create the `pr_info.md` artifact. Only execute + `gh pr create` when explicitly requested to create the PR. + - **Targeting Upstream Repo**: When creating, target upstream using + `--repo bazel-contrib/rules_python` and `--head :`. +4. **Return Status**: Direct subagent to report PR number/draft status to + caller. +5. **Link Artifact Before Asking**: Upon subagent completion, output a + clickable markdown link to `pr_info.md` before prompting for confirmation. +6. **Interactive Actions**: When presenting choices via `ask_question`, always + include the clickable markdown link to `pr_info.md` in the `question` + prompt. diff --git a/.agents/skills/merge-pr/SKILL.md b/.agents/skills/merge-pr/SKILL.md new file mode 100644 index 0000000000..ae3e2e3ad5 --- /dev/null +++ b/.agents/skills/merge-pr/SKILL.md @@ -0,0 +1,33 @@ +--- +name: merge-pr +description: Merge a pull request into main, monitoring the merge queue, retrying CI flakes, and re-enqueuing if necessary +--- + +When the user asks to merge a pull request (e.g., "merge PR ", "merge this PR", or monitor its merge): + +1. **Enqueue for Merge**: Run `gh pr merge --auto --squash` to enable auto-merge or add the pull request to the merge queue. + - **Force / Admin Merge**: **CRITICAL**: Passing `--admin` to bypass the + merge queue or required checks requires explicit user permission or + consent. Only pass `--admin` (e.g., `gh pr merge --admin + --squash`) if the user has explicitly requested or approved a force/admin + merge. Never invoke `--admin` autonomously. +2. **Invoke a Background Shepherd**: Launch a background subagent with the role `Merge PR Shepherd` to continuously watch the PR until it merges. +3. **Leverage Existing CI Skills**: + - Have the subagent use the **`monitor-ci-results`** skill to watch for CI + check failures and generate analysis reports. + - Have the subagent use the **`buildkite-retry-job`** skill + (`retry_buildkite_jobs.py `) to automatically retry any + transient network flakes (e.g., HTTP 504 gateway timeouts, downloader + errors). + - When the PR is queued, actively discover the merge queue branch via + `gh api repos/:owner/:repo/branches --jq '.[].name | select(test("gh-readonly-queue/.*/pr--"))'` + and monitor commit statuses/Buildkite builds running on that temporary + branch. +4. **Queue Shepherding**: Periodically check `gh pr view --json + state,autoMergeRequest,mergeStateStatus,mergeable`. While `state` is + `"OPEN"`, ensure auto-merge is enabled / queued by running `gh pr merge + --auto --squash`. If `autoMergeRequest` is null (e.g., ejected + from the merge queue due to a CI flake in the temporary queue branch), + re-enqueue it for merge by running `gh pr merge --auto --squash` + once checks are retried or green. +5. **Completion Notification**: Once `state` becomes `"MERGED"`, send a high-priority message back to the parent conversation. diff --git a/.agents/skills/monitor-ci-results/SKILL.md b/.agents/skills/monitor-ci-results/SKILL.md new file mode 100644 index 0000000000..e2475b55ad --- /dev/null +++ b/.agents/skills/monitor-ci-results/SKILL.md @@ -0,0 +1,48 @@ +--- +name: monitor-ci-results +description: Monitor remote CI results for a PR and autonomously launch + subagents to analyze CI failures +--- + +When the user requests to monitor remote CI results or watch a pull request, +or when monitoring CI after PR updates: + +> **Note**: `monitor_remote_ci.py` is a single long-running continuous task. +> It continuously watches all current and future CI runs for the PR. Do NOT +> launch duplicate monitoring jobs for the same PR. + +1. **Check Existing Process**: Check if a monitor script is already running for + **this specific PR** using `pgrep -f "monitor_remote_ci.py "`. Do + NOT use a generic grep without ``, as other active agent + conversations may be monitoring different PRs concurrently. If one is + already running for this PR, do not start another instance. +2. **Launch Monitoring Script**: If no monitor process is active for + ``, launch the script in the background: +```bash +./.agents/skills/monitor-ci-results/scripts/monitor_remote_ci.py \ + "" & +``` + +### ✨ Autonomous Subagent Orchestration +1. **Background Polling**: `monitor_remote_ci.py` continuously polls both + GitHub PR checks and Buildkite workflow executions in the background across + new commits and CI re-runs. +2. **Blocked Jobs**: When a Buildkite job or GitHub check is in a blocked state + waiting for user confirmation, it dispatches a notification via `agentapi + send-message` so the user is alerted to confirm running the job. +3. **Failure Reporting**: When any GitHub check or Buildkite job completes + with errors, `monitor_remote_ci.py` dispatches a high-priority notification + message reporting the failed check back to your conversation. +4. **Subagent Analysis**: Upon receiving a failure notification message from + the monitoring script, immediately launch a separate subagent using the + `invoke_subagent` tool with the role "CI Failure Analyzer" to run the + `analyze-ci-failure` skill on the reported failure. +5. **Autonomous Flake Retry**: If the CI Failure Analyzer subagent confirms + that the failure is a transient infrastructure or network flake (e.g. disk + I/O error, 504 gateway, sandbox initialization failure), **immediately and + autonomously** launch a separate subagent using `invoke_subagent` with the + role "CI Job Retrier" and the prompt template in + `.agents/skills/monitor-ci-results/retry-job-prompt.md` to run the + `buildkite-retry-job` skill for ``. Do not execute the script + directly; use the subagent and skill orchestration instead. Continue + monitoring without pausing to ask the user. diff --git a/.agents/skills/monitor-ci-results/retry-job-prompt.md b/.agents/skills/monitor-ci-results/retry-job-prompt.md new file mode 100644 index 0000000000..4d2edad650 --- /dev/null +++ b/.agents/skills/monitor-ci-results/retry-job-prompt.md @@ -0,0 +1,3 @@ +Use the `buildkite-retry-job` skill to retry the failed Buildkite job(s) for PR {pr_number}. +Follow the instructions in `.agents/skills/buildkite-retry-job/SKILL.md` to retry the failing jobs using the skill. +Once the retry request completes, report back to the parent conversation. diff --git a/.agents/skills/monitor-ci-results/scripts/monitor_remote_ci.py b/.agents/skills/monitor-ci-results/scripts/monitor_remote_ci.py new file mode 100755 index 0000000000..57fde0ce8d --- /dev/null +++ b/.agents/skills/monitor-ci-results/scripts/monitor_remote_ci.py @@ -0,0 +1,319 @@ +#!/usr/bin/env python3 + +import argparse +import fcntl +import json +import os +import subprocess +import sys +import time +import urllib.request + + +def check_cli(cmd_name): + try: + subprocess.run( + [cmd_name, "--version"], + check=True, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + ) + return True + except Exception: + return False + + +def get_pr_checks(pr_number): + if not check_cli("gh"): + print("❌ 'gh' CLI not installed.", file=sys.stderr) + return [] + cmd = ["gh", "pr", "checks", str(pr_number), "--json", "name,link,state,bucket"] + try: + res = subprocess.run(cmd, capture_output=True, text=True) + out = res.stdout + json_str = out[out.find("[") : out.rfind("]") + 1] if "[" in out else "[]" + return json.loads(json_str) + except Exception as e: + print(f"⚠️ Error fetching PR checks: {e}", file=sys.stderr) + return [] + + +def get_buildkite_jobs(build_url): + base_url = build_url.split("#")[0] + if base_url.endswith(".json"): + base_url = base_url[:-5] + + jobs_url = f"{base_url}/data/jobs.json" + req = urllib.request.Request(jobs_url, headers={"User-Agent": "ci-monitor"}) + try: + with urllib.request.urlopen(req) as resp: + data = json.loads(resp.read().decode()) + if isinstance(data, list): + return data + elif isinstance(data, dict) and "records" in data: + return data["records"] + except Exception as e: + print( + f"⚠️ Could not fetch Buildkite jobs from {jobs_url}: {e}", + file=sys.stderr, + ) + return [] + + +def main(): + parser = argparse.ArgumentParser( + description="Monitor remote CI for failures and trigger analysis." + ) + parser.add_argument("pr", help="PR number to monitor") + parser.add_argument("conv_id", help="Conversation ID to report back to") + parser.add_argument( + "--interval", + type=int, + default=60, + help="Monitoring polling interval in seconds", + ) + parser.add_argument( + "--max-iterations", + type=int, + default=120, + help="Maximum number of polling cycles", + ) + args = parser.parse_args() + + skill_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) + scratch_dir = os.path.join(skill_dir, "scratch") + os.makedirs(scratch_dir, exist_ok=True) + + # Acquire lock for PR to avoid duplicate monitoring processes + lock_file_path = os.path.join(scratch_dir, f".monitored_pr_{args.pr}.lock") + lock_file = open(lock_file_path, "a+") + running_agent_conv_id = os.environ.get("ANTIGRAVITY_CONVERSATION_ID", "") + try: + fcntl.flock(lock_file.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB) + lock_file.seek(0) + lock_file.truncate(0) + lock_data = { + "pid": os.getpid(), + "agent_conv_id": running_agent_conv_id, + "notify_conv_id": args.conv_id, + } + lock_file.write(json.dumps(lock_data) + "\n") + lock_file.flush() + except (BlockingIOError, OSError): + existing_agent_conv = "" + try: + lock_file.seek(0) + content = lock_file.read().strip() + if content: + data = json.loads(content) + existing_agent_conv = data.get("agent_conv_id", "") or data.get( + "conv_id", "" + ) + except Exception: + pass + + subagent_str = ( + f" under agent/subagent conversation '{existing_agent_conv}'" + if existing_agent_conv + else "" + ) + print( + f"ℹ️ Continuous remote CI monitoring for PR #{args.pr} is already running in another process{subagent_str}. Exiting." + ) + sys.exit(0) + + state_file = os.path.join(scratch_dir, f"monitored_state_pr_{args.pr}.json") + monitored = {} + if os.path.exists(state_file): + try: + with open(state_file) as f: + monitored = json.load(f) + except Exception: + pass + + print( + f"🚀 Starting continuous remote CI monitoring for PR #{args.pr} every {args.interval}s..." + ) + + for i in range(args.max_iterations): + print( + f"🔍 [Cycle {i + 1}/{args.max_iterations}] Polling GitHub PR #{args.pr} checks..." + ) + checks = get_pr_checks(args.pr) + + for check in checks: + name = check.get("name", "unknown") + state = check.get("state", "UNKNOWN") + link = check.get("link", "") + bucket = check.get("bucket", "") + + if "buildkite" in name.lower() and link: + jobs = get_buildkite_jobs(link) + + passed = 0 + failed = 0 + running = 0 + blocked = 0 + other = 0 + + for job in jobs: + if job.get("retried_in_job_uuid") or job.get("retried_at"): + continue + jstate = job.get("state", "unknown") + exit_status = job.get("exit_status") + is_soft_failed = job.get("soft_failed") is True + is_blocked = jstate in ["blocked", "blocked_failed"] + is_failed = ( + ( + jstate in ["failed", "failing"] + or (exit_status != 0 and exit_status is not None) + ) + and not is_soft_failed + and not is_blocked + ) + is_passed = ( + jstate in ["passed", "success"] + or (jstate == "finished" and exit_status == 0) + or is_soft_failed + ) and not is_blocked + is_running = jstate in ["running", "scheduled"] and not is_blocked + + if is_blocked: + blocked += 1 + elif is_failed: + failed += 1 + elif is_passed: + passed += 1 + elif is_running: + running += 1 + else: + other += 1 + + build_id = link.split("/")[-1].split("#")[0] + print( + f"Buildkite #{build_id}: {len(jobs)} total jobs " + f"(Passed: {passed}, Failed: {failed}, Running: {running}, Blocked: {blocked}, Other: {other})" + ) + + for job in jobs: + if job.get("retried_in_job_uuid") or job.get("retried_at"): + continue + jname = job.get("name", "unknown_job") + jstate = job.get("state", "unknown") + jid = job.get("id", "") + jkey = f"bk_{jid}" + + exit_status = job.get("exit_status") + is_soft_failed = job.get("soft_failed") is True + is_blocked = jstate in ["blocked", "blocked_failed"] + is_failed = ( + ( + jstate in ["failed", "failing"] + or (exit_status != 0 and exit_status is not None) + ) + and not is_soft_failed + and not is_blocked + ) + + if is_blocked: + jkey_blocked = f"bk_blocked_{jid}" + if jkey_blocked not in monitored: + print( + f"⏸️ Notifying blocked state for Buildkite job '{jname}' (ID: {jid})..." + ) + msg = ( + f"⚠️ Remote CI Buildkite Job '{jname}' is blocked!\n\n" + f"Build ID: {build_id} | Job ID: {jid}\n" + f"Log URL: {job.get('log_url', link)}\n\n" + f"The user must confirm whether to run the CI jobs." + ) + subprocess.run( + [ + "agentapi", + "send-message", + "--title=CI Job Blocked", + args.conv_id, + msg, + ] + ) + monitored[jkey_blocked] = time.time() + with open(state_file, "w") as f: + json.dump(monitored, f) + + elif is_failed and jkey not in monitored: + print( + f"🚨 Notifying failure for Buildkite job '{jname}' (ID: {jid})..." + ) + msg = ( + f"⚠️ Remote CI Buildkite Job '{jname}' completed with errors!\n\n" + f"Build ID: {build_id} | Job ID: {jid}\n" + f"Log URL: {job.get('log_url', link)}\n\n" + f"Action required: Start a subagent to run the analyze-ci-failure skill for '{jname}' with build ID '{build_id}' and job ID '{jid}'." + ) + subprocess.run( + [ + "agentapi", + "send-message", + "--title=CI Job Failed", + args.conv_id, + msg, + ] + ) + monitored[jkey] = time.time() + with open(state_file, "w") as f: + json.dump(monitored, f) + + elif ( + state.upper() in ["ACTION_REQUIRED", "BLOCKED"] + and f"gh_blocked_{name}" not in monitored + ): + print(f"⏸️ Notifying blocked state for GitHub check '{name}'...") + msg = ( + f"⚠️ Remote CI GitHub Check '{name}' is blocked!\n\n" + f"Link: {link}\n\n" + f"The user must confirm whether to run the CI jobs." + ) + subprocess.run( + [ + "agentapi", + "send-message", + "--title=CI Check Blocked", + args.conv_id, + msg, + ] + ) + monitored[f"gh_blocked_{name}"] = time.time() + with open(state_file, "w") as f: + json.dump(monitored, f) + + elif ( + state.upper() in ["FAILURE", "FAIL", "CANCELLED", "TIMED_OUT"] + or bucket == "fail" + ) and name not in monitored: + print(f"🚨 Notifying failure for GitHub check '{name}'...") + job_id = link.split("/")[-1] if "/" in link else link + msg = ( + f"⚠️ Remote CI GitHub Check '{name}' completed with errors!\n\n" + f"Link: {link}\n\n" + f"Action required: Start a subagent to run the analyze-ci-failure skill for '{name}' with link '{link}' and job ID '{job_id}'." + ) + subprocess.run( + [ + "agentapi", + "send-message", + "--title=CI Check Failed", + args.conv_id, + msg, + ] + ) + monitored[name] = time.time() + with open(state_file, "w") as f: + json.dump(monitored, f) + + time.sleep(args.interval) + + print("🏁 CI monitoring service completed its scheduled iterations.") + + +if __name__ == "__main__": + main() diff --git a/.agents/skills/review-code/SKILL.md b/.agents/skills/review-code/SKILL.md new file mode 100644 index 0000000000..8646ff2451 --- /dev/null +++ b/.agents/skills/review-code/SKILL.md @@ -0,0 +1,59 @@ +--- +name: review-code +description: High-level code review audit of local changes when preparing to + send a PR for review +trigger: model_decision +--- + +When preparing to send a Pull Request for review, invoke **separate, concurrent +sub-agents** (`invoke_subagent`), where each sub-agent focuses exclusively on +its assigned checklist category. + +**CRITICAL**: Do NOT use a single sub-agent to validate multiple or all +dimensions at once. Each sub-agent must be launched with its own distinct prompt +file from `.agents/skills/review-code/`: + +### Audit Checklist & Sub-Agent Prompts + +1. **Starlark / Bazel Sub-Agent** (`Role: "Starlark Code Auditor"`): + - Prompt file: + `.agents/skills/review-code/review-starlark-prompt.md` + - Focus: Audits Starlark / Bazel changes (`*.bzl`, `BUILD`, `*.bazel` files) + in `git diff` against `.agents/rules/bzl.md` and Starlark rules in + `AGENTS.md`. + +2. **Python Code Sub-Agent** (`Role: "Python Code Auditor"`): + - Prompt file: + `.agents/skills/review-code/review-python-prompt.md` + - Focus: Audits Python source and test changes in `git diff` against + `.agents/rules/python.md` and Python and pytest conventions in `AGENTS.md`. + +3. **Documentation Sub-Agent** (`Role: "Documentation Auditor"`): + - Prompt file: + `.agents/skills/review-code/review-docs-prompt.md` + - Focus: Audits documentation (`.md`) changes and docs build targets against + `.agents/rules/docs.md`, `.agents/rules/news.md`, and `AGENTS.md`. + +4. **Contribution Guidelines Sub-Agent** (`Role: "Contributing Auditor"`): + - Prompt file: + `.agents/skills/review-code/review-contributing-prompt.md` + - Focus: Audits changes, requirements updates, directives, and news entry + files (`news/..md`) against `CONTRIBUTING.md` and + `.agents/rules/news.md`. + +5. **Project Conventions Sub-Agent** (`Role: "Project Conventions Auditor"`): + - Prompt file: + `.agents/skills/review-code/review-agents-prompt.md` + - Focus: Audits overall workspace compliance against `AGENTS.md`. + +6. **Pull Request Standards Sub-Agent** (`Role: "PR Standards Auditor"`): + - Prompt file: + `.agents/skills/review-code/review-pr-standards-prompt.md` + - Focus: Audits PR titles and descriptions against Conventional Commits + formatting and PR update rules in `CONTRIBUTING.md`. + +### Action Instructions +- Launch all sub-agents concurrently using `invoke_subagent`. +- Collect the reports from each sub-agent and report all violations and + suggested improvements clearly with suggested fixes for the user. +- If all domain audits pass, confirm that the PR is ready for review. diff --git a/.agents/skills/review-code/review-agents-prompt.md b/.agents/skills/review-code/review-agents-prompt.md new file mode 100644 index 0000000000..45651fd0de --- /dev/null +++ b/.agents/skills/review-code/review-agents-prompt.md @@ -0,0 +1,18 @@ +You are a specialized Project Conventions (`AGENTS.md`) Auditor sub-agent. +Your sole task is to audit all local changes (`git diff`) and workspace state +against `AGENTS.md`: + +1. Read and strictly enforce `AGENTS.md` and all `.agents/rules/*.md` files + without exception. +2. Verify NO Bazel copyright headers (`# Copyright ... The Bazel Authors`) + were added to new or existing files, unless explicitly instructed by the + user. +3. Verify that tests were executed using `bazel test --config=fast-tests` and + non-test build targets did not use `--config=fast-tests`. +4. Ensure public config settings in `python/config_settings/BUILD.bazel` were + not modified unless explicitly instructed. +5. Check that all repo rules and macro conventions described in `AGENTS.md` + are respected. + +Report any violations found clearly with actionable suggested fixes, or report +that the changes pass project conventions audit. diff --git a/.agents/skills/review-code/review-contributing-prompt.md b/.agents/skills/review-code/review-contributing-prompt.md new file mode 100644 index 0000000000..51fe58db00 --- /dev/null +++ b/.agents/skills/review-code/review-contributing-prompt.md @@ -0,0 +1,37 @@ +You are a specialized Contribution Guidelines Auditor sub-agent. +Your sole task is to audit all local changes (`git diff`), commit messages, +and PR metadata against `CONTRIBUTING.md` and project rules: + +1. Read and strictly enforce `CONTRIBUTING.md`, all `.agents/rules/*.md` files + (especially `.agents/rules/news.md`), and `AGENTS.md`. +2. **News Entry File Audit**: + - Check if the PR introduces user-visible features (`feat:`), bug fixes + (`fix:`), behavioral changes (`changed:`), breaking changes, or removals + (`removed:`). If so, verify that a news fragment file is added under + `news/..md`. + - Verify `CHANGELOG.md` is NOT modified directly for unreleased changes. + - Verify news filename format: `news/..md` where `` is the + PR or issue number (or placeholder ID) and `` is strictly one of + `added`, `changed`, `fixed`, or `removed`. + - Verify news entry content rules: + - Brief, human-friendly description without leading bullet points (`*` + or `-`). + - Subsystem prefix in parentheses when applicable (e.g. `(gazelle) ...`, + `(cc) ...`). + - Use Sphinx MyST cross-reference syntax `{obj}\`\`` for rules, + macros, targets, providers, attributes, and args. + - Append GitHub issue cross-references at the end in markdown link format: + `([#1234](https://github.com/bazel-contrib/rules_python/issues/1234))`. + - Lines wrapped at 80 columns. +3. Verify that `{versionadded}` and `{versionchanged}` directives use + `VERSION_NEXT_FEATURE` for unreleased features and are placed at the end of + the documentation text. +4. If locked/resolved requirements files (`requirements.txt`, `pyproject.toml`, + `requirements.in`) were modified, verify that the associated + `requirements.update` target was executed to keep locked requirement files + in sync. +5. Ensure style and conventions described in `CONTRIBUTING.md` are respected + across all changes. + +Report any violations found clearly with actionable suggested fixes, or report +that the changes pass contribution audit. diff --git a/.agents/skills/review-code/review-docs-prompt.md b/.agents/skills/review-code/review-docs-prompt.md new file mode 100644 index 0000000000..eaeb91232d --- /dev/null +++ b/.agents/skills/review-code/review-docs-prompt.md @@ -0,0 +1,24 @@ +You are a specialized Documentation & Sphinx/MyST Auditor sub-agent. +Your sole task is to audit all documentation (`.md`) changes and new `.bzl` APIs +in `git diff` against the project's documentation rules: + +1. Read and strictly enforce `.agents/rules/docs.md`, `.agents/rules/news.md`, + `AGENTS.md`, and `CONTRIBUTING.md`. +2. Check that lines wrap at 80 columns. +3. Ensure markdown filenames use hyphens (`-`) rather than underscores (`_`) + (except news entry files under `news/`, which follow `..md`). +4. Verify Sphinx MyST colon indentation hierarchy (outer directives must have + more colons than inner directives). +5. Verify `{versionadded}` and `{versionchanged}` sections are placed at the + end of the documentation text. +6. For unreleased features or attributes, ensure `{versionadded}` / + `{versionchanged}` directives use `VERSION_NEXT_FEATURE` (not hardcoded + version numbers). +7. Check documentation build correctness: ensure new `.bzl` files or public + APIs are included in `//docs:docs` or relevant docs build targets. +8. For any added or modified news entries in `news/`, verify they adhere to + `.agents/rules/news.md` and `CONTRIBUTING.md` (proper `..md` + name, no leading bullets, subsystem prefix, `{obj}` refs, issue links). + +Report any violations found clearly with actionable suggested fixes, or report +that the documentation changes pass audit. diff --git a/.agents/skills/review-code/review-pr-standards-prompt.md b/.agents/skills/review-code/review-pr-standards-prompt.md new file mode 100644 index 0000000000..a4de4296a1 --- /dev/null +++ b/.agents/skills/review-code/review-pr-standards-prompt.md @@ -0,0 +1,17 @@ +You are a specialized PR Standards Auditor sub-agent. +Your sole task is to audit PR titles and PR descriptions against +`CONTRIBUTING.md` and project rules: + +1. Read and strictly enforce `.agents/rules/pr.md`, `.agents/rules/news.md`, + `AGENTS.md`, and `CONTRIBUTING.md`. +2. Check PR title formatting: must follow Conventional Commits format (e.g. + `feat(cc): ...`, `docs(python): ...`). For agent rules/skills, use `agents:` + prefix. +3. Ensure PR descriptions explain *why* a change is made and provide a + high-level overview of *how*, following advice in `CONTRIBUTING.md`. +4. If a PR has already been created, enforce PR update rules: do NOT amend or + rebase existing commits (create new commits and merges instead, to preserve + code review comment threads). + +Report any violations found clearly with actionable suggested fixes, or report +that the PR standards pass audit. diff --git a/.agents/skills/review-code/review-python-prompt.md b/.agents/skills/review-code/review-python-prompt.md new file mode 100644 index 0000000000..8e477ab3a7 --- /dev/null +++ b/.agents/skills/review-code/review-python-prompt.md @@ -0,0 +1,15 @@ +You are a specialized Python & pytest Auditor sub-agent. +Your sole task is to audit all Python source (`.py`) and test changes in +`git diff` against the project's Python conventions: + +1. Read and strictly enforce `.agents/rules/python.md`, `AGENTS.md`, and + `CONTRIBUTING.md`. +2. Check Python pytest conventions: when registering pytest fixtures from helper + modules in test files, use `pytest_plugins = [""]`. +3. Name fixture functions with a `fixture_` prefix (e.g. `def fixture_foo():`) + and pass the public fixture name using `@pytest.fixture(name="foo")`. +4. Verify that tests were executed using Bazel + (`bazel test --config=fast-tests`) and passed. + +Report any violations found clearly with actionable suggested fixes, or report +that the Python changes pass audit. diff --git a/.agents/skills/review-code/review-starlark-prompt.md b/.agents/skills/review-code/review-starlark-prompt.md new file mode 100644 index 0000000000..8bd8cdbe21 --- /dev/null +++ b/.agents/skills/review-code/review-starlark-prompt.md @@ -0,0 +1,17 @@ +You are a specialized Starlark & Bazel Code Auditor sub-agent. +Your sole task is to audit all Starlark (`.bzl`, `BUILD`, `*.bazel`) changes +in `git diff` against the project's Starlark coding rules and conventions: + +1. Read and strictly enforce `.agents/rules/starlark.md`, + `.agents/rules/bzl.md`, `AGENTS.md`, and `CONTRIBUTING.md`. +2. Verify iterative algorithms are used (no recursion, no `while` loops). +3. Ensure every `.bzl` file outside `tests/` has a corresponding `bzl_library` + target in its `BUILD` file with proper dependencies. +4. Ensure loads from `/private/` in test files have + `# buildifier: disable=bzl-visibility`. +5. Check multi-line rule/macro doc arguments: use triple-quoted strings + (`"""`), and do NOT use trailing backslashes (`\`) on opening triple-quotes. +6. Verify analysis tests use `rules_testing`, not `bazel_skylib`. + +Report any violations found clearly with actionable suggested fixes, or report +that the Starlark changes pass audit. diff --git a/.agents/skills/rule-creator/SKILL.md b/.agents/skills/rule-creator/SKILL.md new file mode 100644 index 0000000000..9ed7978120 --- /dev/null +++ b/.agents/skills/rule-creator/SKILL.md @@ -0,0 +1,56 @@ +--- +name: rule-creator +description: Create and format agent rules with proper front matter in the workspace +--- + +Use this skill when you need to create a new rule for the agent in the +workspace. + +### Rule File Location + +All workspace-specific rules must be created as individual Markdown files under +the `.agents/rules/` directory: +``` +.agents/rules/.md +``` + +### Rule Format + +Every rule file must start with a YAML front matter block defining the trigger +condition, followed by the rule content in Markdown. + +```yaml +--- +trigger: +--- + +# + + +``` + +#### Trigger Conditions +* `always_on`: The rule is always active and must be followed for all tasks. +* Custom triggers: You can specify other trigger conditions if the rule only + applies in certain contexts. + +### Formatting Guidelines +* **Line Wrapping:** Always wrap all text in the rule file (including the + title and description) to **80 columns** to ensure readability and + compatibility. +* **Clarity:** Write clear, actionable directives. + +### Example + +To create a rule that prevents adding copyrights: + +```markdown +--- +trigger: always_on +--- + +# No Copyrights Rule + +Unless directed by the user otherwise, do not add Bazel copyright to new or +existing files. +``` diff --git a/.bazelci/presubmit.yml b/.bazelci/presubmit.yml index 6366741313..b3e308c7c7 100644 --- a/.bazelci/presubmit.yml +++ b/.bazelci/presubmit.yml @@ -25,6 +25,10 @@ buildifier: bazel: 7.x skip_in_bazel_downstream_pipeline: "Bazel 7 required" .reusable_config: &reusable_config + # USE_BAZEL_DIFF is disabled because target_compatible_with is used heavily, + # but BazelCI's bazel-diff integration errors when handling them. + # environment: + # USE_BAZEL_DIFF: "1" build_targets: - "--" - "..." @@ -80,6 +84,9 @@ buildifier: coverage_targets: ["..."] .coverage_targets_example_bzlmod_build_file_generation: &coverage_targets_example_bzlmod_build_file_generation coverage_targets: ["//:bzlmod_build_file_generation_test"] +.coverage_targets_bootstrap: &coverage_targets_bootstrap + coverage_targets: + - //tests/bootstrap_impls:stdlib_shadowing_system_python_test .coverage_targets_example_multi_python: &coverage_targets_example_multi_python coverage_targets: - //tests:my_lib_3_10_test @@ -212,6 +219,7 @@ tasks: bazel: 7.x ubuntu: <<: *reusable_config + <<: *coverage_targets_bootstrap name: "Default: Ubuntu, Bazel {bazel}" platform: ubuntu2204 bazel: ${{ bazel }} diff --git a/.bazelignore b/.bazelignore index afd162998a..88a4c32056 100644 --- a/.bazelignore +++ b/.bazelignore @@ -35,3 +35,6 @@ tests/integration/compile_pip_requirements/bazel-compile_pip_requirements tests/integration/local_toolchains/bazel-local_toolchains tests/integration/py_cc_toolchain_registered/bazel-py_cc_toolchain_registered tests/integration/toolchain_target_settings/bazel-module_under_test +tests/integration/unified_pypi/bazel-unified_pypi +tests/integration/uv_lock/bazel-uv_lock +tests/integration/validate_test_main/bazel-module_under_test diff --git a/.bazelrc b/.bazelrc index 8fedcf1f8f..cd41961452 100644 --- a/.bazelrc +++ b/.bazelrc @@ -19,6 +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 --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 @@ -44,6 +47,11 @@ common --enable_bzlmod # Local disk cache greatly speeds up builds if the regular cache is lost common --disk_cache=~/.cache/bazel/bazel-disk-cache +# Drop `experimental_` prefix once Bazel 7 is no longer supported +common --experimental_downloader_config=downloader_config.cfg +common --http_timeout_scaling=10.0 +common --experimental_repository_downloader_retries=10 + # Additional config to use for readthedocs builds. diff --git a/.bazelrc.deleted_packages b/.bazelrc.deleted_packages index f4ea8527f3..da79f11058 100644 --- a/.bazelrc.deleted_packages +++ b/.bazelrc.deleted_packages @@ -7,6 +7,7 @@ common --deleted_packages=examples/bzlmod/entry_points/tests common --deleted_packages=examples/bzlmod/libs/my_lib common --deleted_packages=examples/bzlmod/other_module common --deleted_packages=examples/bzlmod/other_module/other_module/pkg +common --deleted_packages=examples/bzlmod/other_module/other_module/rule_builder common --deleted_packages=examples/bzlmod/patches common --deleted_packages=examples/bzlmod/runfiles common --deleted_packages=examples/bzlmod/tests @@ -38,7 +39,12 @@ common --deleted_packages=tests/integration/pip_parse common --deleted_packages=tests/integration/pip_parse/empty common --deleted_packages=tests/integration/pip_parse_isolated common --deleted_packages=tests/integration/py_cc_toolchain_registered +common --deleted_packages=tests/integration/runtime_manifests common --deleted_packages=tests/integration/toolchain_target_settings +common --deleted_packages=tests/integration/unified_pypi +common --deleted_packages=tests/integration/uv_lock +common --deleted_packages=tests/integration/validate_test_main +common --deleted_packages=tests/integration/whl_library common --deleted_packages=tests/modules/another_module common --deleted_packages=tests/modules/other common --deleted_packages=tests/modules/other/nspkg_delta @@ -47,3 +53,4 @@ common --deleted_packages=tests/modules/other/nspkg_single common --deleted_packages=tests/modules/other/simple_v1 common --deleted_packages=tests/modules/other/simple_v2 common --deleted_packages=tests/modules/other/with_external_data +common --deleted_packages=tests/modules/rules_pyrefly_stub/pyrefly diff --git a/.bazelversion b/.bazelversion index 512e4c889e..44931da266 100644 --- a/.bazelversion +++ b/.bazelversion @@ -1 +1 @@ -9.x +9.1.1 diff --git a/.gitattributes b/.gitattributes index eae260e931..4f93d89d33 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,2 +1,7 @@ python/features.bzl export-subst tools/publish/*.txt linguist-generated=true +tests/uv/lock/testdata/requirements.txt text eol=lf +python/private/runtimes_manifest_workspace.bzl text eol=lf +python/private/runtimes_manifest.txt text eol=lf + +*.bat text eol=crlf diff --git a/.github/ISSUE_TEMPLATE/release_tracking_template.md b/.github/ISSUE_TEMPLATE/release_tracking_template.md new file mode 100644 index 0000000000..ab634e75f3 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/release_tracking_template.md @@ -0,0 +1,34 @@ +--- +name: Release Tracking Issue +about: Checklist for tracking a new release of rules_python. +title: 'Release ' +labels: ['type: release'] +--- +# Release tasks +- [ ] Prepare Release | status=awaiting-preparation +- [ ] Create Release branch +- [ ] Tag RC0 +- [ ] Tag Final + +## Backports + +To request a backport, add it to the checklist below and process it. See [RELEASING.md: How to add backports](https://github.com/bazel-contrib/rules_python/blob/main/RELEASING.md#how-to-add-backports) for details. + +--- + +To manually control the release flow, see the [RELEASING.md: Manual Editing](https://github.com/bazel-contrib/rules_python/blob/main/RELEASING.md#manual-editing-of-tracking-issue) section. + +
+Available Commands + +Comment commands: +- `/prepare`: Determines version, creates tracking issue and preparation PR. +- `/prepare-complete [PR]`: Marks preparation task as complete. +- `/create-release-branch`: Cuts and pushes the release branch. +- `/create-rc`: Tags and publishes a new release candidate (RC). +- `/process-backports`: Cherry-picks pending backports. +- `/add-backports `: Adds PRs to the backports and processes backports. +- `/promote`: Promotes the latest RC to final release. + +See [RELEASING.md](https://github.com/bazel-contrib/rules_python/blob/main/RELEASING.md) for details on how to use them. +
diff --git a/.github/workflows/automated_pr_review.yaml b/.github/workflows/automated_pr_review.yaml new file mode 100644 index 0000000000..01b63e42a4 --- /dev/null +++ b/.github/workflows/automated_pr_review.yaml @@ -0,0 +1,104 @@ +name: Automated Code Review + +# TODO: Eventually, use pull_request_target instead of pull_request. +# pull_request_target runs in the base branch context and has access +# to secrets (like GEMINI_API_KEY) even for fork PRs. +# Using pull_request for now during setup/testing. +on: + issue_comment: + types: [created] + +permissions: + contents: read + pull-requests: read + +jobs: + # Always runs so the workflow run exits cleanly without a "No jobs ran" error + # when the review job's if-condition evaluates to false. + noop: + runs-on: ubuntu-latest + steps: + - name: Workflow trigger check + run: echo "Workflow triggered successfully." + + # Job 1: Runs without secrets to extract the git diff from untrusted PR code. + # Keeps GEMINI_API_KEY away from any environment that checks out untrusted PR files. + prepare_diff: + runs-on: ubuntu-latest + if: > + github.event_name == 'issue_comment' && github.event.issue.pull_request != null && + (startsWith(github.event.comment.body, '/review') || + contains(github.event.comment.body, '\n/review') || + contains(github.event.comment.body, '\r\n/review')) && + contains(fromJson('["OWNER", "MEMBER", "COLLABORATOR"]'), github.event.comment.author_association) + steps: + - name: Checkout PR Branch (Data Only) + uses: actions/checkout@v7 + with: + ref: refs/pull/${{ github.event.pull_request.number || github.event.issue.number }}/head + path: untrusted_pr_head + persist-credentials: false + + - name: Fetch Base Branch and Negotiate Minimal Diff History + env: + PR_COMMITS: ${{ github.event.pull_request.commits }} + run: | + cd untrusted_pr_head + # 1. Fetch the tip of main + git fetch origin main:refs/remotes/origin/main --depth=1 + + # 2. Check if we already have the merge base (common ancestor) + if ! git merge-base origin/main HEAD >/dev/null 2>&1; then + # If we know the exact number of commits in the PR, deepen by (PR_COMMITS + 10) + if [ -n "$PR_COMMITS" ] && [ "$PR_COMMITS" != "null" ]; then + git fetch --deepen="$((PR_COMMITS + 10))" + fi + # 3. If it's an issue_comment event (where PR_COMMITS is null) or still shallow, unshallow/deepen + if ! git merge-base origin/main HEAD >/dev/null 2>&1; then + git fetch --unshallow || git fetch --deepen=50 + fi + fi + git diff origin/main...HEAD > ../pr_diff.txt + + # Upload extracted diff as artifact to pass to Job 2 safely as text data. + - name: Upload Diff Artifact + uses: actions/upload-artifact@v7 + with: + name: pr_diff + path: pr_diff.txt + + # Job 2: Runs with secrets in base branch context. + review: + needs: prepare_diff + runs-on: ubuntu-latest + steps: + - name: Download Diff Artifact + uses: actions/download-artifact@v8 + with: + name: pr_diff + + # Check out reviewbot into a separate directory to isolate base branch tool code. + - name: Checkout Reviewbot (Base Branch Only) + uses: actions/checkout@v7 + with: + sparse-checkout: | + tools/private/reviewbot + path: reviewbot + + - name: Install uv + uses: astral-sh/setup-uv@v9.0.0 + + - name: Run Antigravity Review + # Run inside reviewbot directory so execution context is the trusted base branch. + # This also helps prevent uv from looking for config files in locations it shouldn't, + # i.e. by default, uv will look in $PWD for a pyproject file to build. + working-directory: reviewbot + env: + GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + # Use --no-project to prevent uv from discovering or building pyproject.toml/setup.py + # in the workspace, ensuring only standalone script dependencies are resolved. + uv run --no-project --directory . tools/private/reviewbot/antigravity_review.py \ + --prompt tools/private/reviewbot/prompt.txt \ + --diff-file ../pr_diff.txt diff --git a/.github/workflows/backport_create_releases.yaml b/.github/workflows/backport_create_releases.yaml new file mode 100644 index 0000000000..346bfdbbdb --- /dev/null +++ b/.github/workflows/backport_create_releases.yaml @@ -0,0 +1,47 @@ +name: "Backport: Create Releases" + +on: + workflow_dispatch: + inputs: + issue: + description: 'The Backport Tracking Issue Number' + required: true + type: string + workflow_call: + inputs: + issue: + description: 'The Backport Tracking Issue Number' + required: true + type: string + +permissions: + contents: write + issues: write + pull-requests: write + +jobs: + backport_create_releases: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v7 + with: + fetch-depth: 0 + + - name: Setup Bazel + uses: bazel-contrib/setup-bazel@0.19.0 + with: + bazelisk-version: 1.20.0 + + - name: Configure Git Identity + run: | + git config --global user.name "github-actions[bot]" + git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com" + + - name: Run Backport Create Releases Pipeline + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + ISSUE: ${{ inputs.issue }} + run: | + bazel run //tools/private/release -- \ + backport-create-releases --issue=$ISSUE --no-dry-run diff --git a/.github/workflows/backport_prepare.yaml b/.github/workflows/backport_prepare.yaml new file mode 100644 index 0000000000..587c0fc357 --- /dev/null +++ b/.github/workflows/backport_prepare.yaml @@ -0,0 +1,76 @@ +name: "Backport: Prepare" + +on: + workflow_dispatch: + inputs: + issue: + description: 'The Backport Tracking Issue Number' + required: false + type: string + pr: + description: 'PR to backport (e.g. #123) (if no issue)' + required: false + type: string + from_minor: + description: 'From minor version (e.g. 1.7) (if no issue)' + required: false + type: string + to_minor: + description: 'To minor version (e.g. 1.9) (if no issue, optional)' + required: false + type: string + workflow_call: + inputs: + issue: + description: 'The Backport Tracking Issue Number' + required: false + type: string + +permissions: + contents: write + issues: write + pull-requests: write + +jobs: + backport_prepare: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v7 + with: + fetch-depth: 0 + + - name: Setup Bazel + uses: bazel-contrib/setup-bazel@0.19.0 + with: + bazelisk-version: 1.20.0 + + - name: Configure Git Identity + run: | + git config --global user.name "github-actions[bot]" + git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com" + + - name: Run Backport Preparation Pipeline + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + ISSUE: ${{ inputs.issue }} + PR: ${{ inputs.pr }} + FROM_MINOR: ${{ inputs.from_minor }} + TO_MINOR: ${{ inputs.to_minor }} + run: | + ARGS=() + if [ -n "$ISSUE" ]; then + ARGS+=("--issue=$ISSUE") + else + if [ -n "$PR" ]; then + ARGS+=("--pr=$PR") + fi + if [ -n "$FROM_MINOR" ]; then + ARGS+=("--from-minor=$FROM_MINOR") + fi + if [ -n "$TO_MINOR" ]; then + ARGS+=("--to-minor=$TO_MINOR") + fi + fi + bazel run //tools/private/release -- \ + backport-prepare "${ARGS[@]}" --no-dry-run diff --git a/.github/workflows/check_do_not_merge_label.yml b/.github/workflows/check_do_not_merge_label.yml deleted file mode 100644 index 97b91b156a..0000000000 --- a/.github/workflows/check_do_not_merge_label.yml +++ /dev/null @@ -1,20 +0,0 @@ -name: "Check 'do not merge' label" - -on: - pull_request_target: - types: - - opened - - synchronize - - reopened - - labeled - - unlabeled - -jobs: - block-do-not-merge: - runs-on: ubuntu-latest - steps: - - name: Check for "do not merge" label - if: "contains(github.event.pull_request.labels.*.name, 'do not merge')" - run: | - echo "This PR has the 'do not merge' label and cannot be merged." - exit 1 diff --git a/.github/workflows/check_version_markers.sh b/.github/workflows/check_version_markers.sh index b8d35aec9d..15a0a67dc8 100755 --- a/.github/workflows/check_version_markers.sh +++ b/.github/workflows/check_version_markers.sh @@ -20,9 +20,8 @@ grep_exit_code=0 # Exclude CONTRIBUTING.md, RELEASING.md because they document how to use these strings. grep --exclude=CONTRIBUTING.md \ --exclude=RELEASING.md \ - --exclude=release.py \ - --exclude=release_test.py \ --exclude-dir=.* \ + --exclude-dir=release \ VERSION_NEXT_ -r || grep_exit_code=$? if [[ $grep_exit_code -eq 0 ]]; then diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index e324bfbf2a..7990f652e6 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -17,27 +17,16 @@ permissions: contents: read jobs: - mypy: - runs-on: ubuntu-latest - steps: - # Checkout the code - - uses: actions/checkout@v6 - - uses: jpetrucciani/mypy-check@master - with: - path: 'python/runfiles' - - uses: jpetrucciani/mypy-check@master - with: - path: 'tests/runfiles' ruff: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 - - uses: astral-sh/ruff-action@v4.0.0 + - uses: actions/checkout@v7 + - uses: astral-sh/ruff-action@v4.1.0 with: # Keep in sync with .pre-commit-config.yaml version: 0.15.14 args: check --extend-exclude testdata - - uses: astral-sh/ruff-action@v4.0.0 + - uses: astral-sh/ruff-action@v4.1.0 with: version: 0.15.14 args: format --check --exclude testdata diff --git a/.github/workflows/on_comment.yaml b/.github/workflows/on_comment.yaml new file mode 100644 index 0000000000..a4330a0d6f --- /dev/null +++ b/.github/workflows/on_comment.yaml @@ -0,0 +1,208 @@ +name: "On Comment" + +on: + issue_comment: + types: [created] + +permissions: + contents: write + issues: write + pull-requests: write + +jobs: + # This job always runs to prevent GHA from marking the run as failed when + # all other jobs are skipped. + noop: + runs-on: ubuntu-latest + steps: + - run: echo "No-op" + + parse_comment: + runs-on: ubuntu-latest + if: | + github.event.comment.author_association == 'OWNER' || + github.event.comment.author_association == 'MEMBER' || + github.event.comment.author_association == 'COLLABORATOR' + outputs: + command: ${{ steps.parse.outputs.command }} + issue_number: ${{ steps.parse.outputs.issue_number }} + pr_number: ${{ steps.parse.outputs.pr_number }} + backports: ${{ steps.parse.outputs.backports }} + steps: + - name: Parse comment + id: parse + env: + COMMENT_BODY: ${{ github.event.comment.body }} + IS_PR: "${{ github.event.issue.pull_request != null }}" + EVENT_NUMBER: "${{ github.event.issue.number }}" + HAS_RELEASE_LABEL: "${{ contains(github.event.issue.labels.*.name, 'type: release') }}" + HAS_BACKPORT_LABEL: "${{ contains(github.event.issue.labels.*.name, 'type: backport-pr') }}" + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + if [ "$IS_PR" = "false" ]; then + # Set issue number for non-PR issues (release or backport tracking issues) + issue_number=$EVENT_NUMBER + echo "issue_number=$issue_number" >> "$GITHUB_OUTPUT" + echo "issue_number=$issue_number" >> "$GITHUB_ENV" + + # Check if it's a release tracking issue + if [ "$HAS_RELEASE_LABEL" = "true" ]; then + # Handle /create-rc comment + if echo "$COMMENT_BODY" | grep -qE '^[[:space:]]*/create-rc([[:space:]]|$)'; then + echo "command=create-rc" >> "$GITHUB_OUTPUT" + # Handle /prepare-complete comment + elif echo "$COMMENT_BODY" | grep -qE '^[[:space:]]*/prepare-complete([[:space:]]|$)'; then + echo "command=prepare-complete" >> "$GITHUB_OUTPUT" + pr_arg=$(echo "$COMMENT_BODY" | grep -E '^[[:space:]]*/prepare-complete([[:space:]]|$)' | sed -E 's/^[[:space:]]*\/prepare-complete[[:space:]]*//' | tr -d '[:space:]#') + if [ -n "$pr_arg" ]; then + echo "pr_number=$pr_arg" >> "$GITHUB_OUTPUT" + fi + # Handle /create-release-branch comment + elif echo "$COMMENT_BODY" | grep -qE '^[[:space:]]*/create-release-branch([[:space:]]|$)'; then + echo "command=create-release-branch" >> "$GITHUB_OUTPUT" + # Handle /prepare comment + elif echo "$COMMENT_BODY" | grep -qE '^[[:space:]]*/prepare([[:space:]]|$)'; then + echo "command=prepare" >> "$GITHUB_OUTPUT" + # Handle /process-backports comment + elif echo "$COMMENT_BODY" | grep -qE '^[[:space:]]*/process-backports([[:space:]]|$)'; then + echo "command=process-backports" >> "$GITHUB_OUTPUT" + # Handle /add-backports comment + elif echo "$COMMENT_BODY" | grep -qE '^[[:space:]]*/add-backports([[:space:]]|$)'; then + args=$(echo "$COMMENT_BODY" | grep -E '^[[:space:]]*/add-backports([[:space:]]|$)' | sed -E 's/^[[:space:]]*\/add-backports[[:space:]]*//') + args=$(echo "$args" | sed -e 's/^[[:space:],]*//' -e 's/[[:space:],]*$//') + csv=$(echo "$args" | sed -E 's/[[:space:],]+/ /g' | tr ' ' ',') + if [ -n "$csv" ]; then + echo "command=add-backports" >> "$GITHUB_OUTPUT" + echo "backports=$csv" >> "$GITHUB_OUTPUT" + else + echo "command=none" >> "$GITHUB_OUTPUT" + echo "Error: No PRs specified for add-backports." >&2 + gh api \ + --method POST \ + -H "Accept: application/vnd.github+json" \ + -H "X-GitHub-Api-Version: 2022-11-28" \ + /repos/${{ github.repository }}/issues/comments/${{ github.event.comment.id }}/reactions \ + -f "content=-1" + fi + # Handle /promote comment + elif echo "$COMMENT_BODY" | grep -qE '^[[:space:]]*/promote([[:space:]]|$)'; then + echo "command=promote" >> "$GITHUB_OUTPUT" + else + echo "command=none" >> "$GITHUB_OUTPUT" + fi + # Check if it's a backport tracking issue + elif [ "$HAS_BACKPORT_LABEL" = "true" ]; then + # Handle /prepare comment for backports + if echo "$COMMENT_BODY" | grep -qE '^[[:space:]]*/prepare([[:space:]]|$)'; then + echo "command=backport-prepare" >> "$GITHUB_OUTPUT" + # Handle /create-releases comment for backports + elif echo "$COMMENT_BODY" | grep -qE '^[[:space:]]*/create-releases([[:space:]]|$)'; then + echo "command=backport-create-releases" >> "$GITHUB_OUTPUT" + else + echo "command=none" >> "$GITHUB_OUTPUT" + fi + else + echo "command=none" >> "$GITHUB_OUTPUT" + fi + elif [ "$IS_PR" = "true" ]; then + pr_number=$EVENT_NUMBER + # Handle /backport comment on PR + if echo "$COMMENT_BODY" | grep -qE '^[[:space:]]*/backport([[:space:]]|$)'; then + echo "command=pr-backport" >> "$GITHUB_OUTPUT" + echo "pr_number=$pr_number" >> "$GITHUB_OUTPUT" + elif echo "$COMMENT_BODY" | grep -qE '^[[:space:]]*/prepare-complete([[:space:]]|$)'; then + echo "command=prepare-complete" >> "$GITHUB_OUTPUT" + echo "pr_number=$pr_number" >> "$GITHUB_OUTPUT" + else + echo "command=none" >> "$GITHUB_OUTPUT" + fi + else + echo "command=none" >> "$GITHUB_OUTPUT" + fi + + call_create_rc: + needs: parse_comment + if: needs.parse_comment.outputs.command == 'create-rc' + uses: ./.github/workflows/release_create_rc.yaml + with: + issue: ${{ needs.parse_comment.outputs.issue_number }} + comment_id: "${{ github.event.comment.id }}" + secrets: inherit + + call_prepare_complete: + needs: parse_comment + if: needs.parse_comment.outputs.command == 'prepare-complete' + uses: ./.github/workflows/release_complete_prepare.yaml + with: + pr: ${{ needs.parse_comment.outputs.pr_number }} + issue: ${{ needs.parse_comment.outputs.issue_number }} + secrets: inherit + + call_create_release_branch: + needs: parse_comment + if: needs.parse_comment.outputs.command == 'create-release-branch' + uses: ./.github/workflows/release_create_release_branch.yaml + with: + issue: ${{ needs.parse_comment.outputs.issue_number }} + secrets: inherit + + call_prepare: + needs: parse_comment + if: needs.parse_comment.outputs.command == 'prepare' + uses: ./.github/workflows/release_prepare.yaml + with: + issue: ${{ needs.parse_comment.outputs.issue_number }} + secrets: inherit + + call_add_backports: + needs: parse_comment + if: | + needs.parse_comment.outputs.command == 'add-backports' || + needs.parse_comment.outputs.command == 'pr-backport' + uses: ./.github/workflows/release_add_backports.yaml + with: + prs: ${{ needs.parse_comment.outputs.command == 'pr-backport' && needs.parse_comment.outputs.pr_number || needs.parse_comment.outputs.backports }} + issue: ${{ needs.parse_comment.outputs.issue_number }} + secrets: inherit + + call_process_backports_after_add: + needs: [parse_comment, call_add_backports] + if: needs.parse_comment.outputs.command == 'add-backports' + uses: ./.github/workflows/release_process_backports.yaml + with: + issue: ${{ needs.parse_comment.outputs.issue_number }} + comment_id: "${{ github.event.comment.id }}" + secrets: inherit + + call_process_backports_only: + needs: parse_comment + if: needs.parse_comment.outputs.command == 'process-backports' + uses: ./.github/workflows/release_process_backports.yaml + with: + issue: ${{ needs.parse_comment.outputs.issue_number }} + comment_id: "${{ github.event.comment.id }}" + secrets: inherit + + call_promote: + needs: parse_comment + if: needs.parse_comment.outputs.command == 'promote' + uses: ./.github/workflows/release_promote.yaml + with: + issue: ${{ needs.parse_comment.outputs.issue_number }} + secrets: inherit + + call_backport_prepare: + needs: parse_comment + if: needs.parse_comment.outputs.command == 'backport-prepare' + uses: ./.github/workflows/backport_prepare.yaml + with: + issue: ${{ needs.parse_comment.outputs.issue_number }} + secrets: inherit + + call_backport_create_releases: + needs: parse_comment + if: needs.parse_comment.outputs.command == 'backport-create-releases' + uses: ./.github/workflows/backport_create_releases.yaml + with: + issue: ${{ needs.parse_comment.outputs.issue_number }} + secrets: inherit diff --git a/.github/workflows/on_pr_closed.yaml b/.github/workflows/on_pr_closed.yaml new file mode 100644 index 0000000000..94c49b6561 --- /dev/null +++ b/.github/workflows/on_pr_closed.yaml @@ -0,0 +1,113 @@ +name: "On PR Closed" + +on: + pull_request: + types: [closed] + +permissions: + contents: read + issues: read + pull-requests: read + +jobs: + # This job always runs to prevent GHA from marking the run as failed when + # all other jobs are skipped. + noop: + runs-on: ubuntu-latest + steps: + - run: echo "No-op" + + check_if_backport: + runs-on: ubuntu-latest + if: github.event.pull_request.merged == true + outputs: + should_process: ${{ steps.check.outputs.should_process }} + steps: + - name: Check if PR is a backport candidate + id: check + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PR_NUMBER: ${{ github.event.pull_request.number }} + run: | + # Check if there is any active release issue + ACTIVE_ISSUES=$(gh issue list --repo ${{ github.repository }} --label "type: release" --state open --json number) + if [ "$ACTIVE_ISSUES" = "[]" ] || [ -z "$ACTIVE_ISSUES" ]; then + echo "No active release tracking issue found. Skipping." + echo "should_process=false" >> "$GITHUB_OUTPUT" + exit 0 + fi + + # Check if PR has "/backport" in comments (only comments, not body) + PR_DATA=$(gh pr view "$PR_NUMBER" --repo ${{ github.repository }} --json comments) + + if echo "$PR_DATA" | jq -r '.comments[].body' | grep -qE '^[[:space:]]*/backport([[:space:]]|$)'; then + echo "Found /backport comment. Proceeding." + echo "should_process=true" >> "$GITHUB_OUTPUT" + else + echo "No /backport comment found. Skipping." + echo "should_process=false" >> "$GITHUB_OUTPUT" + fi + + process_backports: + needs: check_if_backport + if: needs.check_if_backport.outputs.should_process == 'true' + runs-on: ubuntu-latest + permissions: + contents: write + issues: write + pull-requests: read + steps: + - name: Checkout repository + uses: actions/checkout@v7 + with: + fetch-depth: 0 + + - name: Setup Bazel + uses: bazel-contrib/setup-bazel@0.19.0 + with: + bazelisk-version: 1.20.0 + + - name: Configure Git Identity + run: | + git config --global user.name "github-actions[bot]" + git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com" + + - name: Process Backports + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PR_NUMBER: ${{ github.event.pull_request.number }} + run: | + bazel run //tools/private/release -- on-pr-merged \ + "$PR_NUMBER" \ + --remote origin \ + --no-dry-run + + complete_sync_changelog: + if: | + github.event.pull_request.merged == true && + contains(github.event.pull_request.labels.*.name, 'type: sync-changelog') + runs-on: ubuntu-latest + permissions: + contents: write + issues: write + pull-requests: read + steps: + - name: Checkout repository + uses: actions/checkout@v7 + with: + fetch-depth: 0 + + - name: Setup Bazel + uses: bazel-contrib/setup-bazel@0.19.0 + with: + bazelisk-version: 1.20.0 + + - name: Complete Sync Changelog + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PR_NUMBER: ${{ github.event.pull_request.number }} + run: | + bazel run //tools/private/release -- complete-sync-changelog \ + --pr "$PR_NUMBER" + + diff --git a/.github/workflows/pr-metadata-checks.yaml b/.github/workflows/pr-metadata-checks.yaml new file mode 100644 index 0000000000..8060ff921e --- /dev/null +++ b/.github/workflows/pr-metadata-checks.yaml @@ -0,0 +1,60 @@ +name: "PR Metadata Checks" + +on: + pull_request_target: + types: + - opened + - synchronize + - reopened + - labeled + - unlabeled + - edited + +jobs: + blocks-do-not-merge: + runs-on: ubuntu-latest + steps: + - name: Check for "do not merge" label + if: "contains(github.event.pull_request.labels.*.name, 'do not merge')" + run: | + echo "::error::This PR has the 'do not merge' label" \ + "and cannot be merged." + exit 1 + + - name: Check PR description for DO NOT SUBMIT/MERGE + env: + PR_BODY: ${{ github.event.pull_request.body }} + run: | + echo "Checking PR description..." + if echo "$PR_BODY" | grep -Ei "DO NOT SUBMIT|DO NOT MERGE"; then + echo "::error::This PR description contains" \ + "'DO NOT SUBMIT' or 'DO NOT MERGE'" \ + "and cannot be merged." + exit 1 + fi + echo "PR description is clean." + + block-transient-agent-files: + runs-on: ubuntu-latest + permissions: + pull-requests: read + steps: + - name: Check for blocked files + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PR_NUMBER: ${{ github.event.pull_request.number }} + run: | + echo "Checking for blocked files..." + # We use --repo to avoid needing actions/checkout. + # This keeps the job lightweight and avoids "not a git repository" errors. + changed_files=$(gh pr diff "$PR_NUMBER" --name-only --repo "${{ github.repository }}") + blocked_files=$(echo "$changed_files" | grep -E '^.agents/(plans|scratch)(/|$)' || true) + if [ -n "$blocked_files" ]; then + echo "::error::Files in .agents/plans and" \ + ".agents/scratch are permitted in PRs to" \ + "facilitate discussion, but are not allowed" \ + "to be merged. Please remove them before merging:" + echo "$blocked_files" + exit 1 + fi + echo "No blocked files found." diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 5ef65e83a6..7b358956d9 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -21,7 +21,7 @@ on: type: string jobs: publish: - uses: bazel-contrib/publish-to-bcr/.github/workflows/publish.yaml@v1.1.0 + uses: bazel-contrib/publish-to-bcr/.github/workflows/publish.yaml@v1.4.2 with: tag_name: ${{ inputs.tag_name }} # GitHub repository which is a fork of the upstream where the Pull Request will be opened. diff --git a/.github/workflows/publish_pypi.yaml b/.github/workflows/publish_pypi.yaml new file mode 100644 index 0000000000..44f0a1d9fb --- /dev/null +++ b/.github/workflows/publish_pypi.yaml @@ -0,0 +1,32 @@ +name: Publish to PyPI + +on: + workflow_call: + inputs: + tag_name: + description: "release tag: tag that will be released" + required: true + type: string + workflow_dispatch: + inputs: + tag_name: + description: "release tag: tag that will be released" + required: true + type: string + +jobs: + publish_pypi: + name: Publish runfiles to PyPI + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v7 + with: + ref: ${{ inputs.tag_name || github.ref_name }} + - env: + # This special value tells pypi that the user identity is supplied within the token + TWINE_USERNAME: __token__ + # Note, the PYPI_API_TOKEN is for the rules-python pypi user, added by @rickylev on + # https://github.com/bazel-contrib/rules_python/settings/secrets/actions + TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }} + run: bazel run --stamp --embed_label=${{ inputs.tag_name || github.ref_name }} //python/runfiles:wheel.publish diff --git a/.github/workflows/release_add_backports.yaml b/.github/workflows/release_add_backports.yaml new file mode 100644 index 0000000000..d9d6d84e34 --- /dev/null +++ b/.github/workflows/release_add_backports.yaml @@ -0,0 +1,58 @@ +name: "Release: Add Backports" + +on: + workflow_dispatch: + inputs: + prs: + description: 'CSV list of PR numbers to add (e.g., 123,456)' + required: true + type: string + issue: + description: 'The Release Tracking Issue Number (optional)' + required: false + type: string + workflow_call: + inputs: + prs: + description: 'CSV list of PR numbers to add (e.g., 123,456)' + required: true + type: string + issue: + description: 'The Release Tracking Issue Number (optional)' + required: false + type: string + +permissions: + issues: write + +jobs: + add_backports: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v7 + with: + fetch-depth: 0 + + - name: Setup Bazel + uses: bazel-contrib/setup-bazel@0.19.0 + with: + bazelisk-version: 1.20.0 + + - name: Add Backports to Tracking Issue + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PRS: ${{ inputs.prs }} + ISSUE: ${{ inputs.issue }} + run: | + ARGS=() + if [ -n "$ISSUE" ]; then + ARGS+=("--issue=$ISSUE") + fi + + # Convert CSV to array + IFS=',' read -r -a pr_array <<< "$PRS" + + bazel run //tools/private/release -- add-backports \ + "${pr_array[@]}" \ + "${ARGS[@]}" diff --git a/.github/workflows/release_complete_prepare.yaml b/.github/workflows/release_complete_prepare.yaml new file mode 100644 index 0000000000..2496dd9cd2 --- /dev/null +++ b/.github/workflows/release_complete_prepare.yaml @@ -0,0 +1,78 @@ +name: "Release: Prepare: Complete" + +on: + pull_request: + types: [closed] + workflow_dispatch: + inputs: + pr: + description: 'The merged preparation PR number (optional if issue provided)' + required: false + type: string + issue: + description: 'The Release Tracking Issue Number (optional if pr provided)' + required: false + type: string + workflow_call: + inputs: + pr: + description: 'The merged preparation PR number (optional if issue provided)' + required: false + type: string + issue: + description: 'The Release Tracking Issue Number (optional if pr provided)' + required: false + type: string + +permissions: + contents: write + issues: write + +jobs: + complete_prepare: + # Run if triggered manually or if the release-prepared PR was merged + if: | + github.event_name == 'workflow_dispatch' || + github.event_name == 'workflow_call' || + (github.event.pull_request.merged == true && + contains(github.event.pull_request.labels.*.name, 'release-prepared')) + runs-on: ubuntu-latest + outputs: + issue: ${{ steps.complete_prepare.outputs.issue }} + steps: + - name: Checkout repository + uses: actions/checkout@v7 + with: + fetch-depth: 0 + + - name: Setup Bazel + uses: bazel-contrib/setup-bazel@0.19.0 + with: + bazelisk-version: 1.20.0 + + - name: Mark Prepare Release Complete + id: complete_prepare + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PR_NUMBER: ${{ inputs.pr || github.event.pull_request.number }} + ISSUE: ${{ inputs.issue }} + run: | + ARGS=() + if [ -n "$PR_NUMBER" ]; then + PR_NUMBER="${PR_NUMBER#\#}" + ARGS+=("--pr=$PR_NUMBER") + fi + if [ -n "$ISSUE" ]; then + ISSUE="${ISSUE#\#}" + ARGS+=("--issue=$ISSUE") + fi + # Run the complete-prepare subcommand in the release tool to cleanly update checklist metadata + bazel run //tools/private/release -- \ + complete-prepare "${ARGS[@]}" + + call_create_release_branch: + needs: complete_prepare + uses: ./.github/workflows/release_create_release_branch.yaml + with: + issue: ${{ needs.complete_prepare.outputs.issue }} + secrets: inherit diff --git a/.github/workflows/release_create_rc.yaml b/.github/workflows/release_create_rc.yaml new file mode 100644 index 0000000000..a96a04a0ec --- /dev/null +++ b/.github/workflows/release_create_rc.yaml @@ -0,0 +1,69 @@ +name: "Release: Create RC" + +on: + workflow_dispatch: + inputs: + issue: + description: 'The Release Tracking Issue Number (e.g., 142)' + required: true + type: string + comment_id: + description: 'The ID of the comment that triggered this run (optional)' + required: false + type: string + workflow_call: + inputs: + issue: + description: 'The Release Tracking Issue Number (e.g., 142)' + required: true + type: string + comment_id: + description: 'The ID of the comment that triggered this run (optional)' + required: false + type: string + +permissions: + contents: write + issues: write + +jobs: + tag_rc: + runs-on: ubuntu-latest + outputs: + tag_name: ${{ steps.tagger.outputs.tag_name }} + steps: + - name: Checkout repository + uses: actions/checkout@v7 + with: + fetch-depth: 0 + + - name: Setup Bazel + uses: bazel-contrib/setup-bazel@0.19.0 + with: + bazelisk-version: 1.20.0 + + - name: Configure Git Identity + run: | + git config --global user.name "github-actions[bot]" + git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com" + + - name: Attempt RC Tagging + id: tagger + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + ISSUE: ${{ inputs.issue }} + COMMENT_ID: ${{ inputs.comment_id }} + run: | + ARGS=() + if [ -n "$COMMENT_ID" ]; then + ARGS+=("--triggering-comment=$COMMENT_ID") + fi + bazel run //tools/private/release -- \ + create-rc --issue "$ISSUE" --remote origin "${ARGS[@]}" + + call_release: + needs: tag_rc + uses: ./.github/workflows/release_publish.yaml + with: + tag_name: ${{ needs.tag_rc.outputs.tag_name }} + secrets: inherit diff --git a/.github/workflows/release_create_release_branch.yaml b/.github/workflows/release_create_release_branch.yaml new file mode 100644 index 0000000000..d251b1e394 --- /dev/null +++ b/.github/workflows/release_create_release_branch.yaml @@ -0,0 +1,55 @@ +name: "Release: Create Release Branch" + +on: + workflow_dispatch: + inputs: + issue: + description: 'The Release Tracking Issue Number (e.g., 142)' + required: true + type: string + workflow_call: + inputs: + issue: + description: 'The Release Tracking Issue Number (e.g., 142)' + required: true + type: string + +permissions: + contents: write + issues: write + +jobs: + cut_branch: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v7 + with: + fetch-depth: 0 + + - name: Setup Bazel + uses: bazel-contrib/setup-bazel@0.19.0 + with: + bazelisk-version: 1.20.0 + + - name: Configure Git Identity + run: | + git config --global user.name "github-actions[bot]" + git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com" + + - name: Attempt Branch Creation + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + bazel run //tools/private/release -- \ + create-release-branch --issue "${{ inputs.issue }}" --remote origin + + # A no-op job that always runs to prevent "no jobs ran" failures + # when the main job is skipped. + suppress-no-jobs-ran-error: + runs-on: ubuntu-latest + steps: + - name: Echo Success + run: echo "Success" + + diff --git a/.github/workflows/release_prepare.yaml b/.github/workflows/release_prepare.yaml new file mode 100644 index 0000000000..c83a7b74f9 --- /dev/null +++ b/.github/workflows/release_prepare.yaml @@ -0,0 +1,51 @@ +name: "Release: Prepare" + +on: + workflow_dispatch: + inputs: + issue: + description: 'The Release Tracking Issue Number (e.g., 142)' + required: false + type: string + workflow_call: + inputs: + issue: + description: 'The Release Tracking Issue Number (e.g., 142)' + required: false + type: string + +permissions: + contents: write + issues: write + pull-requests: write + +jobs: + prepare_release: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v7 + with: + fetch-depth: 0 + + - name: Setup Bazel + uses: bazel-contrib/setup-bazel@0.19.0 + with: + bazelisk-version: 1.20.0 + + - name: Configure Git Identity + run: | + git config --global user.name "github-actions[bot]" + git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com" + + - name: Run Release Preparation Pipeline + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + ISSUE: ${{ inputs.issue }} + run: | + ARGS=() + if [ -n "$ISSUE" ]; then + ARGS+=("--issue=$ISSUE") + fi + bazel run //tools/private/release -- \ + prepare "${ARGS[@]}" --no-dry-run diff --git a/.github/workflows/release_process_backports.yaml b/.github/workflows/release_process_backports.yaml new file mode 100644 index 0000000000..ca719778df --- /dev/null +++ b/.github/workflows/release_process_backports.yaml @@ -0,0 +1,76 @@ +name: "Release: Process Backports" + +on: + workflow_dispatch: + inputs: + issue: + description: 'The Release Tracking Issue Number (e.g., 142)' + required: true + type: string + add_backports: + description: 'CSV list of PR numbers to add and process (optional)' + required: false + type: string + comment_id: + description: 'The ID of the comment that triggered this run (optional)' + required: false + type: string + workflow_call: + inputs: + issue: + description: 'The Release Tracking Issue Number (e.g., 142)' + required: true + type: string + add_backports: + description: 'CSV list of PR numbers to add and process (optional)' + required: false + type: string + comment_id: + description: 'The ID of the comment that triggered this run (optional)' + required: false + type: string + +permissions: + contents: write + issues: write + +jobs: + process_backports: + # Always gate GHA runs to ensure we are operating on a type: release labeled issue if metadata is queried + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v7 + with: + fetch-depth: 0 + + - name: Setup Bazel + uses: bazel-contrib/setup-bazel@0.19.0 + with: + bazelisk-version: 1.20.0 + + - name: Configure Git Identity + run: | + git config --global user.name "github-actions[bot]" + git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com" + + - name: Process Pending Backports + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + ADD_BACKPORTS: ${{ inputs.add_backports }} + COMMENT_ID: ${{ inputs.comment_id }} + ISSUE: ${{ inputs.issue }} + run: | + ARGS=() + if [ -n "$ADD_BACKPORTS" ]; then + ARGS+=("--add=$ADD_BACKPORTS") + fi + if [ -n "$COMMENT_ID" ]; then + ARGS+=("--triggering-comment=$COMMENT_ID") + fi + + bazel run //tools/private/release -- process-backports \ + --issue "$ISSUE" \ + --remote origin \ + --no-dry-run \ + "${ARGS[@]}" diff --git a/.github/workflows/release_promote.yaml b/.github/workflows/release_promote.yaml new file mode 100644 index 0000000000..93ec23a882 --- /dev/null +++ b/.github/workflows/release_promote.yaml @@ -0,0 +1,71 @@ +name: "Release: Promote" + +on: + workflow_dispatch: + inputs: + version: + description: 'The final version to release (e.g., 0.38.0)' + required: true + type: string + workflow_call: + inputs: + version: + description: 'The final version to release (e.g., 0.38.0)' + required: false + type: string + issue: + description: 'The tracking issue number' + required: true + type: string + +permissions: + contents: write + issues: write + +jobs: + promote: + runs-on: ubuntu-latest + outputs: + version: ${{ steps.promote.outputs.version }} + steps: + - name: Checkout repository + uses: actions/checkout@v7 + with: + fetch-depth: 0 + + - name: Setup Bazel + uses: bazel-contrib/setup-bazel@0.19.0 + with: + bazelisk-version: 1.20.0 + + - name: Configure Git Identity + run: | + git config --global user.name "github-actions[bot]" + git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com" + + - name: Run Promote RC + id: promote + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + VERSION: ${{ inputs.version }} + ISSUE: ${{ inputs.issue }} + run: | + ARGS=() + if [ -n "$VERSION" ]; then + ARGS+=("$VERSION") + fi + if [ -n "$ISSUE" ]; then + ARGS+=("--issue=$ISSUE") + fi + ARGS+=("--remote=origin") + ARGS+=("--no-dry-run") + + bazel run //tools/private/release -- promote "${ARGS[@]}" + + publish: + needs: promote + uses: ./.github/workflows/release_publish.yaml + with: + tag_name: ${{ needs.promote.outputs.version }} + publish_to_pypi: true + secrets: inherit diff --git a/.github/workflows/release.yml b/.github/workflows/release_publish.yaml similarity index 70% rename from .github/workflows/release.yml rename to .github/workflows/release_publish.yaml index 1e4f26981b..eedf653fc1 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release_publish.yaml @@ -13,7 +13,7 @@ # limitations under the License. # Cut a release whenever a new tag is pushed to the repo. -name: Release +name: "Release: Publish" on: push: @@ -33,6 +33,17 @@ on: secrets: publish_token: required: false + workflow_call: + inputs: + tag_name: + description: "release tag: tag that will be released" + required: true + type: string + publish_to_pypi: + description: 'Publish to PyPI' + required: false + type: boolean + default: true jobs: release: @@ -40,9 +51,9 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@v7 with: - ref: ${{ github.ref_name }} + ref: ${{ inputs.tag_name || github.ref_name }} - name: Create release archive and notes run: .github/workflows/create_archive_and_notes.sh ${{ inputs.tag_name || github.ref_name }} - name: Release @@ -66,19 +77,9 @@ jobs: publish_pypi: # We just want publish_pypi last, since once uploaded, it can't be changed. - name: Publish runfiles to PyPI needs: publish_bcr - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v6 - with: - ref: ${{ github.tag_name || github.ref_name }} - - if: github.event_name == 'push' || github.event.inputs.publish_to_pypi - env: - # This special value tells pypi that the user identity is supplied within the token - TWINE_USERNAME: __token__ - # Note, the PYPI_API_TOKEN is for the rules-python pypi user, added by @rickylev on - # https://github.com/bazel-contrib/rules_python/settings/secrets/actions - TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }} - run: bazel run --stamp --embed_label=${{ inputs.tag_name || github.ref_name }} //python/runfiles:wheel.publish + if: github.event_name == 'push' || inputs.publish_to_pypi + uses: ./.github/workflows/publish_pypi.yaml + with: + tag_name: ${{ inputs.tag_name || github.ref_name }} + secrets: inherit diff --git a/.gitignore b/.gitignore index fb1b17e466..cb5bdec980 100644 --- a/.gitignore +++ b/.gitignore @@ -54,3 +54,10 @@ user.bazelrc # MODULE.bazel.lock is ignored for now as per recommendation from upstream. # See https://github.com/bazelbuild/bazel/issues/20369 MODULE.bazel.lock + +# Buildkite logs +*Windows*.log + +# Agent scratch directories +.agents/**/scratch + diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index cada605a26..f4107880b2 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -48,3 +48,26 @@ repos: entry: ./tools/update_deleted_packages.sh files: ^((examples|tests)/.*/(MODULE.bazel|WORKSPACE|WORKSPACE.bzlmod|BUILD.bazel)|.bazelrc)$ pass_filenames: false + - id: sort-runtimes-manifest + name: Sort runtimes manifest + language: system + entry: ./python/private/tools/sort_manifest.py + files: ^python/private/runtimes_manifest\.txt$ + - id: sync-runtimes-manifest + name: Sync runtimes manifest workspace + language: system + entry: ./python/private/tools/sync_runtimes_manifest_workspace.py + files: ^python/private/runtimes_manifest\.txt$ + pass_filenames: false + - id: sync-downloader-configs + name: Sync downloader configs + language: system + entry: ./tools/private/sync_downloader_configs.py + files: downloader_config\.cfg$ + pass_filenames: false + - id: gazelle + name: Run Gazelle + language: system + entry: bazel run //tools/private/gazelle + files: (\.bzl|\.bazel|BUILD|WORKSPACE(\.bzlmod)?)$ + pass_filenames: false diff --git a/AGENTS.md b/AGENTS.md index 73d254f27c..07b523e410 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -15,6 +15,7 @@ Ask for user input and provide a justificaiton if trying to violate them. * NEVER run `bazel clean --expunge`. * Once a PR is created, do not amend or rebase. +* Do not add Bazel copyright to new or existing files. ## Style and conventions @@ -31,11 +32,24 @@ into the sentence, not verbatim. When adding `{versionadded}` or `{versionchanged}` sections, add them add the end of the documentation text. +### PR Updates + +Once a PR is created, create new commits and merges. Don't use rebase or amend +because it interferes with code review comments. + ### PR descriptions Follow the advice in `CONTRIBUTING.md` for PR descriptions. PR descriptions become the commit message upon merge. +### Python pytest conventions + +* When registering pytest fixtures from helper modules in test files, use + `pytest_plugins = [""]`. +* Name fixture functions with a `fixture_` prefix (e.g. `def fixture_foo():`), + and pass the public fixture name using the `name` parameter in + `@pytest.fixture(name="foo")`. + ### Starlark style For doc strings, using triple quoted strings when the doc string is more than @@ -92,7 +106,6 @@ def foo_test_suite(name): test_suite(name=name, tests=_tests) ``` - #### Repository rules The function argument `rctx` is a hint that the function is a repository rule, @@ -115,7 +128,6 @@ The `repository_ctx` API docs are at: https://bazel.build/rules/lib/builtins/rep e.g. given `load("//foo:bar.bzl", ...)`, the target is `//foo:bar_bzl`. * For files outside rules_python: remove the `.bzl` suffix. e.g. given `load("@foo//foo:bar.bzl", ...)`, the target is `@foo//foo:bar`. -* `bzl_library()` targets should be kept in alphabetical order by name. Example: @@ -137,7 +149,10 @@ Tests are under the `tests/` directory. When testing, add `--config=fast-tests`. -When building, add `--config=fast-tests`. +When building test targets, add `--config=fast-tests`. Do NOT use +`--config=fast-tests` when building non-test targets (such as `//docs:docs`), +because `--config=fast-tests` enables `--build_tests_only=true`, which causes +non-test targets to be silently ignored (finding 0 targets). The `--config=fast-tests` flag avoids running expensive and slow tests can that freeze the host machine or cause flakiness. @@ -150,6 +165,7 @@ This repository contains 3 Bazel bzlmod modules. * `gazelle/` is for the `@rules_python_gazelle_plugin` module. * All other code is part of `@rules_python`. +`tests/support/` contains utility code and helpers for testing. `python/config_settings/BUILD.bazel` contains build flags that are part of the public API. DO NOT add, remove, or modify these build flags unless specifically @@ -195,6 +211,14 @@ When modifying locked/resolved requirements files: the requirements.txt file. That will update the locked/resolved requirements.txt file. +When updating the minimum Python version for dev/docs tooling (e.g. following +Dependabot alerts): + * Update `python_version` in both `lock(name = "requirements", ...)` and + `lock(name = "uv_lock", ...)` within `dev/BUILD.bazel`. + * Regenerate lockfiles by running: + `bazel run //dev:requirements.update && bazel run //dev:uv_lock.update` + * Verify docs build via `bazel build //docs:docs`. + ## rules_python idiosyncrasies When building `//docs:docs`, ignore an error about exit code 2; this is a flake, @@ -207,3 +231,24 @@ e.g. ``` load("//python/private:foo.bzl", "foo") # buildifier: disable=bzl-visibility ``` + +### CI Failure Inspection + +When inspecting CI failures, if the failure is due to a network error +downloading a repository, check if that rule set is mirrored on +mirror.bazel.build. If so, add it to the downloader config. + +### CI Flakiness and Monitoring + +CI is known to have flakey network issues. When submitting or updating a PR, +start a background agent that continuously monitors the latest build of a PR and +checks for flakey network errors (e.g., 504 gateway errors). + +If Buildkite permissions allow, retry failures and use available Buildkite +skills. If permissions do not allow, modify `.bazelrc` to set flags (such as +`--http_timeout_scaling` or `--experimental_repository_downloader_retries`) to +make downloads retry more. + +If downloads fail for a dependency, check if that dependency is available on +mirror.bazel.build and, if so, add it to downloader_config.cfg if it isn't +already there. diff --git a/BUILD.bazel b/BUILD.bazel index 7da18ebaa4..f978126da7 100644 --- a/BUILD.bazel +++ b/BUILD.bazel @@ -14,6 +14,41 @@ load("@bazel_skylib//:bzl_library.bzl", "bzl_library") +# Resolve rules_cc Starlark libraries to their correct targets +# gazelle:resolve starlark @rules_cc//cc/common:cc_info.bzl @rules_cc//cc/common:common +# gazelle:resolve starlark @rules_cc//cc/common:cc_common.bzl @rules_cc//cc/common:common + +# Resolve protobuf Starlark libraries +# gazelle:resolve starlark @com_google_protobuf//bazel:py_proto_library.bzl @com_google_protobuf//bazel:py_proto_library_bzl + +# Resolve bazel_tools repo rules to our internal wrapper to avoid transitive dependencies +# gazelle:resolve starlark @bazel_tools//tools/build_defs/repo:http.bzl //python/private:bazel_tools +# gazelle:resolve starlark @bazel_tools//tools/build_defs/repo:utils.bzl //python/private:bazel_tools + +# Prevent Gazelle from incorrectly stripping the .bzl suffix from the toml.bzl repo name +# gazelle:resolve starlark @toml.bzl//:toml.bzl @toml.bzl//:toml + +# Override Gazelle's incorrect default resolution for platforms host constraints Starlark library +# gazelle:resolve starlark @platforms//host:constraints.bzl @platforms//host:constraints_lib + +# Override Gazelle's incorrect default resolution for rules_cc Starlark libraries +# gazelle:resolve starlark @rules_cc//cc:cc_import.bzl @rules_cc//cc:core_rules +# gazelle:resolve starlark @rules_cc//cc:cc_library.bzl @rules_cc//cc:core_rules + +# Exclude directories that are separate packages/workspaces or only for testing/examples +# gazelle:exclude tests +# gazelle:exclude examples +# gazelle:exclude gazelle + +# Exclude internal development tools and dependencies that users don't need +# gazelle:exclude internal_dev_setup.bzl +# gazelle:exclude internal_dev_deps.bzl +# gazelle:exclude python/private/internal_dev_deps.bzl +# gazelle:exclude workspace_bazel9.bzl + +# Exclude legacy paths that are only kept for backwards compatibility +# gazelle:exclude python/private/common + package(default_visibility = ["//visibility:public"]) licenses(["notice"]) @@ -52,12 +87,6 @@ filegroup( ], ) -bzl_library( - name = "version_bzl", - srcs = ["version.bzl"], - visibility = ["//:__subpackages__"], -) - # Reexport of all bzl files used to allow downstream rules to generate docs # without shipping with a dependency on Skylib filegroup( @@ -73,3 +102,12 @@ filegroup( ], visibility = ["//visibility:public"], ) + +# keep +bzl_library( + name = "version", + srcs = ["version.bzl"], + visibility = ["//:__subpackages__"], +) + +# No-op change to verify PR metadata checks diff --git a/CHANGELOG.md b/CHANGELOG.md index e57e0969d3..ede22fc6db 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,47 +20,156 @@ A brief description of the categories of changes: * Particular sub-systems are identified using parentheses, e.g. `(bzlmod)` or `(docs)`. - -{#v0-0-0} -## Unreleased -[0.0.0]: https://github.com/bazel-contrib/rules_python/releases/tag/0.0.0 +{#v2-1-0} +## [2.1.0] - 2026-06-17 + +[2.1.0]: https://github.com/bazel-contrib/rules_python/releases/tag/2.1.0 -{#v0-0-0-removed} +{#v2-1-0-removed} ### Removed * (build_data) Removed CONFIG_MODE from build data ([#3793](https://github.com/bazel-contrib/rules_python/issues/3793)). * (coverage) Support for python 3.8 has been dropped from the bundled `coverage.py` wheel set, since coverage.py 7.6.2 dropped it. -{#v0-0-0-changed} +{#v2-1-0-changed} ### Changed +* (bzlmod) How default runtimes are registered has changed to use a manifest + of SHAs and URLs. `TOOL_VERSIONS` in `python/versions.bzl` is now empty under + bzlmod. * (gazelle) WORKSPACE's bazel-gazelle dependency bumped from 0.36.0 to 0.47.0. The go version was also bumped from 1.21.13 to 1.22.9. * (gazelle) `python_generate_pyi_deps` and `python_generate_pyi_srcs` now @@ -72,8 +181,13 @@ END_UNRELEASED_TEMPLATE platform, a warning is now printed instead of silently producing an empty coverage report. -{#v0-0-0-fixed} +{#v2-1-0-fixed} ### Fixed +* (gazelle) `py_library` and `py_test` targets with missing source files can now be + removed by Gazelle ([#3375](https://github.com/bazel-contrib/rules_python/issues/3375)). + However `map_kind` and `alias_kind` will not be removed unless people are running a + gazelle version that includes + [bazel-gazelle#2362](https://github.com/bazel-contrib/bazel-gazelle/pull/2362) * (bootstrap) Fixed a potential race condition with symlink creation during startup. * (gazelle) Fixed handling of auto-included `__init__.py` files when generating `py_binary` @@ -98,14 +212,24 @@ END_UNRELEASED_TEMPLATE * Fix the forwarding of `target_compatible_with` from `compile_pip_requirements` towards the underlying `*.update` target. ([#3787](https://github.com/bazel-contrib/rules_python/pull/3787)) -* (pypi) Assume that all of the packages are available on a particular hub if - there is only a single PyPI compatible index to be used. This saves us an expensive - PyPI download and supports PyPI mirror implementations that do not support the root - index functionality. Fixes - ([#3769](https://github.com/bazel-contrib/rules_python/pull/3769)). - -{#v0-0-0-added} +* (uv) allow user overwrite the build environment using `--action_env` to allow + setting authentication for the index URL. + ([#3405](https://github.com/bazel-contrib/rules_python/issues/3405)) +* (uv) fix the execution of the `uv pip compile` in the sandbox. Work + towards better supporting `uv` out of the box on our platforms. + ([#1975](https://github.com/bazel-contrib/rules_python/issues/1975)) +* (uv) automatically pass the `--project` parameter based on the source files. + ([#3087](https://github.com/bazel-contrib/rules_python/issues/3087)) +* (coverage) handle nested coverage collection + ([#3823](https://github.com/bazel-contrib/rules_python/pull/3823)) + +{#v2-1-0-added} ### Added +* (toolchain) Added {obj}`PyRuntimeInfo.interpreter_files_to_run` so action + consumers can execute an in-build runtime interpreter with its runfiles. +* (toolchains) Support dynamically fetching and registering Python runtimes + from a python-build-standalone manifest file using + `python.override(add_runtime_manifest_urls = ..., runtime_manifest_sha = ...)`. * (toolchain) Added {obj}`python.override.toolchain_target_settings` to allow adding `config_setting` labels to all registered toolchains. * (windows) Full venv support for Windows is available. Set @@ -120,6 +244,19 @@ END_UNRELEASED_TEMPLATE [#2054](https://github.com/bazel-contrib/rules_python/issues/2054). * (coverage) Add support for python 3.14 and bump `coverage.py` to 7.10.7. + +{#v2-0-3} +## [2.0.3] - 2026-06-15 + +[2.0.3]: https://github.com/bazel-contrib/rules_python/releases/tag/2.0.3 + +{#v2-0-3-fixed} +### Fixed +* (pypi) Assume that all of the packages are available on a particular hub if + there is only a single PyPI compatible index to be used. This saves us an expensive + PyPI download and supports PyPI mirror implementations that do not support the root + index functionality. Fixes [#3769](https://github.com/bazel-contrib/rules_python/pull/3769). + {#v2-0-2} ## [2.0.2] - 2026-05-14 @@ -660,7 +797,7 @@ Other changes: * (core) `#!/usr/bin/env bash` is now used as a shebang in the stage1 bootstrap template. * (gazelle:docs) The Gazelle docs have been migrated from {gh-path}`gazelle/README.md` to {gh-path}`gazelle/docs` and are now available on the primary documentation site - at https://rules-python.readthedocs.io/en/latest/gazelle/docs/index.html + at [20250808]: https://github.com/astral-sh/python-build-standalone/releases/tag/20250808 @@ -1072,7 +1209,7 @@ Other changes: {#v1-2-0-changed} ### Changed * (rules) `py_proto_library` is deprecated in favour of the - implementation in https://github.com/protocolbuffers/protobuf. It will be + implementation in . It will be removed in the future release. * (pypi) {obj}`pip.override` will now be ignored instead of raising an error, fixes [#2550](https://github.com/bazel-contrib/rules_python/issues/2550). @@ -2056,7 +2193,7 @@ Other changes: `common --@rules_python//python/config_settings:python_version=X.Y.Z`. * New Python versions available: `3.11.7`, `3.12.1` using - https://github.com/indygreg/python-build-standalone/releases/tag/20240107. + . * (toolchain) Allow setting `x.y` as the `python_version` parameter in the version-aware `py_binary` and `py_test` rules. This allows users to @@ -2208,7 +2345,7 @@ Other changes: * (docs) bzlmod extensions are now documented on rules-python.readthedocs.io * (docs) Support and backwards compatibility policies have been documented. - See https://rules-python.readthedocs.io/en/latest/support.html + See * (gazelle) `file` generation mode can now also add `__init__.py` to the srcs attribute for every target in the package. This is enabled through a separate directive `python_generation_mode_per_file_include_init`. @@ -2340,7 +2477,7 @@ Breaking changes: the `py_binary` rule used to build it. * New Python versions available: `3.8.17`, `3.11.5` using - https://github.com/indygreg/python-build-standalone/releases/tag/20230826. + . * (gazelle) New `# gazelle:python_generation_mode file` directive to support generating one `py_library` per file. @@ -2360,7 +2497,7 @@ Breaking changes: time being. * New Python versions available: `3.8.18`, `3.9.18`, `3.10.13`, `3.11.6`, `3.12.0` using - https://github.com/indygreg/python-build-standalone/releases/tag/20231002. + . `3.12.0` support is considered beta and may have issues. ### Removed @@ -2445,4 +2582,4 @@ Breaking changes: * (pip) Create all_data_requirements alias * Expose Python C headers through the toolchain. -[0.24.0]: https://github.com/bazel-contrib/rules_python/releases/tag/0.24.0 +[0.24.0]: https://github.com/bazel-contrib/rules_python/releases/tag/0.24.0 \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 917110978b..9efce5a148 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -171,11 +171,31 @@ validation function. ### Documenting changes -Changes are documented in two places: CHANGELOG.md and API docs. +Changes are documented in two places: news entries and API docs. -CHANGELOG.md contains a brief, human friendly, description. This text is -intended for easy skimming so that, when people upgrade, they can quickly get a -sense of what's relevant to them. +Instead of modifying `CHANGELOG.md` directly, you should create a news entry file in the `news/` directory. These files are automatically assembled into `CHANGELOG.md` at release time. + +#### Creating a news entry + +Create a `.md` file in the `news/` directory. The filename must follow the format `..md`: + +* ``: A unique identifier, typically the GitHub Pull Request number or Issue number (e.g., `1234`). +* ``: The category of the change, which must be one of: + * `added`: For new features or behavior added in a backwards-compatible manner. + * `changed`: For changes in existing behavior. + * `fixed`: For bug fixes. + * `removed`: For removed features or behavior. + +The content of the file should be a brief, human-friendly description of the change. Do not include a leading bullet point (e.g. `*` or `-`), as this is automatically added during assembly. If your change is specific to a subsystem, prefix it with the subsystem in parentheses, e.g., `(gazelle) Fixed handling of...`. + +Example: `news/1234.fixed.md` +```markdown +(gazelle) Fixed handling of auto-included `__init__.py` files when generating `py_binary` targets. +``` + +Do not edit `CHANGELOG.md` directly for unreleased changes. + +#### API documentation API documentation are the doc strings for functions, fields, attributes, etc. When user-visible or notable behavior is added, changed, or removed, the @@ -256,7 +276,7 @@ merged: * **requirements lock files**: These are usually generated by a `compile_pip_requirements` update target, which is usually in the same directory. - e.g. `bazel run //docs:requirements.update` + e.g. `bazel run //dev:requirements.update` ## Binary artifacts diff --git a/MODULE.bazel b/MODULE.bazel index ab1c41bc09..027277ab64 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -9,6 +9,7 @@ bazel_dep(name = "bazel_skylib", version = "1.8.2") bazel_dep(name = "package_metadata", version = "0.0.7") bazel_dep(name = "platforms", version = "0.0.11") bazel_dep(name = "rules_cc", version = "0.2.17") +bazel_dep(name = "toml.bzl", version = "0.4.1") # Those are loaded only when using py_proto_library # Use py_proto_library directly from protobuf repository @@ -95,6 +96,22 @@ bazel_dep(name = "another_module", version = "0", dev_dependency = True) # Extra gazelle plugin deps so that WORKSPACE.bzlmod can continue including it for e2e tests. # We use `WORKSPACE.bzlmod` because it is impossible to have dev-only local overrides. bazel_dep(name = "rules_go", version = "0.60.0", dev_dependency = True, repo_name = "io_bazel_rules_go") +bazel_dep(name = "gazelle", version = "0.40.0", dev_dependency = True, repo_name = "bazel_gazelle") +bazel_dep(name = "bazel_skylib_gazelle_plugin", version = "1.8.2", dev_dependency = True) +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, +) internal_dev_deps = use_extension( "//python/private:internal_dev_deps.bzl", @@ -106,6 +123,7 @@ use_repo( "buildkite_config", "implicit_namespace_ns_sub1", "implicit_namespace_ns_sub2", + "patch_whl_pkg", "pkgutil_nspkg1", "pkgutil_nspkg2", "rules_python_runtime_env_tc_info", @@ -176,12 +194,13 @@ dev_pip = use_extension( hub_name = "dev_pip", parallel_download = False, python_version = python_version, - requirements_lock = "//docs:requirements.txt", + requirements_lock = "//dev:requirements.txt", # Ensure that we are setting up the following platforms target_platforms = [ "{os}_{arch}", "{os}_{arch}_freethreaded", ], + uv_lock = "//dev:uv.lock", ) for python_version in [ "3.9", @@ -209,7 +228,7 @@ dev_pip.parse( python_version = "3.11", requirements_lock = "//tests/multi_pypi/beta:requirements.txt", ) -use_repo(dev_pip, "dev_pip", "pypi_alpha", "pypi_beta", "pypiserver") +use_repo(dev_pip, "dev_pip", "pypi", "pypi_alpha", "pypi_beta", "pypiserver") # Bazel integration test setup below @@ -321,25 +340,22 @@ uv_dev.configure( version = "0.11.2", ) -# Temporarily comment out these flag aliases because they break Bazel 9 -# when transitions are also used with a target. -# -# flag_alias( -# name = "build_python_zip", -# starlark_flag = "//python/config_settings:build_python_zip", -# ) - -# flag_alias( -# name = "incompatible_default_to_explicit_init_py", -# starlark_flag = "//python/config_settings:incompatible_default_to_explicit_init_py", -# ) - -# flag_alias( -# name = "python_path", -# starlark_flag = "//python/config_settings:python_path", -# ) - -# flag_alias( -# name = "experimental_python_import_all_repositories", -# starlark_flag = "//python/config_settings:experimental_python_import_all_repositories", -# ) +flag_alias( + name = "build_python_zip", + starlark_flag = "//python/config_settings:build_python_zip", +) + +flag_alias( + name = "incompatible_default_to_explicit_init_py", + starlark_flag = "//python/config_settings:incompatible_default_to_explicit_init_py", +) + +flag_alias( + name = "python_path", + starlark_flag = "//python/config_settings:python_path", +) + +flag_alias( + name = "experimental_python_import_all_repositories", + starlark_flag = "//python/config_settings:experimental_python_import_all_repositories", +) diff --git a/RELEASING.md b/RELEASING.md index e4cf738f3d..f16b13e582 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -8,44 +8,45 @@ existing Bazel workspace to sanity check functionality. ## Releasing from HEAD -These are the steps for a regularly scheduled release from HEAD. +Releases are managed using a semi-automated process centered around a GitHub +Release Tracking Issue and automated workflows triggered by comments or issue edits. + +> [!NOTE] +> Comment-based commands must be posted by project maintainers (Owner, +> Member, or Collaborator) and must be on their own line (leading and trailing +> whitespace is ignored). ### Steps -1. Update the changelog and replace the version placeholders by running the - release tool. The next version number will by automatically determined - based on the presence of `VERSION_NEXT_*` placeholders and git tags. - - ```shell - bazel run //tools/private/release - ``` - -1. Send these changes for review and get them merged. -1. Create a branch for the new release, named `release/X.Y` - ``` - git branch --no-track release/X.Y upstream/main && git push upstream release/X.Y - ``` - -The next step is to create tags to trigger release workflow, **however** -we start by using release candidate tags (`X.Y.Z-rcN`) before tagging the -final release (`X.Y.Z`). - -1. Create release candidate tag and push. The first RC uses `N=0`. Increment - `N` for each RC. - ``` - git tag X.Y.0-rcN upstream/release/X.Y && git push upstream tag X.Y.0-rcN - ``` -2. Announce the RC release: see [Announcing Releases] -3. Wait a week for feedback. - * Follow [Patch release with cherry picks] to pull bug fixes into the - release branch. - * Repeat the RC tagging step, incrementing `N`. -4. Finally, tag the final release tag: - ```shell - git tag X.Y.0 upstream/release/X.Y && git push upstream tag X.Y.0 - ``` - -Release automation will create a GitHub release and BCR pull request. +1. **Prepare the Release**: Run the [Release: Prepare](https://github.com/bazel-contrib/rules_python/actions/workflows/release_prepare.yaml) + workflow manually. You can trigger it from the GitHub Actions UI or using + the GitHub CLI: + ```shell + gh workflow run release_prepare.yaml --repo bazel-contrib/rules_python + ``` + This will automatically determine the next version, create a release tracking + issue, and send a preparation PR. + +2. **Approve and Merge**: Approve and merge the PR. Once merged, the + [Release: Prepare: Complete](https://github.com/bazel-contrib/rules_python/actions/workflows/release_complete_prepare.yaml) + workflow automatically updates the tracking issue checklist, and a release + branch will be created. If the automatic trigger is missed, comment + `/prepare-complete` on the tracking issue or on the PR, or run the workflow + manually with the PR number. + +3. **Add Backports (if needed)**: If there are backports, add them following + the [How to add backports](#how-to-add-backports) steps. + +4. **Create an RC**: Comment `/create-rc` on the tracking issue. This will + automatically process pending backports before creating the RC. If any + backport fails, the RC creation will abort. + +5. **Iterate**: Repeat steps 3 and 4 until backports and RCs are no longer + needed. + +6. **Finalize the Release**: Comment `/promote` on the tracking issue to + finalize the release. + ### Manually triggering the release workflow @@ -56,14 +57,24 @@ specific commit. To trigger the workflow, use the `gh workflow run` command: ```shell -gh workflow run release.yml --ref +gh workflow run release_publish.yaml --ref ``` By default, the workflow will publish the wheel to PyPI. To skip this step, you can set the `publish_to_pypi` input to `false`: ```shell -gh workflow run release.yml --ref -f publish_to_pypi=false +gh workflow run release_publish.yaml --ref -f publish_to_pypi=false +``` + +### Manually publishing to PyPI + +If PyPI publishing failed or was skipped during the main release, the PyPI +publishing workflow can be triggered manually using the GitHub CLI (`gh`) or +via the [GitHub Actions UI](https://github.com/bazel-contrib/rules_python/actions/workflows/publish_pypi.yaml): + +```shell +gh workflow run publish_pypi.yaml --ref -f tag_name= ``` ### Determining Semantic Version @@ -72,13 +83,123 @@ gh workflow run release.yml --ref -f publish_to_pypi=false API changes and new features bump the minor, and those with only bug fixes and other minor changes bump the patch digit. -The release tool will automatically determine the next version number. To find -if there were any features added or incompatible changes made, review -[CHANGELOG.md](CHANGELOG.md) and the commit history. This can be done using -github by going to the url: -`https://github.com/bazel-contrib/rules_python/compare/...main`. +The release tool will automatically determine the next version number based on +the `VERSION_NEXT_*` placeholders in the codebase. To see what changes are +being accumulated for the next release, review the pending news entries in the +`news/` directory. + +## How to add backports + +To add backports to an active release, you can use one of the following +methods: -## Patch release with cherry picks +### Method A: Comment on the PR + +Comment `/backport` on the PR you wish to backport. This will automatically +add the PR to the active release's backports checklist. Once the PR is merged, +the backports will be automatically processed. + +> [!NOTE] +> Commenting `/backport` on an open PR will block further release publishing +> (like creating RCs or promoting) until the PR is merged or manually set to +> status=ignore in the checklist. + +### Method B: Comment on the Tracking Issue + +Comment `/add-backports [ ...]` (space or comma separated) on +the tracking issue. The `` can be a PR number (optionally prefixed with +`#`) or a PR URL (strictly for the configured repository). This will +automatically add the PRs to the checklist and trigger processing. + +### Method C: Manual Checklist Update +1. Manually add checklist items under the `## Backports` section of the + Release Tracking Issue. The format must be: `- [ ] #` (e.g., + `- [ ] #1234`). +2. When ready, comment `/process-backports` on the tracking issue to trigger + processing. + +### Method D: Release Tool CLI +You can use the release tool to add backports from your local checkout: +```shell +bazel run //tools/private/release -- add-backports [ ...] +``` +The `` can be: +* A PR number (e.g., `124` or `#124`) +* A PR URL (e.g., `https://github.com/bazel-contrib/rules_python/pull/124` + or `https://github.com/bazel-contrib/rules_python/pull/124/files`) +* Only URLs for the configured repository are accepted. + +### Failure Behavior +If a backport fails to process (e.g., due to cherry-pick conflicts): +* The failed backport checklist item will remain unchecked with + `status=error-`. +* You must resolve the conflict manually: checkout the release branch, + cherry-pick the PR, resolve conflicts, push to remote, and manually check + the box on the tracking issue checklist with `status=done` metadata. + +## Automated patch releases to multiple versions + +If you need to backport a PR to multiple older active release branches (e.g., +backporting a fix to `1.7`, `1.8`, and `1.9` when the latest release is +`2.2.0`), you can automate the creation of release tracking issues and +verification of cherry-picks using a Backport Tracking Issue. + +### Steps + +1. **Create a Backport Tracking Issue**: Create a new issue on GitHub with + the label `type: backport-pr`. + * The title should describe the backport, e.g., `Backport: #1234` + (referencing the PR to backport). + * The body must contain the target range in the following format: + ```markdown + * PR: #1234 + * From version: 1.7 + * To version: 2.2 + ``` + This tells the tool to target all active release branches between + `release/1.7` and `release/2.2` inclusive. + +2. **Prepare the Backports**: Trigger the preparation by commenting `/prepare` + on the backport tracking issue, or by manually running the [Backport: + Prepare](https://github.com/bazel-contrib/rules_python/actions/workflows/backport_prepare.yaml) + workflow with the issue number. + * The workflow will checkout each release branch in the range, attempt to + cherry-pick the PR, and verify if the changelog can be updated. + * It will then update the backport tracking issue body with a list of + tasks: + * `Verify apply `: Automatically checked if the + cherry-pick succeeded, or left unchecked with + `status=failed-conflict` or `status=failed-changelog` if it failed. + * `Release issue `: A task to initiate the release for + each target version. + +3. **Resolve Conflicts (if any)**: If any `Verify apply` task failed: + * You must resolve the conflict manually on that specific release branch + (checkout branch, cherry-pick, resolve conflicts, push to remote). + * Once resolved, manually check the corresponding `Verify apply` task box + on the tracking issue and set its metadata to `status=success` (e.g., + `- [x] Verify apply 1.8 | status=success`). + +4. **Initiate Releases**: Once all `Verify apply` tasks are successful + (either automatically or after manual resolution), comment + `/create-releases` on the backport tracking issue, or manually run the + [Backport: Create + Releases](https://github.com/bazel-contrib/rules_python/actions/workflows/backport_create_releases.yaml) + workflow. + * This will automatically create a standard Release Tracking Issue for + each target version (e.g., `Release 1.7.1`, `Release 1.8.1`, etc.). + * For patch releases, the created release tracking issues will have `Tag + RC` tasks automatically removed, as release candidates are not + required for patch releases. + * The backport PR will be automatically added to the checklist of each + created release tracking issue. + +5. **Execute Releases**: Follow the standard release process for each created + release tracking issue. Since these are patch releases, you can skip the + `/create-rc` step and comment `/promote` directly to tag and publish the + release from the release branch head. + +## Manual patch release with cherry picks If a patch release from head would contain changes that aren't appropriate for a patch release, then the patch release needs to be based on the original @@ -96,8 +217,8 @@ The fix being included is commit `deadbeef`. If multiple commits need to be applied, repeat the `git cherry-pick` step for each. -Once the release branch is in the desired state, use `git tag` to tag it, as -done with a release from head. Release automation will do the rest. +Once the release branch is in the desired state, comment `/create-rc` on the +tracking issue to tag it, as done with a release from head. ### Announcing releases @@ -130,10 +251,23 @@ The two points of no return are: If release steps fail _prior_ to those steps, then its OK to change the tag. You may need to manually delete the GitHub release. +## Manual Editing of Tracking Issue + +You can manually edit the Release Tracking Issue to control the release flow. +The checklist items use metadata suffix: `| key=value key2=value2`. + +* **Retry Prepare Release**: Reset the task to `- [ ] Prepare Release | status=awaiting-preparation`. +* **Complete Prepare Release**: Comment `/prepare-complete` on the tracking + issue (or on the preparation PR) to mark the preparation task as done. +* **Create Release Branch**: Comment `/create-release-branch` on the tracking + issue to cut and push the release branch. +* **Force Task Done**: Check the box `- [x]` and add appropriate metadata (e.g. `status=done`). + ## Secrets ### PyPI user rules-python Part of the release process uploads packages to PyPI as the user `rules-python`. -This account is managed by Google; contact rules-python-pyi@google.com if -something needs to be done with the PyPI account. +This account is managed by `rickeylev`. Note that Google also has recovery access +and can be contacted at rules-python-pypi@google.com if something needs to be +done with the PyPI account. diff --git a/WORKSPACE b/WORKSPACE index 077ddb5e68..a692f1b289 100644 --- a/WORKSPACE +++ b/WORKSPACE @@ -17,13 +17,10 @@ workspace(name = "rules_python") # Everything below this line is used only for developing rules_python. Users # should not copy it to their WORKSPACE. -# Necessary so that Bazel 9 recognizes this as rules_python and doesn't try -# to load the version Bazel itself uses by default. -# buildifier: disable=duplicated-name -local_repository( - name = "rules_python", - path = ".", -) +# Workaround for Bazel 9 duplicate name issue in Gazelle. +load("//:workspace_bazel9.bzl", "bazel_9_workaround") + +bazel_9_workaround() load("//:internal_dev_deps.bzl", "rules_python_internal_deps") @@ -141,20 +138,20 @@ pip_parse( requirements_lock = "//examples/wheel:requirements_server.txt", ) -load("@pypiserver//:requirements.bzl", install_pypiserver = "install_deps") +load("@pypiserver//:requirements.bzl", install_pypiserver = "install_deps", pypiserver_requirements = "all_requirements") install_pypiserver() ##################### -# Install sphinx for doc generation. +# Install dev dependencies. pip_parse( name = "dev_pip", python_interpreter_target = interpreter, - requirements_lock = "//docs:requirements.txt", + requirements_lock = "//dev:requirements.txt", ) -load("@dev_pip//:requirements.bzl", docs_install_deps = "install_deps") +load("@dev_pip//:requirements.bzl", dev_pip_requirements = "all_requirements", docs_install_deps = "install_deps") docs_install_deps() @@ -167,7 +164,7 @@ pip_parse( requirements_lock = "//tests/multi_pypi/alpha:requirements.txt", ) -load("@pypi_alpha//:requirements.bzl", pypi_alpha_install_deps = "install_deps") +load("@pypi_alpha//:requirements.bzl", pypi_alpha_install_deps = "install_deps", pypi_alpha_requirements = "all_requirements") pypi_alpha_install_deps() @@ -177,6 +174,22 @@ pip_parse( requirements_lock = "//tests/multi_pypi/beta:requirements.txt", ) -load("@pypi_beta//:requirements.bzl", pypi_beta_install_deps = "install_deps") +load("@pypi_beta//:requirements.bzl", pypi_beta_install_deps = "install_deps", pypi_beta_requirements = "all_requirements") pypi_beta_install_deps() + +load( + "//python/private/pypi:unified_hub_repo.bzl", + "unified_workspace_hub_repo", +) # buildifier: disable=bzl-visibility + +unified_workspace_hub_repo( + name = "pypi", + default_hub = "dev_pip", + hubs = { + "dev_pip": dev_pip_requirements, + "pypi_alpha": pypi_alpha_requirements, + "pypi_beta": pypi_beta_requirements, + "pypiserver": pypiserver_requirements, + }, +) diff --git a/command_line_option/BUILD.bazel b/command_line_option/BUILD.bazel index a4d5f0f80d..15c22f360b 100644 --- a/command_line_option/BUILD.bazel +++ b/command_line_option/BUILD.bazel @@ -18,6 +18,11 @@ alias( actual = "//python:none", ) +alias( + name = "extra_toolchains", + actual = "//python:none", +) + filegroup( name = "distribution", srcs = glob(["**"]), diff --git a/dev/BUILD.bazel b/dev/BUILD.bazel new file mode 100644 index 0000000000..22fe5bb918 --- /dev/null +++ b/dev/BUILD.bazel @@ -0,0 +1,29 @@ +load("//python/uv:lock.bzl", "lock") # buildifier: disable=bzl-visibility + +licenses(["notice"]) + +# Run bazel run //dev:requirements.update +lock( + name = "requirements", + srcs = ["pyproject.toml"], + out = "requirements.txt", + args = [ + "--emit-index-url", + "--universal", + "--upgrade", + ], + # NOTE @aignas 2025-08-17: here we select the lowest actively supported + # version so that the requirements file is generated to be compatible with + # Python version 3.10 or greater. + python_version = "3.10", + visibility = ["//:__subpackages__"], +) + +# Run bazel run //dev:uv_lock.update +lock( + name = "uv_lock", + srcs = ["pyproject.toml"], + out = "uv.lock", + python_version = "3.10", + visibility = ["//:__subpackages__"], +) diff --git a/docs/pyproject.toml b/dev/pyproject.toml similarity index 86% rename from docs/pyproject.toml rename to dev/pyproject.toml index f0ca3928ff..cdc00b902c 100644 --- a/docs/pyproject.toml +++ b/dev/pyproject.toml @@ -1,5 +1,5 @@ [project] -name = "rules_python_docs" +name = "rules_python_dev" version = "0.0.0" dependencies = [ @@ -17,4 +17,7 @@ dependencies = [ "pyelftools", "macholib", "markupsafe", + "pytest", + "pytest-bazel", + "pytest-mock", ] diff --git a/dev/requirements.txt b/dev/requirements.txt new file mode 100644 index 0000000000..6efb1b8bdb --- /dev/null +++ b/dev/requirements.txt @@ -0,0 +1,558 @@ +# This file was autogenerated by uv via the following command: +# bazel run //dev:requirements.update +--index-url https://pypi.org/simple + +absl-py==2.5.0 \ + --hash=sha256:0c996f25c0490700fadabe6351630f6111534fa0ae252cc6d2014ea3b141135f \ + --hash=sha256:0f17b89f2a4eaaedc4f28c622998aa690564b3012a396a4ffad0821007fe03ba + # via rules-python-dev (dev/pyproject.toml) +alabaster==1.0.0 \ + --hash=sha256:c00dca57bca26fa62a6d7d0a9fcce65f3e026e9bfe33e9c538fd3fbb2144fd9e \ + --hash=sha256:fc6786402dc3fcb2de3cabd5fe455a2db534b371124f1f21de8731783dec828b + # via sphinx +altgraph==0.17.5 \ + --hash=sha256:c87b395dd12fabde9c99573a9749d67da8d29ef9de0125c7f536699b4a9bc9e7 \ + --hash=sha256:f3a22400bce1b0c701683820ac4f3b159cd301acab067c51c653e06961600597 + # via macholib +astroid==3.3.11 \ + --hash=sha256:1e5a5011af2920c7c67a53f65d536d65bfa7116feeaf2354d8b94f29573bb0ce \ + --hash=sha256:54c760ae8322ece1abd213057c4b5bba7c49818853fc901ef09719a60dbf9dec + # via sphinx-autodoc2 +babel==2.18.0 \ + --hash=sha256:b80b99a14bd085fcacfa15c9165f651fbb3406e66cc603abf11c5750937c992d \ + --hash=sha256:e2b422b277c2b9a9630c1d7903c2a00d0830c409c59ac8cae9081c92f1aeba35 + # via sphinx +certifi==2026.7.22 \ + --hash=sha256:62f22742b58a1a33014a2b6b706588a8d7e2a88ae7bd1a6ebe8c992928483775 \ + --hash=sha256:741e2c3b351ddf169a738da9f2c048608ff7f2c5cc02f1ebc6b118bb090d5d55 + # via requests +charset-normalizer==3.4.9 \ + --hash=sha256:0327fcd59a935777d83410750c50600ee9571af2846f71ce40f25b13da1ef380 \ + --hash=sha256:03d07803992c6c7bbc976327f34b18b6160327fc81cb82c9d504720ac0be3b62 \ + --hash=sha256:04ce310cb89c15df659582aee80a0603788732a5e017d5bd5c81158106ce249c \ + --hash=sha256:0d861473f743244d349b50f850d10eb87aeb22bbdcc8e64f79273c94af5a8226 \ + --hash=sha256:0e94703ec9684807f20cfb5eed95c70f67f2a8f21ad620146d7b5a13677b93e5 \ + --hash=sha256:0fa1aec2d32bcc03c8fa0f6f1712caad1adc38509f31142112e5c9daf5b9c833 \ + --hash=sha256:16b65ea0f2465b6fb52aa22de5eca612aa964ddfec00a912e26f4656cbef890b \ + --hash=sha256:16d10d789dd9bcca1173c95af82c58433122564b7bc39385124be735a35cbe99 \ + --hash=sha256:19ac87f93086ce37b86e098888555c4b4bc48102279bae3350098c0ed664b501 \ + --hash=sha256:1d22856ffbe153a602df38e4a5464f0b748a54002e0d69ac6d2ad0a197cc99ec \ + --hash=sha256:21e764fd1e70b6a3e205a0e46f3051701f98a8cb3fad66eeb80e48bb502f8698 \ + --hash=sha256:231ddcbb35e2ff8973e1365db41fe0572662893b99a05deb183b68ad4c0c8bd4 \ + --hash=sha256:253a4a220747e8b5faf57ec320c4f5efb0cef05f647420bf267143ec15dba10a \ + --hash=sha256:280081916dc341820640489a66e4696049401ef1cf6dd672f672e70ad915aca3 \ + --hash=sha256:2a441ea71902098ffe78c5abe6c494f44160b4af614ed16c3d9a3b1d17fd8ee2 \ + --hash=sha256:304b13570067b2547562e308af560b3963857b1fa90bd6afd978130130fe2d6a \ + --hash=sha256:32286a2c8d167e897177b673176c1e3e00d4057caf5d2b64eef9a3666b03018e \ + --hash=sha256:33bdcc2a32c0a0e861f60841a512c8acc658c87c2ac59d89e3a46dacf7d866e4 \ + --hash=sha256:375b83ed0aecfce76c16d198fbc21f3b11b337d68662bea0a995046682a11419 \ + --hash=sha256:3c09a49d6cde137258beb3d551994a2927fd35ad5cf96aed573f61bbd67c5f84 \ + --hash=sha256:3d92613ec25e43b05f042302531ec0f00b8445190e43325880cbd6ab7c2581da \ + --hash=sha256:40a126142a56b2dfc0aacbad1de8310cbf60da7656db0e6b16eebd48e3e93519 \ + --hash=sha256:416c229f77e5ea25b3dfd4b582f8d73d7e43c22320302b9ab128a2d3a0b38efe \ + --hash=sha256:432786d3561e69aeeae6c7e8648964ce0ad05736120135601f87ac26b9c83381 \ + --hash=sha256:43b9e366a31fdd1c87d0eb08f579b4a82b723ea54338f040d6b4e518a026ea29 \ + --hash=sha256:440eede837960000d74978f0eba527be106b5b9aee0daf779d395276ed0b0614 \ + --hash=sha256:45b0cc4e3556cd875e09102988d1ab8356c998b596c9fced84547c8138b487a0 \ + --hash=sha256:476743fe6dfe14a2da12e3ac79125dc84a3b2cf8094369a47a1529b0cd8549fe \ + --hash=sha256:4773092f8019072343a7447203308b176e10199920eb02d6195e81bbb3274c29 \ + --hash=sha256:4b3dac63058cc36820b0dd072f89898604e2d39686fe05321729d00d8ac185a0 \ + --hash=sha256:4d1c96a7a18b9690a4d46df09e3e3382406ae3213727cd1019ebade1c4a81917 \ + --hash=sha256:51307f5c71007673a2bf8232ad973483d281e74cb99c8c5a990af1eefa6277d9 \ + --hash=sha256:51447e9aa2684679af07ca5021c3db526e0284347ebf4ffcec1154c3350cfe32 \ + --hash=sha256:58150c9f9b9a552505912d182ccdf26f6396fb6094816ceebcbb20eecabaed94 \ + --hash=sha256:5b10cd92fc5c498b35a8635df6d5a100207f88b63a4dc1de7ef9a548e1e2cd63 \ + --hash=sha256:5e226f6218febc71f6c1fc2fafb91c226f75bdc1d8fb12d66823716e891608fd \ + --hash=sha256:609b3ba8fcc0fb5ab7af00719d0fb6ad0cb518e48e7712d12fd68f1327951198 \ + --hash=sha256:60f44ade2cf573dad7a277e6f8ca9a51a21dda572b13bd7d8539bb3cd5dbedde \ + --hash=sha256:611057cc5d5c0afc743ba8be6bd828c17e0aaa8643f9d0a9b9bb7dea80eb8012 \ + --hash=sha256:6366a16e1a25018694d6a5d784d09b046edc9eac40ea2b54065c3052672516a1 \ + --hash=sha256:65a7ff3f705e57d392f7261b6d0550fe137c3019477431f1c355e0db0a7d3e15 \ + --hash=sha256:673611bbd43f0810bec0b0f028ddeaaa501190339cac411f347ac76917c3ae7b \ + --hash=sha256:67830fc78e67501f47bb950471b2dcb9b35b140084429318e862895a8e89c993 \ + --hash=sha256:68ce9f4d6b26d5ccbf7fd4459bf75f74a0a146677ebba80597df60cbdb20e6f4 \ + --hash=sha256:68e5f26a1ad57ded6d1cfb85331d1c1a195314756471d97758c48498bb4dcdf5 \ + --hash=sha256:69b157c5d3292bcd443faca052f3096f637f1e074b98212a933c074ae23dc3b8 \ + --hash=sha256:75286256590a6320cf106a0d28970d3560aad9ee09aa7b34fb40524792436d35 \ + --hash=sha256:78841cccf1af7b40f6f716338d50c0902dbe88d9f800b3c973b7a9a0a693a642 \ + --hash=sha256:78fa18e436a1a0e58dbd7e02fc4473f3f32cceb12df9dfca542d075961c307d2 \ + --hash=sha256:79580094b00d1789d1f93ea55bc43cb2f611910c72235b7657f3482ddcc1b22d \ + --hash=sha256:7b86a2b16095d250c6f58b3d9b2eee6f4147754344f3dab0922f7c9bf7d226c9 \ + --hash=sha256:83aed2c10721ddd90f68140685391b50811a880af20654c59af6b6c66c40513c \ + --hash=sha256:84fd18bcc17526fc2b3c1af7d2b9217d32c9c04448c16ec693b9b4f1985c3d33 \ + --hash=sha256:871ff67ea1aad4dfd91736464934d56b32dac49f9fbe16cddba36198a7b3a0db \ + --hash=sha256:898f0e9068ca27d37f8e83a5b962821df851532e6c4a7d615c1c033f9da6eedf \ + --hash=sha256:8a79d9f4d8001473a30c163556b3c3bfebec837495a412dde78b51672f6134f9 \ + --hash=sha256:8c041122946b7ba21bb32c45b1aa57b1be35527690aeb3c5c234521085632eee \ + --hash=sha256:90c44bc373b7687f6948b693cceaea1348ae0975d7474746559494468e3c1d84 \ + --hash=sha256:9104ed0bd76a429d46f9ec0dbc9b08ad1d2dcdf2b00a5a0daa1c145329b35b44 \ + --hash=sha256:920079c3f7456fa213e0829ed2073aaa727fd39d889ead5b4f35d0de5460d04f \ + --hash=sha256:93d59d504b230e83c7a843251681959a0b6a9cd76f6e146ce1b8a80eb8739af9 \ + --hash=sha256:9b2aff1c7b3884512b9512c3eaadd9bab39fb45042ffaaa1dd08ff2b9f8109d9 \ + --hash=sha256:9b8e0f3107e2200b76f6054de99016eac3ee6762713587b36baaa7e4bd2ae177 \ + --hash=sha256:9bb41182d93ea91f60b4bc8fbf4c820c69ef8a12ab2d917f3f1834f1acad07e8 \ + --hash=sha256:9cdef90ae47919cae358d8ab15797a800ed41da7aba5d72419fb510729e2ed4b \ + --hash=sha256:a1786910334ed46ab1dd73222f2cd1e05c2c3bb39f6dddb4f8b36fc382058a39 \ + --hash=sha256:a4cfde78a9f2880208d16a93b795726a3017d5977e08d1e162a7a31322479c41 \ + --hash=sha256:a4fbdde9dd4a9ce5fd52c2b3a347bb50cc89483ef783f1cb00d408c13f7a96c0 \ + --hash=sha256:aa99adc8f081b475a12843953db36831eaf83ec33eb46a90629ca6a5de45a616 \ + --hash=sha256:ac351b3b8014eead140e77e9717e2992c6bbe30b63bc3422422eb84865412e3d \ + --hash=sha256:ad41ba96094304aa090f5a30cb6e4fb3b3f1c264c523394b4c39bbacc4dc92ba \ + --hash=sha256:b5314963fce9b0b12743891de876e724997864ee22aa496f903f426c7e2fa5b2 \ + --hash=sha256:bcf74c1df76758a395bf0af608c04c82257523f55c9868b334f06270d0f2112b \ + --hash=sha256:bd47ba7fc3ca94896759ea0109775132d3e7ab921fbf54038e1bab2e46c313c9 \ + --hash=sha256:c0323c9daef75ef2e5083624b4585018a0c9d5e3b40f607eed81a311270b934b \ + --hash=sha256:c1225416b463483160e4af85d5fc3a9690ccb53fd4b1865a6437825f5ede3209 \ + --hash=sha256:c1c948747b03be832dceed96ca815cef7360de9aa19d37c730f8e3f6101aca48 \ + --hash=sha256:c25fe15c70c59eb7c5ce8c06a1f3fa1da0ecc5ea1e7a5922c40fd2fa9b0d5046 \ + --hash=sha256:cc1b0fff8ead343dae06305f954eb8468ba0ec1a97881f42489d198e4ce3c632 \ + --hash=sha256:cd6280cf040f233bd7d3407b743b4b4c74f70e8e1c4199cb112a62c941c0772a \ + --hash=sha256:cd6c3d4b783c556fa00bf540854e42f135e2f256abd29669fcd0da0f2dec79c2 \ + --hash=sha256:d4d6fcde76f94f5cb9e43e9e9a61f16dacefd228cbbf6f1a09bd9b219a92f1a1 \ + --hash=sha256:ddf4af30b417d9fe16481e9b81c27ab2a7cde1ff7ba3e85653b02db7d145dc7b \ + --hash=sha256:df115d4d83168fdf2cae48ef1ff6d1cb4c466364e30861b37121de0f3bf1b990 \ + --hash=sha256:df7276909358e5635ae203673ab7e509ddd224225a8d6b0790bf13eb2bde1cc5 \ + --hash=sha256:e4fd89cc178bced6ad29cb3e6dd4aa63fa5017c3524dbd0b25998fb64a87cc8b \ + --hash=sha256:e9701d0049d92c16703a42771b98d560b95248949f23f8cf7b4eddd201814fb9 \ + --hash=sha256:ee2f2a527e3c1a6e6411eb4209642e138b544a2d72fe5d0d76daf77b24063534 \ + --hash=sha256:f7fb7d750cfa0a070d2c24e831fd3481019a60dd317ea2b39acbcebc08b6ed81 \ + --hash=sha256:f840ed6d8ecba8255df8c42b87fadeda98ddfc6eeec05e2dc66e26d46dd6f58a \ + --hash=sha256:f86c6358749bd4fda175388691e3ba8c46e24c5347d0afd20f9b7edfc9faf07d \ + --hash=sha256:fa36ec09ef71d158186bc79e359ff5fdd6e7996fe8ab638f00d6b93139ba4fcf \ + --hash=sha256:fe2c7201c642b7c308f1675355ad7ff7b66acfe3541625efe5a3ad38f29d6115 + # via requests +colorama==0.4.6 ; sys_platform == 'win32' \ + --hash=sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44 \ + --hash=sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6 + # via + # pytest + # sphinx +docutils==0.21.2 ; python_full_version < '3.11' \ + --hash=sha256:3a6b18732edf182daa3cd12775bbb338cf5691468f91eeeb109deff6ebfa986f \ + --hash=sha256:dafca5b9e384f0e419294eb4d2ff9fa826435bf15f15b7bd45723e8ad76811b2 + # via + # myst-parser + # sphinx + # sphinx-rtd-theme +docutils==0.22.4 ; python_full_version >= '3.11' \ + --hash=sha256:4db53b1fde9abecbb74d91230d32ab626d94f6badfc575d6db9194a49df29968 \ + --hash=sha256:d0013f540772d1420576855455d050a2180186c91c15779301ac2ccb3eeb68de + # via + # myst-parser + # sphinx + # sphinx-rtd-theme +exceptiongroup==1.3.1 ; python_full_version < '3.11' \ + --hash=sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219 \ + --hash=sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598 + # via pytest +idna==3.18 \ + --hash=sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2 \ + --hash=sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848 + # via requests +imagesize==2.0.0 \ + --hash=sha256:5667c5bbb57ab3f1fa4bc366f4fbc971db3d5ed011fd2715fd8001f782718d96 \ + --hash=sha256:8e8358c4a05c304f1fccf7ff96f036e7243a189e9e42e90851993c558cfe9ee3 + # via sphinx +iniconfig==2.3.0 \ + --hash=sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730 \ + --hash=sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12 + # via pytest +jinja2==3.1.6 \ + --hash=sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d \ + --hash=sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67 + # via + # myst-parser + # readthedocs-sphinx-ext + # sphinx +macholib==1.16.4 \ + --hash=sha256:da1a3fa8266e30f0ce7e97c6a54eefaae8edd1e5f86f3eb8b95457cae90265ea \ + --hash=sha256:f408c93ab2e995cd2c46e34fe328b130404be143469e41bc366c807448979362 + # via rules-python-dev (dev/pyproject.toml) +markdown-it-py==3.0.0 ; python_full_version < '3.11' \ + --hash=sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1 \ + --hash=sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb + # via + # mdit-py-plugins + # myst-parser +markdown-it-py==4.2.0 ; python_full_version >= '3.11' \ + --hash=sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49 \ + --hash=sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a + # via + # mdit-py-plugins + # myst-parser +markupsafe==3.0.3 \ + --hash=sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f \ + --hash=sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a \ + --hash=sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf \ + --hash=sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19 \ + --hash=sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf \ + --hash=sha256:0f4b68347f8c5eab4a13419215bdfd7f8c9b19f2b25520968adfad23eb0ce60c \ + --hash=sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175 \ + --hash=sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219 \ + --hash=sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb \ + --hash=sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6 \ + --hash=sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab \ + --hash=sha256:15d939a21d546304880945ca1ecb8a039db6b4dc49b2c5a400387cdae6a62e26 \ + --hash=sha256:177b5253b2834fe3678cb4a5f0059808258584c559193998be2601324fdeafb1 \ + --hash=sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce \ + --hash=sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218 \ + --hash=sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634 \ + --hash=sha256:1ba88449deb3de88bd40044603fafffb7bc2b055d626a330323a9ed736661695 \ + --hash=sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad \ + --hash=sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73 \ + --hash=sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c \ + --hash=sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe \ + --hash=sha256:2a15a08b17dd94c53a1da0438822d70ebcd13f8c3a95abe3a9ef9f11a94830aa \ + --hash=sha256:2f981d352f04553a7171b8e44369f2af4055f888dfb147d55e42d29e29e74559 \ + --hash=sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa \ + --hash=sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37 \ + --hash=sha256:3537e01efc9d4dccdf77221fb1cb3b8e1a38d5428920e0657ce299b20324d758 \ + --hash=sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f \ + --hash=sha256:38664109c14ffc9e7437e86b4dceb442b0096dfe3541d7864d9cbe1da4cf36c8 \ + --hash=sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d \ + --hash=sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c \ + --hash=sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97 \ + --hash=sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a \ + --hash=sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19 \ + --hash=sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9 \ + --hash=sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9 \ + --hash=sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc \ + --hash=sha256:591ae9f2a647529ca990bc681daebdd52c8791ff06c2bfa05b65163e28102ef2 \ + --hash=sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4 \ + --hash=sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354 \ + --hash=sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50 \ + --hash=sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698 \ + --hash=sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9 \ + --hash=sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b \ + --hash=sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc \ + --hash=sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115 \ + --hash=sha256:7c3fb7d25180895632e5d3148dbdc29ea38ccb7fd210aa27acbd1201a1902c6e \ + --hash=sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485 \ + --hash=sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f \ + --hash=sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12 \ + --hash=sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025 \ + --hash=sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009 \ + --hash=sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d \ + --hash=sha256:949b8d66bc381ee8b007cd945914c721d9aba8e27f71959d750a46f7c282b20b \ + --hash=sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a \ + --hash=sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5 \ + --hash=sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f \ + --hash=sha256:a320721ab5a1aba0a233739394eb907f8c8da5c98c9181d1161e77a0c8e36f2d \ + --hash=sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1 \ + --hash=sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287 \ + --hash=sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6 \ + --hash=sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f \ + --hash=sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581 \ + --hash=sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed \ + --hash=sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b \ + --hash=sha256:c0c0b3ade1c0b13b936d7970b1d37a57acde9199dc2aecc4c336773e1d86049c \ + --hash=sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026 \ + --hash=sha256:c4ffb7ebf07cfe8931028e3e4c85f0357459a3f9f9490886198848f4fa002ec8 \ + --hash=sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676 \ + --hash=sha256:d2ee202e79d8ed691ceebae8e0486bd9a2cd4794cec4824e1c99b6f5009502f6 \ + --hash=sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e \ + --hash=sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d \ + --hash=sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d \ + --hash=sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01 \ + --hash=sha256:df2449253ef108a379b8b5d6b43f4b1a8e81a061d6537becd5582fba5f9196d7 \ + --hash=sha256:e1c1493fb6e50ab01d20a22826e57520f1284df32f2d8601fdd90b6304601419 \ + --hash=sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795 \ + --hash=sha256:e2103a929dfa2fcaf9bb4e7c091983a49c9ac3b19c9061b6d5427dd7d14d81a1 \ + --hash=sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5 \ + --hash=sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d \ + --hash=sha256:e8fc20152abba6b83724d7ff268c249fa196d8259ff481f3b1476383f8f24e42 \ + --hash=sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe \ + --hash=sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda \ + --hash=sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e \ + --hash=sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737 \ + --hash=sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523 \ + --hash=sha256:f42d0984e947b8adf7dd6dde396e720934d12c506ce84eea8476409563607591 \ + --hash=sha256:f71a396b3bf33ecaa1626c255855702aca4d3d9fea5e051b41ac59a9c1c41edc \ + --hash=sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a \ + --hash=sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50 + # via + # rules-python-dev (dev/pyproject.toml) + # jinja2 +mdit-py-plugins==0.6.1 \ + --hash=sha256:214c82fb2ac524472ab6a5bcab1de80f73b50443e187f401bfd77efbc7c6481d \ + --hash=sha256:a2bca0f039f39dbd35fb74ae1b5f998608c437463371f0ff7f49a19a17a114d0 + # via myst-parser +mdurl==0.1.2 \ + --hash=sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8 \ + --hash=sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba + # via markdown-it-py +myst-parser==4.0.1 ; python_full_version < '3.11' \ + --hash=sha256:5cfea715e4f3574138aecbf7d54132296bfd72bb614d31168f48c477a830a7c4 \ + --hash=sha256:9134e88959ec3b5780aedf8a99680ea242869d012e8821db3126d427edc9c95d + # via rules-python-dev (dev/pyproject.toml) +myst-parser==5.1.0 ; python_full_version >= '3.11' \ + --hash=sha256:9c91c52b3cdb4d94a6506e4fab4e2f296c7623a0da0dcbe6de1565c3dad67a8a \ + --hash=sha256:ab69322dc6719dcc7f296479dbb70181b66df6ed315064f92dbc85c0e1bf2f02 + # via rules-python-dev (dev/pyproject.toml) +packaging==26.3 \ + --hash=sha256:94edc256424af38762eb31306eed28beb9f0efc50a8837492c9d6fd6004aed79 \ + --hash=sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c + # via + # pytest + # readthedocs-sphinx-ext + # sphinx +pefile==2024.8.26 \ + --hash=sha256:3ff6c5d8b43e8c37bb6e6dd5085658d658a7a0bdcd20b6a07b1fcfc1c4e9d632 \ + --hash=sha256:76f8b485dcd3b1bb8166f1128d395fa3d87af26360c2358fb75b80019b957c6f + # via rules-python-dev (dev/pyproject.toml) +pluggy==1.6.0 \ + --hash=sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3 \ + --hash=sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746 + # via pytest +pyelftools==0.33 \ + --hash=sha256:660d82dcbeb8e83d1702bd97f223f761625da06111c0cc988eac6b8ab0c1b61f \ + --hash=sha256:f215ad5f47d3f1373a21496a6c9e0707c622840d0622f23ff7ce08678b020036 + # via rules-python-dev (dev/pyproject.toml) +pygments==2.20.0 \ + --hash=sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f \ + --hash=sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176 + # via + # pytest + # sphinx +pytest==9.1.1 \ + --hash=sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313 \ + --hash=sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c + # via + # rules-python-dev (dev/pyproject.toml) + # pytest-bazel + # pytest-mock +pytest-bazel==0.1.6 \ + --hash=sha256:a29e80e1d67c3db801bdd4d0b6b742f2bfb48cd6841caa33401458e5c4e29c21 + # via rules-python-dev (dev/pyproject.toml) +pytest-mock==3.15.1 \ + --hash=sha256:0a25e2eb88fe5168d535041d09a4529a188176ae608a6d249ee65abc0949630d \ + --hash=sha256:1849a238f6f396da19762269de72cb1814ab44416fa73a8686deac10b0d87a0f + # via rules-python-dev (dev/pyproject.toml) +pyyaml==6.0.3 \ + --hash=sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c \ + --hash=sha256:0150219816b6a1fa26fb4699fb7daa9caf09eb1999f3b70fb6e786805e80375a \ + --hash=sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3 \ + --hash=sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956 \ + --hash=sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6 \ + --hash=sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c \ + --hash=sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65 \ + --hash=sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a \ + --hash=sha256:1ebe39cb5fc479422b83de611d14e2c0d3bb2a18bbcb01f229ab3cfbd8fee7a0 \ + --hash=sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b \ + --hash=sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1 \ + --hash=sha256:22ba7cfcad58ef3ecddc7ed1db3409af68d023b7f940da23c6c2a1890976eda6 \ + --hash=sha256:27c0abcb4a5dac13684a37f76e701e054692a9b2d3064b70f5e4eb54810553d7 \ + --hash=sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e \ + --hash=sha256:2e71d11abed7344e42a8849600193d15b6def118602c4c176f748e4583246007 \ + --hash=sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310 \ + --hash=sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4 \ + --hash=sha256:3c5677e12444c15717b902a5798264fa7909e41153cdf9ef7ad571b704a63dd9 \ + --hash=sha256:3ff07ec89bae51176c0549bc4c63aa6202991da2d9a6129d7aef7f1407d3f295 \ + --hash=sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea \ + --hash=sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0 \ + --hash=sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e \ + --hash=sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac \ + --hash=sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9 \ + --hash=sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7 \ + --hash=sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35 \ + --hash=sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb \ + --hash=sha256:5cf4e27da7e3fbed4d6c3d8e797387aaad68102272f8f9752883bc32d61cb87b \ + --hash=sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69 \ + --hash=sha256:5ed875a24292240029e4483f9d4a4b8a1ae08843b9c54f43fcc11e404532a8a5 \ + --hash=sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b \ + --hash=sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c \ + --hash=sha256:6344df0d5755a2c9a276d4473ae6b90647e216ab4757f8426893b5dd2ac3f369 \ + --hash=sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd \ + --hash=sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824 \ + --hash=sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198 \ + --hash=sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065 \ + --hash=sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c \ + --hash=sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c \ + --hash=sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764 \ + --hash=sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196 \ + --hash=sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b \ + --hash=sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00 \ + --hash=sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac \ + --hash=sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8 \ + --hash=sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e \ + --hash=sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28 \ + --hash=sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3 \ + --hash=sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5 \ + --hash=sha256:9c57bb8c96f6d1808c030b1687b9b5fb476abaa47f0db9c0101f5e9f394e97f4 \ + --hash=sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b \ + --hash=sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf \ + --hash=sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5 \ + --hash=sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702 \ + --hash=sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8 \ + --hash=sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788 \ + --hash=sha256:b865addae83924361678b652338317d1bd7e79b1f4596f96b96c77a5a34b34da \ + --hash=sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d \ + --hash=sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc \ + --hash=sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c \ + --hash=sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba \ + --hash=sha256:c2514fceb77bc5e7a2f7adfaa1feb2fb311607c9cb518dbc378688ec73d8292f \ + --hash=sha256:c3355370a2c156cffb25e876646f149d5d68f5e0a3ce86a5084dd0b64a994917 \ + --hash=sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5 \ + --hash=sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26 \ + --hash=sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f \ + --hash=sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b \ + --hash=sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be \ + --hash=sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c \ + --hash=sha256:efd7b85f94a6f21e4932043973a7ba2613b059c4a000551892ac9f1d11f5baf3 \ + --hash=sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6 \ + --hash=sha256:fa160448684b4e94d80416c0fa4aac48967a969efe22931448d853ada8baf926 \ + --hash=sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0 + # via myst-parser +readthedocs-sphinx-ext==2.2.5 \ + --hash=sha256:ee5fd5b99db9f0c180b2396cbce528aa36671951b9526bb0272dbfce5517bd27 \ + --hash=sha256:f8c56184ea011c972dd45a90122568587cc85b0127bc9cf064d17c68bc809daa + # via rules-python-dev (dev/pyproject.toml) +requests==2.34.2 \ + --hash=sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0 \ + --hash=sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed + # via + # readthedocs-sphinx-ext + # sphinx +roman-numerals==4.1.0 ; python_full_version >= '3.11' \ + --hash=sha256:1af8b147eb1405d5839e78aeb93131690495fe9da5c91856cb33ad55a7f1e5b2 \ + --hash=sha256:647ba99caddc2cc1e55a51e4360689115551bf4476d90e8162cf8c345fe233c7 + # via sphinx +snowballstemmer==3.1.1 \ + --hash=sha256:7e207fa178741da09cdee59d3ecec3827ad5f92b1fc5c9ff3755b639f71f5752 \ + --hash=sha256:e07bbc54a0d798fe6010a12398422e62a8bfbba95c394fd0956ef58cb4d3e260 + # via sphinx +sphinx==8.1.3 ; python_full_version < '3.11' \ + --hash=sha256:09719015511837b76bf6e03e42eb7595ac8c2e41eeb9c29c5b755c6b677992a2 \ + --hash=sha256:43c1911eecb0d3e161ad78611bc905d1ad0e523e4ddc202a58a821773dc4c927 + # via + # rules-python-dev (dev/pyproject.toml) + # myst-parser + # sphinx-reredirects + # sphinx-rtd-theme + # sphinxcontrib-jquery +sphinx==9.0.4 ; python_full_version == '3.11.*' \ + --hash=sha256:594ef59d042972abbc581d8baa577404abe4e6c3b04ef61bd7fc2acbd51f3fa3 \ + --hash=sha256:5bebc595a5e943ea248b99c13814c1c5e10b3ece718976824ffa7959ff95fffb + # via + # rules-python-dev (dev/pyproject.toml) + # myst-parser + # sphinx-reredirects + # sphinx-rtd-theme + # sphinxcontrib-jquery +sphinx==9.1.0 ; python_full_version >= '3.12' \ + --hash=sha256:7741722357dd75f8190766926071fed3bdc211c74dd2d7d4df5404da95930ddb \ + --hash=sha256:c84fdd4e782504495fe4f2c0b3413d6c2bf388589bb352d439b2a3bb99991978 + # via + # rules-python-dev (dev/pyproject.toml) + # myst-parser + # sphinx-reredirects + # sphinx-rtd-theme + # sphinxcontrib-jquery +sphinx-autodoc2==0.5.0 \ + --hash=sha256:7d76044aa81d6af74447080182b6868c7eb066874edc835e8ddf810735b6565a \ + --hash=sha256:e867013b1512f9d6d7e6f6799f8b537d6884462acd118ef361f3f619a60b5c9e + # via rules-python-dev (dev/pyproject.toml) +sphinx-reredirects==0.1.6 ; python_full_version < '3.11' \ + --hash=sha256:c491cba545f67be9697508727818d8626626366245ae64456fe29f37e9bbea64 \ + --hash=sha256:efd50c766fbc5bf40cd5148e10c00f2c00d143027de5c5e48beece93cc40eeea + # via rules-python-dev (dev/pyproject.toml) +sphinx-reredirects==1.1.0 ; python_full_version >= '3.11' \ + --hash=sha256:4b5692273c72cd2d4d917f4c6f87d5919e4d6114a752d4be033f7f5f6310efd9 \ + --hash=sha256:fb9b195335ab14b43f8273287d0c7eeb637ba6c56c66581c11b47202f6718b29 + # via rules-python-dev (dev/pyproject.toml) +sphinx-rtd-theme==3.1.0 \ + --hash=sha256:1785824ae8e6632060490f67cf3a72d404a85d2d9fc26bce3619944de5682b89 \ + --hash=sha256:b44276f2c276e909239a4f6c955aa667aaafeb78597923b1c60babc76db78e4c + # via rules-python-dev (dev/pyproject.toml) +sphinxcontrib-applehelp==2.0.0 \ + --hash=sha256:2f29ef331735ce958efa4734873f084941970894c6090408b079c61b2e1c06d1 \ + --hash=sha256:4cd3f0ec4ac5dd9c17ec65e9ab272c9b867ea77425228e68ecf08d6b28ddbdb5 + # via sphinx +sphinxcontrib-devhelp==2.0.0 \ + --hash=sha256:411f5d96d445d1d73bb5d52133377b4248ec79db5c793ce7dbe59e074b4dd1ad \ + --hash=sha256:aefb8b83854e4b0998877524d1029fd3e6879210422ee3780459e28a1f03a8a2 + # via sphinx +sphinxcontrib-htmlhelp==2.1.0 \ + --hash=sha256:166759820b47002d22914d64a075ce08f4c46818e17cfc9470a9786b759b19f8 \ + --hash=sha256:c9e2916ace8aad64cc13a0d233ee22317f2b9025b9cf3295249fa985cc7082e9 + # via sphinx +sphinxcontrib-jquery==4.1 \ + --hash=sha256:1620739f04e36a2c779f1a131a2dfd49b2fd07351bf1968ced074365933abc7a \ + --hash=sha256:f936030d7d0147dd026a4f2b5a57343d233f1fc7b363f68b3d4f1cb0993878ae + # via sphinx-rtd-theme +sphinxcontrib-jsmath==1.0.1 \ + --hash=sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178 \ + --hash=sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8 + # via sphinx +sphinxcontrib-qthelp==2.0.0 \ + --hash=sha256:4fe7d0ac8fc171045be623aba3e2a8f613f8682731f9153bb2e40ece16b9bbab \ + --hash=sha256:b18a828cdba941ccd6ee8445dbe72ffa3ef8cbe7505d8cd1fa0d42d3f2d5f3eb + # via sphinx +sphinxcontrib-serializinghtml==2.0.0 \ + --hash=sha256:6e2cb0eef194e10c27ec0023bfeb25badbbb5868244cf5bc5bdc04e4464bf331 \ + --hash=sha256:e9d912827f872c029017a53f0ef2180b327c3f7fd23c87229f7a8e8b70031d4d + # via sphinx +tomli==2.4.1 ; python_full_version < '3.11' \ + --hash=sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853 \ + --hash=sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe \ + --hash=sha256:136443dbd7e1dee43c68ac2694fde36b2849865fa258d39bf822c10e8068eac5 \ + --hash=sha256:1d8591993e228b0c930c4bb0db464bdad97b3289fb981255d6c9a41aedc84b2d \ + --hash=sha256:2190f2e9dd7508d2a90ded5ed369255980a1bcdd58e52f7fe24b8162bf9fedbd \ + --hash=sha256:2c1c351919aca02858f740c6d33adea0c5deea37f9ecca1cc1ef9e884a619d26 \ + --hash=sha256:36d2bd2ad5fb9eaddba5226aa02c8ec3fa4f192631e347b3ed28186d43be6b54 \ + --hash=sha256:3d48a93ee1c9b79c04bb38772ee1b64dcf18ff43085896ea460ca8dec96f35f6 \ + --hash=sha256:47149d5bd38761ac8be13a84864bf0b7b70bc051806bc3669ab1cbc56216b23c \ + --hash=sha256:4ab97e64ccda8756376892c53a72bd1f964e519c77236368527f758fbc36a53a \ + --hash=sha256:4b605484e43cdc43f0954ddae319fb75f04cc10dd80d830540060ee7cd0243cd \ + --hash=sha256:504aa796fe0569bb43171066009ead363de03675276d2d121ac1a4572397870f \ + --hash=sha256:51529d40e3ca50046d7606fa99ce3956a617f9b36380da3b7f0dd3dd28e68cb5 \ + --hash=sha256:52c8ef851d9a240f11a88c003eacb03c31fc1c9c4ec64a99a0f922b93874fda9 \ + --hash=sha256:559db847dc486944896521f68d8190be1c9e719fced785720d2216fe7022b662 \ + --hash=sha256:5a881ab208c0baf688221f8cecc5401bd291d67e38a1ac884d6736cbcd8247e9 \ + --hash=sha256:5cb41aa38891e073ee49d55fbc7839cfdb2bc0e600add13874d048c94aadddd1 \ + --hash=sha256:5e262d41726bc187e69af7825504c933b6794dc3fbd5945e41a79bb14c31f585 \ + --hash=sha256:5ee18d9ebdb417e384b58fe414e8d6af9f4e7a0ae761519fb50f721de398dd4e \ + --hash=sha256:7008df2e7655c495dd12d2a4ad038ff878d4ca4b81fccaf82b714e07eae4402c \ + --hash=sha256:734e20b57ba95624ecf1841e72b53f6e186355e216e5412de414e3c51e5e3c41 \ + --hash=sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f \ + --hash=sha256:7f86fd587c4ed9dd76f318225e7d9b29cfc5a9d43de44e5754db8d1128487085 \ + --hash=sha256:7f94b27a62cfad8496c8d2513e1a222dd446f095fca8987fceef261225538a15 \ + --hash=sha256:88dceee75c2c63af144e456745e10101eb67361050196b0b6af5d717254dddf7 \ + --hash=sha256:8a650c2dbafa08d42e51ba0b62740dae4ecb9338eefa093aa5c78ceb546fcd5c \ + --hash=sha256:8d65a2fbf9d2f8352685bc1364177ee3923d6baf5e7f43ea4959d7d8bc326a36 \ + --hash=sha256:96481a5786729fd470164b47cdb3e0e58062a496f455ee41b4403be77cb5a076 \ + --hash=sha256:a120733b01c45e9a0c34aeef92bf0cf1d56cfe81ed9d47d562f9ed591a9828ac \ + --hash=sha256:b1d22e6e9387bf4739fbe23bfa80e93f6b0373a7f1b96c6227c32bef95a4d7a8 \ + --hash=sha256:b8c198f8c1805dc42708689ed6864951fd2494f924149d3e4bce7710f8eb5232 \ + --hash=sha256:c2541745709bad0264b7d4705ad453b76ccd191e64aa6f0fc66b69a293a45ece \ + --hash=sha256:c742f741d58a28940ce01d58f0ab2ea3ced8b12402f162f4d534dfe18ba1cd6a \ + --hash=sha256:c7f2c7f2b9ca6bdeef8f0fa897f8e05085923eb091721675170254cbc5b02897 \ + --hash=sha256:d312ef37c91508b0ab2cee7da26ec0b3ed2f03ce12bd87a588d771ae15dcf82d \ + --hash=sha256:d4d8fe59808a54658fcc0160ecfb1b30f9089906c50b23bcb4c69eddc19ec2b4 \ + --hash=sha256:da25dc3563bff5965356133435b757a795a17b17d01dbc0f42fb32447ddfd917 \ + --hash=sha256:eab21f45c7f66c13f2a9e0e1535309cee140182a9cdae1e041d02e47291e8396 \ + --hash=sha256:eb0dc4e38e6a1fd579e5d50369aa2e10acfc9cace504579b2faabb478e76941a \ + --hash=sha256:ec9bfaf3ad2df51ace80688143a6a4ebc09a248f6ff781a9945e51937008fcbc \ + --hash=sha256:ede3e6487c5ef5d28634ba3f31f989030ad6af71edfb0055cbbd14189ff240ba \ + --hash=sha256:f3c6818a1a86dd6dca7ddcaaf76947d5ba31aecc28cb1b67009a5877c9a64f3f \ + --hash=sha256:f758f1b9299d059cc3f6546ae2af89670cb1c4d48ea29c3cacc4fe7de3058257 \ + --hash=sha256:f8f0fc26ec2cc2b965b7a3b87cd19c5c6b8c5e5f436b984e85f486d652285c30 \ + --hash=sha256:fd0409a3653af6c147209d267a0e4243f0ae46b011aa978b1080359fddc9b6cf \ + --hash=sha256:ff18e6a727ee0ab0388507b89d1bc6a22b138d1e2fa56d1ad494586d61d2eae9 \ + --hash=sha256:ff2983983d34813c1aeb0fa89091e76c3a22889ee83ab27c5eeb45100560c049 + # via + # pytest + # sphinx + # sphinx-autodoc2 +typing-extensions==4.16.0 \ + --hash=sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8 \ + --hash=sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5 + # via + # rules-python-dev (dev/pyproject.toml) + # astroid + # exceptiongroup + # sphinx-autodoc2 +urllib3==2.7.0 \ + --hash=sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c \ + --hash=sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897 + # via requests diff --git a/dev/uv.lock b/dev/uv.lock new file mode 100644 index 0000000000..b524eb1f61 --- /dev/null +++ b/dev/uv.lock @@ -0,0 +1,985 @@ +version = 1 +revision = 3 +requires-python = ">=3.10" +resolution-markers = [ + "python_full_version >= '3.12'", + "python_full_version == '3.11.*'", + "python_full_version < '3.11'", +] + +[[package]] +name = "absl-py" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/64/c7/8de93764ad66968d19329a7e0c147a2bb3c7054c554d4a119111b8f9440f/absl_py-2.4.0.tar.gz", hash = "sha256:8c6af82722b35cf71e0f4d1d47dcaebfff286e27110a99fc359349b247dfb5d4", size = 116543, upload-time = "2026-01-28T10:17:05.322Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/a6/907a406bb7d359e6a63f99c313846d9eec4f7e6f7437809e03aa00fa3074/absl_py-2.4.0-py3-none-any.whl", hash = "sha256:88476fd881ca8aab94ffa78b7b6c632a782ab3ba1cd19c9bd423abc4fb4cd28d", size = 135750, upload-time = "2026-01-28T10:17:04.19Z" }, +] + +[[package]] +name = "alabaster" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a6/f8/d9c74d0daf3f742840fd818d69cfae176fa332022fd44e3469487d5a9420/alabaster-1.0.0.tar.gz", hash = "sha256:c00dca57bca26fa62a6d7d0a9fcce65f3e026e9bfe33e9c538fd3fbb2144fd9e", size = 24210, upload-time = "2024-07-26T18:15:03.762Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/b3/6b4067be973ae96ba0d615946e314c5ae35f9f993eca561b356540bb0c2b/alabaster-1.0.0-py3-none-any.whl", hash = "sha256:fc6786402dc3fcb2de3cabd5fe455a2db534b371124f1f21de8731783dec828b", size = 13929, upload-time = "2024-07-26T18:15:02.05Z" }, +] + +[[package]] +name = "altgraph" +version = "0.17.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/f8/97fdf103f38fed6792a1601dbc16cc8aac56e7459a9fff08c812d8ae177a/altgraph-0.17.5.tar.gz", hash = "sha256:c87b395dd12fabde9c99573a9749d67da8d29ef9de0125c7f536699b4a9bc9e7", size = 48428, upload-time = "2025-11-21T20:35:50.583Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a9/ba/000a1996d4308bc65120167c21241a3b205464a2e0b58deda26ae8ac21d1/altgraph-0.17.5-py2.py3-none-any.whl", hash = "sha256:f3a22400bce1b0c701683820ac4f3b159cd301acab067c51c653e06961600597", size = 21228, upload-time = "2025-11-21T20:35:49.444Z" }, +] + +[[package]] +name = "astroid" +version = "3.3.11" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/18/74/dfb75f9ccd592bbedb175d4a32fc643cf569d7c218508bfbd6ea7ef9c091/astroid-3.3.11.tar.gz", hash = "sha256:1e5a5011af2920c7c67a53f65d536d65bfa7116feeaf2354d8b94f29573bb0ce", size = 400439, upload-time = "2025-07-13T18:04:23.177Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/af/0f/3b8fdc946b4d9cc8cc1e8af42c4e409468c84441b933d037e101b3d72d86/astroid-3.3.11-py3-none-any.whl", hash = "sha256:54c760ae8322ece1abd213057c4b5bba7c49818853fc901ef09719a60dbf9dec", size = 275612, upload-time = "2025-07-13T18:04:21.07Z" }, +] + +[[package]] +name = "babel" +version = "2.18.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/b2/51899539b6ceeeb420d40ed3cd4b7a40519404f9baf3d4ac99dc413a834b/babel-2.18.0.tar.gz", hash = "sha256:b80b99a14bd085fcacfa15c9165f651fbb3406e66cc603abf11c5750937c992d", size = 9959554, upload-time = "2026-02-01T12:30:56.078Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/77/f5/21d2de20e8b8b0408f0681956ca2c69f1320a3848ac50e6e7f39c6159675/babel-2.18.0-py3-none-any.whl", hash = "sha256:e2b422b277c2b9a9630c1d7903c2a00d0830c409c59ac8cae9081c92f1aeba35", size = 10196845, upload-time = "2026-02-01T12:30:53.445Z" }, +] + +[[package]] +name = "certifi" +version = "2026.4.22" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/25/ee/6caf7a40c36a1220410afe15a1cc64993a1f864871f698c0f93acb72842a/certifi-2026.4.22.tar.gz", hash = "sha256:8d455352a37b71bf76a79caa83a3d6c25afee4a385d632127b6afb3963f1c580", size = 137077, upload-time = "2026-04-22T11:26:11.191Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/30/7cd8fdcdfbc5b869528b079bfb76dcdf6056b1a2097a662e5e8c04f42965/certifi-2026.4.22-py3-none-any.whl", hash = "sha256:3cb2210c8f88ba2318d29b0388d1023c8492ff72ecdde4ebdaddbb13a31b1c4a", size = 135707, upload-time = "2026-04-22T11:26:09.372Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/a1/67fe25fac3c7642725500a3f6cfe5821ad557c3abb11c9d20d12c7008d3e/charset_normalizer-3.4.7.tar.gz", hash = "sha256:ae89db9e5f98a11a4bf50407d4363e7b09b31e55bc117b4f7d80aab97ba009e5", size = 144271, upload-time = "2026-04-02T09:28:39.342Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/26/08/0f303cb0b529e456bb116f2d50565a482694fbb94340bf56d44677e7ed03/charset_normalizer-3.4.7-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cdd68a1fb318e290a2077696b7eb7a21a49163c455979c639bf5a5dcdc46617d", size = 315182, upload-time = "2026-04-02T09:25:40.673Z" }, + { url = "https://files.pythonhosted.org/packages/24/47/b192933e94b546f1b1fe4df9cc1f84fcdbf2359f8d1081d46dd029b50207/charset_normalizer-3.4.7-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e17b8d5d6a8c47c85e68ca8379def1303fd360c3e22093a807cd34a71cd082b8", size = 209329, upload-time = "2026-04-02T09:25:42.354Z" }, + { url = "https://files.pythonhosted.org/packages/c2/b4/01fa81c5ca6141024d89a8fc15968002b71da7f825dd14113207113fabbd/charset_normalizer-3.4.7-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:511ef87c8aec0783e08ac18565a16d435372bc1ac25a91e6ac7f5ef2b0bff790", size = 231230, upload-time = "2026-04-02T09:25:44.281Z" }, + { url = "https://files.pythonhosted.org/packages/20/f7/7b991776844dfa058017e600e6e55ff01984a063290ca5622c0b63162f68/charset_normalizer-3.4.7-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:007d05ec7321d12a40227aae9e2bc6dca73f3cb21058999a1df9e193555a9dcc", size = 225890, upload-time = "2026-04-02T09:25:45.475Z" }, + { url = "https://files.pythonhosted.org/packages/20/e7/bed0024a0f4ab0c8a9c64d4445f39b30c99bd1acd228291959e3de664247/charset_normalizer-3.4.7-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cf29836da5119f3c8a8a70667b0ef5fdca3bb12f80fd06487cfa575b3909b393", size = 216930, upload-time = "2026-04-02T09:25:46.58Z" }, + { url = "https://files.pythonhosted.org/packages/e2/ab/b18f0ab31cdd7b3ddb8bb76c4a414aeb8160c9810fdf1bc62f269a539d87/charset_normalizer-3.4.7-cp310-cp310-manylinux_2_31_armv7l.whl", hash = "sha256:12d8baf840cc7889b37c7c770f478adea7adce3dcb3944d02ec87508e2dcf153", size = 202109, upload-time = "2026-04-02T09:25:48.031Z" }, + { url = "https://files.pythonhosted.org/packages/82/e5/7e9440768a06dfb3075936490cb82dbf0ee20a133bf0dd8551fa096914ec/charset_normalizer-3.4.7-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d560742f3c0d62afaccf9f41fe485ed69bd7661a241f86a3ef0f0fb8b1a397af", size = 214684, upload-time = "2026-04-02T09:25:49.245Z" }, + { url = "https://files.pythonhosted.org/packages/71/94/8c61d8da9f062fdf457c80acfa25060ec22bf1d34bbeaca4350f13bcfd07/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b14b2d9dac08e28bb8046a1a0434b1750eb221c8f5b87a68f4fa11a6f97b5e34", size = 212785, upload-time = "2026-04-02T09:25:50.671Z" }, + { url = "https://files.pythonhosted.org/packages/66/cd/6e9889c648e72c0ab2e5967528bb83508f354d706637bc7097190c874e13/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:bc17a677b21b3502a21f66a8cc64f5bfad4df8a0b8434d661666f8ce90ac3af1", size = 203055, upload-time = "2026-04-02T09:25:51.802Z" }, + { url = "https://files.pythonhosted.org/packages/92/2e/7a951d6a08aefb7eb8e1b54cdfb580b1365afdd9dd484dc4bee9e5d8f258/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:750e02e074872a3fad7f233b47734166440af3cdea0add3e95163110816d6752", size = 232502, upload-time = "2026-04-02T09:25:53.388Z" }, + { url = "https://files.pythonhosted.org/packages/58/d5/abcf2d83bf8e0a1286df55cd0dc1d49af0da4282aa77e986df343e7de124/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:4e5163c14bffd570ef2affbfdd77bba66383890797df43dc8b4cc7d6f500bf53", size = 214295, upload-time = "2026-04-02T09:25:54.765Z" }, + { url = "https://files.pythonhosted.org/packages/47/3a/7d4cd7ed54be99973a0dc176032cba5cb1f258082c31fa6df35cff46acfc/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:6ed74185b2db44f41ef35fd1617c5888e59792da9bbc9190d6c7300617182616", size = 227145, upload-time = "2026-04-02T09:25:55.904Z" }, + { url = "https://files.pythonhosted.org/packages/1d/98/3a45bf8247889cf28262ebd3d0872edff11565b2a1e3064ccb132db3fbb0/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:94e1885b270625a9a828c9793b4d52a64445299baa1fea5a173bf1d3dd9a1a5a", size = 218884, upload-time = "2026-04-02T09:25:57.074Z" }, + { url = "https://files.pythonhosted.org/packages/ad/80/2e8b7f8915ed5c9ef13aa828d82738e33888c485b65ebf744d615040c7ea/charset_normalizer-3.4.7-cp310-cp310-win32.whl", hash = "sha256:6785f414ae0f3c733c437e0f3929197934f526d19dfaa75e18fdb4f94c6fb374", size = 148343, upload-time = "2026-04-02T09:25:58.199Z" }, + { url = "https://files.pythonhosted.org/packages/35/1b/3b8c8c77184af465ee9ad88b5aea46ea6b2e1f7b9dc9502891e37af21e30/charset_normalizer-3.4.7-cp310-cp310-win_amd64.whl", hash = "sha256:6696b7688f54f5af4462118f0bfa7c1621eeb87154f77fa04b9295ce7a8f2943", size = 159174, upload-time = "2026-04-02T09:25:59.322Z" }, + { url = "https://files.pythonhosted.org/packages/be/c1/feb40dca40dbb21e0a908801782d9288c64fc8d8e562c2098e9994c8c21b/charset_normalizer-3.4.7-cp310-cp310-win_arm64.whl", hash = "sha256:66671f93accb62ed07da56613636f3641f1a12c13046ce91ffc923721f23c008", size = 147805, upload-time = "2026-04-02T09:26:00.756Z" }, + { url = "https://files.pythonhosted.org/packages/c2/d7/b5b7020a0565c2e9fa8c09f4b5fa6232feb326b8c20081ccded47ea368fd/charset_normalizer-3.4.7-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7641bb8895e77f921102f72833904dcd9901df5d6d72a2ab8f31d04b7e51e4e7", size = 309705, upload-time = "2026-04-02T09:26:02.191Z" }, + { url = "https://files.pythonhosted.org/packages/5a/53/58c29116c340e5456724ecd2fff4196d236b98f3da97b404bc5e51ac3493/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:202389074300232baeb53ae2569a60901f7efadd4245cf3a3bf0617d60b439d7", size = 206419, upload-time = "2026-04-02T09:26:03.583Z" }, + { url = "https://files.pythonhosted.org/packages/b2/02/e8146dc6591a37a00e5144c63f29fb7c97a734ea8a111190783c0e60ab63/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:30b8d1d8c52a48c2c5690e152c169b673487a2a58de1ec7393196753063fcd5e", size = 227901, upload-time = "2026-04-02T09:26:04.738Z" }, + { url = "https://files.pythonhosted.org/packages/fb/73/77486c4cd58f1267bf17db420e930c9afa1b3be3fe8c8b8ebbebc9624359/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:532bc9bf33a68613fd7d65e4b1c71a6a38d7d42604ecf239c77392e9b4e8998c", size = 222742, upload-time = "2026-04-02T09:26:06.36Z" }, + { url = "https://files.pythonhosted.org/packages/a1/fa/f74eb381a7d94ded44739e9d94de18dc5edc9c17fb8c11f0a6890696c0a9/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2fe249cb4651fd12605b7288b24751d8bfd46d35f12a20b1ba33dea122e690df", size = 214061, upload-time = "2026-04-02T09:26:08.347Z" }, + { url = "https://files.pythonhosted.org/packages/dc/92/42bd3cefcf7687253fb86694b45f37b733c97f59af3724f356fa92b8c344/charset_normalizer-3.4.7-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:65bcd23054beab4d166035cabbc868a09c1a49d1efe458fe8e4361215df40265", size = 199239, upload-time = "2026-04-02T09:26:09.823Z" }, + { url = "https://files.pythonhosted.org/packages/4c/3d/069e7184e2aa3b3cddc700e3dd267413dc259854adc3380421c805c6a17d/charset_normalizer-3.4.7-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:08e721811161356f97b4059a9ba7bafb23ea5ee2255402c42881c214e173c6b4", size = 210173, upload-time = "2026-04-02T09:26:10.953Z" }, + { url = "https://files.pythonhosted.org/packages/62/51/9d56feb5f2e7074c46f93e0ebdbe61f0848ee246e2f0d89f8e20b89ebb8f/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e060d01aec0a910bdccb8be71faf34e7799ce36950f8294c8bf612cba65a2c9e", size = 209841, upload-time = "2026-04-02T09:26:12.142Z" }, + { url = "https://files.pythonhosted.org/packages/d2/59/893d8f99cc4c837dda1fe2f1139079703deb9f321aabcb032355de13b6c7/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:38c0109396c4cfc574d502df99742a45c72c08eff0a36158b6f04000043dbf38", size = 200304, upload-time = "2026-04-02T09:26:13.711Z" }, + { url = "https://files.pythonhosted.org/packages/7d/1d/ee6f3be3464247578d1ed5c46de545ccc3d3ff933695395c402c21fa6b77/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:1c2a768fdd44ee4a9339a9b0b130049139b8ce3c01d2ce09f67f5a68048d477c", size = 229455, upload-time = "2026-04-02T09:26:14.941Z" }, + { url = "https://files.pythonhosted.org/packages/54/bb/8fb0a946296ea96a488928bdce8ef99023998c48e4713af533e9bb98ef07/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:1a87ca9d5df6fe460483d9a5bbf2b18f620cbed41b432e2bddb686228282d10b", size = 210036, upload-time = "2026-04-02T09:26:16.478Z" }, + { url = "https://files.pythonhosted.org/packages/9a/bc/015b2387f913749f82afd4fcba07846d05b6d784dd16123cb66860e0237d/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:d635aab80466bc95771bb78d5370e74d36d1fe31467b6b29b8b57b2a3cd7d22c", size = 224739, upload-time = "2026-04-02T09:26:17.751Z" }, + { url = "https://files.pythonhosted.org/packages/17/ab/63133691f56baae417493cba6b7c641571a2130eb7bceba6773367ab9ec5/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ae196f021b5e7c78e918242d217db021ed2a6ace2bc6ae94c0fc596221c7f58d", size = 216277, upload-time = "2026-04-02T09:26:18.981Z" }, + { url = "https://files.pythonhosted.org/packages/06/6d/3be70e827977f20db77c12a97e6a9f973631a45b8d186c084527e53e77a4/charset_normalizer-3.4.7-cp311-cp311-win32.whl", hash = "sha256:adb2597b428735679446b46c8badf467b4ca5f5056aae4d51a19f9570301b1ad", size = 147819, upload-time = "2026-04-02T09:26:20.295Z" }, + { url = "https://files.pythonhosted.org/packages/20/d9/5f67790f06b735d7c7637171bbfd89882ad67201891b7275e51116ed8207/charset_normalizer-3.4.7-cp311-cp311-win_amd64.whl", hash = "sha256:8e385e4267ab76874ae30db04c627faaaf0b509e1ccc11a95b3fc3e83f855c00", size = 159281, upload-time = "2026-04-02T09:26:21.74Z" }, + { url = "https://files.pythonhosted.org/packages/ca/83/6413f36c5a34afead88ce6f66684d943d91f233d76dd083798f9602b75ae/charset_normalizer-3.4.7-cp311-cp311-win_arm64.whl", hash = "sha256:d4a48e5b3c2a489fae013b7589308a40146ee081f6f509e047e0e096084ceca1", size = 147843, upload-time = "2026-04-02T09:26:22.901Z" }, + { url = "https://files.pythonhosted.org/packages/0c/eb/4fc8d0a7110eb5fc9cc161723a34a8a6c200ce3b4fbf681bc86feee22308/charset_normalizer-3.4.7-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:eca9705049ad3c7345d574e3510665cb2cf844c2f2dcfe675332677f081cbd46", size = 311328, upload-time = "2026-04-02T09:26:24.331Z" }, + { url = "https://files.pythonhosted.org/packages/f8/e3/0fadc706008ac9d7b9b5be6dc767c05f9d3e5df51744ce4cc9605de7b9f4/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6178f72c5508bfc5fd446a5905e698c6212932f25bcdd4b47a757a50605a90e2", size = 208061, upload-time = "2026-04-02T09:26:25.568Z" }, + { url = "https://files.pythonhosted.org/packages/42/f0/3dd1045c47f4a4604df85ec18ad093912ae1344ac706993aff91d38773a2/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e1421b502d83040e6d7fb2fb18dff63957f720da3d77b2fbd3187ceb63755d7b", size = 229031, upload-time = "2026-04-02T09:26:26.865Z" }, + { url = "https://files.pythonhosted.org/packages/dc/67/675a46eb016118a2fbde5a277a5d15f4f69d5f3f5f338e5ee2f8948fcf43/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:edac0f1ab77644605be2cbba52e6b7f630731fc42b34cb0f634be1a6eface56a", size = 225239, upload-time = "2026-04-02T09:26:28.044Z" }, + { url = "https://files.pythonhosted.org/packages/4b/f8/d0118a2f5f23b02cd166fa385c60f9b0d4f9194f574e2b31cef350ad7223/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5649fd1c7bade02f320a462fdefd0b4bd3ce036065836d4f42e0de958038e116", size = 216589, upload-time = "2026-04-02T09:26:29.239Z" }, + { url = "https://files.pythonhosted.org/packages/b1/f1/6d2b0b261b6c4ceef0fcb0d17a01cc5bc53586c2d4796fa04b5c540bc13d/charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:203104ed3e428044fd943bc4bf45fa73c0730391f9621e37fe39ecf477b128cb", size = 202733, upload-time = "2026-04-02T09:26:30.5Z" }, + { url = "https://files.pythonhosted.org/packages/6f/c0/7b1f943f7e87cc3db9626ba17807d042c38645f0a1d4415c7a14afb5591f/charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:298930cec56029e05497a76988377cbd7457ba864beeea92ad7e844fe74cd1f1", size = 212652, upload-time = "2026-04-02T09:26:31.709Z" }, + { url = "https://files.pythonhosted.org/packages/38/dd/5a9ab159fe45c6e72079398f277b7d2b523e7f716acc489726115a910097/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:708838739abf24b2ceb208d0e22403dd018faeef86ddac04319a62ae884c4f15", size = 211229, upload-time = "2026-04-02T09:26:33.282Z" }, + { url = "https://files.pythonhosted.org/packages/d5/ff/531a1cad5ca855d1c1a8b69cb71abfd6d85c0291580146fda7c82857caa1/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:0f7eb884681e3938906ed0434f20c63046eacd0111c4ba96f27b76084cd679f5", size = 203552, upload-time = "2026-04-02T09:26:34.845Z" }, + { url = "https://files.pythonhosted.org/packages/c1/4c/a5fb52d528a8ca41f7598cb619409ece30a169fbdf9cdce592e53b46c3a6/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4dc1e73c36828f982bfe79fadf5919923f8a6f4df2860804db9a98c48824ce8d", size = 230806, upload-time = "2026-04-02T09:26:36.152Z" }, + { url = "https://files.pythonhosted.org/packages/59/7a/071feed8124111a32b316b33ae4de83d36923039ef8cf48120266844285b/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:aed52fea0513bac0ccde438c188c8a471c4e0f457c2dd20cdbf6ea7a450046c7", size = 212316, upload-time = "2026-04-02T09:26:37.672Z" }, + { url = "https://files.pythonhosted.org/packages/fd/35/f7dba3994312d7ba508e041eaac39a36b120f32d4c8662b8814dab876431/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:fea24543955a6a729c45a73fe90e08c743f0b3334bbf3201e6c4bc1b0c7fa464", size = 227274, upload-time = "2026-04-02T09:26:38.93Z" }, + { url = "https://files.pythonhosted.org/packages/8a/2d/a572df5c9204ab7688ec1edc895a73ebded3b023bb07364710b05dd1c9be/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:bb6d88045545b26da47aa879dd4a89a71d1dce0f0e549b1abcb31dfe4a8eac49", size = 218468, upload-time = "2026-04-02T09:26:40.17Z" }, + { url = "https://files.pythonhosted.org/packages/86/eb/890922a8b03a568ca2f336c36585a4713c55d4d67bf0f0c78924be6315ca/charset_normalizer-3.4.7-cp312-cp312-win32.whl", hash = "sha256:2257141f39fe65a3fdf38aeccae4b953e5f3b3324f4ff0daf9f15b8518666a2c", size = 148460, upload-time = "2026-04-02T09:26:41.416Z" }, + { url = "https://files.pythonhosted.org/packages/35/d9/0e7dffa06c5ab081f75b1b786f0aefc88365825dfcd0ac544bdb7b2b6853/charset_normalizer-3.4.7-cp312-cp312-win_amd64.whl", hash = "sha256:5ed6ab538499c8644b8a3e18debabcd7ce684f3fa91cf867521a7a0279cab2d6", size = 159330, upload-time = "2026-04-02T09:26:42.554Z" }, + { url = "https://files.pythonhosted.org/packages/9e/5d/481bcc2a7c88ea6b0878c299547843b2521ccbc40980cb406267088bc701/charset_normalizer-3.4.7-cp312-cp312-win_arm64.whl", hash = "sha256:56be790f86bfb2c98fb742ce566dfb4816e5a83384616ab59c49e0604d49c51d", size = 147828, upload-time = "2026-04-02T09:26:44.075Z" }, + { url = "https://files.pythonhosted.org/packages/c1/3b/66777e39d3ae1ddc77ee606be4ec6d8cbd4c801f65e5a1b6f2b11b8346dd/charset_normalizer-3.4.7-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:f496c9c3cc02230093d8330875c4c3cdfc3b73612a5fd921c65d39cbcef08063", size = 309627, upload-time = "2026-04-02T09:26:45.198Z" }, + { url = "https://files.pythonhosted.org/packages/2e/4e/b7f84e617b4854ade48a1b7915c8ccfadeba444d2a18c291f696e37f0d3b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ea948db76d31190bf08bd371623927ee1339d5f2a0b4b1b4a4439a65298703c", size = 207008, upload-time = "2026-04-02T09:26:46.824Z" }, + { url = "https://files.pythonhosted.org/packages/c4/bb/ec73c0257c9e11b268f018f068f5d00aa0ef8c8b09f7753ebd5f2880e248/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a277ab8928b9f299723bc1a2dabb1265911b1a76341f90a510368ca44ad9ab66", size = 228303, upload-time = "2026-04-02T09:26:48.397Z" }, + { url = "https://files.pythonhosted.org/packages/85/fb/32d1f5033484494619f701e719429c69b766bfc4dbc61aa9e9c8c166528b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3bec022aec2c514d9cf199522a802bd007cd588ab17ab2525f20f9c34d067c18", size = 224282, upload-time = "2026-04-02T09:26:49.684Z" }, + { url = "https://files.pythonhosted.org/packages/fa/07/330e3a0dda4c404d6da83b327270906e9654a24f6c546dc886a0eb0ffb23/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e044c39e41b92c845bc815e5ae4230804e8e7bc29e399b0437d64222d92809dd", size = 215595, upload-time = "2026-04-02T09:26:50.915Z" }, + { url = "https://files.pythonhosted.org/packages/e3/7c/fc890655786e423f02556e0216d4b8c6bcb6bdfa890160dc66bf52dee468/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:f495a1652cf3fbab2eb0639776dad966c2fb874d79d87ca07f9d5f059b8bd215", size = 201986, upload-time = "2026-04-02T09:26:52.197Z" }, + { url = "https://files.pythonhosted.org/packages/d8/97/bfb18b3db2aed3b90cf54dc292ad79fdd5ad65c4eae454099475cbeadd0d/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e712b419df8ba5e42b226c510472b37bd57b38e897d3eca5e8cfd410a29fa859", size = 211711, upload-time = "2026-04-02T09:26:53.49Z" }, + { url = "https://files.pythonhosted.org/packages/6f/a5/a581c13798546a7fd557c82614a5c65a13df2157e9ad6373166d2a3e645d/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7804338df6fcc08105c7745f1502ba68d900f45fd770d5bdd5288ddccb8a42d8", size = 210036, upload-time = "2026-04-02T09:26:54.975Z" }, + { url = "https://files.pythonhosted.org/packages/8c/bf/b3ab5bcb478e4193d517644b0fb2bf5497fbceeaa7a1bc0f4d5b50953861/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:481551899c856c704d58119b5025793fa6730adda3571971af568f66d2424bb5", size = 202998, upload-time = "2026-04-02T09:26:56.303Z" }, + { url = "https://files.pythonhosted.org/packages/e7/4e/23efd79b65d314fa320ec6017b4b5834d5c12a58ba4610aa353af2e2f577/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f59099f9b66f0d7145115e6f80dd8b1d847176df89b234a5a6b3f00437aa0832", size = 230056, upload-time = "2026-04-02T09:26:57.554Z" }, + { url = "https://files.pythonhosted.org/packages/b9/9f/1e1941bc3f0e01df116e68dc37a55c4d249df5e6fa77f008841aef68264f/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:f59ad4c0e8f6bba240a9bb85504faa1ab438237199d4cce5f622761507b8f6a6", size = 211537, upload-time = "2026-04-02T09:26:58.843Z" }, + { url = "https://files.pythonhosted.org/packages/80/0f/088cbb3020d44428964a6c97fe1edfb1b9550396bf6d278330281e8b709c/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:3dedcc22d73ec993f42055eff4fcfed9318d1eeb9a6606c55892a26964964e48", size = 226176, upload-time = "2026-04-02T09:27:00.437Z" }, + { url = "https://files.pythonhosted.org/packages/6a/9f/130394f9bbe06f4f63e22641d32fc9b202b7e251c9aef4db044324dac493/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:64f02c6841d7d83f832cd97ccf8eb8a906d06eb95d5276069175c696b024b60a", size = 217723, upload-time = "2026-04-02T09:27:02.021Z" }, + { url = "https://files.pythonhosted.org/packages/73/55/c469897448a06e49f8fa03f6caae97074fde823f432a98f979cc42b90e69/charset_normalizer-3.4.7-cp313-cp313-win32.whl", hash = "sha256:4042d5c8f957e15221d423ba781e85d553722fc4113f523f2feb7b188cc34c5e", size = 148085, upload-time = "2026-04-02T09:27:03.192Z" }, + { url = "https://files.pythonhosted.org/packages/5d/78/1b74c5bbb3f99b77a1715c91b3e0b5bdb6fe302d95ace4f5b1bec37b0167/charset_normalizer-3.4.7-cp313-cp313-win_amd64.whl", hash = "sha256:3946fa46a0cf3e4c8cb1cc52f56bb536310d34f25f01ca9b6c16afa767dab110", size = 158819, upload-time = "2026-04-02T09:27:04.454Z" }, + { url = "https://files.pythonhosted.org/packages/68/86/46bd42279d323deb8687c4a5a811fd548cb7d1de10cf6535d099877a9a9f/charset_normalizer-3.4.7-cp313-cp313-win_arm64.whl", hash = "sha256:80d04837f55fc81da168b98de4f4b797ef007fc8a79ab71c6ec9bc4dd662b15b", size = 147915, upload-time = "2026-04-02T09:27:05.971Z" }, + { url = "https://files.pythonhosted.org/packages/97/c8/c67cb8c70e19ef1960b97b22ed2a1567711de46c4ddf19799923adc836c2/charset_normalizer-3.4.7-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:c36c333c39be2dbca264d7803333c896ab8fa7d4d6f0ab7edb7dfd7aea6e98c0", size = 309234, upload-time = "2026-04-02T09:27:07.194Z" }, + { url = "https://files.pythonhosted.org/packages/99/85/c091fdee33f20de70d6c8b522743b6f831a2f1cd3ff86de4c6a827c48a76/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1c2aed2e5e41f24ea8ef1590b8e848a79b56f3a5564a65ceec43c9d692dc7d8a", size = 208042, upload-time = "2026-04-02T09:27:08.749Z" }, + { url = "https://files.pythonhosted.org/packages/87/1c/ab2ce611b984d2fd5d86a5a8a19c1ae26acac6bad967da4967562c75114d/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:54523e136b8948060c0fa0bc7b1b50c32c186f2fceee897a495406bb6e311d2b", size = 228706, upload-time = "2026-04-02T09:27:09.951Z" }, + { url = "https://files.pythonhosted.org/packages/a8/29/2b1d2cb00bf085f59d29eb773ce58ec2d325430f8c216804a0a5cd83cbca/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:715479b9a2802ecac752a3b0efa2b0b60285cf962ee38414211abdfccc233b41", size = 224727, upload-time = "2026-04-02T09:27:11.175Z" }, + { url = "https://files.pythonhosted.org/packages/47/5c/032c2d5a07fe4d4855fea851209cca2b6f03ebeb6d4e3afdb3358386a684/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bd6c2a1c7573c64738d716488d2cdd3c00e340e4835707d8fdb8dc1a66ef164e", size = 215882, upload-time = "2026-04-02T09:27:12.446Z" }, + { url = "https://files.pythonhosted.org/packages/2c/c2/356065d5a8b78ed04499cae5f339f091946a6a74f91e03476c33f0ab7100/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:c45e9440fb78f8ddabcf714b68f936737a121355bf59f3907f4e17721b9d1aae", size = 200860, upload-time = "2026-04-02T09:27:13.721Z" }, + { url = "https://files.pythonhosted.org/packages/0c/cd/a32a84217ced5039f53b29f460962abb2d4420def55afabe45b1c3c7483d/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3534e7dcbdcf757da6b85a0bbf5b6868786d5982dd959b065e65481644817a18", size = 211564, upload-time = "2026-04-02T09:27:15.272Z" }, + { url = "https://files.pythonhosted.org/packages/44/86/58e6f13ce26cc3b8f4a36b94a0f22ae2f00a72534520f4ae6857c4b81f89/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e8ac484bf18ce6975760921bb6148041faa8fef0547200386ea0b52b5d27bf7b", size = 211276, upload-time = "2026-04-02T09:27:16.834Z" }, + { url = "https://files.pythonhosted.org/packages/8f/fe/d17c32dc72e17e155e06883efa84514ca375f8a528ba2546bee73fc4df81/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:a5fe03b42827c13cdccd08e6c0247b6a6d4b5e3cdc53fd1749f5896adcdc2356", size = 201238, upload-time = "2026-04-02T09:27:18.229Z" }, + { url = "https://files.pythonhosted.org/packages/6a/29/f33daa50b06525a237451cdb6c69da366c381a3dadcd833fa5676bc468b3/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:2d6eb928e13016cea4f1f21d1e10c1cebd5a421bc57ddf5b1142ae3f86824fab", size = 230189, upload-time = "2026-04-02T09:27:19.445Z" }, + { url = "https://files.pythonhosted.org/packages/b6/6e/52c84015394a6a0bdcd435210a7e944c5f94ea1055f5cc5d56c5fe368e7b/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e74327fb75de8986940def6e8dee4f127cc9752bee7355bb323cc5b2659b6d46", size = 211352, upload-time = "2026-04-02T09:27:20.79Z" }, + { url = "https://files.pythonhosted.org/packages/8c/d7/4353be581b373033fb9198bf1da3cf8f09c1082561e8e922aa7b39bf9fe8/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:d6038d37043bced98a66e68d3aa2b6a35505dc01328cd65217cefe82f25def44", size = 227024, upload-time = "2026-04-02T09:27:22.063Z" }, + { url = "https://files.pythonhosted.org/packages/30/45/99d18aa925bd1740098ccd3060e238e21115fffbfdcb8f3ece837d0ace6c/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7579e913a5339fb8fa133f6bbcfd8e6749696206cf05acdbdca71a1b436d8e72", size = 217869, upload-time = "2026-04-02T09:27:23.486Z" }, + { url = "https://files.pythonhosted.org/packages/5c/05/5ee478aa53f4bb7996482153d4bfe1b89e0f087f0ab6b294fcf92d595873/charset_normalizer-3.4.7-cp314-cp314-win32.whl", hash = "sha256:5b77459df20e08151cd6f8b9ef8ef1f961ef73d85c21a555c7eed5b79410ec10", size = 148541, upload-time = "2026-04-02T09:27:25.146Z" }, + { url = "https://files.pythonhosted.org/packages/48/77/72dcb0921b2ce86420b2d79d454c7022bf5be40202a2a07906b9f2a35c97/charset_normalizer-3.4.7-cp314-cp314-win_amd64.whl", hash = "sha256:92a0a01ead5e668468e952e4238cccd7c537364eb7d851ab144ab6627dbbe12f", size = 159634, upload-time = "2026-04-02T09:27:26.642Z" }, + { url = "https://files.pythonhosted.org/packages/c6/a3/c2369911cd72f02386e4e340770f6e158c7980267da16af8f668217abaa0/charset_normalizer-3.4.7-cp314-cp314-win_arm64.whl", hash = "sha256:67f6279d125ca0046a7fd386d01b311c6363844deac3e5b069b514ba3e63c246", size = 148384, upload-time = "2026-04-02T09:27:28.271Z" }, + { url = "https://files.pythonhosted.org/packages/94/09/7e8a7f73d24dba1f0035fbbf014d2c36828fc1bf9c88f84093e57d315935/charset_normalizer-3.4.7-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:effc3f449787117233702311a1b7d8f59cba9ced946ba727bdc329ec69028e24", size = 330133, upload-time = "2026-04-02T09:27:29.474Z" }, + { url = "https://files.pythonhosted.org/packages/8d/da/96975ddb11f8e977f706f45cddd8540fd8242f71ecdb5d18a80723dcf62c/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fbccdc05410c9ee21bbf16a35f4c1d16123dcdeb8a1d38f33654fa21d0234f79", size = 216257, upload-time = "2026-04-02T09:27:30.793Z" }, + { url = "https://files.pythonhosted.org/packages/e5/e8/1d63bf8ef2d388e95c64b2098f45f84758f6d102a087552da1485912637b/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:733784b6d6def852c814bce5f318d25da2ee65dd4839a0718641c696e09a2960", size = 234851, upload-time = "2026-04-02T09:27:32.44Z" }, + { url = "https://files.pythonhosted.org/packages/9b/40/e5ff04233e70da2681fa43969ad6f66ca5611d7e669be0246c4c7aaf6dc8/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a89c23ef8d2c6b27fd200a42aa4ac72786e7c60d40efdc76e6011260b6e949c4", size = 233393, upload-time = "2026-04-02T09:27:34.03Z" }, + { url = "https://files.pythonhosted.org/packages/be/c1/06c6c49d5a5450f76899992f1ee40b41d076aee9279b49cf9974d2f313d5/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6c114670c45346afedc0d947faf3c7f701051d2518b943679c8ff88befe14f8e", size = 223251, upload-time = "2026-04-02T09:27:35.369Z" }, + { url = "https://files.pythonhosted.org/packages/2b/9f/f2ff16fb050946169e3e1f82134d107e5d4ae72647ec8a1b1446c148480f/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:a180c5e59792af262bf263b21a3c49353f25945d8d9f70628e73de370d55e1e1", size = 206609, upload-time = "2026-04-02T09:27:36.661Z" }, + { url = "https://files.pythonhosted.org/packages/69/d5/a527c0cd8d64d2eab7459784fb4169a0ac76e5a6fc5237337982fd61347e/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3c9a494bc5ec77d43cea229c4f6db1e4d8fe7e1bbffa8b6f0f0032430ff8ab44", size = 220014, upload-time = "2026-04-02T09:27:38.019Z" }, + { url = "https://files.pythonhosted.org/packages/7e/80/8a7b8104a3e203074dc9aa2c613d4b726c0e136bad1cc734594b02867972/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8d828b6667a32a728a1ad1d93957cdf37489c57b97ae6c4de2860fa749b8fc1e", size = 218979, upload-time = "2026-04-02T09:27:39.37Z" }, + { url = "https://files.pythonhosted.org/packages/02/9a/b759b503d507f375b2b5c153e4d2ee0a75aa215b7f2489cf314f4541f2c0/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:cf1493cd8607bec4d8a7b9b004e699fcf8f9103a9284cc94962cb73d20f9d4a3", size = 209238, upload-time = "2026-04-02T09:27:40.722Z" }, + { url = "https://files.pythonhosted.org/packages/c2/4e/0f3f5d47b86bdb79256e7290b26ac847a2832d9a4033f7eb2cd4bcf4bb5b/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0c96c3b819b5c3e9e165495db84d41914d6894d55181d2d108cc1a69bfc9cce0", size = 236110, upload-time = "2026-04-02T09:27:42.33Z" }, + { url = "https://files.pythonhosted.org/packages/96/23/bce28734eb3ed2c91dcf93abeb8a5cf393a7b2749725030bb630e554fdd8/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:752a45dc4a6934060b3b0dab47e04edc3326575f82be64bc4fc293914566503e", size = 219824, upload-time = "2026-04-02T09:27:43.924Z" }, + { url = "https://files.pythonhosted.org/packages/2c/6f/6e897c6984cc4d41af319b077f2f600fc8214eb2fe2d6bcb79141b882400/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:8778f0c7a52e56f75d12dae53ae320fae900a8b9b4164b981b9c5ce059cd1fcb", size = 233103, upload-time = "2026-04-02T09:27:45.348Z" }, + { url = "https://files.pythonhosted.org/packages/76/22/ef7bd0fe480a0ae9b656189ec00744b60933f68b4f42a7bb06589f6f576a/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ce3412fbe1e31eb81ea42f4169ed94861c56e643189e1e75f0041f3fe7020abe", size = 225194, upload-time = "2026-04-02T09:27:46.706Z" }, + { url = "https://files.pythonhosted.org/packages/c5/a7/0e0ab3e0b5bc1219bd80a6a0d4d72ca74d9250cb2382b7c699c147e06017/charset_normalizer-3.4.7-cp314-cp314t-win32.whl", hash = "sha256:c03a41a8784091e67a39648f70c5f97b5b6a37f216896d44d2cdcb82615339a0", size = 159827, upload-time = "2026-04-02T09:27:48.053Z" }, + { url = "https://files.pythonhosted.org/packages/7a/1d/29d32e0fb40864b1f878c7f5a0b343ae676c6e2b271a2d55cc3a152391da/charset_normalizer-3.4.7-cp314-cp314t-win_amd64.whl", hash = "sha256:03853ed82eeebbce3c2abfdbc98c96dc205f32a79627688ac9a27370ea61a49c", size = 174168, upload-time = "2026-04-02T09:27:49.795Z" }, + { url = "https://files.pythonhosted.org/packages/de/32/d92444ad05c7a6e41fb2036749777c163baf7a0301a040cb672d6b2b1ae9/charset_normalizer-3.4.7-cp314-cp314t-win_arm64.whl", hash = "sha256:c35abb8bfff0185efac5878da64c45dafd2b37fb0383add1be155a763c1f083d", size = 153018, upload-time = "2026-04-02T09:27:51.116Z" }, + { url = "https://files.pythonhosted.org/packages/db/8f/61959034484a4a7c527811f4721e75d02d653a35afb0b6054474d8185d4c/charset_normalizer-3.4.7-py3-none-any.whl", hash = "sha256:3dce51d0f5e7951f8bb4900c257dad282f49190fdbebecd4ba99bcc41fef404d", size = 61958, upload-time = "2026-04-02T09:28:37.794Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "docutils" +version = "0.21.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11'", +] +sdist = { url = "https://files.pythonhosted.org/packages/ae/ed/aefcc8cd0ba62a0560c3c18c33925362d46c6075480bfa4df87b28e169a9/docutils-0.21.2.tar.gz", hash = "sha256:3a6b18732edf182daa3cd12775bbb338cf5691468f91eeeb109deff6ebfa986f", size = 2204444, upload-time = "2024-04-23T18:57:18.24Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8f/d7/9322c609343d929e75e7e5e6255e614fcc67572cfd083959cdef3b7aad79/docutils-0.21.2-py3-none-any.whl", hash = "sha256:dafca5b9e384f0e419294eb4d2ff9fa826435bf15f15b7bd45723e8ad76811b2", size = 587408, upload-time = "2024-04-23T18:57:14.835Z" }, +] + +[[package]] +name = "docutils" +version = "0.22.4" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.12'", + "python_full_version == '3.11.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/ae/b6/03bb70946330e88ffec97aefd3ea75ba575cb2e762061e0e62a213befee8/docutils-0.22.4.tar.gz", hash = "sha256:4db53b1fde9abecbb74d91230d32ab626d94f6badfc575d6db9194a49df29968", size = 2291750, upload-time = "2025-12-18T19:00:26.443Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/02/10/5da547df7a391dcde17f59520a231527b8571e6f46fc8efb02ccb370ab12/docutils-0.22.4-py3-none-any.whl", hash = "sha256:d0013f540772d1420576855455d050a2180186c91c15779301ac2ccb3eeb68de", size = 633196, upload-time = "2025-12-18T19:00:18.077Z" }, +] + +[[package]] +name = "exceptiongroup" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" }, +] + +[[package]] +name = "idna" +version = "3.15" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/82/77/7b3966d0b9d1d31a36ddf1746926a11dface89a83409bf1483f0237aa758/idna-3.15.tar.gz", hash = "sha256:ca962446ea538f7092a95e057da437618e886f4d349216d2b1e294abfdb65fdc", size = 199245, upload-time = "2026-05-12T22:45:57.011Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/23/408243171aa9aaba178d3e2559159c24c1171a641aa83b67bdd3394ead8e/idna-3.15-py3-none-any.whl", hash = "sha256:048adeaf8c2d788c40fee287673ccaa74c24ffd8dcf09ffa555a2fbb59f10ac8", size = 72340, upload-time = "2026-05-12T22:45:55.733Z" }, +] + +[[package]] +name = "imagesize" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6c/e6/7bf14eeb8f8b7251141944835abd42eb20a658d89084b7e1f3e5fe394090/imagesize-2.0.0.tar.gz", hash = "sha256:8e8358c4a05c304f1fccf7ff96f036e7243a189e9e42e90851993c558cfe9ee3", size = 1773045, upload-time = "2026-03-03T14:18:29.941Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5f/53/fb7122b71361a0d121b669dcf3d31244ef75badbbb724af388948de543e2/imagesize-2.0.0-py2.py3-none-any.whl", hash = "sha256:5667c5bbb57ab3f1fa4bc366f4fbc971db3d5ed011fd2715fd8001f782718d96", size = 9441, upload-time = "2026-03-03T14:18:27.892Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + +[[package]] +name = "jinja2" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, +] + +[[package]] +name = "macholib" +version = "1.16.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "altgraph" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/10/2f/97589876ea967487978071c9042518d28b958d87b17dceb7cdc1d881f963/macholib-1.16.4.tar.gz", hash = "sha256:f408c93ab2e995cd2c46e34fe328b130404be143469e41bc366c807448979362", size = 59427, upload-time = "2025-11-22T08:28:38.373Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/d1/a9f36f8ecdf0fb7c9b1e78c8d7af12b8c8754e74851ac7b94a8305540fc7/macholib-1.16.4-py2.py3-none-any.whl", hash = "sha256:da1a3fa8266e30f0ce7e97c6a54eefaae8edd1e5f86f3eb8b95457cae90265ea", size = 38117, upload-time = "2025-11-22T08:28:36.939Z" }, +] + +[[package]] +name = "markdown-it-py" +version = "3.0.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11'", +] +dependencies = [ + { name = "mdurl", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596, upload-time = "2023-06-03T06:41:14.443Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528, upload-time = "2023-06-03T06:41:11.019Z" }, +] + +[[package]] +name = "markdown-it-py" +version = "4.2.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.12'", + "python_full_version == '3.11.*'", +] +dependencies = [ + { name = "mdurl", marker = "python_full_version >= '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/ff/7841249c247aa650a76b9ee4bbaeae59370dc8bfd2f6c01f3630c35eb134/markdown_it_py-4.2.0.tar.gz", hash = "sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49", size = 82454, upload-time = "2026-05-07T12:08:28.36Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl", hash = "sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a", size = 91687, upload-time = "2026-05-07T12:08:27.182Z" }, +] + +[[package]] +name = "markupsafe" +version = "3.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e8/4b/3541d44f3937ba468b75da9eebcae497dcf67adb65caa16760b0a6807ebb/markupsafe-3.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2f981d352f04553a7171b8e44369f2af4055f888dfb147d55e42d29e29e74559", size = 11631, upload-time = "2025-09-27T18:36:05.558Z" }, + { url = "https://files.pythonhosted.org/packages/98/1b/fbd8eed11021cabd9226c37342fa6ca4e8a98d8188a8d9b66740494960e4/markupsafe-3.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e1c1493fb6e50ab01d20a22826e57520f1284df32f2d8601fdd90b6304601419", size = 12057, upload-time = "2025-09-27T18:36:07.165Z" }, + { url = "https://files.pythonhosted.org/packages/40/01/e560d658dc0bb8ab762670ece35281dec7b6c1b33f5fbc09ebb57a185519/markupsafe-3.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ba88449deb3de88bd40044603fafffb7bc2b055d626a330323a9ed736661695", size = 22050, upload-time = "2025-09-27T18:36:08.005Z" }, + { url = "https://files.pythonhosted.org/packages/af/cd/ce6e848bbf2c32314c9b237839119c5a564a59725b53157c856e90937b7a/markupsafe-3.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f42d0984e947b8adf7dd6dde396e720934d12c506ce84eea8476409563607591", size = 20681, upload-time = "2025-09-27T18:36:08.881Z" }, + { url = "https://files.pythonhosted.org/packages/c9/2a/b5c12c809f1c3045c4d580b035a743d12fcde53cf685dbc44660826308da/markupsafe-3.0.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c0c0b3ade1c0b13b936d7970b1d37a57acde9199dc2aecc4c336773e1d86049c", size = 20705, upload-time = "2025-09-27T18:36:10.131Z" }, + { url = "https://files.pythonhosted.org/packages/cf/e3/9427a68c82728d0a88c50f890d0fc072a1484de2f3ac1ad0bfc1a7214fd5/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f", size = 21524, upload-time = "2025-09-27T18:36:11.324Z" }, + { url = "https://files.pythonhosted.org/packages/bc/36/23578f29e9e582a4d0278e009b38081dbe363c5e7165113fad546918a232/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:d2ee202e79d8ed691ceebae8e0486bd9a2cd4794cec4824e1c99b6f5009502f6", size = 20282, upload-time = "2025-09-27T18:36:12.573Z" }, + { url = "https://files.pythonhosted.org/packages/56/21/dca11354e756ebd03e036bd8ad58d6d7168c80ce1fe5e75218e4945cbab7/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:177b5253b2834fe3678cb4a5f0059808258584c559193998be2601324fdeafb1", size = 20745, upload-time = "2025-09-27T18:36:13.504Z" }, + { url = "https://files.pythonhosted.org/packages/87/99/faba9369a7ad6e4d10b6a5fbf71fa2a188fe4a593b15f0963b73859a1bbd/markupsafe-3.0.3-cp310-cp310-win32.whl", hash = "sha256:2a15a08b17dd94c53a1da0438822d70ebcd13f8c3a95abe3a9ef9f11a94830aa", size = 14571, upload-time = "2025-09-27T18:36:14.779Z" }, + { url = "https://files.pythonhosted.org/packages/d6/25/55dc3ab959917602c96985cb1253efaa4ff42f71194bddeb61eb7278b8be/markupsafe-3.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:c4ffb7ebf07cfe8931028e3e4c85f0357459a3f9f9490886198848f4fa002ec8", size = 15056, upload-time = "2025-09-27T18:36:16.125Z" }, + { url = "https://files.pythonhosted.org/packages/d0/9e/0a02226640c255d1da0b8d12e24ac2aa6734da68bff14c05dd53b94a0fc3/markupsafe-3.0.3-cp310-cp310-win_arm64.whl", hash = "sha256:e2103a929dfa2fcaf9bb4e7c091983a49c9ac3b19c9061b6d5427dd7d14d81a1", size = 13932, upload-time = "2025-09-27T18:36:17.311Z" }, + { url = "https://files.pythonhosted.org/packages/08/db/fefacb2136439fc8dd20e797950e749aa1f4997ed584c62cfb8ef7c2be0e/markupsafe-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad", size = 11631, upload-time = "2025-09-27T18:36:18.185Z" }, + { url = "https://files.pythonhosted.org/packages/e1/2e/5898933336b61975ce9dc04decbc0a7f2fee78c30353c5efba7f2d6ff27a/markupsafe-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a", size = 12058, upload-time = "2025-09-27T18:36:19.444Z" }, + { url = "https://files.pythonhosted.org/packages/1d/09/adf2df3699d87d1d8184038df46a9c80d78c0148492323f4693df54e17bb/markupsafe-3.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50", size = 24287, upload-time = "2025-09-27T18:36:20.768Z" }, + { url = "https://files.pythonhosted.org/packages/30/ac/0273f6fcb5f42e314c6d8cd99effae6a5354604d461b8d392b5ec9530a54/markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf", size = 22940, upload-time = "2025-09-27T18:36:22.249Z" }, + { url = "https://files.pythonhosted.org/packages/19/ae/31c1be199ef767124c042c6c3e904da327a2f7f0cd63a0337e1eca2967a8/markupsafe-3.0.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f", size = 21887, upload-time = "2025-09-27T18:36:23.535Z" }, + { url = "https://files.pythonhosted.org/packages/b2/76/7edcab99d5349a4532a459e1fe64f0b0467a3365056ae550d3bcf3f79e1e/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a", size = 23692, upload-time = "2025-09-27T18:36:24.823Z" }, + { url = "https://files.pythonhosted.org/packages/a4/28/6e74cdd26d7514849143d69f0bf2399f929c37dc2b31e6829fd2045b2765/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115", size = 21471, upload-time = "2025-09-27T18:36:25.95Z" }, + { url = "https://files.pythonhosted.org/packages/62/7e/a145f36a5c2945673e590850a6f8014318d5577ed7e5920a4b3448e0865d/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a", size = 22923, upload-time = "2025-09-27T18:36:27.109Z" }, + { url = "https://files.pythonhosted.org/packages/0f/62/d9c46a7f5c9adbeeeda52f5b8d802e1094e9717705a645efc71b0913a0a8/markupsafe-3.0.3-cp311-cp311-win32.whl", hash = "sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19", size = 14572, upload-time = "2025-09-27T18:36:28.045Z" }, + { url = "https://files.pythonhosted.org/packages/83/8a/4414c03d3f891739326e1783338e48fb49781cc915b2e0ee052aa490d586/markupsafe-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01", size = 15077, upload-time = "2025-09-27T18:36:29.025Z" }, + { url = "https://files.pythonhosted.org/packages/35/73/893072b42e6862f319b5207adc9ae06070f095b358655f077f69a35601f0/markupsafe-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c", size = 13876, upload-time = "2025-09-27T18:36:29.954Z" }, + { url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615, upload-time = "2025-09-27T18:36:30.854Z" }, + { url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020, upload-time = "2025-09-27T18:36:31.971Z" }, + { url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332, upload-time = "2025-09-27T18:36:32.813Z" }, + { url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947, upload-time = "2025-09-27T18:36:33.86Z" }, + { url = "https://files.pythonhosted.org/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962, upload-time = "2025-09-27T18:36:35.099Z" }, + { url = "https://files.pythonhosted.org/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760, upload-time = "2025-09-27T18:36:36.001Z" }, + { url = "https://files.pythonhosted.org/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529, upload-time = "2025-09-27T18:36:36.906Z" }, + { url = "https://files.pythonhosted.org/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015, upload-time = "2025-09-27T18:36:37.868Z" }, + { url = "https://files.pythonhosted.org/packages/2f/e1/78ee7a023dac597a5825441ebd17170785a9dab23de95d2c7508ade94e0e/markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", size = 14540, upload-time = "2025-09-27T18:36:38.761Z" }, + { url = "https://files.pythonhosted.org/packages/aa/5b/bec5aa9bbbb2c946ca2733ef9c4ca91c91b6a24580193e891b5f7dbe8e1e/markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", size = 15105, upload-time = "2025-09-27T18:36:39.701Z" }, + { url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906, upload-time = "2025-09-27T18:36:40.689Z" }, + { url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" }, + { url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" }, + { url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" }, + { url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" }, + { url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" }, + { url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" }, + { url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" }, + { url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" }, + { url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" }, + { url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" }, + { url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" }, + { url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" }, + { url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" }, + { url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" }, + { url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" }, + { url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" }, + { url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" }, + { url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" }, + { url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" }, + { url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" }, + { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" }, + { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" }, + { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" }, + { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" }, + { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" }, + { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" }, + { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" }, + { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" }, + { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" }, + { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" }, + { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" }, + { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" }, + { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" }, + { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" }, + { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" }, + { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" }, + { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" }, + { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, +] + +[[package]] +name = "mdit-py-plugins" +version = "0.6.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py", version = "3.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "markdown-it-py", version = "4.2.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/59/fc/f8d0863f8862f25602c0404d75568e89fb6b4109804645e5cdfb1be5cf56/mdit_py_plugins-0.6.1.tar.gz", hash = "sha256:a2bca0f039f39dbd35fb74ae1b5f998608c437463371f0ff7f49a19a17a114d0", size = 56114, upload-time = "2026-05-13T09:03:38.91Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a5/69/6da5581c6a7fede7dc261bf4e67d6adca4196f176b43288b55b3db395b6e/mdit_py_plugins-0.6.1-py3-none-any.whl", hash = "sha256:214c82fb2ac524472ab6a5bcab1de80f73b50443e187f401bfd77efbc7c6481d", size = 66663, upload-time = "2026-05-13T09:03:37.76Z" }, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, +] + +[[package]] +name = "myst-parser" +version = "4.0.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11'", +] +dependencies = [ + { name = "docutils", version = "0.21.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "jinja2", marker = "python_full_version < '3.11'" }, + { name = "markdown-it-py", version = "3.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "mdit-py-plugins", marker = "python_full_version < '3.11'" }, + { name = "pyyaml", marker = "python_full_version < '3.11'" }, + { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/a5/9626ba4f73555b3735ad86247a8077d4603aa8628537687c839ab08bfe44/myst_parser-4.0.1.tar.gz", hash = "sha256:5cfea715e4f3574138aecbf7d54132296bfd72bb614d31168f48c477a830a7c4", size = 93985, upload-time = "2025-02-12T10:53:03.833Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5f/df/76d0321c3797b54b60fef9ec3bd6f4cfd124b9e422182156a1dd418722cf/myst_parser-4.0.1-py3-none-any.whl", hash = "sha256:9134e88959ec3b5780aedf8a99680ea242869d012e8821db3126d427edc9c95d", size = 84579, upload-time = "2025-02-12T10:53:02.078Z" }, +] + +[[package]] +name = "myst-parser" +version = "5.1.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.12'", + "python_full_version == '3.11.*'", +] +dependencies = [ + { name = "docutils", version = "0.22.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "jinja2", marker = "python_full_version >= '3.11'" }, + { name = "markdown-it-py", version = "4.2.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "mdit-py-plugins", marker = "python_full_version >= '3.11'" }, + { name = "pyyaml", marker = "python_full_version >= '3.11'" }, + { name = "sphinx", version = "9.0.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, + { name = "sphinx", version = "9.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/21/dc/603751677fff302f34396e206b610f556a59d7fe58b9a2145f54e96b48e8/myst_parser-5.1.0.tar.gz", hash = "sha256:ab69322dc6719dcc7f296479dbb70181b66df6ed315064f92dbc85c0e1bf2f02", size = 101182, upload-time = "2026-05-13T09:38:19.361Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/09/dc/f3dfb7488b770f3f67e6545085bf2abea5172e88f57b8ad25ef860ca704c/myst_parser-5.1.0-py3-none-any.whl", hash = "sha256:9c91c52b3cdb4d94a6506e4fab4e2f296c7623a0da0dcbe6de1565c3dad67a8a", size = 85817, upload-time = "2026-05-13T09:38:17.904Z" }, +] + +[[package]] +name = "packaging" +version = "26.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/f1/e7a6dd94a8d4a5626c03e4e99c87f241ba9e350cd9e6d75123f992427270/packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661", size = 228134, upload-time = "2026-04-24T20:15:23.917Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" }, +] + +[[package]] +name = "pefile" +version = "2024.8.26" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/03/4f/2750f7f6f025a1507cd3b7218691671eecfd0bbebebe8b39aa0fe1d360b8/pefile-2024.8.26.tar.gz", hash = "sha256:3ff6c5d8b43e8c37bb6e6dd5085658d658a7a0bdcd20b6a07b1fcfc1c4e9d632", size = 76008, upload-time = "2024-08-26T20:58:38.155Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/16/12b82f791c7f50ddec566873d5bdd245baa1491bac11d15ffb98aecc8f8b/pefile-2024.8.26-py3-none-any.whl", hash = "sha256:76f8b485dcd3b1bb8166f1128d395fa3d87af26360c2358fb75b80019b957c6f", size = 74766, upload-time = "2024-08-26T21:01:02.632Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "pyelftools" +version = "0.32" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b9/ab/33968940b2deb3d92f5b146bc6d4009a5f95d1d06c148ea2f9ee965071af/pyelftools-0.32.tar.gz", hash = "sha256:6de90ee7b8263e740c8715a925382d4099b354f29ac48ea40d840cf7aa14ace5", size = 15047199, upload-time = "2025-02-19T14:20:05.549Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/af/43/700932c4f0638c3421177144a2e86448c0d75dbaee2c7936bda3f9fd0878/pyelftools-0.32-py3-none-any.whl", hash = "sha256:013df952a006db5e138b1edf6d8a68ecc50630adbd0d83a2d41e7f846163d738", size = 188525, upload-time = "2025-02-19T14:19:59.919Z" }, +] + +[[package]] +name = "pygments" +version = "2.20.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, +] + +[[package]] +name = "pytest" +version = "9.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e4/47/b9efed96c114afcfa3c9d3fe98a76a1d14c74a9e266d397cf6eb64be5e01/pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313", size = 1636369, upload-time = "2026-06-19T10:58:32.857Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536, upload-time = "2026-06-19T10:58:31.347Z" }, +] + +[[package]] +name = "pytest-bazel" +version = "0.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/90/4726a39728fb5a5f77a773c5e9d7bfd3a0120aa2029963a88fd06e2c5fb2/pytest_bazel-0.1.6-py3-none-any.whl", hash = "sha256:a29e80e1d67c3db801bdd4d0b6b742f2bfb48cd6841caa33401458e5c4e29c21", size = 10126, upload-time = "2025-10-31T08:41:17.165Z" }, +] + +[[package]] +name = "pytest-mock" +version = "3.15.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/68/14/eb014d26be205d38ad5ad20d9a80f7d201472e08167f0bb4361e251084a9/pytest_mock-3.15.1.tar.gz", hash = "sha256:1849a238f6f396da19762269de72cb1814ab44416fa73a8686deac10b0d87a0f", size = 34036, upload-time = "2025-09-16T16:37:27.081Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/cc/06253936f4a7fa2e0f48dfe6d851d9c56df896a9ab09ac019d70b760619c/pytest_mock-3.15.1-py3-none-any.whl", hash = "sha256:0a25e2eb88fe5168d535041d09a4529a188176ae608a6d249ee65abc0949630d", size = 10095, upload-time = "2025-09-16T16:37:25.734Z" }, +] + +[[package]] +name = "pyyaml" +version = "6.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/a0/39350dd17dd6d6c6507025c0e53aef67a9293a6d37d3511f23ea510d5800/pyyaml-6.0.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b", size = 184227, upload-time = "2025-09-25T21:31:46.04Z" }, + { url = "https://files.pythonhosted.org/packages/05/14/52d505b5c59ce73244f59c7a50ecf47093ce4765f116cdb98286a71eeca2/pyyaml-6.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956", size = 174019, upload-time = "2025-09-25T21:31:47.706Z" }, + { url = "https://files.pythonhosted.org/packages/43/f7/0e6a5ae5599c838c696adb4e6330a59f463265bfa1e116cfd1fbb0abaaae/pyyaml-6.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8", size = 740646, upload-time = "2025-09-25T21:31:49.21Z" }, + { url = "https://files.pythonhosted.org/packages/2f/3a/61b9db1d28f00f8fd0ae760459a5c4bf1b941baf714e207b6eb0657d2578/pyyaml-6.0.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198", size = 840793, upload-time = "2025-09-25T21:31:50.735Z" }, + { url = "https://files.pythonhosted.org/packages/7a/1e/7acc4f0e74c4b3d9531e24739e0ab832a5edf40e64fbae1a9c01941cabd7/pyyaml-6.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b", size = 770293, upload-time = "2025-09-25T21:31:51.828Z" }, + { url = "https://files.pythonhosted.org/packages/8b/ef/abd085f06853af0cd59fa5f913d61a8eab65d7639ff2a658d18a25d6a89d/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0", size = 732872, upload-time = "2025-09-25T21:31:53.282Z" }, + { url = "https://files.pythonhosted.org/packages/1f/15/2bc9c8faf6450a8b3c9fc5448ed869c599c0a74ba2669772b1f3a0040180/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69", size = 758828, upload-time = "2025-09-25T21:31:54.807Z" }, + { url = "https://files.pythonhosted.org/packages/a3/00/531e92e88c00f4333ce359e50c19b8d1de9fe8d581b1534e35ccfbc5f393/pyyaml-6.0.3-cp310-cp310-win32.whl", hash = "sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e", size = 142415, upload-time = "2025-09-25T21:31:55.885Z" }, + { url = "https://files.pythonhosted.org/packages/2a/fa/926c003379b19fca39dd4634818b00dec6c62d87faf628d1394e137354d4/pyyaml-6.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c", size = 158561, upload-time = "2025-09-25T21:31:57.406Z" }, + { url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" }, + { url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" }, + { url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" }, + { url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114, upload-time = "2025-09-25T21:32:03.376Z" }, + { url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638, upload-time = "2025-09-25T21:32:04.553Z" }, + { url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463, upload-time = "2025-09-25T21:32:06.152Z" }, + { url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986, upload-time = "2025-09-25T21:32:07.367Z" }, + { url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543, upload-time = "2025-09-25T21:32:08.95Z" }, + { url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763, upload-time = "2025-09-25T21:32:09.96Z" }, + { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" }, + { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" }, + { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" }, + { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" }, + { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" }, + { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" }, + { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" }, + { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" }, + { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" }, + { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" }, + { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" }, + { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" }, + { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, + { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" }, + { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" }, + { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" }, + { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, + { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, + { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, + { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, + { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, + { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, + { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, + { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, + { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, + { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, + { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, + { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, + { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, + { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, + { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, + { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, + { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, +] + +[[package]] +name = "readthedocs-sphinx-ext" +version = "2.2.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jinja2" }, + { name = "packaging" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e8/ce/38130d8dec600bf5413eb89a3413dd38f204c7c728c4947e12ff8cb793b7/readthedocs-sphinx-ext-2.2.5.tar.gz", hash = "sha256:ee5fd5b99db9f0c180b2396cbce528aa36671951b9526bb0272dbfce5517bd27", size = 12303, upload-time = "2023-12-19T10:00:49.573Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/64/71/c89e7709a0d4f93af1848e9855112299a820b470d84f917b4dd5998bdd07/readthedocs_sphinx_ext-2.2.5-py2.py3-none-any.whl", hash = "sha256:f8c56184ea011c972dd45a90122568587cc85b0127bc9cf064d17c68bc809daa", size = 11332, upload-time = "2023-12-19T10:00:43.972Z" }, +] + +[[package]] +name = "requests" +version = "2.34.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ac/c3/e2a2b89f2d3e2179abd6d00ebd70bff6273f37fb3e0cc209f48b39d00cbf/requests-2.34.2.tar.gz", hash = "sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed", size = 142856, upload-time = "2026-05-14T19:25:27.735Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0", size = 73075, upload-time = "2026-05-14T19:25:26.443Z" }, +] + +[[package]] +name = "roman-numerals" +version = "4.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ae/f9/41dc953bbeb056c17d5f7a519f50fdf010bd0553be2d630bc69d1e022703/roman_numerals-4.1.0.tar.gz", hash = "sha256:1af8b147eb1405d5839e78aeb93131690495fe9da5c91856cb33ad55a7f1e5b2", size = 9077, upload-time = "2025-12-17T18:25:34.381Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/54/6f679c435d28e0a568d8e8a7c0a93a09010818634c3c3907fc98d8983770/roman_numerals-4.1.0-py3-none-any.whl", hash = "sha256:647ba99caddc2cc1e55a51e4360689115551bf4476d90e8162cf8c345fe233c7", size = 7676, upload-time = "2025-12-17T18:25:33.098Z" }, +] + +[[package]] +name = "rules-python-dev" +version = "0.0.0" +source = { virtual = "." } +dependencies = [ + { name = "absl-py" }, + { name = "macholib" }, + { name = "markupsafe" }, + { name = "myst-parser", version = "4.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "myst-parser", version = "5.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "pefile" }, + { name = "pyelftools" }, + { name = "pytest" }, + { name = "pytest-bazel" }, + { name = "pytest-mock" }, + { name = "readthedocs-sphinx-ext" }, + { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "sphinx", version = "9.0.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, + { name = "sphinx", version = "9.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "sphinx-autodoc2" }, + { name = "sphinx-reredirects", version = "0.1.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "sphinx-reredirects", version = "1.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "sphinx-rtd-theme" }, + { name = "typing-extensions" }, +] + +[package.metadata] +requires-dist = [ + { name = "absl-py" }, + { name = "macholib" }, + { name = "markupsafe" }, + { name = "myst-parser" }, + { name = "pefile" }, + { name = "pyelftools" }, + { name = "pytest" }, + { name = "pytest-bazel" }, + { name = "pytest-mock" }, + { name = "readthedocs-sphinx-ext" }, + { name = "sphinx" }, + { name = "sphinx-autodoc2" }, + { name = "sphinx-reredirects" }, + { name = "sphinx-rtd-theme", specifier = ">=2.0" }, + { name = "typing-extensions" }, +] + +[[package]] +name = "snowballstemmer" +version = "3.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/75/a7/9810d872919697c9d01295633f5d574fb416d47e535f258272ca1f01f447/snowballstemmer-3.0.1.tar.gz", hash = "sha256:6d5eeeec8e9f84d4d56b847692bacf79bc2c8e90c7f80ca4444ff8b6f2e52895", size = 105575, upload-time = "2025-05-09T16:34:51.843Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c8/78/3565d011c61f5a43488987ee32b6f3f656e7f107ac2782dd57bdd7d91d9a/snowballstemmer-3.0.1-py3-none-any.whl", hash = "sha256:6cd7b3897da8d6c9ffb968a6781fa6532dce9c3618a4b127d920dab764a19064", size = 103274, upload-time = "2025-05-09T16:34:50.371Z" }, +] + +[[package]] +name = "sphinx" +version = "8.1.3" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11'", +] +dependencies = [ + { name = "alabaster", marker = "python_full_version < '3.11'" }, + { name = "babel", marker = "python_full_version < '3.11'" }, + { name = "colorama", marker = "python_full_version < '3.11' and sys_platform == 'win32'" }, + { name = "docutils", version = "0.21.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "imagesize", marker = "python_full_version < '3.11'" }, + { name = "jinja2", marker = "python_full_version < '3.11'" }, + { name = "packaging", marker = "python_full_version < '3.11'" }, + { name = "pygments", marker = "python_full_version < '3.11'" }, + { name = "requests", marker = "python_full_version < '3.11'" }, + { name = "snowballstemmer", marker = "python_full_version < '3.11'" }, + { name = "sphinxcontrib-applehelp", marker = "python_full_version < '3.11'" }, + { name = "sphinxcontrib-devhelp", marker = "python_full_version < '3.11'" }, + { name = "sphinxcontrib-htmlhelp", marker = "python_full_version < '3.11'" }, + { name = "sphinxcontrib-jsmath", marker = "python_full_version < '3.11'" }, + { name = "sphinxcontrib-qthelp", marker = "python_full_version < '3.11'" }, + { name = "sphinxcontrib-serializinghtml", marker = "python_full_version < '3.11'" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/be0b61178fe2cdcb67e2a92fc9ebb488e3c51c4f74a36a7824c0adf23425/sphinx-8.1.3.tar.gz", hash = "sha256:43c1911eecb0d3e161ad78611bc905d1ad0e523e4ddc202a58a821773dc4c927", size = 8184611, upload-time = "2024-10-13T20:27:13.93Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/26/60/1ddff83a56d33aaf6f10ec8ce84b4c007d9368b21008876fceda7e7381ef/sphinx-8.1.3-py3-none-any.whl", hash = "sha256:09719015511837b76bf6e03e42eb7595ac8c2e41eeb9c29c5b755c6b677992a2", size = 3487125, upload-time = "2024-10-13T20:27:10.448Z" }, +] + +[[package]] +name = "sphinx" +version = "9.0.4" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.11.*'", +] +dependencies = [ + { name = "alabaster", marker = "python_full_version == '3.11.*'" }, + { name = "babel", marker = "python_full_version == '3.11.*'" }, + { name = "colorama", marker = "python_full_version == '3.11.*' and sys_platform == 'win32'" }, + { name = "docutils", version = "0.22.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, + { name = "imagesize", marker = "python_full_version == '3.11.*'" }, + { name = "jinja2", marker = "python_full_version == '3.11.*'" }, + { name = "packaging", marker = "python_full_version == '3.11.*'" }, + { name = "pygments", marker = "python_full_version == '3.11.*'" }, + { name = "requests", marker = "python_full_version == '3.11.*'" }, + { name = "roman-numerals", marker = "python_full_version == '3.11.*'" }, + { name = "snowballstemmer", marker = "python_full_version == '3.11.*'" }, + { name = "sphinxcontrib-applehelp", marker = "python_full_version == '3.11.*'" }, + { name = "sphinxcontrib-devhelp", marker = "python_full_version == '3.11.*'" }, + { name = "sphinxcontrib-htmlhelp", marker = "python_full_version == '3.11.*'" }, + { name = "sphinxcontrib-jsmath", marker = "python_full_version == '3.11.*'" }, + { name = "sphinxcontrib-qthelp", marker = "python_full_version == '3.11.*'" }, + { name = "sphinxcontrib-serializinghtml", marker = "python_full_version == '3.11.*'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/42/50/a8c6ccc36d5eacdfd7913ddccd15a9cee03ecafc5ee2bc40e1f168d85022/sphinx-9.0.4.tar.gz", hash = "sha256:594ef59d042972abbc581d8baa577404abe4e6c3b04ef61bd7fc2acbd51f3fa3", size = 8710502, upload-time = "2025-12-04T07:45:27.343Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c6/3f/4bbd76424c393caead2e1eb89777f575dee5c8653e2d4b6afd7a564f5974/sphinx-9.0.4-py3-none-any.whl", hash = "sha256:5bebc595a5e943ea248b99c13814c1c5e10b3ece718976824ffa7959ff95fffb", size = 3917713, upload-time = "2025-12-04T07:45:24.944Z" }, +] + +[[package]] +name = "sphinx" +version = "9.1.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.12'", +] +dependencies = [ + { name = "alabaster", marker = "python_full_version >= '3.12'" }, + { name = "babel", marker = "python_full_version >= '3.12'" }, + { name = "colorama", marker = "python_full_version >= '3.12' and sys_platform == 'win32'" }, + { name = "docutils", version = "0.22.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "imagesize", marker = "python_full_version >= '3.12'" }, + { name = "jinja2", marker = "python_full_version >= '3.12'" }, + { name = "packaging", marker = "python_full_version >= '3.12'" }, + { name = "pygments", marker = "python_full_version >= '3.12'" }, + { name = "requests", marker = "python_full_version >= '3.12'" }, + { name = "roman-numerals", marker = "python_full_version >= '3.12'" }, + { name = "snowballstemmer", marker = "python_full_version >= '3.12'" }, + { name = "sphinxcontrib-applehelp", marker = "python_full_version >= '3.12'" }, + { name = "sphinxcontrib-devhelp", marker = "python_full_version >= '3.12'" }, + { name = "sphinxcontrib-htmlhelp", marker = "python_full_version >= '3.12'" }, + { name = "sphinxcontrib-jsmath", marker = "python_full_version >= '3.12'" }, + { name = "sphinxcontrib-qthelp", marker = "python_full_version >= '3.12'" }, + { name = "sphinxcontrib-serializinghtml", marker = "python_full_version >= '3.12'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cd/bd/f08eb0f4eed5c83f1ba2a3bd18f7745a2b1525fad70660a1c00224ec468a/sphinx-9.1.0.tar.gz", hash = "sha256:7741722357dd75f8190766926071fed3bdc211c74dd2d7d4df5404da95930ddb", size = 8718324, upload-time = "2025-12-31T15:09:27.646Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/73/f7/b1884cb3188ab181fc81fa00c266699dab600f927a964df02ec3d5d1916a/sphinx-9.1.0-py3-none-any.whl", hash = "sha256:c84fdd4e782504495fe4f2c0b3413d6c2bf388589bb352d439b2a3bb99991978", size = 3921742, upload-time = "2025-12-31T15:09:25.561Z" }, +] + +[[package]] +name = "sphinx-autodoc2" +version = "0.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "astroid" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/17/5f/5350046d1aa1a56b063ae08b9ad871025335c9d55fe2372896ea48711da9/sphinx_autodoc2-0.5.0.tar.gz", hash = "sha256:7d76044aa81d6af74447080182b6868c7eb066874edc835e8ddf810735b6565a", size = 115077, upload-time = "2023-11-27T07:27:51.407Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/19/e6/48d47961bbdae755ba9c17dfc65d89356312c67668dcb36c87cfadfa1964/sphinx_autodoc2-0.5.0-py3-none-any.whl", hash = "sha256:e867013b1512f9d6d7e6f6799f8b537d6884462acd118ef361f3f619a60b5c9e", size = 43385, upload-time = "2023-11-27T07:27:49.929Z" }, +] + +[[package]] +name = "sphinx-reredirects" +version = "0.1.6" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11'", +] +dependencies = [ + { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/16/6b/bcca2785de4071f604a722444d4d7ba8a9d40de3c14ad52fce93e6d92694/sphinx_reredirects-0.1.6.tar.gz", hash = "sha256:c491cba545f67be9697508727818d8626626366245ae64456fe29f37e9bbea64", size = 7080, upload-time = "2025-03-22T10:52:30.271Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ac/6f/0b3625be30a1a50f9e4c2cb2ec147b08f15ed0e9f8444efcf274b751300b/sphinx_reredirects-0.1.6-py3-none-any.whl", hash = "sha256:efd50c766fbc5bf40cd5148e10c00f2c00d143027de5c5e48beece93cc40eeea", size = 5675, upload-time = "2025-03-22T10:52:29.113Z" }, +] + +[[package]] +name = "sphinx-reredirects" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.12'", + "python_full_version == '3.11.*'", +] +dependencies = [ + { name = "sphinx", version = "9.0.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, + { name = "sphinx", version = "9.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1b/8d/0e39fe2740d7d71417edf9a6424aa80ca2c27c17fc21282cdc39f90d5a40/sphinx_reredirects-1.1.0.tar.gz", hash = "sha256:fb9b195335ab14b43f8273287d0c7eeb637ba6c56c66581c11b47202f6718b29", size = 614624, upload-time = "2025-12-22T08:28:02.792Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/81/b5dd07067f3daac6d23687ec737b2d593740671ebcd145830c8f92d381c5/sphinx_reredirects-1.1.0-py3-none-any.whl", hash = "sha256:4b5692273c72cd2d4d917f4c6f87d5919e4d6114a752d4be033f7f5f6310efd9", size = 6351, upload-time = "2025-12-22T08:27:59.724Z" }, +] + +[[package]] +name = "sphinx-rtd-theme" +version = "3.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "docutils", version = "0.21.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "docutils", version = "0.22.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "sphinx", version = "9.0.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, + { name = "sphinx", version = "9.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "sphinxcontrib-jquery" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/84/68/a1bfbf38c0f7bccc9b10bbf76b94606f64acb1552ae394f0b8285bfaea25/sphinx_rtd_theme-3.1.0.tar.gz", hash = "sha256:b44276f2c276e909239a4f6c955aa667aaafeb78597923b1c60babc76db78e4c", size = 7620915, upload-time = "2026-01-12T16:03:31.17Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/87/c7/b5c8015d823bfda1a346adb2c634a2101d50bb75d421eb6dcb31acd25ebc/sphinx_rtd_theme-3.1.0-py2.py3-none-any.whl", hash = "sha256:1785824ae8e6632060490f67cf3a72d404a85d2d9fc26bce3619944de5682b89", size = 7655617, upload-time = "2026-01-12T16:03:28.101Z" }, +] + +[[package]] +name = "sphinxcontrib-applehelp" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ba/6e/b837e84a1a704953c62ef8776d45c3e8d759876b4a84fe14eba2859106fe/sphinxcontrib_applehelp-2.0.0.tar.gz", hash = "sha256:2f29ef331735ce958efa4734873f084941970894c6090408b079c61b2e1c06d1", size = 20053, upload-time = "2024-07-29T01:09:00.465Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5d/85/9ebeae2f76e9e77b952f4b274c27238156eae7979c5421fba91a28f4970d/sphinxcontrib_applehelp-2.0.0-py3-none-any.whl", hash = "sha256:4cd3f0ec4ac5dd9c17ec65e9ab272c9b867ea77425228e68ecf08d6b28ddbdb5", size = 119300, upload-time = "2024-07-29T01:08:58.99Z" }, +] + +[[package]] +name = "sphinxcontrib-devhelp" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/d2/5beee64d3e4e747f316bae86b55943f51e82bb86ecd325883ef65741e7da/sphinxcontrib_devhelp-2.0.0.tar.gz", hash = "sha256:411f5d96d445d1d73bb5d52133377b4248ec79db5c793ce7dbe59e074b4dd1ad", size = 12967, upload-time = "2024-07-29T01:09:23.417Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/35/7a/987e583882f985fe4d7323774889ec58049171828b58c2217e7f79cdf44e/sphinxcontrib_devhelp-2.0.0-py3-none-any.whl", hash = "sha256:aefb8b83854e4b0998877524d1029fd3e6879210422ee3780459e28a1f03a8a2", size = 82530, upload-time = "2024-07-29T01:09:21.945Z" }, +] + +[[package]] +name = "sphinxcontrib-htmlhelp" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/93/983afd9aa001e5201eab16b5a444ed5b9b0a7a010541e0ddfbbfd0b2470c/sphinxcontrib_htmlhelp-2.1.0.tar.gz", hash = "sha256:c9e2916ace8aad64cc13a0d233ee22317f2b9025b9cf3295249fa985cc7082e9", size = 22617, upload-time = "2024-07-29T01:09:37.889Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0a/7b/18a8c0bcec9182c05a0b3ec2a776bba4ead82750a55ff798e8d406dae604/sphinxcontrib_htmlhelp-2.1.0-py3-none-any.whl", hash = "sha256:166759820b47002d22914d64a075ce08f4c46818e17cfc9470a9786b759b19f8", size = 98705, upload-time = "2024-07-29T01:09:36.407Z" }, +] + +[[package]] +name = "sphinxcontrib-jquery" +version = "4.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "sphinx", version = "9.0.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, + { name = "sphinx", version = "9.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/de/f3/aa67467e051df70a6330fe7770894b3e4f09436dea6881ae0b4f3d87cad8/sphinxcontrib-jquery-4.1.tar.gz", hash = "sha256:1620739f04e36a2c779f1a131a2dfd49b2fd07351bf1968ced074365933abc7a", size = 122331, upload-time = "2023-03-14T15:01:01.944Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/85/749bd22d1a68db7291c89e2ebca53f4306c3f205853cf31e9de279034c3c/sphinxcontrib_jquery-4.1-py2.py3-none-any.whl", hash = "sha256:f936030d7d0147dd026a4f2b5a57343d233f1fc7b363f68b3d4f1cb0993878ae", size = 121104, upload-time = "2023-03-14T15:01:00.356Z" }, +] + +[[package]] +name = "sphinxcontrib-jsmath" +version = "1.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b2/e8/9ed3830aeed71f17c026a07a5097edcf44b692850ef215b161b8ad875729/sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8", size = 5787, upload-time = "2019-01-21T16:10:16.347Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/42/4c8646762ee83602e3fb3fbe774c2fac12f317deb0b5dbeeedd2d3ba4b77/sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl", hash = "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178", size = 5071, upload-time = "2019-01-21T16:10:14.333Z" }, +] + +[[package]] +name = "sphinxcontrib-qthelp" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/68/bc/9104308fc285eb3e0b31b67688235db556cd5b0ef31d96f30e45f2e51cae/sphinxcontrib_qthelp-2.0.0.tar.gz", hash = "sha256:4fe7d0ac8fc171045be623aba3e2a8f613f8682731f9153bb2e40ece16b9bbab", size = 17165, upload-time = "2024-07-29T01:09:56.435Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/27/83/859ecdd180cacc13b1f7e857abf8582a64552ea7a061057a6c716e790fce/sphinxcontrib_qthelp-2.0.0-py3-none-any.whl", hash = "sha256:b18a828cdba941ccd6ee8445dbe72ffa3ef8cbe7505d8cd1fa0d42d3f2d5f3eb", size = 88743, upload-time = "2024-07-29T01:09:54.885Z" }, +] + +[[package]] +name = "sphinxcontrib-serializinghtml" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3b/44/6716b257b0aa6bfd51a1b31665d1c205fb12cb5ad56de752dfa15657de2f/sphinxcontrib_serializinghtml-2.0.0.tar.gz", hash = "sha256:e9d912827f872c029017a53f0ef2180b327c3f7fd23c87229f7a8e8b70031d4d", size = 16080, upload-time = "2024-07-29T01:10:09.332Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/52/a7/d2782e4e3f77c8450f727ba74a8f12756d5ba823d81b941f1b04da9d033a/sphinxcontrib_serializinghtml-2.0.0-py3-none-any.whl", hash = "sha256:6e2cb0eef194e10c27ec0023bfeb25badbbb5868244cf5bc5bdc04e4464bf331", size = 92072, upload-time = "2024-07-29T01:10:08.203Z" }, +] + +[[package]] +name = "tomli" +version = "2.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/22/de/48c59722572767841493b26183a0d1cc411d54fd759c5607c4590b6563a6/tomli-2.4.1.tar.gz", hash = "sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f", size = 17543, upload-time = "2026-03-25T20:22:03.828Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/11/db3d5885d8528263d8adc260bb2d28ebf1270b96e98f0e0268d32b8d9900/tomli-2.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f8f0fc26ec2cc2b965b7a3b87cd19c5c6b8c5e5f436b984e85f486d652285c30", size = 154704, upload-time = "2026-03-25T20:21:10.473Z" }, + { url = "https://files.pythonhosted.org/packages/6d/f7/675db52c7e46064a9aa928885a9b20f4124ecb9bc2e1ce74c9106648d202/tomli-2.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4ab97e64ccda8756376892c53a72bd1f964e519c77236368527f758fbc36a53a", size = 149454, upload-time = "2026-03-25T20:21:12.036Z" }, + { url = "https://files.pythonhosted.org/packages/61/71/81c50943cf953efa35bce7646caab3cf457a7d8c030b27cfb40d7235f9ee/tomli-2.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96481a5786729fd470164b47cdb3e0e58062a496f455ee41b4403be77cb5a076", size = 237561, upload-time = "2026-03-25T20:21:13.098Z" }, + { url = "https://files.pythonhosted.org/packages/48/c1/f41d9cb618acccca7df82aaf682f9b49013c9397212cb9f53219e3abac37/tomli-2.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a881ab208c0baf688221f8cecc5401bd291d67e38a1ac884d6736cbcd8247e9", size = 243824, upload-time = "2026-03-25T20:21:14.569Z" }, + { url = "https://files.pythonhosted.org/packages/22/e4/5a816ecdd1f8ca51fb756ef684b90f2780afc52fc67f987e3c61d800a46d/tomli-2.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47149d5bd38761ac8be13a84864bf0b7b70bc051806bc3669ab1cbc56216b23c", size = 242227, upload-time = "2026-03-25T20:21:15.712Z" }, + { url = "https://files.pythonhosted.org/packages/6b/49/2b2a0ef529aa6eec245d25f0c703e020a73955ad7edf73e7f54ddc608aa5/tomli-2.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ec9bfaf3ad2df51ace80688143a6a4ebc09a248f6ff781a9945e51937008fcbc", size = 247859, upload-time = "2026-03-25T20:21:17.001Z" }, + { url = "https://files.pythonhosted.org/packages/83/bd/6c1a630eaca337e1e78c5903104f831bda934c426f9231429396ce3c3467/tomli-2.4.1-cp311-cp311-win32.whl", hash = "sha256:ff2983983d34813c1aeb0fa89091e76c3a22889ee83ab27c5eeb45100560c049", size = 97204, upload-time = "2026-03-25T20:21:18.079Z" }, + { url = "https://files.pythonhosted.org/packages/42/59/71461df1a885647e10b6bb7802d0b8e66480c61f3f43079e0dcd315b3954/tomli-2.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:5ee18d9ebdb417e384b58fe414e8d6af9f4e7a0ae761519fb50f721de398dd4e", size = 108084, upload-time = "2026-03-25T20:21:18.978Z" }, + { url = "https://files.pythonhosted.org/packages/b8/83/dceca96142499c069475b790e7913b1044c1a4337e700751f48ed723f883/tomli-2.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:c2541745709bad0264b7d4705ad453b76ccd191e64aa6f0fc66b69a293a45ece", size = 95285, upload-time = "2026-03-25T20:21:20.309Z" }, + { url = "https://files.pythonhosted.org/packages/c1/ba/42f134a3fe2b370f555f44b1d72feebb94debcab01676bf918d0cb70e9aa/tomli-2.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c742f741d58a28940ce01d58f0ab2ea3ced8b12402f162f4d534dfe18ba1cd6a", size = 155924, upload-time = "2026-03-25T20:21:21.626Z" }, + { url = "https://files.pythonhosted.org/packages/dc/c7/62d7a17c26487ade21c5422b646110f2162f1fcc95980ef7f63e73c68f14/tomli-2.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7f86fd587c4ed9dd76f318225e7d9b29cfc5a9d43de44e5754db8d1128487085", size = 150018, upload-time = "2026-03-25T20:21:23.002Z" }, + { url = "https://files.pythonhosted.org/packages/5c/05/79d13d7c15f13bdef410bdd49a6485b1c37d28968314eabee452c22a7fda/tomli-2.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff18e6a727ee0ab0388507b89d1bc6a22b138d1e2fa56d1ad494586d61d2eae9", size = 244948, upload-time = "2026-03-25T20:21:24.04Z" }, + { url = "https://files.pythonhosted.org/packages/10/90/d62ce007a1c80d0b2c93e02cab211224756240884751b94ca72df8a875ca/tomli-2.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:136443dbd7e1dee43c68ac2694fde36b2849865fa258d39bf822c10e8068eac5", size = 253341, upload-time = "2026-03-25T20:21:25.177Z" }, + { url = "https://files.pythonhosted.org/packages/1a/7e/caf6496d60152ad4ed09282c1885cca4eea150bfd007da84aea07bcc0a3e/tomli-2.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5e262d41726bc187e69af7825504c933b6794dc3fbd5945e41a79bb14c31f585", size = 248159, upload-time = "2026-03-25T20:21:26.364Z" }, + { url = "https://files.pythonhosted.org/packages/99/e7/c6f69c3120de34bbd882c6fba7975f3d7a746e9218e56ab46a1bc4b42552/tomli-2.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5cb41aa38891e073ee49d55fbc7839cfdb2bc0e600add13874d048c94aadddd1", size = 253290, upload-time = "2026-03-25T20:21:27.46Z" }, + { url = "https://files.pythonhosted.org/packages/d6/2f/4a3c322f22c5c66c4b836ec58211641a4067364f5dcdd7b974b4c5da300c/tomli-2.4.1-cp312-cp312-win32.whl", hash = "sha256:da25dc3563bff5965356133435b757a795a17b17d01dbc0f42fb32447ddfd917", size = 98141, upload-time = "2026-03-25T20:21:28.492Z" }, + { url = "https://files.pythonhosted.org/packages/24/22/4daacd05391b92c55759d55eaee21e1dfaea86ce5c571f10083360adf534/tomli-2.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:52c8ef851d9a240f11a88c003eacb03c31fc1c9c4ec64a99a0f922b93874fda9", size = 108847, upload-time = "2026-03-25T20:21:29.386Z" }, + { url = "https://files.pythonhosted.org/packages/68/fd/70e768887666ddd9e9f5d85129e84910f2db2796f9096aa02b721a53098d/tomli-2.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:f758f1b9299d059cc3f6546ae2af89670cb1c4d48ea29c3cacc4fe7de3058257", size = 95088, upload-time = "2026-03-25T20:21:30.677Z" }, + { url = "https://files.pythonhosted.org/packages/07/06/b823a7e818c756d9a7123ba2cda7d07bc2dd32835648d1a7b7b7a05d848d/tomli-2.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:36d2bd2ad5fb9eaddba5226aa02c8ec3fa4f192631e347b3ed28186d43be6b54", size = 155866, upload-time = "2026-03-25T20:21:31.65Z" }, + { url = "https://files.pythonhosted.org/packages/14/6f/12645cf7f08e1a20c7eb8c297c6f11d31c1b50f316a7e7e1e1de6e2e7b7e/tomli-2.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:eb0dc4e38e6a1fd579e5d50369aa2e10acfc9cace504579b2faabb478e76941a", size = 149887, upload-time = "2026-03-25T20:21:33.028Z" }, + { url = "https://files.pythonhosted.org/packages/5c/e0/90637574e5e7212c09099c67ad349b04ec4d6020324539297b634a0192b0/tomli-2.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7f2c7f2b9ca6bdeef8f0fa897f8e05085923eb091721675170254cbc5b02897", size = 243704, upload-time = "2026-03-25T20:21:34.51Z" }, + { url = "https://files.pythonhosted.org/packages/10/8f/d3ddb16c5a4befdf31a23307f72828686ab2096f068eaf56631e136c1fdd/tomli-2.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f3c6818a1a86dd6dca7ddcaaf76947d5ba31aecc28cb1b67009a5877c9a64f3f", size = 251628, upload-time = "2026-03-25T20:21:36.012Z" }, + { url = "https://files.pythonhosted.org/packages/e3/f1/dbeeb9116715abee2485bf0a12d07a8f31af94d71608c171c45f64c0469d/tomli-2.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d312ef37c91508b0ab2cee7da26ec0b3ed2f03ce12bd87a588d771ae15dcf82d", size = 247180, upload-time = "2026-03-25T20:21:37.136Z" }, + { url = "https://files.pythonhosted.org/packages/d3/74/16336ffd19ed4da28a70959f92f506233bd7cfc2332b20bdb01591e8b1d1/tomli-2.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51529d40e3ca50046d7606fa99ce3956a617f9b36380da3b7f0dd3dd28e68cb5", size = 251674, upload-time = "2026-03-25T20:21:38.298Z" }, + { url = "https://files.pythonhosted.org/packages/16/f9/229fa3434c590ddf6c0aa9af64d3af4b752540686cace29e6281e3458469/tomli-2.4.1-cp313-cp313-win32.whl", hash = "sha256:2190f2e9dd7508d2a90ded5ed369255980a1bcdd58e52f7fe24b8162bf9fedbd", size = 97976, upload-time = "2026-03-25T20:21:39.316Z" }, + { url = "https://files.pythonhosted.org/packages/6a/1e/71dfd96bcc1c775420cb8befe7a9d35f2e5b1309798f009dca17b7708c1e/tomli-2.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:8d65a2fbf9d2f8352685bc1364177ee3923d6baf5e7f43ea4959d7d8bc326a36", size = 108755, upload-time = "2026-03-25T20:21:40.248Z" }, + { url = "https://files.pythonhosted.org/packages/83/7a/d34f422a021d62420b78f5c538e5b102f62bea616d1d75a13f0a88acb04a/tomli-2.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:4b605484e43cdc43f0954ddae319fb75f04cc10dd80d830540060ee7cd0243cd", size = 95265, upload-time = "2026-03-25T20:21:41.219Z" }, + { url = "https://files.pythonhosted.org/packages/3c/fb/9a5c8d27dbab540869f7c1f8eb0abb3244189ce780ba9cd73f3770662072/tomli-2.4.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fd0409a3653af6c147209d267a0e4243f0ae46b011aa978b1080359fddc9b6cf", size = 155726, upload-time = "2026-03-25T20:21:42.23Z" }, + { url = "https://files.pythonhosted.org/packages/62/05/d2f816630cc771ad836af54f5001f47a6f611d2d39535364f148b6a92d6b/tomli-2.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a120733b01c45e9a0c34aeef92bf0cf1d56cfe81ed9d47d562f9ed591a9828ac", size = 149859, upload-time = "2026-03-25T20:21:43.386Z" }, + { url = "https://files.pythonhosted.org/packages/ce/48/66341bdb858ad9bd0ceab5a86f90eddab127cf8b046418009f2125630ecb/tomli-2.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:559db847dc486944896521f68d8190be1c9e719fced785720d2216fe7022b662", size = 244713, upload-time = "2026-03-25T20:21:44.474Z" }, + { url = "https://files.pythonhosted.org/packages/df/6d/c5fad00d82b3c7a3ab6189bd4b10e60466f22cfe8a08a9394185c8a8111c/tomli-2.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853", size = 252084, upload-time = "2026-03-25T20:21:45.62Z" }, + { url = "https://files.pythonhosted.org/packages/00/71/3a69e86f3eafe8c7a59d008d245888051005bd657760e96d5fbfb0b740c2/tomli-2.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7f94b27a62cfad8496c8d2513e1a222dd446f095fca8987fceef261225538a15", size = 247973, upload-time = "2026-03-25T20:21:46.937Z" }, + { url = "https://files.pythonhosted.org/packages/67/50/361e986652847fec4bd5e4a0208752fbe64689c603c7ae5ea7cb16b1c0ca/tomli-2.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ede3e6487c5ef5d28634ba3f31f989030ad6af71edfb0055cbbd14189ff240ba", size = 256223, upload-time = "2026-03-25T20:21:48.467Z" }, + { url = "https://files.pythonhosted.org/packages/8c/9a/b4173689a9203472e5467217e0154b00e260621caa227b6fa01feab16998/tomli-2.4.1-cp314-cp314-win32.whl", hash = "sha256:3d48a93ee1c9b79c04bb38772ee1b64dcf18ff43085896ea460ca8dec96f35f6", size = 98973, upload-time = "2026-03-25T20:21:49.526Z" }, + { url = "https://files.pythonhosted.org/packages/14/58/640ac93bf230cd27d002462c9af0d837779f8773bc03dee06b5835208214/tomli-2.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:88dceee75c2c63af144e456745e10101eb67361050196b0b6af5d717254dddf7", size = 109082, upload-time = "2026-03-25T20:21:50.506Z" }, + { url = "https://files.pythonhosted.org/packages/d5/2f/702d5e05b227401c1068f0d386d79a589bb12bf64c3d2c72ce0631e3bc49/tomli-2.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:b8c198f8c1805dc42708689ed6864951fd2494f924149d3e4bce7710f8eb5232", size = 96490, upload-time = "2026-03-25T20:21:51.474Z" }, + { url = "https://files.pythonhosted.org/packages/45/4b/b877b05c8ba62927d9865dd980e34a755de541eb65fffba52b4cc495d4d2/tomli-2.4.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:d4d8fe59808a54658fcc0160ecfb1b30f9089906c50b23bcb4c69eddc19ec2b4", size = 164263, upload-time = "2026-03-25T20:21:52.543Z" }, + { url = "https://files.pythonhosted.org/packages/24/79/6ab420d37a270b89f7195dec5448f79400d9e9c1826df982f3f8e97b24fd/tomli-2.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7008df2e7655c495dd12d2a4ad038ff878d4ca4b81fccaf82b714e07eae4402c", size = 160736, upload-time = "2026-03-25T20:21:53.674Z" }, + { url = "https://files.pythonhosted.org/packages/02/e0/3630057d8eb170310785723ed5adcdfb7d50cb7e6455f85ba8a3deed642b/tomli-2.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1d8591993e228b0c930c4bb0db464bdad97b3289fb981255d6c9a41aedc84b2d", size = 270717, upload-time = "2026-03-25T20:21:55.129Z" }, + { url = "https://files.pythonhosted.org/packages/7a/b4/1613716072e544d1a7891f548d8f9ec6ce2faf42ca65acae01d76ea06bb0/tomli-2.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:734e20b57ba95624ecf1841e72b53f6e186355e216e5412de414e3c51e5e3c41", size = 278461, upload-time = "2026-03-25T20:21:56.228Z" }, + { url = "https://files.pythonhosted.org/packages/05/38/30f541baf6a3f6df77b3df16b01ba319221389e2da59427e221ef417ac0c/tomli-2.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8a650c2dbafa08d42e51ba0b62740dae4ecb9338eefa093aa5c78ceb546fcd5c", size = 274855, upload-time = "2026-03-25T20:21:57.653Z" }, + { url = "https://files.pythonhosted.org/packages/77/a3/ec9dd4fd2c38e98de34223b995a3b34813e6bdadf86c75314c928350ed14/tomli-2.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:504aa796fe0569bb43171066009ead363de03675276d2d121ac1a4572397870f", size = 283144, upload-time = "2026-03-25T20:21:59.089Z" }, + { url = "https://files.pythonhosted.org/packages/ef/be/605a6261cac79fba2ec0c9827e986e00323a1945700969b8ee0b30d85453/tomli-2.4.1-cp314-cp314t-win32.whl", hash = "sha256:b1d22e6e9387bf4739fbe23bfa80e93f6b0373a7f1b96c6227c32bef95a4d7a8", size = 108683, upload-time = "2026-03-25T20:22:00.214Z" }, + { url = "https://files.pythonhosted.org/packages/12/64/da524626d3b9cc40c168a13da8335fe1c51be12c0a63685cc6db7308daae/tomli-2.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:2c1c351919aca02858f740c6d33adea0c5deea37f9ecca1cc1ef9e884a619d26", size = 121196, upload-time = "2026-03-25T20:22:01.169Z" }, + { url = "https://files.pythonhosted.org/packages/5a/cd/e80b62269fc78fc36c9af5a6b89c835baa8af28ff5ad28c7028d60860320/tomli-2.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:eab21f45c7f66c13f2a9e0e1535309cee140182a9cdae1e041d02e47291e8396", size = 100393, upload-time = "2026-03-25T20:22:02.137Z" }, + { url = "https://files.pythonhosted.org/packages/7b/61/cceae43728b7de99d9b847560c262873a1f6c98202171fd5ed62640b494b/tomli-2.4.1-py3-none-any.whl", hash = "sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe", size = 14583, upload-time = "2026-03-25T20:22:03.012Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] + +[[package]] +name = "urllib3" +version = "2.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", size = 433602, upload-time = "2026-05-07T16:13:18.596Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" }, +] diff --git a/docs/BUILD.bazel b/docs/BUILD.bazel index 67f21b253b..baa1a1e3db 100644 --- a/docs/BUILD.bazel +++ b/docs/BUILD.bazel @@ -17,9 +17,9 @@ load("@sphinxdocs//sphinxdocs:readthedocs.bzl", "readthedocs_install") load("@sphinxdocs//sphinxdocs:sphinx.bzl", "sphinx_build_binary", "sphinx_docs") load("@sphinxdocs//sphinxdocs:sphinx_docs_library.bzl", "sphinx_docs_library") load("@sphinxdocs//sphinxdocs:sphinx_stardoc.bzl", "sphinx_stardoc", "sphinx_stardocs") +load("//python:defs.bzl", "py_binary") load("//python/private:bzlmod_enabled.bzl", "BZLMOD_ENABLED") # buildifier: disable=bzl-visibility load("//python/private:common_labels.bzl", "labels") # buildifier: disable=bzl-visibility -load("//python/uv:lock.bzl", "lock") # buildifier: disable=bzl-visibility package(default_visibility = ["//:__subpackages__"]) @@ -37,6 +37,27 @@ _TARGET_COMPATIBLE_WITH = select({ "//conditions:default": ["@platforms//:incompatible"], }) if BZLMOD_ENABLED else ["@platforms//:incompatible"] +py_binary( + name = "merge_changelog", + srcs = ["merge_changelog.py"], + target_compatible_with = _TARGET_COMPATIBLE_WITH, + deps = [ + "//tools/private/release:changelog_news", + ], +) + +genrule( + name = "merged_changelog", + srcs = [ + "//:CHANGELOG.md", + "//news:news_files", + ], + outs = ["changelog.md"], + cmd = "$(location :merge_changelog) --changelog $(location //:CHANGELOG.md) --news-dir news --output $@", + target_compatible_with = _TARGET_COMPATIBLE_WITH, + tools = [":merge_changelog"], +) + # See README.md for instructions. Short version: # * `bazel run //docs:docs.serve` in a separate terminal # * `ibazel build //docs:docs` to automatically rebuild docs @@ -60,8 +81,8 @@ sphinx_docs( "html", ], renamed_srcs = { - "//:CHANGELOG.md": "changelog.md", "//:CONTRIBUTING.md": "contributing.md", + ":merged_changelog": "changelog.md", "@sphinxdocs//sphinxdocs/inventories:bazel_inventory": "bazel_inventory.inv", }, sphinx = ":sphinx-build", @@ -84,58 +105,60 @@ build_test( sphinx_stardocs( name = "bzl_api_docs", srcs = [ - "//python:defs_bzl", - "//python:features_bzl", - "//python:packaging_bzl", - "//python:pip_bzl", - "//python:proto_bzl", - "//python:py_binary_bzl", - "//python:py_cc_link_params_info_bzl", - "//python:py_exec_tools_info_bzl", - "//python:py_exec_tools_toolchain_bzl", - "//python:py_executable_info_bzl", - "//python:py_library_bzl", - "//python:py_runtime_bzl", - "//python:py_runtime_info_bzl", - "//python:py_test_bzl", - "//python:repositories_bzl", - "//python/api:api_bzl", - "//python/api:attr_builders_bzl", - "//python/api:executables_bzl", - "//python/api:libraries_bzl", - "//python/api:rule_builders_bzl", - "//python/cc:py_cc_toolchain_bzl", - "//python/cc:py_cc_toolchain_info_bzl", - "//python/entry_points:py_console_script_binary_bzl", - "//python/extensions:config_bzl", - "//python/extensions:python_bzl", - "//python/local_toolchains:repos_bzl", - "//python/private:attr_builders_bzl", - "//python/private:builders_bzl", - "//python/private:builders_util_bzl", - "//python/private:py_binary_rule_bzl", - "//python/private:py_cc_toolchain_rule_bzl", - "//python/private:py_info_bzl", - "//python/private:py_library_rule_bzl", - "//python/private:py_runtime_rule_bzl", - "//python/private:py_test_rule_bzl", - "//python/private:rule_builders_bzl", - "//python/private/api:py_common_api_bzl", - "//python/private/pypi:config_settings_bzl", - "//python/private/pypi:env_marker_info_bzl", - "//python/private/pypi:pkg_aliases_bzl", - "//python/private/pypi:whl_config_setting_bzl", - "//python/private/pypi:whl_library_bzl", - "//python/private/zipapp:py_zipapp_rule_bzl", - "//python/uv:lock_bzl", - "//python/uv:uv_bzl", - "//python/uv:uv_toolchain_bzl", - "//python/uv:uv_toolchain_info_bzl", - "//python/zipapp:py_zipapp_binary_bzl", - "//python/zipapp:py_zipapp_test_bzl", + "//python:defs", + "//python:features", + "//python:packaging", + "//python:pip", + "//python:proto", + "//python:py_binary", + "//python:py_cc_link_params_info", + "//python:py_exec_tools_info", + "//python:py_exec_tools_toolchain", + "//python:py_executable_info", + "//python:py_info", + "//python:py_library", + "//python:py_runtime", + "//python:py_runtime_info", + "//python:py_test", + "//python:repositories", + "//python/api", + "//python/api:attr_builders", + "//python/api:executables", + "//python/api:libraries", + "//python/api:rule_builders", + "//python/cc:py_cc_toolchain", + "//python/cc:py_cc_toolchain_info", + "//python/cc:py_extension", + "//python/entry_points:py_console_script_binary", + "//python/extensions:config", + "//python/extensions:python", + "//python/local_toolchains:repos", + "//python/private:attr_builders", + "//python/private:builders", + "//python/private:builders_util", + "//python/private:py_binary_rule", + "//python/private:py_cc_toolchain_rule", + "//python/private:py_info", + "//python/private:py_library_rule", + "//python/private:py_runtime_rule", + "//python/private:py_test_rule", + "//python/private:rule_builders", + "//python/private/api:py_common_api", + "//python/private/pypi:config_settings", + "//python/private/pypi:env_marker_info", + "//python/private/pypi:pkg_aliases", + "//python/private/pypi:whl_config_setting", + "//python/private/pypi:whl_library", + "//python/private/zipapp:py_zipapp_rule", + "//python/uv", + "//python/uv:lock", + "//python/uv:uv_toolchain", + "//python/uv:uv_toolchain_info", + "//python/zipapp:py_zipapp_binary", + "//python/zipapp:py_zipapp_test", ] + ([ # This depends on @pythons_hub, which is only created under bzlmod, - "//python/extensions:pip_bzl", + "//python/extensions:pip", ] if BZLMOD_ENABLED else []), prefix = "api/rules_python/", tags = ["docs"], @@ -144,7 +167,7 @@ sphinx_stardocs( sphinx_stardoc( name = "py_runtime_pair", - src = "//python/private:py_runtime_pair_rule_bzl", + src = "//python/private:py_runtime_pair_rule", prefix = "api/rules_python/", tags = ["docs"], target_compatible_with = _TARGET_COMPATIBLE_WITH, @@ -175,6 +198,7 @@ sphinx_build_binary( target_compatible_with = _TARGET_COMPATIBLE_WITH, deps = [ "@dev_pip//myst_parser", + "@dev_pip//pytest", "@dev_pip//readthedocs_sphinx_ext", "@dev_pip//sphinx", "@dev_pip//sphinx_autodoc2", @@ -184,19 +208,3 @@ sphinx_build_binary( "@sphinxdocs//sphinxdocs/src/sphinx_bzl", ], ) - -# Run bazel run //docs:requirements.update -lock( - name = "requirements", - srcs = ["pyproject.toml"], - out = "requirements.txt", - args = [ - "--emit-index-url", - "--universal", - "--upgrade", - ], - # NOTE @aignas 2025-08-17: here we select the lowest actively supported version so that the - # requirements file is generated to be compatible with Python version 3.9 or greater. - python_version = "3.9", - visibility = ["//:__subpackages__"], -) diff --git a/docs/api/rules_python/command_line_option/index.md b/docs/api/rules_python/command_line_option/index.md index a7dfa340eb..02d29206f5 100644 --- a/docs/api/rules_python/command_line_option/index.md +++ b/docs/api/rules_python/command_line_option/index.md @@ -51,3 +51,26 @@ See the [Bazel documentation for --enable_runfiles](https://bazel.build/referenc The special value `INHERIT` can be specified to use the existing flag value. ::: + +## extra_toolchains + +:::{bzl:target} extra_toolchains + +Special target for the Bazel-builtin `//command_line_option:extra_toolchains` +flag. + +See the [Bazel documentation for --extra_toolchains](https://bazel.build/reference/command-line-reference#flag--extra_toolchains). + +The special value `INHERIT` can be specified to use the existing flag value. + +:::{note} +Unlike the normal Bazel built-in flag, which accepts a list of labels, this +pseudo-flag must be specified as a single, comma-separated string when set +using the `config_settings` attribute. For example: + +```python +"//command_line_option:extra_toolchains": "//my/tc1,//my/tc2" +``` +::: +::: + diff --git a/docs/api/rules_python/python/config_settings/index.md b/docs/api/rules_python/python/config_settings/index.md index d7bc296296..9ae0665da2 100644 --- a/docs/api/rules_python/python/config_settings/index.md +++ b/docs/api/rules_python/python/config_settings/index.md @@ -242,6 +242,44 @@ The `auto` value The `omit_if_generated_source` value was removed :::: +::::{bzl:flag} validate_test_main +Determines if `py_test` runs a build-time validation that its main module +actually runs tests. + +A common `py_test` pitfall is to define test classes or functions but forget +to add code that runs them (for example, assuming `py_test` automatically +invokes `unittest` or `pytest`). When that happens, the test does nothing and +silently passes. + +When enabled, a validation action statically analyzes the main module and +fails the build if it defines test classes or functions but its top-level body +is "inert" -- i.e. it only contains definitions, imports, assignments, and +docstrings, with nothing that actually runs tests (such as an +`if __name__ == "__main__":` guard that invokes a test runner). + +A module that defines no classes or functions at all (for example, one that +only imports other modules) is always allowed, since it isn't the +"defined some tests but forgot to run them" case this check targets. + +This is only applicable to `py_test` targets that have a `main` source file; +targets using `main_module` are not checked. + +Values: + +* `auto`: (default) Automatically decide the effective value; the current + behavior is `disabled`. +* `enabled`: Run the validation action. +* `disabled`: Don't run the validation action. + +:::{note} +Enabling this requires the exec tools toolchain (with an exec interpreter) to +be registered, which is the case for the default hermetic toolchains. +::: + +:::{versionadded} 2.3.0 +::: +:::: + ::::{bzl:flag} py_linux_libc Set what libc is used for the target platform. This will affect which whl binaries will be pulled and what toolchain will be auto-detected. Currently `rules_python` only supplies toolchains compatible with `glibc`. @@ -350,6 +388,22 @@ Values: :::: +::::{bzl:flag} venv +Determines which PyPI repository hub is used when resolving package dependencies. + +This flag is transitioned on automatically by executable targets (`py_binary`, `py_test`) +to select the appropriate concrete PyPI hub (e.g., when fallback or disjoint packages exist across multiple hubs). + +Values: +* `auto`: (default) Resolves dependencies using the fallback or first available hub. +* ``: Explicitly forces resolution of packages from the + specified concrete PyPI hub (corresponding to a + {obj}`pip.parse.hub_name` value). + +:::{versionadded} 2.2.0 +::: +:::: + ::::{bzl:flag} venvs_use_declare_symlink Determines if relative symlinks are created using `declare_symlink()` at build @@ -373,9 +427,10 @@ is created. ::: :::: + ## Removed Flags -:::{versionremoved} VERSION_NEXT_FEATURE +:::{versionremoved} 2.1.0 The following flags were removed: * `pip_whl` diff --git a/docs/conf.py b/docs/conf.py index 17b3c17106..750e344285 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,6 +1,9 @@ # Configuration file for the Sphinx documentation builder. import os +import re + +from docutils import nodes # -- Project information project = "rules_python" @@ -219,9 +222,19 @@ ] +def _pep_role(name, rawtext, text, lineno, inliner, options={}, content=[]): + match = re.search(r"\d+", text) + pep_num = match.group(0) if match else text + pep_url = f"https://peps.python.org/pep-{pep_num.zfill(4)}/" + display_text = text if text.startswith("PEP") else f"PEP {pep_num}" + node = nodes.reference(rawtext, display_text, refuri=pep_url, **options) + return [node], [] + + def setup(app): # Pygments says it supports starlark, but it doesn't seem to actually # recognize `starlark` as a name. So just manually map it to python. from sphinx.highlighting import lexer_classes app.add_lexer("starlark", lexer_classes["python"]) + app.add_role("pep", _pep_role) diff --git a/docs/coverage.md b/docs/coverage.md index 41c95a1b75..8c162d86fd 100644 --- a/docs/coverage.md +++ b/docs/coverage.md @@ -39,9 +39,16 @@ need to manually configure coverage (see below). :::{note} The bundled `coverage` wheel set covers CPython 3.9 through 3.14 (with -freethreaded variants for 3.13+). For Python versions outside that range, -`configure_coverage_tool = True` is a silent no-op and `bazel coverage` will -produce empty lcov data; manually configure coverage (see below) instead. +freethreaded variants for 3.13+), and not every platform within that range. +When the interpreter that `bazel coverage` actually selects has no bundled +wheel, `configure_coverage_tool = True` produces no coverage tool and +`bazel coverage` emits empty lcov data; manually configure coverage (see +below) instead. + +`py_runtime` warns when this happens. The warning is emitted during analysis, +for the runtime toolchain resolution selected and only when coverage is being +collected, so it does not fire for the many platforms toolchains are +registered for but a given build never uses. ::: ## Manually configuring coverage diff --git a/docs/environment-variables.md b/docs/environment-variables.md index 35792415cf..3eb62221c0 100644 --- a/docs/environment-variables.md +++ b/docs/environment-variables.md @@ -118,6 +118,19 @@ which will effectively disable pyc caching. ::: +:::{envvar} RULES_PYTHON_PYPI_HUB_RESERVED + +When `1`, any PyPI hub named `"pypi"` will be renamed to `_pypi` +to prevent name collisions with the unified `@pypi` proxy repository, and +a warning is printed indicating that the renaming occurred. If not set (defaulting +to `0`), a warning is printed advising to rename the hub, and the collision +is not resolved. + +:::{versionadded} 2.2.0 +::: + +::: + :::{envvar} RULES_PYTHON_REPO_DEBUG When `1`, repository rules will print debug information about what they're @@ -151,7 +164,7 @@ When `1`, debug information about coverage behavior is printed to stderr. ## Removed Environment Variables -:::{versionremoved} VERSION_NEXT_FEATURE +:::{versionremoved} 2.1.0 The following environment variables were removed: * `RULES_PYTHON_ENABLE_PYSTAR`: Used to enable the Starlark implementation of diff --git a/docs/extending.md b/docs/extending.md index 00018fbd74..1c91b01795 100644 --- a/docs/extending.md +++ b/docs/extending.md @@ -23,7 +23,7 @@ Extending the core rules is most useful when you want all or most of the behavior of a core rule. ::: -Follow or comment on https://github.com/bazel-contrib/rules_python/issues/1647 +Follow or comment on {gh-issue}`1647` for the development of APIs to support custom derived rules. ## Creating custom rules 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/docs/howto/optional-feature-loading.md b/docs/howto/optional-feature-loading.md new file mode 100644 index 0000000000..144c0f65cb --- /dev/null +++ b/docs/howto/optional-feature-loading.md @@ -0,0 +1,180 @@ +:::{default-domain} bzl +::: + +# How to optionally load symbols using `features.loadable_symbols` + +When writing Bazel rules, macros, or repository extensions that support +multiple versions of `rules_python`, you may want to detect whether a public +symbol (such as {obj}`py_extension` in `//python/cc:py_extension.bzl`) is +available before attempting to load or use it. + +Because Starlark `load()` statements are evaluated at parse time and must be at +the top level of a `.bzl` file, unconditionally loading a symbol that does not +exist in older versions of `rules_python` will cause a build error. + +The {bzl:obj}`features.loadable_symbols` dictionary in `//python:features.bzl` +allows you to programmatically inspect which symbols are exported by `.bzl` +files in the current `rules_python` version. + +## The `features.loadable_symbols` structure + +{bzl:obj}`features.loadable_symbols` is a `dict[str, list[str]]` mapping label +strings of `.bzl` files to the list of public symbols they export: + +```starlark +load("@rules_python//python:features.bzl", "features") + +# Example structure of features.loadable_symbols: +# { +# "//python/cc:py_extension.bzl": [ +# "py_extension", +# ], +# "//python:py_info.bzl": [ +# "PyInfo", +# ], +# } +``` + +## Using load() with optional symbols + +In repository rules or Bazel module extensions (`repository_ctx` or +`module_ctx`), you generate `.bzl` files dynamically. You can inspect +`features.loadable_symbols` to determine which `load()` statements to write into +a generated compatibility repository. + +Re-export the symbol under its standard name if available, or set it to `None` +if it is absent. By generating compatibility files and empty `BUILD.bazel` +files at the exact same relative package paths as `rules_python`, the only +difference in downstream `load()` statements is the repository name: + +```starlark +load("@rules_python//python:features.bzl", "features") + +def _rules_python_compat_impl(rctx): + for bzl, symbol_list in rctx.attr.symbols.items(): + loadable = features.loadable_symbols.get(bzl, []) + lines = [] + for symbol in symbol_list: + if symbol in loadable: + lines.append( + 'load("{}", _{} = "{}")'.format(bzl, symbol, symbol), + ) + lines.append("{} = _{}".format(symbol, symbol)) + else: + lines.append("{} = None".format(symbol)) + + package, _, filename = bzl.lstrip("/").partition(":") + path = package + "/" + filename if package else filename + build_path = package + "/BUILD.bazel" if package else "BUILD.bazel" + + rctx.file(path, content = "\n".join(lines) + "\n") + rctx.file(build_path, content = "") + +rules_python_compat = repository_rule( + implementation = _rules_python_compat_impl, + attrs = { + "symbols": attr.string_list_dict( + mandatory = True, + doc = "Map of bzl paths to lists of symbols to optionally load", + ), + }, +) +``` + +Instantiate the repository rule by providing a mapping of `.bzl` paths to their +symbols of interest: + +```starlark +rules_python_compat( + name = "rules_python_compat", + symbols = { + "//python/cc:py_extension.bzl": ["py_extension"], + }, +) +``` + +### Using the generated compatibility files + +Your macros and rules can load from `@rules_python_compat` using the same +file path as `@rules_python`, testing whether the symbol is `None` before +using it: + +```starlark +load("@rules_python_compat//python/cc:py_extension.bzl", "py_extension") + +def my_macro(name, **kwargs): + if py_extension != None: + py_extension( + name = name + "_ext", + **kwargs + ) + else: + # Fall back to default behavior for older rules_python versions + pass +``` + +## Handling optional targets + +In addition to symbol loading, you may need to check whether a specific Bazel +target exists in `rules_python` before referencing its label in dependencies, +toolchains, or attribute defaults. + +The {bzl:obj}`features.targets` dictionary in `//python:features.bzl` is a +`dict[str, bool]` mapping public API target labels to `True` when available. + +In a macro: + +```starlark +load("@rules_python//python:features.bzl", "features") + +def my_cc_extension_macro(name, deps = [], **kwargs): + if features.targets.get("//python/cc:current_py_cc_headers_abi3"): + deps = deps + ["@rules_python//python/cc:current_py_cc_headers_abi3"] + + # ... define target with deps +``` + +In a `BUILD` file: + +```starlark +load("@rules_python//python:features.bzl", "features") +load("@rules_python//python:py_library.bzl", "py_library") + +py_library( + name = "my_lib", + srcs = ["my_lib.py"], + deps = [ + "//my/app:base_lib", + ] + ( + ["@rules_python//python/cc:current_py_cc_headers_abi3"] + if features.targets.get("//python/cc:current_py_cc_headers_abi3") + else [] + ), +) +``` + +## Checking versions with `features.version` + +When a behavioral change or capability is not directly reflected by a public +target or loadable symbol, you can inspect {bzl:obj}`features.version` in +`//python:features.bzl`. + +{bzl:obj}`features.version` returns a semver-formatted version string (such as +`"1.0.0"`, `"2.0.0-rc2"`, or `""` for unreleased development builds): + +```starlark +load("@rules_python//python:features.bzl", "features") + +def _to_tuple(v): + return tuple([ + int(x) if x.isdigit() else x + for x in v.replace("-", ".").split(".") + ]) + +def has_foo(): + # If version is empty, it is an unreleased build from main which includes + # all features. + if not features.version: + return True + return _to_tuple(features.version) >= _to_tuple("0.38.0") +``` diff --git a/docs/merge_changelog.py b/docs/merge_changelog.py new file mode 100644 index 0000000000..47af98b2a2 --- /dev/null +++ b/docs/merge_changelog.py @@ -0,0 +1,38 @@ +"""A tool to merge news entries into CHANGELOG.md for documentation preview.""" + +import argparse +import pathlib + +from tools.private.release import changelog_news + + +def main(): + parser = argparse.ArgumentParser(description="Merge news entries into CHANGELOG.md") + parser.add_argument( + "--changelog", type=pathlib.Path, required=True, help="Path to CHANGELOG.md" + ) + parser.add_argument( + "--news-dir", type=pathlib.Path, required=True, help="Path to news directory" + ) + parser.add_argument( + "--output", + type=pathlib.Path, + required=True, + help="Path to output merged changelog", + ) + args = parser.parse_args() + + # Call the public merge_new_into_changelog function + # Using deterministic date "0000-00-00" and version "0.0.0" + changelog_news.merge_new_into_changelog( + changelog_path=args.changelog, + output_path=args.output, + news_dir=args.news_dir, + version="unreleased", + release_date="0000-00-00", + delete_news=False, + ) + + +if __name__ == "__main__": + main() diff --git a/docs/pypi/download.md b/docs/pypi/download.md index f0e70cf850..fb7f38dc24 100644 --- a/docs/pypi/download.md +++ b/docs/pypi/download.md @@ -50,6 +50,114 @@ You can use the pip extension multiple times. This configuration will create multiple external repos that have no relation to one another and may result in downloading the same wheels numerous times. +(unified-pypi-hub)= +## Unified `@pypi` Hub for Multi-Hub Configurations + +:::{versionadded} 2.2.0 +Unified `@pypi` hub repository for Bzlmod multi-hub configurations. +::: + +When you call the `pip` extension multiple times with different `hub_name` +attributes, `rules_python` automatically generates a unified `@pypi` hub +repository (unless one of your concrete hubs is explicitly named `"pypi"`). + +This unified `@pypi` repository acts as a dynamic proxy that routes package +dependencies to the active concrete hub at build time. This is especially +useful in monorepos where shared library targets need to depend on PyPI +packages without knowing which specific hub or requirements lock file the +consuming binary is using. + +#### Reserved `"pypi"` Hub Name + +The hub name `"pypi"` is **reserved** for the automatically generated unified +hub repository. Defining a concrete hub named `"pypi"` will cause a collision. + +For details on how this collision is handled and resolved via environment +variables, see the {envvar}`RULES_PYTHON_PYPI_HUB_RESERVED` documentation. + +#### Configuring the Unified Hub + +To configure the unified hub, define your concrete hubs as usual, and +optionally designate a default hub using the `pip.default` tag's +`default_hub` attribute: + +```starlark +pip = use_extension("@rules_python//python/extensions:pip.bzl", "pip") + +# Define concrete hub 'pypi_a' +pip.parse( + hub_name = "pypi_a", + python_version = "3.11", + requirements_lock = "//:requirements_a.txt", +) + +# Define concrete hub 'pypi_b' +pip.parse( + hub_name = "pypi_b", + python_version = "3.11", + requirements_lock = "//:requirements_b.txt", +) + +# Designate 'pypi_b' as the default hub for the unified '@pypi' repository +pip.default(default_hub = "pypi_b") + +# Import the unified hub repository +use_repo(pip, "pypi") +``` + +#### Dynamic Routing at Build Time + +By default, the unified `@pypi` repository will resolve packages from the +designated `default_hub`. You can dynamically switch the active hub for a build +using the `--@rules_python//python/config_settings:venv` command-line flag +or via target transitions: + +```bash +# Build using packages from 'pypi_a' +bazel build --@rules_python//python/config_settings:venv=pypi_a //my:binary +``` + +Shared library targets can simply depend on the unified hub (e.g., +`@pypi//numpy`), and the dependency will automatically resolve to the correct +wheel version from the active hub during the build. + +### Declaring Abstract Dependencies (pip.dep) + +:::{versionadded} 2.2.0 +Declaring abstract PyPI dependencies via `pip.dep` tags. +::: + +Sometimes a shared library target or a ruleset needs to depend on a PyPI +package (e.g., `@pypi//numpy`), but does not want to force a specific package +version or a concrete `requirements.txt` lock file on its consumers. + +Instead of calling `pip.parse()`, the module can declare its dependency using +the `pip.dep` tag: + +```starlark +pip = use_extension("@rules_python//python/extensions:pip.bzl", "pip") + +# Declare an abstract dependency on 'numpy' and specify extra targets that +# are expected to be available in the package. +pip.dep( + name = "numpy", + extra_targets = ["extra-alias"], +) +``` + +This ensures that the target structure `@pypi//numpy` (and +`@pypi//numpy:extra-alias`) exists in the unified `@pypi` hub repository, so the +declaring module can compile and analyze successfully without needing any local +requirements file. + +The actual concrete implementation and version of the package must be provided +by a downstream module calling `pip.parse`. + +If a downstream module attempts to build a target that depends on an abstract +dependency, but has not provided a concrete implementation for it via any +`pip.parse` call, the build will fail at execution time. + + As with any repository rule or extension, if you would like to ensure that `pip_parse` is re-executed to pick up a non-hermetic change to your environment (e.g., updating your system `python` interpreter), you can force it to re-execute by running `bazel sync --only [pip_parse @@ -234,8 +342,9 @@ This does not mean that `rules_python` is fetching the wheels eagerly; rather, it means that it is calling the PyPI server to get the Simple API response to get the list of all available source and wheel distributions. Once it has gotten all of the available distributions, it will select the right ones depending -on the `sha256` values in your `requirements_lock.txt` file. If `sha256` hashes -are not present in the requirements file, we will fall back to matching by version +on the `--hash` values in your `requirements_lock.txt` file (any hash algorithm +advertised by the index can be matched, not only `sha256`). If hashes are not +present in the requirements file, we will fall back to matching by version specified in the lock file. Fetching the distribution information from the PyPI allows `rules_python` to @@ -334,6 +443,7 @@ into whatever HTTP(S) request it performs against `example.com`. See the [Credential Helper Spec][cred-helper-spec] for more details. + [rfc7617]: https://datatracker.ietf.org/doc/html/rfc7617 [cred-helper-design]: https://github.com/bazelbuild/proposals/blob/main/designs/2022-06-07-bazel-credential-helpers.md [cred-helper-spec]: https://github.com/EngFlow/credential-helper-spec/blob/main/spec.md diff --git a/docs/pypi/lock.md b/docs/pypi/lock.md index b5d8ec24f7..5c9f0646dc 100644 --- a/docs/pypi/lock.md +++ b/docs/pypi/lock.md @@ -68,8 +68,55 @@ compile_pip_requirements( ### uv pip compile (bzlmod only) -We also have experimental setup for the `uv pip compile` way of generating lock files. +We also have an experimental setup for the `uv pip compile` way of generating lock files. This is well tested with the public PyPI index, but you may hit some rough edges with private mirrors. -For more documentation see {obj}`lock` documentation. +#### Example usage + +```starlark +load("@rules_python//python/uv:lock.bzl", "lock") + +lock( + name = "requirements", + srcs = ["pyproject.toml", "requirements.in"], + out = "requirements_lock.txt", +) +``` + +#### `[tool.uv]` settings from pyproject.toml + +When a `pyproject.toml` file is among the {attr}`lock.srcs`, the +{obj}`lock` rule auto-detects the project directory and passes +`--project ` to `uv pip compile`. This causes `uv` to read +`[tool.uv]` settings from that `pyproject.toml`, such as +`no-build-isolation`, `exclude-dependencies`, and workspace members. + +If multiple `pyproject.toml` files are in {attr}`lock.srcs`, the one +with the shortest directory path is selected (this heuristic works for +typical uv workspace layouts where the root configuration is at the +shortest path). + +If the auto-detection picks the wrong project directory, use the +`project` parameter to override: + +```starlark +lock( + name = "requirements", + srcs = ["pyproject.toml", "requirements.in"], + out = "requirements_lock.txt", + project = "subproject", +) +``` + +:::{warning} +**Known limitations of auto-detection** + +1. **Workspace heuristic** — the shortest-path selection may incorrectly assume the upper-most + workspace `pyproject.toml` is the correct one. For monorepos with multiple independent + sub-projects, you must set `project` explicitly for each {obj}`lock` target. +1. **No test target** — unlike {obj}`compile_pip_requirements`, no test target is auto-created; see + the {obj}`lock` docs for how to add one manually using `diff_test` from `bazel_skylib`. +::: + +For more documentation see {obj}`lock`. diff --git a/docs/toolchains.md b/docs/toolchains.md index 09aaed412b..f154fc29ed 100644 --- a/docs/toolchains.md +++ b/docs/toolchains.md @@ -242,6 +242,9 @@ existing attributes: {attr}`python.single_version_platform_override.coverage_tool`. * Adding additional Python versions via {bzl:obj}`python.single_version_override` or {bzl:obj}`python.single_version_platform_override`. +* Adding additional Python versions dynamically from a manifest file or URL + via {attr}`python.override.add_runtime_manifest_files` or + {attr}`python.override.add_runtime_manifest_urls`. ### Registering custom runtimes @@ -310,6 +313,73 @@ Added support for custom platform names, `target_compatible_with`, and `target_settings` with `single_version_platform_override`. ::: +### Registering runtimes from a manifest + +If you want to register multiple custom runtimes or versions at once, you can +use a python-build-standalone manifest file. This is useful if you want to +adopt new versions that are not yet built into `rules_python` without having +to manually define each one using `single_version_platform_override`. + +To do this, specify the `add_runtime_manifest_files` or +`add_runtime_manifest_urls` (and `runtime_manifest_sha`) attributes in +`python.override` in your `MODULE.bazel`. + +In the example below, we register all runtimes available in a specific local +or remote PBS release manifest: + +```starlark +# File: MODULE.bazel +python = use_extension("@rules_python//python/extensions:python.bzl", "python") +python.override( + add_runtime_manifest_files = [ + "@//:SHA256SUMS", + ], + add_runtime_manifest_urls = [ + "https://github.com/astral-sh/python-build-standalone/releases/download/20260414/SHA256SUMS", + ], + base_url = "https://example.com/downloads", + runtime_manifest_sha = "ce18fdfd47c66830a40ea9b9e314a14b1636bbfd684501bc5ca1fc6d55a7933f", +) +``` + +#### Manifest file format + +The manifest must be a plain text file where each line contains the SHA256 hash +and the location of a runtime archive, separated by whitespace: + +``` + +``` + +The `` can be either: +- A relative filename (e.g., + `cpython-3.10.20+20260414-x86_64-unknown-linux-gnu-install_only.tar.zst`). + In this case, the download URL is constructed by appending the filename to + the `base_url` attribute (if using `add_runtime_manifest_files`) or to the + parent directory of each URL in `add_runtime_manifest_urls` (treating them + as mirrors). +- An absolute URL (e.g., + `https://example.com/downloads/cpython-3.10.20+20260414-x86_64-unknown-linux-gnu-install_only.tar.zst`). + In this case, the URL is used directly to download the archive. + +In both cases, the filename or the last path segment of the URL must follow +the standard python-build-standalone naming convention. `rules_python` parses +this name to extract runtime metadata (such as Python version, target +architecture, operating system, and libc). + +Notes: +- `rules_python` will read or download the manifest, parse it, and + automatically register toolchains for all valid Python runtimes found in it + that match supported platforms. +- Only runtimes matching known platforms in `rules_python` will be registered. + +:::{versionadded} 2.1.0 +Added support for registering runtimes from a manifest using +`add_runtime_manifest_files`, `add_runtime_manifest_urls`, and +`runtime_manifest_sha` in `python.override`. +::: + + ### Using defined toolchains from WORKSPACE It is possible to use toolchains defined in `MODULE.bazel` in `WORKSPACE`. For example, @@ -403,7 +473,7 @@ pip_parse( ``` After registration, your Python targets will use the toolchain's interpreter during execution, but a system-installed interpreter -is still used to "bootstrap" Python targets (see https://github.com/bazel-contrib/rules_python/issues/691). +is still used to "bootstrap" Python targets (see {gh-issue}`691`). You may also find some quirks while using this toolchain. Please refer to [python-build-standalone documentation's _Quirks_ section](https://gregoryszorc.com/docs/python-build-standalone/main/quirks.html). ## Local toolchain diff --git a/downloader_config.cfg b/downloader_config.cfg new file mode 100644 index 0000000000..3fa6264eda --- /dev/null +++ b/downloader_config.cfg @@ -0,0 +1,21 @@ +# Try GitHub first (primary) +rewrite ^github\.com/bazel-contrib/bazel_features/(.*) github.com/bazel-contrib/bazel_features/$1 +rewrite ^github\.com/bazel-contrib/rules_go/(.*) github.com/bazel-contrib/rules_go/$1 +rewrite ^github\.com/bazelbuild/bazel-skylib/(.*) github.com/bazelbuild/bazel-skylib/$1 +rewrite ^github\.com/bazelbuild/platforms/(.*) github.com/bazelbuild/platforms/$1 +rewrite ^github\.com/bazelbuild/rules_kotlin/(.*) github.com/bazelbuild/rules_kotlin/$1 +rewrite ^github\.com/bazelbuild/rules_shell/(.*) github.com/bazelbuild/rules_shell/$1 +rewrite ^github\.com/bazelbuild/rules_java/(.*) github.com/bazelbuild/rules_java/$1 +rewrite ^github\.com/bazelbuild/stardoc/(.*) github.com/bazelbuild/stardoc/$1 + + +# Fall back to mirror (secondary) +# Tracking upstream BCR mirror addition: https://github.com/bazelbuild/platforms/issues/139 +rewrite ^github\.com/bazel-contrib/bazel_features/(.*) mirror.bazel.build/github.com/bazel-contrib/bazel_features/$1 +rewrite ^github\.com/bazel-contrib/rules_go/(.*) mirror.bazel.build/github.com/bazel-contrib/rules_go/$1 +rewrite ^github\.com/bazelbuild/bazel-skylib/(.*) mirror.bazel.build/github.com/bazelbuild/bazel-skylib/$1 +rewrite ^github\.com/bazelbuild/platforms/(.*) mirror.bazel.build/github.com/bazelbuild/platforms/$1 +rewrite ^github\.com/bazelbuild/rules_kotlin/(.*) mirror.bazel.build/github.com/bazelbuild/rules_kotlin/$1 +rewrite ^github\.com/bazelbuild/rules_shell/(.*) mirror.bazel.build/github.com/bazelbuild/rules_shell/$1 +rewrite ^github\.com/bazelbuild/rules_java/(.*) mirror.bazel.build/github.com/bazelbuild/rules_java/$1 +rewrite ^github\.com/bazelbuild/stardoc/(.*) mirror.bazel.build/github.com/bazelbuild/stardoc/$1 diff --git a/examples/bzlmod/MODULE.bazel b/examples/bzlmod/MODULE.bazel index 106f25134e..d6b066fd1f 100644 --- a/examples/bzlmod/MODULE.bazel +++ b/examples/bzlmod/MODULE.bazel @@ -22,6 +22,10 @@ bazel_dep(name = "rules_java", version = "8.16.1") # were fixed. bazel_dep(name = "rules_rust", version = "0.67.0") +# Adopt migration away from legacy __init__.py generation. +rules_python_config = use_extension("@rules_python//python/extensions:config.bzl", "config") +rules_python_config.explicit_init_py(default = True) + # We next initialize the python toolchain using the extension. # You can set different Python versions in this block. python = use_extension("@rules_python//python/extensions:python.bzl", "python") diff --git a/examples/bzlmod/other_module/MODULE.bazel b/examples/bzlmod/other_module/MODULE.bazel index a128c39ca0..be18ff21fd 100644 --- a/examples/bzlmod/other_module/MODULE.bazel +++ b/examples/bzlmod/other_module/MODULE.bazel @@ -10,6 +10,10 @@ local_path_override( path = "../../..", ) +# Adopt migration away from legacy __init__.py generation. +rules_python_config = use_extension("@rules_python//python/extensions:config.bzl", "config") +rules_python_config.explicit_init_py(default = True) + python = use_extension("@rules_python//python/extensions:python.bzl", "python") python.defaults( # In a submodule this is ignored diff --git a/examples/bzlmod/other_module/other_module/rule_builder/BUILD.bazel b/examples/bzlmod/other_module/other_module/rule_builder/BUILD.bazel new file mode 100644 index 0000000000..40f66b3322 --- /dev/null +++ b/examples/bzlmod/other_module/other_module/rule_builder/BUILD.bazel @@ -0,0 +1,13 @@ +load(":rule.bzl", "custom_py_binary") + +# This target's mere existence is the regression test: analyzing it exercises +# the implicit attr defaults (build_data_writer, debugger_if_target_config, +# uncachable_version_file) that py_binary_rule_builder() bundles from +# rules_python's private package, from a rule() call made in this external +# module. +custom_py_binary( + name = "app", + srcs = ["app.py"], + main = "app.py", + visibility = ["//visibility:public"], +) diff --git a/examples/bzlmod/other_module/other_module/rule_builder/app.py b/examples/bzlmod/other_module/other_module/rule_builder/app.py new file mode 100644 index 0000000000..db51494ed6 --- /dev/null +++ b/examples/bzlmod/other_module/other_module/rule_builder/app.py @@ -0,0 +1 @@ +print("hello from a rule built via py_binary_rule_builder()") diff --git a/examples/bzlmod/other_module/other_module/rule_builder/rule.bzl b/examples/bzlmod/other_module/other_module/rule_builder/rule.bzl new file mode 100644 index 0000000000..7b1505987a --- /dev/null +++ b/examples/bzlmod/other_module/other_module/rule_builder/rule.bzl @@ -0,0 +1,17 @@ +"""A minimal custom py_binary built via the executables rule builder API. + +Regression coverage for https://github.com/bazel-contrib/rules_python/pull/3919: +py_binary_rule_builder()'s implicit attr defaults (build_data_writer, +debugger_if_target_config, uncachable_version_file) previously had no +visibility outside of rules_python, so any external module (like this one) +constructing a rule via the builder failed at analysis time with a +visibility error. +""" + +load("@rules_python//python/api:executables.bzl", "executables") + +def _make_rule(): + builder = executables.py_binary_rule_builder() + return builder.build() + +custom_py_binary = _make_rule() diff --git a/examples/bzlmod/tests/other_module/BUILD.bazel b/examples/bzlmod/tests/other_module/BUILD.bazel index 24231e651a..f4756e7c3e 100644 --- a/examples/bzlmod/tests/other_module/BUILD.bazel +++ b/examples/bzlmod/tests/other_module/BUILD.bazel @@ -14,6 +14,17 @@ build_test( ], ) +# Regression test for https://github.com/bazel-contrib/rules_python/pull/3919: py_binary_rule_builder()'s implicit +# attr defaults previously had no visibility outside of rules_python, so +# building this target (defined via the builder from an external module) +# failed at analysis time with a visibility error. +build_test( + name = "other_module_rule_builder_build_test", + targets = [ + "@our_other_module//other_module/rule_builder:app", + ], +) + py_test( name = "other_module_import_test", srcs = ["other_module_import_test.py"], 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/gazelle/.bazelrc b/gazelle/.bazelrc index 9a38133e9d..e30216814a 100644 --- a/gazelle/.bazelrc +++ b/gazelle/.bazelrc @@ -1,5 +1,9 @@ common --deleted_packages=examples/bzlmod_build_file_generation common --deleted_packages=examples/bzlmod_build_file_generation/runfiles +common --experimental_downloader_config=downloader_config.cfg +common --http_timeout_scaling=10.0 +common --experimental_repository_downloader_retries=10 + test --test_output=errors diff --git a/gazelle/BUILD.bazel b/gazelle/BUILD.bazel index 0938be3dfc..30758181ed 100644 --- a/gazelle/BUILD.bazel +++ b/gazelle/BUILD.bazel @@ -1,9 +1,12 @@ load("@bazel_gazelle//:def.bzl", "gazelle") +load("@bazel_skylib//:bzl_library.bzl", "bzl_library") # Gazelle configuration options. # See https://github.com/bazelbuild/bazel-gazelle#running-gazelle-with-bazel # gazelle:prefix github.com/bazel-contrib/rules_python/gazelle # gazelle:exclude bazel-out +# gazelle:exclude deps.bzl +# gazelle:exclude internal_dev_deps.bzl gazelle( name = "gazelle", ) @@ -36,3 +39,9 @@ filegroup( ], visibility = ["@rules_python//:__pkg__"], ) + +bzl_library( + name = "def", + srcs = ["def.bzl"], + visibility = ["//visibility:public"], +) diff --git a/gazelle/MODULE.bazel b/gazelle/MODULE.bazel index cff6341a2b..b6eed23c6f 100644 --- a/gazelle/MODULE.bazel +++ b/gazelle/MODULE.bazel @@ -5,7 +5,7 @@ module( ) bazel_dep(name = "bazel_skylib", version = "1.8.2") -bazel_dep(name = "rules_python", version = "0.18.0") +bazel_dep(name = "rules_python", version = "1.5.0") bazel_dep(name = "rules_go", version = "0.59.0", repo_name = "io_bazel_rules_go") bazel_dep(name = "gazelle", version = "0.47.0", repo_name = "bazel_gazelle") bazel_dep(name = "rules_cc", version = "0.0.16") diff --git a/gazelle/deps.bzl b/gazelle/deps.bzl index 7072c6a372..c57d5193b8 100644 --- a/gazelle/deps.bzl +++ b/gazelle/deps.bzl @@ -26,9 +26,9 @@ def python_stdlib_list_deps(): http_archive( name = "python_stdlib_list", build_file_content = """exports_files(glob(["stdlib_list/lists/*.txt"]))""", - sha256 = "aa21a4f219530e85ecc364f0bbff2df4e6097a8954c63652af060f4e64afa65d", - strip_prefix = "stdlib-list-0.11.0", - url = "https://github.com/pypi/stdlib-list/releases/download/v0.11.0/v0.11.0.tar.gz", + sha256 = "517824f27ee89e591d8ae7c1dd9ff34f672eae50ee886ea31bb8816d77535675", + strip_prefix = "stdlib_list-0.12.0", + url = "https://files.pythonhosted.org/packages/8c/25/f1540879c8815387980e56f973e54605bd924612399ace31487f7444171c/stdlib_list-0.12.0.tar.gz", ) def gazelle_deps(): diff --git a/gazelle/docs/directives.md b/gazelle/docs/directives.md index dfa439358d..60f948e4af 100644 --- a/gazelle/docs/directives.md +++ b/gazelle/docs/directives.md @@ -681,7 +681,7 @@ that are relative to the current package. {gh-pr}`3014` ::: -:::{versionchanged} VERSION_NEXT_FEATURE +:::{versionchanged} 2.1.0 The default was changed from `false` to `true`. {gh-pr}`3753` ::: @@ -725,7 +725,7 @@ not write `pyi_deps`. {gh-pr}`3356` ::: -:::{versionchanged} VERSION_NEXT_FEATURE +:::{versionchanged} 2.1.0 The default was changed from `false` to `true`. {gh-pr}`3753` ::: @@ -837,7 +837,7 @@ Version 1.9.0 includes a fix ({gh-pr}`3498`) for a long-standing issue ({gh-issue}`3497`) where ancestor `conftest.py` files were not automatically added as dependencies of {bzl:obj}`py_test` targets. -However, some people may not want this behavior (see https://xkcd.com/1172/). +However, some people may not want this behavior (see ). Thus the `python_include_ancestor_conftest` directive controls this behavior. It defaults to `true`, which causes all ancestor `conftest.py` files to be included as dependencies for {bzl:obj}`py_test` targets. diff --git a/gazelle/downloader_config.cfg b/gazelle/downloader_config.cfg new file mode 100644 index 0000000000..3fa6264eda --- /dev/null +++ b/gazelle/downloader_config.cfg @@ -0,0 +1,21 @@ +# Try GitHub first (primary) +rewrite ^github\.com/bazel-contrib/bazel_features/(.*) github.com/bazel-contrib/bazel_features/$1 +rewrite ^github\.com/bazel-contrib/rules_go/(.*) github.com/bazel-contrib/rules_go/$1 +rewrite ^github\.com/bazelbuild/bazel-skylib/(.*) github.com/bazelbuild/bazel-skylib/$1 +rewrite ^github\.com/bazelbuild/platforms/(.*) github.com/bazelbuild/platforms/$1 +rewrite ^github\.com/bazelbuild/rules_kotlin/(.*) github.com/bazelbuild/rules_kotlin/$1 +rewrite ^github\.com/bazelbuild/rules_shell/(.*) github.com/bazelbuild/rules_shell/$1 +rewrite ^github\.com/bazelbuild/rules_java/(.*) github.com/bazelbuild/rules_java/$1 +rewrite ^github\.com/bazelbuild/stardoc/(.*) github.com/bazelbuild/stardoc/$1 + + +# Fall back to mirror (secondary) +# Tracking upstream BCR mirror addition: https://github.com/bazelbuild/platforms/issues/139 +rewrite ^github\.com/bazel-contrib/bazel_features/(.*) mirror.bazel.build/github.com/bazel-contrib/bazel_features/$1 +rewrite ^github\.com/bazel-contrib/rules_go/(.*) mirror.bazel.build/github.com/bazel-contrib/rules_go/$1 +rewrite ^github\.com/bazelbuild/bazel-skylib/(.*) mirror.bazel.build/github.com/bazelbuild/bazel-skylib/$1 +rewrite ^github\.com/bazelbuild/platforms/(.*) mirror.bazel.build/github.com/bazelbuild/platforms/$1 +rewrite ^github\.com/bazelbuild/rules_kotlin/(.*) mirror.bazel.build/github.com/bazelbuild/rules_kotlin/$1 +rewrite ^github\.com/bazelbuild/rules_shell/(.*) mirror.bazel.build/github.com/bazelbuild/rules_shell/$1 +rewrite ^github\.com/bazelbuild/rules_java/(.*) mirror.bazel.build/github.com/bazelbuild/rules_java/$1 +rewrite ^github\.com/bazelbuild/stardoc/(.*) mirror.bazel.build/github.com/bazelbuild/stardoc/$1 diff --git a/gazelle/examples/bzlmod_build_file_generation/MODULE.bazel b/gazelle/examples/bzlmod_build_file_generation/MODULE.bazel index 1f92ea3826..81719be01f 100644 --- a/gazelle/examples/bzlmod_build_file_generation/MODULE.bazel +++ b/gazelle/examples/bzlmod_build_file_generation/MODULE.bazel @@ -13,7 +13,7 @@ module( # For typical setups you set the version. # See the releases page for available versions. # https://github.com/bazel-contrib/rules_python/releases -bazel_dep(name = "rules_python", version = "1.4.0") +bazel_dep(name = "rules_python", version = "1.5.0") # The following stanza defines the dependency rules_python_gazelle_plugin. # For typical setups you set the version. diff --git a/gazelle/manifest/BUILD.bazel b/gazelle/manifest/BUILD.bazel index ea81d85fbe..68f4e1be30 100644 --- a/gazelle/manifest/BUILD.bazel +++ b/gazelle/manifest/BUILD.bazel @@ -1,3 +1,4 @@ +load("@bazel_skylib//:bzl_library.bzl", "bzl_library") load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") exports_files([ @@ -11,8 +12,8 @@ go_library( importpath = "github.com/bazel-contrib/rules_python/gazelle/manifest", visibility = ["//visibility:public"], deps = [ - "@com_github_emirpasic_gods//sets/treeset", - "@in_gopkg_yaml_v2//:yaml_v2", + "@com_github_emirpasic_gods//sets/treeset:go_default_library", + "@in_gopkg_yaml_v2//:go_default_library", ], ) @@ -32,3 +33,14 @@ filegroup( ], visibility = ["//:__pkg__"], ) + +bzl_library( + name = "defs", + srcs = ["defs.bzl"], + visibility = ["//visibility:public"], + deps = [ + "@bazel_skylib//rules:diff_test", + "@io_bazel_rules_go//go:def", + "@rules_python//python:defs_bzl", + ], +) diff --git a/gazelle/modules_mapping/BUILD.bazel b/gazelle/modules_mapping/BUILD.bazel index 3423f34e51..0e740f2636 100644 --- a/gazelle/modules_mapping/BUILD.bazel +++ b/gazelle/modules_mapping/BUILD.bazel @@ -1,3 +1,4 @@ +load("@bazel_skylib//:bzl_library.bzl", "bzl_library") load("@bazel_skylib//rules:copy_file.bzl", "copy_file") load("@rules_python//python:defs.bzl", "py_binary", "py_test") @@ -56,3 +57,9 @@ filegroup( srcs = glob(["**"]), visibility = ["//:__pkg__"], ) + +bzl_library( + name = "def", + srcs = ["def.bzl"], + visibility = ["//visibility:public"], +) diff --git a/gazelle/python/BUILD.bazel b/gazelle/python/BUILD.bazel index b988e493c7..1ffa2890e1 100644 --- a/gazelle/python/BUILD.bazel +++ b/gazelle/python/BUILD.bazel @@ -30,19 +30,19 @@ go_library( visibility = ["//visibility:public"], deps = [ "//pythonconfig", - "@bazel_gazelle//config:go_default_library", - "@bazel_gazelle//label:go_default_library", - "@bazel_gazelle//language:go_default_library", - "@bazel_gazelle//repo:go_default_library", - "@bazel_gazelle//resolve:go_default_library", - "@bazel_gazelle//rule:go_default_library", + "@bazel_gazelle//config", + "@bazel_gazelle//label", + "@bazel_gazelle//language", + "@bazel_gazelle//repo", + "@bazel_gazelle//resolve", + "@bazel_gazelle//rule", "@com_github_bazelbuild_buildtools//build", "@com_github_bmatcuk_doublestar_v4//:doublestar", - "@com_github_emirpasic_gods//lists/singlylinkedlist", - "@com_github_emirpasic_gods//sets/treeset", - "@com_github_emirpasic_gods//utils", - "@com_github_smacker_go_tree_sitter//:go-tree-sitter", - "@com_github_smacker_go_tree_sitter//python", + "@com_github_emirpasic_gods//lists/singlylinkedlist:go_default_library", + "@com_github_emirpasic_gods//sets/treeset:go_default_library", + "@com_github_emirpasic_gods//utils:go_default_library", + "@com_github_smacker_go_tree_sitter//:go_default_library", + "@com_github_smacker_go_tree_sitter//python:go_default_library", "@org_golang_x_sync//errgroup", ], ) @@ -54,10 +54,12 @@ copy_file( "@rules_python//python/config_settings:is_python_3.10": "@python_stdlib_list//:stdlib_list/lists/3.10.txt", "@rules_python//python/config_settings:is_python_3.11": "@python_stdlib_list//:stdlib_list/lists/3.11.txt", "@rules_python//python/config_settings:is_python_3.12": "@python_stdlib_list//:stdlib_list/lists/3.12.txt", + "@rules_python//python/config_settings:is_python_3.13": "@python_stdlib_list//:stdlib_list/lists/3.13.txt", + "@rules_python//python/config_settings:is_python_3.14": "@python_stdlib_list//:stdlib_list/lists/3.14.txt", "@rules_python//python/config_settings:is_python_3.8": "@python_stdlib_list//:stdlib_list/lists/3.8.txt", "@rules_python//python/config_settings:is_python_3.9": "@python_stdlib_list//:stdlib_list/lists/3.9.txt", - # This is the same behaviour as previously - "//conditions:default": "@python_stdlib_list//:stdlib_list/lists/3.11.txt", + # Fall back to the latest stdlib list + "//conditions:default": "@python_stdlib_list//:stdlib_list/lists/3.14.txt", }, ), out = "stdlib_list.txt", @@ -65,6 +67,9 @@ copy_file( ) # gazelle:exclude testdata/ +# gazelle:exclude extensions.bzl +# Exclude test-only Starlark helper from Gazelle to avoid generating an unnecessary bzl_library +# gazelle:exclude gazelle_test.bzl gazelle_test( name = "python_test", diff --git a/gazelle/python/generate.go b/gazelle/python/generate.go index 6fa6252ebb..90e06a1546 100644 --- a/gazelle/python/generate.go +++ b/gazelle/python/generate.go @@ -593,26 +593,53 @@ func (py *Python) getRulesWithInvalidSrcs(args language.GenerateArgs, validFiles validFilesMap[file] = struct{}{} } + // allFilesMap extends validFilesMap with all regular files on disk. + // py_binary uses validFilesMap (main modules + generated files), while py_library + // and py_test use allFilesMap since any file is a valid src for them. + allFilesMap := make(map[string]struct{}, len(validFilesMap)+len(args.RegularFiles)) + for file := range validFilesMap { + allFilesMap[file] = struct{}{} + } + for _, file := range args.RegularFiles { + allFilesMap[file] = struct{}{} + } + isTarget := func(src string) bool { return strings.HasPrefix(src, "@") || strings.HasPrefix(src, "//") || strings.HasPrefix(src, ":") } for _, existingRule := range args.File.Rules { - if !kindMatches(args.Config, existingRule, pyBinaryKind) { + var matchedKind string + var filesMap map[string]struct{} + if kindMatches(args.Config, existingRule, pyBinaryKind) { + matchedKind = pyBinaryKind + filesMap = validFilesMap + } else if kindMatches(args.Config, existingRule, pyLibraryKind) { + matchedKind = pyLibraryKind + filesMap = allFilesMap + } else if kindMatches(args.Config, existingRule, pyTestKind) { + matchedKind = pyTestKind + filesMap = allFilesMap + } else { + continue + } + + srcs := existingRule.AttrStrings("srcs") + if len(srcs) == 0 { continue } var hasValidSrcs bool - for _, src := range existingRule.AttrStrings("srcs") { + for _, src := range srcs { if isTarget(src) { hasValidSrcs = true break } - if _, ok := validFilesMap[src]; ok { + if _, ok := filesMap[src]; ok { hasValidSrcs = true break } } if !hasValidSrcs { - invalidRules = append(invalidRules, newTargetBuilder(pyBinaryKind, existingRule.Name(), "", "", nil, false).build()) + invalidRules = append(invalidRules, newTargetBuilder(matchedKind, existingRule.Name(), "", "", nil, false).build()) } } return invalidRules diff --git a/gazelle/python/private/BUILD.bazel b/gazelle/python/private/BUILD.bazel index e69de29bb2..ccdc080664 100644 --- a/gazelle/python/private/BUILD.bazel +++ b/gazelle/python/private/BUILD.bazel @@ -0,0 +1 @@ +# gazelle:exclude extensions.bzl diff --git a/gazelle/python/testdata/remove_invalid_library/BUILD.in b/gazelle/python/testdata/remove_invalid_library_package_mode/BUILD.in similarity index 100% rename from gazelle/python/testdata/remove_invalid_library/BUILD.in rename to gazelle/python/testdata/remove_invalid_library_package_mode/BUILD.in diff --git a/gazelle/python/testdata/remove_invalid_library/BUILD.out b/gazelle/python/testdata/remove_invalid_library_package_mode/BUILD.out similarity index 100% rename from gazelle/python/testdata/remove_invalid_library/BUILD.out rename to gazelle/python/testdata/remove_invalid_library_package_mode/BUILD.out diff --git a/gazelle/python/testdata/remove_invalid_library/README.md b/gazelle/python/testdata/remove_invalid_library_package_mode/README.md similarity index 100% rename from gazelle/python/testdata/remove_invalid_library/README.md rename to gazelle/python/testdata/remove_invalid_library_package_mode/README.md diff --git a/gazelle/python/testdata/remove_invalid_library/WORKSPACE b/gazelle/python/testdata/remove_invalid_library_package_mode/WORKSPACE similarity index 100% rename from gazelle/python/testdata/remove_invalid_library/WORKSPACE rename to gazelle/python/testdata/remove_invalid_library_package_mode/WORKSPACE diff --git a/gazelle/python/testdata/remove_invalid_library/my_test.py b/gazelle/python/testdata/remove_invalid_library_package_mode/my_test.py similarity index 100% rename from gazelle/python/testdata/remove_invalid_library/my_test.py rename to gazelle/python/testdata/remove_invalid_library_package_mode/my_test.py diff --git a/gazelle/python/testdata/remove_invalid_library/others/BUILD.in b/gazelle/python/testdata/remove_invalid_library_package_mode/others/BUILD.in similarity index 100% rename from gazelle/python/testdata/remove_invalid_library/others/BUILD.in rename to gazelle/python/testdata/remove_invalid_library_package_mode/others/BUILD.in diff --git a/gazelle/python/testdata/remove_invalid_library/others/BUILD.out b/gazelle/python/testdata/remove_invalid_library_package_mode/others/BUILD.out similarity index 100% rename from gazelle/python/testdata/remove_invalid_library/others/BUILD.out rename to gazelle/python/testdata/remove_invalid_library_package_mode/others/BUILD.out diff --git a/gazelle/python/testdata/remove_invalid_library/test.yaml b/gazelle/python/testdata/remove_invalid_library_package_mode/test.yaml similarity index 100% rename from gazelle/python/testdata/remove_invalid_library/test.yaml rename to gazelle/python/testdata/remove_invalid_library_package_mode/test.yaml diff --git a/gazelle/python/testdata/remove_invalid_per_file/BUILD.in b/gazelle/python/testdata/remove_invalid_per_file/BUILD.in new file mode 100644 index 0000000000..d3b15a7866 --- /dev/null +++ b/gazelle/python/testdata/remove_invalid_per_file/BUILD.in @@ -0,0 +1,45 @@ +load("@rules_python//python:defs.bzl", "py_binary", "py_library", "py_test") + +# gazelle:python_generation_mode file + +# Valid py_library — bar.py exists on disk, should be kept. +py_library( + name = "bar", + srcs = ["bar.py"], + visibility = ["//:__subpackages__"], +) + +# Stale py_library — deleted.py does not exist, should be removed. +py_library( + name = "deleted_lib", + srcs = ["deleted.py"], + visibility = ["//:__subpackages__"], +) + +# Valid py_binary — __main__.py exists on disk, should be kept. +py_binary( + name = "remove_invalid_per_file_bin", + srcs = ["__main__.py"], + visibility = ["//:__subpackages__"], +) + +# Stale py_binary — deleted_bin.py does not exist, should be removed. +py_binary( + name = "deleted_bin", + srcs = ["deleted_bin.py"], + visibility = ["//:__subpackages__"], +) + +# Valid py_test — bar_test.py exists on disk, should be kept. +py_test( + name = "bar_test", + srcs = ["bar_test.py"], + visibility = ["//:__subpackages__"], +) + +# Stale py_test — deleted_test.py does not exist, should be removed. +py_test( + name = "deleted_test", + srcs = ["deleted_test.py"], + visibility = ["//:__subpackages__"], +) diff --git a/gazelle/python/testdata/remove_invalid_per_file/BUILD.out b/gazelle/python/testdata/remove_invalid_per_file/BUILD.out new file mode 100644 index 0000000000..376c14c0e1 --- /dev/null +++ b/gazelle/python/testdata/remove_invalid_per_file/BUILD.out @@ -0,0 +1,24 @@ +load("@rules_python//python:defs.bzl", "py_binary", "py_library", "py_test") + +# gazelle:python_generation_mode file + +# Valid py_library — bar.py exists on disk, should be kept. +py_library( + name = "bar", + srcs = ["bar.py"], + visibility = ["//:__subpackages__"], +) + +# Valid py_test — bar_test.py exists on disk, should be kept. +py_test( + name = "bar_test", + srcs = ["bar_test.py"], + visibility = ["//:__subpackages__"], +) + +py_binary( + name = "remove_invalid_per_file_bin", + srcs = ["__main__.py"], + main = "__main__.py", + visibility = ["//:__subpackages__"], +) diff --git a/gazelle/python/testdata/remove_invalid_per_file/WORKSPACE b/gazelle/python/testdata/remove_invalid_per_file/WORKSPACE new file mode 100644 index 0000000000..faff6af87a --- /dev/null +++ b/gazelle/python/testdata/remove_invalid_per_file/WORKSPACE @@ -0,0 +1 @@ +# This is a Bazel workspace for the Gazelle test data. diff --git a/gazelle/python/testdata/remove_invalid_per_file/__main__.py b/gazelle/python/testdata/remove_invalid_per_file/__main__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/gazelle/python/testdata/remove_invalid_per_file/alias_kind/BUILD.in b/gazelle/python/testdata/remove_invalid_per_file/alias_kind/BUILD.in new file mode 100644 index 0000000000..e56ea1617a --- /dev/null +++ b/gazelle/python/testdata/remove_invalid_per_file/alias_kind/BUILD.in @@ -0,0 +1,37 @@ +load(":mylib.bzl", "my_py_library", "my_py_test") + +# gazelle:python_generation_mode file +# gazelle:alias_kind my_py_library py_library +# gazelle:alias_kind my_py_test py_test + +# Valid aliased py_library — bar.py exists on disk, should be kept. +my_py_library( + name = "bar", + srcs = ["bar.py"], + visibility = ["//:__subpackages__"], +) + +# Stale aliased py_library — deleted.py does not exist, should be removed. +# TODO: Known limitation: not fully deleted until gazelle is bumped to include +# https://github.com/bazel-contrib/bazel-gazelle/pull/2362 +my_py_library( + name = "deleted_lib", + srcs = ["deleted.py"], + visibility = ["//:__subpackages__"], +) + +# Valid aliased py_test — bar_test.py exists on disk, should be kept. +my_py_test( + name = "bar_test", + srcs = ["bar_test.py"], + visibility = ["//:__subpackages__"], +) + +# Stale aliased py_test — deleted_test.py does not exist, should be removed. +# TODO: Known limitation: not fully deleted until gazelle is bumped to include +# https://github.com/bazel-contrib/bazel-gazelle/pull/2362 +my_py_test( + name = "deleted_test", + srcs = ["deleted_test.py"], + visibility = ["//:__subpackages__"], +) diff --git a/gazelle/python/testdata/remove_invalid_per_file/alias_kind/BUILD.out b/gazelle/python/testdata/remove_invalid_per_file/alias_kind/BUILD.out new file mode 100644 index 0000000000..19d6c01789 --- /dev/null +++ b/gazelle/python/testdata/remove_invalid_per_file/alias_kind/BUILD.out @@ -0,0 +1,35 @@ +load(":mylib.bzl", "my_py_library", "my_py_test") + +# gazelle:python_generation_mode file +# gazelle:alias_kind my_py_library py_library +# gazelle:alias_kind my_py_test py_test + +# Valid aliased py_library — bar.py exists on disk, should be kept. +my_py_library( + name = "bar", + srcs = ["bar.py"], + visibility = ["//:__subpackages__"], +) + +# Stale aliased py_library — deleted.py does not exist, should be removed. +# TODO: Known limitation: not fully deleted until gazelle is bumped to include +# https://github.com/bazel-contrib/bazel-gazelle/pull/2362 +my_py_library( + name = "deleted_lib", + visibility = ["//:__subpackages__"], +) + +# Valid aliased py_test — bar_test.py exists on disk, should be kept. +my_py_test( + name = "bar_test", + srcs = ["bar_test.py"], + visibility = ["//:__subpackages__"], +) + +# Stale aliased py_test — deleted_test.py does not exist, should be removed. +# TODO: Known limitation: not fully deleted until gazelle is bumped to include +# https://github.com/bazel-contrib/bazel-gazelle/pull/2362 +my_py_test( + name = "deleted_test", + visibility = ["//:__subpackages__"], +) diff --git a/gazelle/python/testdata/remove_invalid_per_file/alias_kind/bar.py b/gazelle/python/testdata/remove_invalid_per_file/alias_kind/bar.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/gazelle/python/testdata/remove_invalid_per_file/alias_kind/bar_test.py b/gazelle/python/testdata/remove_invalid_per_file/alias_kind/bar_test.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/gazelle/python/testdata/remove_invalid_per_file/bar.py b/gazelle/python/testdata/remove_invalid_per_file/bar.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/gazelle/python/testdata/remove_invalid_per_file/bar_test.py b/gazelle/python/testdata/remove_invalid_per_file/bar_test.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/gazelle/python/testdata/remove_invalid_per_file/map_kind/BUILD.in b/gazelle/python/testdata/remove_invalid_per_file/map_kind/BUILD.in new file mode 100644 index 0000000000..00b9a60d6f --- /dev/null +++ b/gazelle/python/testdata/remove_invalid_per_file/map_kind/BUILD.in @@ -0,0 +1,37 @@ +load(":mylib.bzl", "my_py_binary", "my_py_test") + +# gazelle:python_generation_mode file +# gazelle:map_kind py_binary my_py_binary :mylib.bzl +# gazelle:map_kind py_test my_py_test :mylib.bzl + +# Valid py_library — bar.py exists on disk, should be kept. +py_library( + name = "bar", + srcs = ["bar.py"], + visibility = ["//:__subpackages__"], +) + +# Stale py_library — deleted.py does not exist, should be removed. +py_library( + name = "deleted_lib", + srcs = ["deleted.py"], + visibility = ["//:__subpackages__"], +) + +# Stale mapped py_binary — deleted_bin.py does not exist, should be removed. +my_py_binary( + name = "deleted_bin", + srcs = ["deleted_bin.py"], +) + +# Valid mapped py_test — bar_test.py exists on disk, should be kept. +my_py_test( + name = "bar_test", + srcs = ["bar_test.py"], +) + +# Stale mapped py_test — deleted_test.py does not exist, should be removed. +my_py_test( + name = "deleted_test", + srcs = ["deleted_test.py"], +) diff --git a/gazelle/python/testdata/remove_invalid_per_file/map_kind/BUILD.out b/gazelle/python/testdata/remove_invalid_per_file/map_kind/BUILD.out new file mode 100644 index 0000000000..84ca905031 --- /dev/null +++ b/gazelle/python/testdata/remove_invalid_per_file/map_kind/BUILD.out @@ -0,0 +1,19 @@ +load("@rules_python//python:defs.bzl", "py_library") +load(":mylib.bzl", "my_py_test") + +# gazelle:python_generation_mode file +# gazelle:map_kind py_binary my_py_binary :mylib.bzl +# gazelle:map_kind py_test my_py_test :mylib.bzl + +# Valid py_library — bar.py exists on disk, should be kept. +py_library( + name = "bar", + srcs = ["bar.py"], + visibility = ["//:__subpackages__"], +) + +# Valid mapped py_test — bar_test.py exists on disk, should be kept. +my_py_test( + name = "bar_test", + srcs = ["bar_test.py"], +) diff --git a/gazelle/python/testdata/remove_invalid_per_file/map_kind/bar.py b/gazelle/python/testdata/remove_invalid_per_file/map_kind/bar.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/gazelle/python/testdata/remove_invalid_per_file/map_kind/bar_test.py b/gazelle/python/testdata/remove_invalid_per_file/map_kind/bar_test.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/gazelle/python/testdata/remove_invalid_per_file/test.yaml b/gazelle/python/testdata/remove_invalid_per_file/test.yaml new file mode 100644 index 0000000000..ed97d539c0 --- /dev/null +++ b/gazelle/python/testdata/remove_invalid_per_file/test.yaml @@ -0,0 +1 @@ +--- diff --git a/gazelle/pythonconfig/BUILD.bazel b/gazelle/pythonconfig/BUILD.bazel index 711bf2eb42..a82a7f19ca 100644 --- a/gazelle/pythonconfig/BUILD.bazel +++ b/gazelle/pythonconfig/BUILD.bazel @@ -10,8 +10,8 @@ go_library( visibility = ["//visibility:public"], deps = [ "//manifest", - "@bazel_gazelle//label:go_default_library", - "@com_github_emirpasic_gods//lists/singlylinkedlist", + "@bazel_gazelle//label", + "@com_github_emirpasic_gods//lists/singlylinkedlist:go_default_library", ], ) diff --git a/internal_dev_deps.bzl b/internal_dev_deps.bzl index 50277ad4ad..6d4d7656b7 100644 --- a/internal_dev_deps.bzl +++ b/internal_dev_deps.bzl @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -"""Dependencies that are needed for development and testing of rules_python itself.""" +"""Dependencies that are needed for development and testing of rules_python itself in WORKSPACE mode.""" load("@bazel_tools//tools/build_defs/repo:http.bzl", _http_archive = "http_archive", _http_file = "http_file") load("@bazel_tools//tools/build_defs/repo:local.bzl", "local_repository") @@ -34,7 +34,7 @@ def http_file(name, **kwargs): ) def rules_python_internal_deps(): - """Fetches all required dependencies for developing/testing rules_python itself. + """Fetches all required dependencies for developing/testing rules_python itself in WORKSPACE mode. Setup of these dependencies is done by `internal_dev_setup.bzl` @@ -107,6 +107,13 @@ def rules_python_internal_deps(): ], ) + # Stub repository for rules_pyrefly in WORKSPACE mode so that load() + # statements for @rules_pyrefly resolve without requiring full Pyrefly. + local_repository( + name = "rules_pyrefly", + path = "tests/modules/rules_pyrefly_stub", + ) + # The below two deps are required for the integration test with bazel # gazelle. Maybe the test should be moved to the `gazelle` workspace? http_archive( diff --git a/news/.gitkeep b/news/.gitkeep new file mode 100644 index 0000000000..35e8e662aa --- /dev/null +++ b/news/.gitkeep @@ -0,0 +1 @@ +# Keep this directory even if empty so git tracks it. diff --git a/news/3997.added.md b/news/3997.added.md new file mode 100644 index 0000000000..34cbfc20e1 --- /dev/null +++ b/news/3997.added.md @@ -0,0 +1,5 @@ +(bzlmod) Added the `{obj}`explicit_init_py`` tag class to the +`{obj}`config`` module extension for configuring implicit `__init__.py` file +generation module-wide. +([#3997](https://github.com/bazel-contrib/rules_python/pull/3997), +[#2945](https://github.com/bazel-contrib/rules_python/issues/2945)) diff --git a/news/4025.fixed.md b/news/4025.fixed.md new file mode 100644 index 0000000000..80e81333f6 --- /dev/null +++ b/news/4025.fixed.md @@ -0,0 +1,3 @@ +(pypi) Fixed {obj}`RECORD` file paths for extracted `.data` directory contents +so that {obj}`importlib.metadata.files()` correctly locates installed +distribution files ([#4025](https://github.com/bazel-contrib/rules_python/pull/4025)). diff --git a/news/BUILD.bazel b/news/BUILD.bazel new file mode 100644 index 0000000000..79b48675ca --- /dev/null +++ b/news/BUILD.bazel @@ -0,0 +1,6 @@ +package(default_visibility = ["//:__subpackages__"]) + +filegroup( + name = "news_files", + srcs = glob(["**"]), +) diff --git a/news/add_runfiles_raise_api.added.md b/news/add_runfiles_raise_api.added.md new file mode 100644 index 0000000000..5b2ff574e6 --- /dev/null +++ b/news/add_runfiles_raise_api.added.md @@ -0,0 +1,2 @@ +(runfiles) Added {obj}`Runfiles.CreateOrRaise` to return a `Runfiles` instance +or raise an error if runfiles cannot be found. diff --git a/private/BUILD.bazel b/private/BUILD.bazel index ef5652b826..c6bfe6bd9f 100644 --- a/private/BUILD.bazel +++ b/private/BUILD.bazel @@ -13,7 +13,7 @@ multirun( "requirements_linux", ] ] + [ - "//docs:requirements.update", + "//dev:requirements.update", ], tags = ["manual"], ) diff --git a/python/BUILD.bazel b/python/BUILD.bazel index 90b2225ab5..a4dd98c572 100644 --- a/python/BUILD.bazel +++ b/python/BUILD.bazel @@ -24,6 +24,10 @@ In an ideal renaming, we'd move the packaging rules to a different package so that @rules_python//python is only concerned with the core rules. """ +# TODO: make current_py_toolchain.bzl private +# gazelle:resolve starlark @rules_python//python:current_py_toolchain.bzl //python:current_py_toolchain_bzl +# gazelle:resolve starlark //python:current_py_toolchain.bzl //python:current_py_toolchain_bzl + load("@bazel_skylib//:bzl_library.bzl", "bzl_library") load(":current_py_toolchain.bzl", "current_py_toolchain") @@ -54,181 +58,6 @@ filegroup( # ========= bzl_library targets end ========= -bzl_library( - name = "current_py_toolchain_bzl", - srcs = ["current_py_toolchain.bzl"], - deps = ["//python/private:toolchain_types_bzl"], -) - -bzl_library( - name = "defs_bzl", - srcs = [ - "defs.bzl", - ], - visibility = ["//visibility:public"], - deps = [ - ":current_py_toolchain_bzl", - ":py_binary_bzl", - ":py_import_bzl", - ":py_info_bzl", - ":py_library_bzl", - ":py_runtime_bzl", - ":py_runtime_info_bzl", - ":py_runtime_pair_bzl", - ":py_test_bzl", - ], -) - -bzl_library( - name = "features_bzl", - srcs = ["features.bzl"], -) - -bzl_library( - name = "packaging_bzl", - srcs = ["packaging.bzl"], - deps = [ - "//python/entry_points:py_console_script_binary_bzl", - "//python/private:bzlmod_enabled_bzl", - "//python/private:py_package_bzl", - "//python/private:py_wheel_bzl", - "//python/private:util_bzl", - "@bazel_skylib//rules:native_binary", - ], -) - -bzl_library( - name = "pip_bzl", - srcs = ["pip.bzl"], - deps = [ - "//python/private:normalize_name_bzl", - "//python/private/pypi:multi_pip_parse_bzl", - "//python/private/pypi:package_annotation_bzl", - "//python/private/pypi:pip_compile_bzl", - "//python/private/pypi:pip_repository_bzl", - "//python/private/pypi:whl_library_alias_bzl", - "//python/private/whl_filegroup:whl_filegroup_bzl", - ], -) - -bzl_library( - name = "proto_bzl", - srcs = [ - "proto.bzl", - ], - visibility = ["//visibility:public"], - deps = [ - "@com_google_protobuf//bazel:py_proto_library_bzl", - ], -) - -bzl_library( - name = "py_binary_bzl", - srcs = ["py_binary.bzl"], - deps = [ - "//python/private:py_binary_macro_bzl", - ], -) - -bzl_library( - name = "py_cc_link_params_info_bzl", - srcs = ["py_cc_link_params_info.bzl"], - deps = [ - "//python/private:py_cc_link_params_info_bzl", - ], -) - -bzl_library( - name = "py_exec_tools_info_bzl", - srcs = ["py_exec_tools_info.bzl"], - deps = ["//python/private:py_exec_tools_info_bzl"], -) - -bzl_library( - name = "py_exec_tools_toolchain_bzl", - srcs = ["py_exec_tools_toolchain.bzl"], - deps = ["//python/private:py_exec_tools_toolchain_bzl"], -) - -bzl_library( - name = "py_executable_info_bzl", - srcs = ["py_executable_info.bzl"], - deps = ["//python/private:py_executable_info_bzl"], -) - -bzl_library( - name = "py_import_bzl", - srcs = ["py_import.bzl"], - deps = [":py_info_bzl"], -) - -bzl_library( - name = "py_info_bzl", - srcs = ["py_info.bzl"], - deps = [ - "//python/private:py_info_bzl", - ], -) - -bzl_library( - name = "py_library_bzl", - srcs = ["py_library.bzl"], - deps = [ - "//python/private:py_library_macro_bzl", - ], -) - -bzl_library( - name = "py_runtime_bzl", - srcs = ["py_runtime.bzl"], - deps = [ - "//python/private:py_runtime_macro_bzl", - ], -) - -bzl_library( - name = "py_runtime_pair_bzl", - srcs = ["py_runtime_pair.bzl"], - deps = [ - "//python/private:py_runtime_pair_macro_bzl", - ], -) - -bzl_library( - name = "py_runtime_info_bzl", - srcs = ["py_runtime_info.bzl"], - deps = [ - "//python/private:py_runtime_info_bzl", - ], -) - -bzl_library( - name = "py_test_bzl", - srcs = ["py_test.bzl"], - deps = [ - "//python/private:py_test_macro_bzl", - ], -) - -bzl_library( - name = "repositories_bzl", - srcs = ["repositories.bzl"], - deps = [ - "//python/private:is_standalone_interpreter_bzl", - "//python/private:py_repositories_bzl", - "//python/private:python_register_multi_toolchains_bzl", - "//python/private:python_register_toolchains_bzl", - "//python/private:python_repository_bzl", - ], -) - -bzl_library( - name = "versions_bzl", - srcs = ["versions.bzl"], - visibility = ["//:__subpackages__"], - deps = ["//python/private:platform_info_bzl"], -) - # NOTE: Remember to add bzl_library targets to //tests:bzl_libraries # ========= bzl_library targets end ========= @@ -348,3 +177,286 @@ exports_files([ current_py_toolchain( name = "current_py_toolchain", ) + +# Keep this target because it is a public API and Gazelle might otherwise +# remove it if it is not loaded by other bzl_library targets. +# keep +bzl_library( + name = "current_py_toolchain_bzl", + srcs = ["current_py_toolchain.bzl"], + visibility = ["//visibility:public"], +) + +bzl_library( + name = "defs", + srcs = ["defs.bzl"], + deps = [ + ":py_binary", + ":py_import", + ":py_info", + ":py_library", + ":py_runtime", + ":py_runtime_info", + ":py_runtime_pair", + ":py_test", + "//python:current_py_toolchain_bzl", + ], +) + +alias( + name = "defs_bzl", + actual = ":defs", + deprecation = "Use //python:defs instead", +) + +alias( + name = "features_bzl", + actual = ":features", + deprecation = "Use //python:features instead", +) + +bzl_library( + name = "packaging", + srcs = ["packaging.bzl"], + deps = [ + ":py_binary", + "//python/private:bzlmod_enabled", + "//python/private:py_package", + "//python/private:py_wheel", + "//python/private:util", + "@bazel_skylib//rules:native_binary", + ], +) + +alias( + name = "packaging_bzl", + actual = ":packaging", + deprecation = "Use //python:packaging instead", +) + +bzl_library( + name = "pip", + srcs = ["pip.bzl"], + deps = [ + "//python/private:normalize_name", + "//python/private/pypi:multi_pip_parse", + "//python/private/pypi:package_annotation", + "//python/private/pypi:pip_compile", + "//python/private/pypi:pip_repository", + "//python/private/pypi:whl_library_alias", + "//python/private/whl_filegroup", + ], +) + +alias( + name = "pip_bzl", + actual = ":pip", + deprecation = "Use //python:pip instead", +) + +bzl_library( + name = "proto", + srcs = ["proto.bzl"], + deps = ["@com_google_protobuf//bazel:py_proto_library_bzl"], +) + +alias( + name = "proto_bzl", + actual = ":proto", + deprecation = "Use //python:proto instead", +) + +bzl_library( + name = "py_binary", + srcs = ["py_binary.bzl"], + deps = ["//python/private:py_binary_macro"], +) + +alias( + name = "py_binary_bzl", + actual = ":py_binary", + deprecation = "Use //python:py_binary instead", +) + +bzl_library( + name = "py_cc_link_params_info", + srcs = ["py_cc_link_params_info.bzl"], + deps = ["//python/private:py_cc_link_params_info"], +) + +alias( + name = "py_cc_link_params_info_bzl", + actual = ":py_cc_link_params_info", + deprecation = "Use //python:py_cc_link_params_info instead", +) + +bzl_library( + name = "py_exec_tools_info", + srcs = ["py_exec_tools_info.bzl"], + deps = ["//python/private:py_exec_tools_info"], +) + +alias( + name = "py_exec_tools_info_bzl", + actual = ":py_exec_tools_info", + deprecation = "Use //python:py_exec_tools_info instead", +) + +bzl_library( + name = "py_exec_tools_toolchain", + srcs = ["py_exec_tools_toolchain.bzl"], + deps = ["//python/private:py_exec_tools_toolchain"], +) + +alias( + name = "py_exec_tools_toolchain_bzl", + actual = ":py_exec_tools_toolchain", + deprecation = "Use //python:py_exec_tools_toolchain instead", +) + +bzl_library( + name = "py_executable_info", + srcs = ["py_executable_info.bzl"], + deps = ["//python/private:py_executable_info"], +) + +alias( + name = "py_executable_info_bzl", + actual = ":py_executable_info", + deprecation = "Use //python:py_executable_info instead", +) + +bzl_library( + name = "py_import", + srcs = ["py_import.bzl"], + deps = [":py_info"], +) + +alias( + name = "py_import_bzl", + actual = ":py_import", + deprecation = "Use //python:py_import instead", +) + +bzl_library( + name = "py_info", + srcs = ["py_info.bzl"], + deps = ["//python/private:py_info"], +) + +alias( + name = "py_info_bzl", + actual = ":py_info", + deprecation = "Use //python:py_info instead", +) + +bzl_library( + name = "py_library", + srcs = ["py_library.bzl"], + deps = ["//python/private:py_library_macro"], +) + +alias( + name = "py_library_bzl", + actual = ":py_library", + deprecation = "Use //python:py_library instead", +) + +bzl_library( + name = "py_runtime", + srcs = ["py_runtime.bzl"], + deps = ["//python/private:py_runtime_macro"], +) + +alias( + name = "py_runtime_bzl", + actual = ":py_runtime", + deprecation = "Use //python:py_runtime instead", +) + +bzl_library( + name = "py_runtime_info", + srcs = ["py_runtime_info.bzl"], + deps = ["//python/private:py_runtime_info"], +) + +alias( + name = "py_runtime_info_bzl", + actual = ":py_runtime_info", + deprecation = "Use //python:py_runtime_info instead", +) + +bzl_library( + name = "py_runtime_pair", + srcs = ["py_runtime_pair.bzl"], + deps = ["//python/private:py_runtime_pair_macro"], +) + +alias( + name = "py_runtime_pair_bzl", + actual = ":py_runtime_pair", + deprecation = "Use //python:py_runtime_pair instead", +) + +# keep +bzl_library( + name = "py_test", + srcs = ["py_test.bzl"], + deps = ["//python/private:py_test_macro"], +) + +alias( + name = "py_test_bzl", + actual = ":py_test", + deprecation = "Use //python:py_test instead", +) + +alias( + name = "python_bzl", + actual = ":python", + deprecation = "Use //python:python instead", +) + +bzl_library( + name = "repositories", + srcs = ["repositories.bzl"], + deps = [ + "//python/private:is_standalone_interpreter", + "//python/private:py_repositories", + "//python/private:python_register_multi_toolchains", + "//python/private:python_register_toolchains", + "//python/private:python_repository", + ], +) + +alias( + name = "repositories_bzl", + actual = ":repositories", + deprecation = "Use //python:repositories instead", +) + +bzl_library( + name = "versions", + srcs = ["versions.bzl"], + deps = [ + "//python/private:pbs_manifest", + "//python/private:platform_info", + "//python/private:runtimes_manifest_workspace", + ], +) + +alias( + name = "versions_bzl", + actual = ":versions", + deprecation = "Use //python:versions instead", +) + +bzl_library( + name = "features", + srcs = ["features.bzl"], +) + +bzl_library( + name = "python", + srcs = ["python.bzl"], +) diff --git a/python/api/BUILD.bazel b/python/api/BUILD.bazel index 11fee103cb..93c70f4c8b 100644 --- a/python/api/BUILD.bazel +++ b/python/api/BUILD.bazel @@ -18,46 +18,41 @@ package( default_visibility = ["//:__subpackages__"], ) +filegroup( + name = "distribution", + srcs = glob(["**"]), +) + bzl_library( - name = "api_bzl", + name = "api", srcs = ["api.bzl"], - visibility = ["//visibility:public"], - deps = ["//python/private/api:api_bzl"], + deps = ["//python/private/api"], ) bzl_library( - name = "attr_builders_bzl", + name = "attr_builders", srcs = ["attr_builders.bzl"], - deps = ["//python/private:attr_builders_bzl"], + deps = ["//python/private:attr_builders"], ) bzl_library( - name = "executables_bzl", + name = "executables", srcs = ["executables.bzl"], - visibility = ["//visibility:public"], deps = [ - "//python/private:py_binary_rule_bzl", - "//python/private:py_executable_bzl", - "//python/private:py_test_rule_bzl", + "//python/private:py_binary_rule", + "//python/private:py_executable", + "//python/private:py_test_rule", ], ) bzl_library( - name = "libraries_bzl", + name = "libraries", srcs = ["libraries.bzl"], - visibility = ["//visibility:public"], - deps = [ - "//python/private:py_library_bzl", - ], + deps = ["//python/private:py_library"], ) bzl_library( - name = "rule_builders_bzl", + name = "rule_builders", srcs = ["rule_builders.bzl"], - deps = ["//python/private:rule_builders_bzl"], -) - -filegroup( - name = "distribution", - srcs = glob(["**"]), + deps = ["//python/private:rule_builders"], ) 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/cc/BUILD.bazel b/python/cc/BUILD.bazel index f7686c41f6..2fa03dcb5a 100644 --- a/python/cc/BUILD.bazel +++ b/python/cc/BUILD.bazel @@ -47,21 +47,42 @@ toolchain_type( visibility = ["//visibility:public"], ) +filegroup( + name = "distribution", + srcs = glob(["**"]), +) + bzl_library( - name = "py_cc_toolchain_bzl", + name = "py_cc_toolchain", srcs = ["py_cc_toolchain.bzl"], visibility = ["//visibility:public"], - deps = ["//python/private:py_cc_toolchain_macro_bzl"], + deps = ["//python/private:py_cc_toolchain_macro"], ) bzl_library( - name = "py_cc_toolchain_info_bzl", + name = "py_cc_toolchain_info", srcs = ["py_cc_toolchain_info.bzl"], visibility = ["//visibility:public"], - deps = ["//python/private:py_cc_toolchain_info_bzl"], + deps = ["//python/private:py_cc_toolchain_info"], ) -filegroup( - name = "distribution", - srcs = glob(["**"]), +alias( + name = "py_cc_toolchain_bzl", + actual = ":py_cc_toolchain", + deprecation = "Use //python/cc:py_cc_toolchain instead", + visibility = ["//visibility:public"], +) + +alias( + name = "py_cc_toolchain_info_bzl", + actual = ":py_cc_toolchain_info", + deprecation = "Use //python/cc:py_cc_toolchain_info instead", + visibility = ["//visibility:public"], +) + +bzl_library( + name = "py_extension", + srcs = ["py_extension.bzl"], + visibility = ["//visibility:public"], + deps = ["//python/private/cc:py_extension_macro"], ) diff --git a/python/cc/py_extension.bzl b/python/cc/py_extension.bzl new file mode 100644 index 0000000000..8e90517789 --- /dev/null +++ b/python/cc/py_extension.bzl @@ -0,0 +1,23 @@ +"""Rules for creating Python C extension modules. + +This module provides `py_extension` for building Python C extension modules +that can be imported by Python targets (`py_binary`, `py_test`, `py_library`). +It manages dynamic linking, symbol exports (`PyInit_*`), platform-specific link +flags, and target dependencies cleanly. + +See the [Python C API documentation](https://docs.python.org/3/c-api/index.html) +for information on writing C extension modules. + +:::{include} /_includes/experimental_api.md +::: + +:::{versionadded} 2.3.0 +::: +""" + +load( + "//python/private/cc:py_extension_macro.bzl", + _py_extension = "py_extension", +) + +py_extension = _py_extension diff --git a/python/config_settings/BUILD.bazel b/python/config_settings/BUILD.bazel index 5b1317872f..d92aa4261c 100644 --- a/python/config_settings/BUILD.bazel +++ b/python/config_settings/BUILD.bazel @@ -10,13 +10,20 @@ load( "LibcFlag", "PrecompileFlag", "PrecompileSourceRetentionFlag", + "ValidateTestMainFlag", "VenvsSitePackages", "VenvsUseDeclareSymlinkFlag", rp_string_flag = "string_flag", ) +load("//python/private:visibility.bzl", "NOT_ACTUALLY_PUBLIC") # buildifier: disable=bzl-visibility load("//python/private/pypi:flags.bzl", "define_pypi_internal_flags") load(":config_settings.bzl", "construct_config_settings") +# We don't generate bzl_library for these because they aren't public targets +# and should be moved +# gazelle:exclude config_settings.bzl +# gazelle:exclude transition.bzl + filegroup( name = "distribution", srcs = glob(["**"]) + [ @@ -42,7 +49,7 @@ string_flag( build_setting_default = AddSrcsToRunfilesFlag.AUTO, values = AddSrcsToRunfilesFlag.flag_values(), # NOTE: Only public because it is dependency of public rules. - visibility = ["//visibility:public"], + visibility = NOT_ACTUALLY_PUBLIC, ) string_flag( @@ -61,7 +68,7 @@ config_setting( }, # NOTE: Only public because it is used in py_toolchain_suite from toolchain # repositories - visibility = ["//visibility:public"], + visibility = NOT_ACTUALLY_PUBLIC, ) string_flag( @@ -69,7 +76,15 @@ string_flag( build_setting_default = PrecompileFlag.AUTO, values = sorted(PrecompileFlag.__members__.values()), # NOTE: Only public because it's an implicit dependency - visibility = ["//visibility:public"], + visibility = NOT_ACTUALLY_PUBLIC, +) + +string_flag( + name = "validate_test_main", + build_setting_default = ValidateTestMainFlag.AUTO, + values = ValidateTestMainFlag.flag_values(), + # NOTE: Only public because it's an implicit dependency of py_test. + visibility = NOT_ACTUALLY_PUBLIC, ) string_flag( @@ -77,7 +92,7 @@ string_flag( build_setting_default = PrecompileSourceRetentionFlag.AUTO, values = sorted(PrecompileSourceRetentionFlag.__members__.values()), # NOTE: Only public because it's an implicit dependency - visibility = ["//visibility:public"], + visibility = NOT_ACTUALLY_PUBLIC, ) rp_string_flag( @@ -90,7 +105,7 @@ rp_string_flag( }), values = sorted(BootstrapImplFlag.__members__.values()), # NOTE: Only public because it's an implicit dependency - visibility = ["//visibility:public"], + visibility = NOT_ACTUALLY_PUBLIC, ) label_flag( @@ -113,7 +128,7 @@ string_flag( build_setting_default = LibcFlag.GLIBC, values = LibcFlag.flag_values(), # NOTE: Only public because it is used in pip hub and toolchain repos. - visibility = ["//visibility:public"], + visibility = NOT_ACTUALLY_PUBLIC, ) string_flag( @@ -148,18 +163,25 @@ string_flag( # pip.parse related flags +string_flag( + name = "venv", + build_setting_default = "auto", + # NOTE: Only public because it is used in pip hub repos and executable transitions. + visibility = NOT_ACTUALLY_PUBLIC, +) + string_flag( name = "pip_whl_osx_version", build_setting_default = "", # NOTE: Only public because it is used in pip hub repos. - visibility = ["//visibility:public"], + visibility = NOT_ACTUALLY_PUBLIC, ) string_flag( name = "venvs_site_packages", build_setting_default = VenvsSitePackages.NO, # NOTE: Only public because it is used in pip hub repos. - visibility = ["//visibility:public"], + visibility = NOT_ACTUALLY_PUBLIC, ) config_setting( @@ -168,7 +190,7 @@ config_setting( ":venvs_site_packages": VenvsSitePackages.YES, }, # NOTE: Only public because it is used in whl_library repos. - visibility = ["//visibility:public"], + visibility = NOT_ACTUALLY_PUBLIC, ) define_pypi_internal_flags( @@ -179,7 +201,7 @@ label_flag( name = "pip_env_marker_config", build_setting_default = ":_pip_env_marker_default_config", # NOTE: Only public because it is used in pip hub repos. - visibility = ["//visibility:public"], + visibility = NOT_ACTUALLY_PUBLIC, ) bool_flag( diff --git a/python/config_settings/private/py_args.bzl b/python/config_settings/private/py_args.bzl deleted file mode 100644 index 09a26461b7..0000000000 --- a/python/config_settings/private/py_args.bzl +++ /dev/null @@ -1,42 +0,0 @@ -# Copyright 2023 The Bazel Authors. All rights reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""A helper to extract default args for the transition rule.""" - -def py_args(name, kwargs): - """A helper to extract common py_binary and py_test args - - See https://bazel.build/reference/be/python#py_binary and - https://bazel.build/reference/be/python#py_test for the list - that should be returned - - Args: - name: The name of the target. - kwargs: The kwargs to be extracted from; MODIFIED IN-PLACE. - - Returns: - A dict with the extracted arguments - """ - return dict( - args = kwargs.pop("args", None), - data = kwargs.pop("data", None), - env = kwargs.pop("env", None), - srcs = kwargs.pop("srcs", None), - deps = kwargs.pop("deps", None), - # See https://bazel.build/reference/be/python#py_binary.main - # for default logic. - # NOTE: This doesn't match the exact way a regular py_binary searches for - # it's main amongst the srcs, but is close enough for most cases. - main = kwargs.pop("main", name + ".py"), - ) diff --git a/python/entry_points/BUILD.bazel b/python/entry_points/BUILD.bazel index 46dbd9298b..7cfd0cdf25 100644 --- a/python/entry_points/BUILD.bazel +++ b/python/entry_points/BUILD.bazel @@ -21,17 +21,22 @@ exports_files( visibility = ["//docs:__subpackages__"], ) -bzl_library( - name = "py_console_script_binary_bzl", - srcs = [":py_console_script_binary.bzl"], - visibility = ["//visibility:public"], - deps = [ - "//python/private:py_console_script_binary_bzl", - ], -) - filegroup( name = "distribution", srcs = glob(["**"]), visibility = ["//python:__subpackages__"], ) + +bzl_library( + name = "py_console_script_binary", + srcs = ["py_console_script_binary.bzl"], + visibility = ["//visibility:public"], + deps = ["//python/private:py_console_script_binary"], +) + +alias( + name = "py_console_script_binary_bzl", + actual = ":py_console_script_binary", + deprecation = "Use //python/entry_points:py_console_script_binary instead", + visibility = ["//visibility:public"], +) diff --git a/python/extensions/BUILD.bazel b/python/extensions/BUILD.bazel index 50d2b3bbdf..af18fa15b7 100644 --- a/python/extensions/BUILD.bazel +++ b/python/extensions/BUILD.bazel @@ -25,28 +25,47 @@ filegroup( ) bzl_library( - name = "pip_bzl", + name = "config", + srcs = ["config.bzl"], + visibility = ["//:__subpackages__"], + deps = [ + "//python/private:internal_config_repo", + "//python/private/pypi:deps", + "@bazel_features//:features", + ], +) + +bzl_library( + name = "pip", srcs = ["pip.bzl"], visibility = ["//:__subpackages__"], - deps = ["//python/private/pypi:pip_bzl"], + deps = ["//python/private/pypi:pip"], ) bzl_library( - name = "python_bzl", + name = "python", srcs = ["python.bzl"], visibility = ["//:__subpackages__"], - deps = [ - "//python/private:python_bzl", - ], + deps = ["//python/private:python"], ) -bzl_library( +alias( name = "config_bzl", - srcs = ["config.bzl"], + actual = ":config", + deprecation = "Use //python/extensions:config instead", + visibility = ["//:__subpackages__"], +) + +alias( + name = "pip_bzl", + actual = ":pip", + deprecation = "Use //python/extensions:pip instead", + visibility = ["//:__subpackages__"], +) + +alias( + name = "python_bzl", + actual = ":python", + deprecation = "Use //python/extensions:python instead", visibility = ["//:__subpackages__"], - deps = [ - "//python/private:internal_config_repo_bzl", - "//python/private/pypi:deps_bzl", - "@bazel_features//:features", - ], ) diff --git a/python/extensions/config.bzl b/python/extensions/config.bzl index f19a07aaef..a6d03c6f70 100644 --- a/python/extensions/config.bzl +++ b/python/extensions/config.bzl @@ -22,9 +22,43 @@ to repositories that are expensive to create or invalidate frequently. }, ) +_explicit_init_py = tag_class( + doc = """ +Require explicit `__init__.py` files *in this module*. + +Disables the legacy `__init__.py` generation for all `py_*` targets in this +module, requiring all Python targets to explicitly provide `__init__.py` files +when they're needed. + +To override this at a per-target level, set `legacy_create_init` on applicable +`py_binary` or `py_test` targets: + +```starlark +py_binary( + name = "hello_python", + # ... + # This Binary still relies on legacy behavior, so + # enable the legacy behavior as an exceptional case. + legacy_create_init = 1, +) +``` + +:::{note} +In the future, this will be enabled by default. +::: + +:::{versionadded} VERSION_NEXT_FEATURE +::: +""", + attrs = { + "default": attr.bool(doc = "Whether explicit __init__.py files are required by default.", mandatory = True), + }, +) + def _config_impl(module_ctx): transition_setting_generators = {} transition_settings = [] + explicit_init_py_modules = {} for mod in module_ctx.modules: for tag in mod.tags.add_transition_setting: setting = str(tag.setting) @@ -32,11 +66,16 @@ def _config_impl(module_ctx): transition_setting_generators[setting] = [] transition_settings.append(setting) transition_setting_generators[setting].append(mod.name) + for tag in mod.tags.explicit_init_py: + explicit_init_py_modules[mod.name] = str(tag.default) + if mod.is_root: + explicit_init_py_modules[""] = str(tag.default) internal_config_repo( name = "rules_python_internal", transition_setting_generators = transition_setting_generators, transition_settings = transition_settings, + explicit_init_py_modules = explicit_init_py_modules, ) pypi_deps() @@ -55,5 +94,6 @@ config = module_extension( implementation = _config_impl, tag_classes = { "add_transition_setting": _add_transition_setting, + "explicit_init_py": _explicit_init_py, }, ) diff --git a/python/features.bzl b/python/features.bzl index 26acc8c5fe..99050a7ca5 100644 --- a/python/features.bzl +++ b/python/features.bzl @@ -26,6 +26,10 @@ def _features_typedef(): A map of public API targets available in rules_python for feature detection purposes. + :::{seealso} + * {obj}`features.loadable_symbols` + ::: + :::{versionadded} 1.9.0 ::: :::: @@ -49,6 +53,19 @@ def _features_typedef(): ::: :::: + ::::{field} loadable_symbols + :type: dict[str, list[str]] + + A map of bzl paths to the list of public symbols they export. + + :::{seealso} + * {obj}`features.targets` + ::: + + :::{versionadded} 2.2.0 + ::: + :::: + ::::{field} py_info_venv_symlinks True if the `PyInfo.venv_symlinks` field is available. @@ -86,13 +103,56 @@ def _features_typedef(): _TARGETS = { "//command_line_option:build_runfile_links": True, "//command_line_option:enable_runfiles": True, + "//command_line_option:extra_toolchains": True, + "//python/api:api": True, + "//python/api:executables": True, + "//python/api:libraries": True, "//python/cc:current_py_cc_headers_abi3": True, + "//python/cc:py_cc_toolchain": True, + "//python/cc:py_cc_toolchain_info": True, + "//python/cc:py_extension": True, + "//python/config_settings:venv": True, + "//python/entry_points:py_console_script_binary": True, + "//python/local_toolchains:repos": True, + "//python:defs": True, + "//python:features": True, + "//python:packaging": True, + "//python:pip": True, + "//python:proto": True, + "//python:py_binary": True, + "//python:py_cc_link_params_info": True, + "//python:py_exec_tools_info": True, + "//python:py_exec_tools_toolchain": True, + "//python:py_executable_info": True, + "//python:py_import": True, + "//python:py_info": True, + "//python:py_library": True, + "//python:py_runtime": True, + "//python:py_runtime_info": True, + "//python:py_runtime_pair": True, + "//python:py_test": True, + "//python:repositories": True, + "//python:versions": True, +} + +_LOADABLE_SYMBOLS = { + "//python/cc:py_extension.bzl": [ + # keep sorted + "py_extension", + ], + "//python:py_info.bzl": [ + # keep sorted + "PyInfo", + "VenvSymlinkEntry", + "VenvSymlinkKind", + ], } features = struct( TYPEDEF = _features_typedef, # keep sorted headers_abi3 = True, + loadable_symbols = _LOADABLE_SYMBOLS, precompile = True, py_info_venv_symlinks = True, targets = _TARGETS, diff --git a/python/local_toolchains/BUILD.bazel b/python/local_toolchains/BUILD.bazel index 211f3e21a7..256dffb337 100644 --- a/python/local_toolchains/BUILD.bazel +++ b/python/local_toolchains/BUILD.bazel @@ -2,17 +2,24 @@ load("@bazel_skylib//:bzl_library.bzl", "bzl_library") package(default_visibility = ["//:__subpackages__"]) +filegroup( + name = "distribution", + srcs = glob(["**"]), +) + bzl_library( - name = "repos_bzl", + name = "repos", srcs = ["repos.bzl"], visibility = ["//visibility:public"], deps = [ - "//python/private:local_runtime_repo_bzl", - "//python/private:local_runtime_toolchains_repo_bzl", + "//python/private:local_runtime_repo", + "//python/private:local_runtime_toolchains_repo", ], ) -filegroup( - name = "distribution", - srcs = glob(["**"]), +alias( + name = "repos_bzl", + actual = ":repos", + deprecation = "Use //python/local_toolchains:repos instead", + visibility = ["//visibility:public"], ) diff --git a/python/packaging.bzl b/python/packaging.bzl index 537aa6090f..fb333ca72f 100644 --- a/python/packaging.bzl +++ b/python/packaging.bzl @@ -63,7 +63,7 @@ py_wheel_dist = rule( doc = """\ Prepare a dist/ folder, following Python's packaging standard practice. -See https://packaging.python.org/en/latest/tutorials/packaging-projects/#generating-distribution-archives +See which recommends a dist/ folder containing the wheel file(s), source distributions, etc. This also has the advantage that stamping information is included in the wheel's filename. @@ -94,7 +94,7 @@ def py_wheel( **kwargs): """Builds a Python Wheel. - Wheels are Python distribution format defined in https://www.python.org/dev/peps/pep-0427/. + Wheels are Python distribution format defined in {pep}`PEP 427`. This macro packages a set of targets into a single wheel. It wraps the [py_wheel rule](#py_wheel_rule). @@ -144,7 +144,7 @@ def py_wheel( To publish the wheel to PyPI, the twine package is required and it is installed by default on `bzlmod` setups. On legacy `WORKSPACE`, `rules_python` doesn't provide `twine` itself - (see https://github.com/bazel-contrib/rules_python/issues/1016), but + (see {gh-issue}`1016`), but you can install it with `pip_parse`, just like we do any other dependencies. Once you've installed twine, you can pass its label to the `twine` @@ -160,7 +160,7 @@ def py_wheel( ) ``` - Now you can run a command like the following, which publishes to https://test.pypi.org/ + Now you can run a command like the following, which publishes to ```sh % TWINE_USERNAME=__token__ TWINE_PASSWORD=pypi-*** \\ @@ -172,7 +172,7 @@ def py_wheel( name: A unique name for this target. twine: A label of the external location of the py_library target for twine twine_binary: A label of the external location of a binary target for twine. - publish_args: arguments passed to twine, e.g. ["--repository-url", "https://pypi.my.org/simple/"]. + publish_args: arguments passed to twine, e.g. `["--repository-url", "https://pypi.my.org/simple/"]`. These are subject to make var expansion, as with the `args` attribute. Note that you can also pass additional args to the bazel run command as in the example above. **kwargs: other named parameters passed to the underlying [py_wheel rule](#py_wheel_rule) diff --git a/python/pip_install/BUILD.bazel b/python/pip_install/BUILD.bazel index 665375cc5b..b3c1ab1c03 100644 --- a/python/pip_install/BUILD.bazel +++ b/python/pip_install/BUILD.bazel @@ -18,23 +18,6 @@ package( default_visibility = ["//:__subpackages__"], ) -bzl_library( - name = "pip_repository_bzl", - srcs = ["pip_repository.bzl"], - deps = [ - "//python/private/pypi:package_annotation_bzl", - "//python/private/pypi:pip_repository_bzl", - "//python/private/pypi:whl_config_repo_bzl", - "//python/private/pypi:whl_library_bzl", - ], -) - -bzl_library( - name = "requirements_bzl", - srcs = ["requirements.bzl"], - deps = ["//python/private/pypi:pip_compile_bzl"], -) - filegroup( name = "distribution", srcs = glob(["**"]), @@ -51,3 +34,26 @@ exports_files( glob(["*.bzl"]), visibility = ["//docs:__pkg__"], ) + +bzl_library( + name = "pip_repository", + srcs = ["pip_repository.bzl"], + deps = [ + "//python/private/pypi:package_annotation", + "//python/private/pypi:pip_repository", + "//python/private/pypi:whl_config_repo", + "//python/private/pypi:whl_library", + ], +) + +bzl_library( + name = "requirements", + srcs = ["requirements.bzl"], + deps = ["//python/private/pypi:pip_compile"], +) + +bzl_library( + name = "requirements_parser", + srcs = ["requirements_parser.bzl"], + deps = ["//python/private/pypi:parse_requirements_txt"], +) diff --git a/python/private/BUILD.bazel b/python/private/BUILD.bazel index 6ab3d546be..0b0dc97f66 100644 --- a/python/private/BUILD.bazel +++ b/python/private/BUILD.bazel @@ -17,11 +17,12 @@ load("@bazel_skylib//rules:common_settings.bzl", "bool_flag") load("//python:py_binary.bzl", "py_binary") load("//python:py_library.bzl", "py_library") load(":bazel_config_mode.bzl", "bazel_config_mode") -load(":print_toolchain_checksums.bzl", "print_toolchains_checksums") load(":py_exec_tools_toolchain.bzl", "current_interpreter_executable") -load(":sentinel.bzl", "sentinel") -load(":stamp.bzl", "stamp_build_setting") +load(":py_interpreter_program.bzl", "py_interpreter_program") +load(":sentinel_impl.bzl", "sentinel") +load(":stamp_impl.bzl", "stamp_build_setting") load(":uncachable_version_file.bzl", "define_uncachable_version_file") +load(":visibility.bzl", "NOT_ACTUALLY_PUBLIC") package( default_visibility = [ @@ -31,7 +32,11 @@ package( licenses(["notice"]) -exports_files(["runtime_env_toolchain_interpreter.sh"]) +exports_files([ + "runtime_env_toolchain_interpreter.sh", + "runtimes_manifest.txt", + "runtimes_manifest_workspace.bzl", +]) filegroup( name = "distribution", @@ -47,7 +52,7 @@ filegroup( ) filegroup( - name = "coverage_deps", + name = "coverage_deps_filegroup", srcs = ["coverage_deps.bzl"], visibility = ["//tools/private/update_deps:__pkg__"], ) @@ -59,838 +64,974 @@ filegroup( visibility = ["//python:__pkg__"], ) +alias( + name = "build_data_writer", + actual = select({ + "@platforms//os:windows": ":build_data_writer.ps1", + "//conditions:default": ":build_data_writer.sh", + }), + # Not actually public. Only public because it's an implicit dependency of + # rules built via py_binary_rule_builder() / py_test_rule_builder(). + visibility = ["//visibility:public"], +) + +define_uncachable_version_file( + name = "uncachable_version_file", + # Not actually public. Only public because it's an implicit dependency of + # rules built via py_binary_rule_builder() / py_test_rule_builder(). + visibility = ["//visibility:public"], +) + +# Needed to define bzl_library targets for docgen. (We don't define the +# bzl_library target here because it'd give our users a transitive dependency +# on Skylib.) +exports_files( + [ + "coverage.patch", + "py_package.bzl", + "py_wheel.bzl", + "version.bzl", + "reexports.bzl", + "stamp_impl.bzl", + "util.bzl", + ], + visibility = ["//:__subpackages__"], +) + +exports_files( + ["python_bootstrap_template.txt"], + # Not actually public. Only public because it's an implicit dependency of + # py_runtime. + visibility = NOT_ACTUALLY_PUBLIC, +) + +filegroup( + name = "stage1_bootstrap_template", + srcs = ["stage1_bootstrap_template.sh"], + # Not actually public. Only public because it's an implicit dependency of + # py_runtime. + visibility = NOT_ACTUALLY_PUBLIC, +) + +filegroup( + name = "stage2_bootstrap_template", + srcs = ["stage2_bootstrap_template.py"], + # Not actually public. Only public because it's an implicit dependency of + # py_runtime. + visibility = NOT_ACTUALLY_PUBLIC, +) + +filegroup( + name = "site_init_template", + srcs = ["site_init_template.py"], + # Not actually public. Only public because it's an implicit dependency of + # py_runtime. + visibility = NOT_ACTUALLY_PUBLIC, +) + +# NOTE: Windows builds don't use this bootstrap. Instead, a native Windows +# program locates some Python exe and runs `python.exe foo.zip` which +# runs the __main__.py in the zip file. +alias( + name = "bootstrap_template", + actual = select({ + ":is_script_bootstrap_enabled": "stage1_bootstrap_template.sh", + "//conditions:default": "python_bootstrap_template.txt", + }), + # Not actually public. Only public because it's an implicit dependency of + # py_runtime. + visibility = NOT_ACTUALLY_PUBLIC, +) + +# Used to determine the use of `--stamp` in Starlark rules +stamp_build_setting(name = "stamp") + +config_setting( + name = "is_script_bootstrap_enabled", + flag_values = { + "//python/config_settings:bootstrap_impl": "script", + }, +) + +config_setting( + name = "is_bazel_config_mode_target", + flag_values = { + "//python/private:bazel_config_mode": "target", + }, +) + +alias( + name = "debugger_if_target_config", + actual = select({ + ":is_bazel_config_mode_target": "//python/config_settings:debugger", + "//conditions:default": "//python/private:empty", + }), + # Not actually public. Only public because it's an implicit dependency of + # rules built via py_binary_rule_builder() / py_test_rule_builder(). + visibility = ["//visibility:public"], +) + +bazel_config_mode(name = "bazel_config_mode") + +# This should only be set by analysis tests to expose additional metadata to +# aid testing, so a setting instead of a flag. +bool_flag( + name = "visible_for_testing", + build_setting_default = False, + # This is only because it is an implicit dependency by the toolchains. + visibility = NOT_ACTUALLY_PUBLIC, +) + +# Used for py_console_script_gen rule +py_binary( + name = "py_console_script_gen_py", + srcs = ["py_console_script_gen.py"], + main = "py_console_script_gen.py", + visibility = [ + "//visibility:public", + ], +) + +py_binary( + name = "py_wheel_dist", + srcs = ["py_wheel_dist.py"], + visibility = ["//visibility:public"], +) + +py_library( + name = "py_console_script_gen_lib", + srcs = ["py_console_script_gen.py"], + imports = ["../.."], + visibility = [ + "//tests/entry_points:__pkg__", + ], +) + +# The current toolchain's interpreter as an excutable, usable with +# executable=True attributes. +current_interpreter_executable( + name = "current_interpreter_executable", + # Not actually public. Only public because it's an implicit dependency of + # py_exec_tools_toolchain. + visibility = NOT_ACTUALLY_PUBLIC, +) + +py_library( + name = "empty", + # Not actually public. Only public because it's the resolved default of + # debugger_if_target_config, an implicit dependency of rules built via + # py_binary_rule_builder() / py_test_rule_builder(). + visibility = ["//visibility:public"], +) + +sentinel( + name = "sentinel", + # Not actually public. Only public because it's the resolved default of + # uncachable_version_file, an implicit dependency of rules built via + # py_binary_rule_builder() / py_test_rule_builder(). + visibility = ["//visibility:public"], +) + +py_binary( + name = "sync_runtimes_manifest_workspace", + srcs = ["tools/sync_runtimes_manifest_workspace.py"], + visibility = ["//:__subpackages__"], +) + +# Tool used by py_test's validation action to statically check that the main +# module actually runs tests. See the validate_test_main config setting. +py_interpreter_program( + name = "py_test_main_validator", + main = "py_test_main_validator.py", + # Not actually public. Only public because it's an implicit dependency of + # the py_test rule. + visibility = NOT_ACTUALLY_PUBLIC, +) + +py_library( + name = "py_test_main_validator_lib", + srcs = ["py_test_main_validator.py"], + imports = ["../.."], + visibility = [ + "//tests/validate_test_main:__pkg__", + ], +) + bzl_library( - name = "attr_builders_bzl", + name = "attr_builders", srcs = ["attr_builders.bzl"], deps = [ - ":builders_util_bzl", + ":builders_util", "@bazel_skylib//lib:types", ], ) bzl_library( - name = "attributes_bzl", + name = "attributes", srcs = ["attributes.bzl"], deps = [ - ":attr_builders_bzl", - ":common_bzl", - ":common_labels_bzl", - ":enum_bzl", - ":flags_bzl", - ":py_info_bzl", - ":py_internal_bzl", - ":reexports_bzl", - ":rule_builders_bzl", - ":rules_cc_srcs_bzl", + ":attr_builders", + ":common_labels", + ":enum", + ":flags", + ":py_info", + ":reexports", + ":rule_builders", "@bazel_skylib//lib:dicts", "@bazel_skylib//rules:common_settings", + "@rules_cc//cc/common", ], ) bzl_library( - name = "auth_bzl", + name = "auth", srcs = ["auth.bzl"], - deps = [":bazel_tools_bzl"], + deps = ["//python/private:bazel_tools"], ) +# @bazel_tools can't define bzl_library itself, so we just put a wrapper around it. +# Keep this target because Gazelle does not automatically generate +# bzl_library targets for external repositories like @bazel_tools. +# keep bzl_library( - name = "runtime_env_toolchain_bzl", - srcs = ["runtime_env_toolchain.bzl"], - deps = [ - ":config_settings_bzl", - ":py_exec_tools_toolchain_bzl", - ":toolchain_types_bzl", - "//python:py_runtime_bzl", - "//python:py_runtime_pair_bzl", + name = "bazel_tools", + srcs = [ + # This set of sources is overly broad, but it's the only public + # target available across Bazel versions that has all the necessary + # sources. + "@bazel_tools//tools:bzl_srcs", ], ) -alias( - name = "build_data_writer", - actual = select({ - "@platforms//os:windows": ":build_data_writer.ps1", - "//conditions:default": ":build_data_writer.sh", - }), -) - bzl_library( - name = "builders_bzl", + name = "builders", srcs = ["builders.bzl"], - deps = [ - "@bazel_skylib//lib:types", - ], + deps = ["@bazel_skylib//lib:types"], ) bzl_library( - name = "builders_util_bzl", + name = "builders_util", srcs = ["builders_util.bzl"], deps = [ - ":bzlmod_enabled_bzl", + ":bzlmod_enabled", "@bazel_skylib//lib:types", ], ) bzl_library( - name = "bzlmod_enabled_bzl", - srcs = ["bzlmod_enabled.bzl"], -) - -bzl_library( - name = "cc_helper_bzl", + name = "cc_helper", srcs = ["cc_helper.bzl"], - deps = [":py_internal_bzl"], + deps = [":py_internal"], ) bzl_library( - name = "common_bzl", + name = "common", srcs = ["common.bzl"], deps = [ - ":cc_helper_bzl", - ":py_cc_link_params_info_bzl", - ":py_info_bzl", - ":py_internal_bzl", - ":reexports_bzl", - ":rules_cc_srcs_bzl", + ":builders", + ":cc_helper", + ":py_cc_link_params_info", + ":py_info", + ":py_internal", + ":py_interpreter_program", + ":reexports", + ":toolchain_types", "@bazel_skylib//lib:paths", + "@rules_cc//cc/common", + "@rules_python_internal//:rules_python_config", ], ) bzl_library( - name = "common_labels_bzl", - srcs = ["common_labels.bzl"], -) - -bzl_library( - name = "config_settings_bzl", + name = "config_settings", srcs = ["config_settings.bzl"], deps = [ - ":version_bzl", + ":text_util", + ":version", + ":visibility", "@bazel_skylib//lib:selects", "@bazel_skylib//rules:common_settings", ], ) bzl_library( - name = "coverage_deps_bzl", + name = "coverage_deps", srcs = ["coverage_deps.bzl"], deps = [ - ":bazel_tools_bzl", - ":repo_utils_bzl", - ":version_label_bzl", + ":version_label", + "//python/private:bazel_tools", ], ) bzl_library( - name = "deprecation_bzl", - srcs = ["deprecation.bzl"], + name = "current_py_cc_headers", + srcs = ["current_py_cc_headers.bzl"], deps = [ - "@rules_python_internal//:rules_python_config_bzl", + ":toolchain_types", + "@rules_cc//cc/common", ], ) bzl_library( - name = "enum_bzl", - srcs = ["enum.bzl"], + name = "current_py_cc_libs", + srcs = ["current_py_cc_libs.bzl"], + deps = ["@rules_cc//cc/common"], ) bzl_library( - name = "envsubst_bzl", - srcs = ["envsubst.bzl"], + name = "deprecation", + srcs = ["deprecation.bzl"], + deps = ["@rules_python_internal//:rules_python_config"], ) bzl_library( - name = "flags_bzl", + name = "flags", srcs = ["flags.bzl"], deps = [ - ":enum_bzl", + ":enum", "@bazel_skylib//rules:common_settings", ], ) bzl_library( - name = "full_version_bzl", - srcs = ["full_version.bzl"], -) - -bzl_library( - name = "internal_config_repo_bzl", - srcs = ["internal_config_repo.bzl"], - deps = [ - ":repo_utils_bzl", - ":text_util_bzl", - ], -) - -bzl_library( - name = "is_standalone_interpreter_bzl", - srcs = ["is_standalone_interpreter.bzl"], - deps = [ - ":repo_utils_bzl", - ], -) - -bzl_library( - name = "local_runtime_repo_bzl", - srcs = ["local_runtime_repo.bzl"], + name = "hermetic_runtime_repo_setup", + srcs = ["hermetic_runtime_repo_setup.bzl"], deps = [ - ":enum_bzl", - ":repo_utils.bzl", + ":py_exec_tools_toolchain", + ":version", + "//python:py_runtime", + "//python:py_runtime_pair", + "//python/cc:py_cc_toolchain", + "@rules_cc//cc:core_rules", ], ) bzl_library( - name = "local_runtime_toolchains_repo_bzl", - srcs = ["local_runtime_toolchains_repo.bzl"], + name = "internal_config_repo", + srcs = ["internal_config_repo.bzl"], deps = [ - ":repo_utils.bzl", - ":text_util_bzl", + ":repo_utils", + ":text_util", ], ) bzl_library( - name = "normalize_name_bzl", - srcs = ["normalize_name.bzl"], -) - -bzl_library( - name = "precompile_bzl", - srcs = ["precompile.bzl"], + name = "interpreter", + srcs = ["interpreter.bzl"], deps = [ - ":attributes_bzl", - ":py_internal_bzl", - ":py_interpreter_program_bzl", - ":toolchain_types_bzl", + ":common", + ":sentinel_impl", + ":toolchain_types", + "//python:py_runtime_info", "@bazel_skylib//lib:paths", ], ) bzl_library( - name = "platform_info_bzl", - srcs = ["platform_info.bzl"], -) - -bzl_library( - name = "python_bzl", - srcs = ["python.bzl"], - deps = [ - ":full_version_bzl", - ":platform_info_bzl", - ":python_register_toolchains_bzl", - ":pythons_hub_bzl", - ":repo_utils_bzl", - ":toolchains_repo_bzl", - ":version_bzl", - "@bazel_features//:features", - ], + name = "is_standalone_interpreter", + srcs = ["is_standalone_interpreter.bzl"], + deps = [":repo_utils"], ) bzl_library( - name = "python_register_toolchains_bzl", - srcs = ["python_register_toolchains.bzl"], + name = "local_runtime_repo", + srcs = ["local_runtime_repo.bzl"], deps = [ - ":auth_bzl", - ":bazel_tools_bzl", - ":coverage_deps_bzl", - ":full_version_bzl", - ":internal_config_repo_bzl", - ":python_repository_bzl", - ":repo_utils_bzl", - ":toolchains_repo_bzl", - "//python:versions_bzl", - "//python/private/pypi:deps_bzl", + ":enum", + ":repo_utils", ], ) bzl_library( - name = "python_repository_bzl", - srcs = ["python_repository.bzl"], + name = "local_runtime_repo_setup", + srcs = ["local_runtime_repo_setup.bzl"], deps = [ - ":auth_bzl", - ":repo_utils_bzl", - ":text_util_bzl", - "//python:versions_bzl", + "@bazel_skylib//lib:selects", + "@rules_cc//cc:core_rules", + "@rules_python//python:py_runtime", + "@rules_python//python:py_runtime_pair", + "@rules_python//python/cc:py_cc_toolchain", + "@rules_python//python/private:py_exec_tools_toolchain", ], ) bzl_library( - name = "python_register_multi_toolchains_bzl", - srcs = ["python_register_multi_toolchains.bzl"], + name = "local_runtime_toolchains_repo", + srcs = ["local_runtime_toolchains_repo.bzl"], deps = [ - ":python_register_toolchains_bzl", - ":toolchains_repo_bzl", - "//python:versions_bzl", + ":repo_utils", + ":text_util", ], ) bzl_library( - name = "pythons_hub_bzl", - srcs = ["pythons_hub.bzl"], + name = "precompile", + srcs = ["precompile.bzl"], deps = [ - ":py_toolchain_suite_bzl", - ":text_util_bzl", - "//python:versions_bzl", + ":attributes", + ":flags", + ":py_interpreter_program", + ":toolchain_types", + "@bazel_skylib//rules:common_settings", ], ) bzl_library( - name = "py_binary_macro_bzl", + name = "py_binary_macro", srcs = ["py_binary_macro.bzl"], deps = [ - ":py_binary_rule_bzl", - ":py_executable_bzl", + ":py_binary_rule", + ":py_executable", ], ) bzl_library( - name = "py_binary_rule_bzl", + name = "py_binary_rule", srcs = ["py_binary_rule.bzl"], deps = [ - ":attributes_bzl", - ":py_executable_bzl", - ":rule_builders_bzl", - "@bazel_skylib//lib:dicts", + ":attributes", + ":py_executable", ], ) bzl_library( - name = "py_cc_link_params_info_bzl", + name = "py_cc_link_params_info", srcs = ["py_cc_link_params_info.bzl"], - deps = [ - ":rules_cc_srcs_bzl", - ":util_bzl", - ], + deps = ["@rules_cc//cc/common"], ) bzl_library( - name = "py_cc_toolchain_macro_bzl", + name = "py_cc_toolchain_macro", srcs = ["py_cc_toolchain_macro.bzl"], deps = [ - ":py_cc_toolchain_rule_bzl", + ":py_cc_toolchain_rule", + ":util", + "//python/private/pypi:pep508_env", ], ) bzl_library( - name = "py_cc_toolchain_rule_bzl", + name = "py_cc_toolchain_rule", srcs = ["py_cc_toolchain_rule.bzl"], deps = [ - ":common_labels_bzl", - ":py_cc_toolchain_info_bzl", - ":rules_cc_srcs_bzl", - ":sentinel_bzl", - ":util_bzl", + ":common_labels", + ":flags", + ":py_cc_toolchain_info", + ":sentinel_impl", "@bazel_skylib//rules:common_settings", + "@rules_cc//cc/common", ], ) bzl_library( - name = "py_cc_toolchain_info_bzl", - srcs = ["py_cc_toolchain_info.bzl"], -) - -bzl_library( - name = "py_console_script_binary_bzl", - srcs = [ - "py_console_script_binary.bzl", - "py_console_script_gen.bzl", - ], - visibility = ["//python/entry_points:__pkg__"], + name = "py_console_script_binary", + srcs = ["py_console_script_binary.bzl"], deps = [ - "//python:py_binary_bzl", + ":py_console_script_gen", + "//python:py_binary", ], ) bzl_library( - name = "py_exec_tools_info_bzl", - srcs = ["py_exec_tools_info.bzl"], -) - -bzl_library( - name = "py_exec_tools_toolchain_bzl", + name = "py_exec_tools_toolchain", srcs = ["py_exec_tools_toolchain.bzl"], deps = [ - ":common_bzl", - ":common_labels_bzl", - ":py_exec_tools_info_bzl", - ":sentinel_bzl", - ":toolchain_types_bzl", + ":common_labels", + ":py_exec_tools_info", + ":sentinel_impl", + ":toolchain_types", "@bazel_skylib//lib:paths", "@bazel_skylib//rules:common_settings", ], ) bzl_library( - name = "py_executable_bzl", + name = "py_executable", srcs = ["py_executable.bzl"], deps = [ - ":attributes_bzl", - ":cc_helper_bzl", - ":common_bzl", - ":common_labels_bzl", - ":flags_bzl", - ":precompile_bzl", - ":py_cc_link_params_info_bzl", - ":py_executable_info_bzl", - ":py_info_bzl", - ":py_internal_bzl", - ":py_runtime_info_bzl", - ":rules_cc_srcs_bzl", - ":toolchain_types_bzl", - ":transition_labels_bzl", - ":venv_runfiles_bzl", + ":attr_builders", + ":attributes", + ":builders", + ":cc_helper", + ":common", + ":common_labels", + ":flags", + ":precompile", + ":py_cc_link_params_info", + ":py_executable_info", + ":py_info", + ":py_internal", + ":py_runtime_info", + ":reexports", + ":rule_builders", + ":toolchain_types", + ":transition_labels", + ":venv_runfiles", "@bazel_skylib//lib:dicts", "@bazel_skylib//lib:paths", "@bazel_skylib//lib:structs", "@bazel_skylib//rules:common_settings", + "@rules_cc//cc/common", + "@rules_python_internal//:rules_python_config", ], ) bzl_library( - name = "py_executable_info_bzl", - srcs = ["py_executable_info.bzl"], -) - -bzl_library( - name = "py_info_bzl", + name = "py_info", srcs = ["py_info.bzl"], deps = [ - ":builders_bzl", - ":reexports_bzl", + ":builders", + ":reexports", ], ) bzl_library( - name = "py_internal_bzl", + name = "py_internal", srcs = ["py_internal.bzl"], - deps = ["//tools/build_defs/python/private:py_internal_renamed_bzl"], + deps = ["//tools/build_defs/python/private:py_internal_renamed"], ) bzl_library( - name = "py_interpreter_program_bzl", + name = "py_interpreter_program", srcs = ["py_interpreter_program.bzl"], deps = [ - ":sentinel_bzl", + ":sentinel_impl", "@bazel_skylib//rules:common_settings", ], ) bzl_library( - name = "py_library_bzl", + name = "py_library", srcs = ["py_library.bzl"], deps = [ - ":attributes_bzl", - ":common_bzl", - ":common_labels_bzl", - ":flags_bzl", - ":normalize_name_bzl", - ":precompile_bzl", - ":py_cc_link_params_info_bzl", - ":py_internal_bzl", - ":rule_builders_bzl", - ":toolchain_types_bzl", - ":venv_runfiles_bzl", - ":version_bzl", + ":attr_builders", + ":attributes", + ":builders", + ":common", + ":common_labels", + ":flags", + ":normalize_name", + ":precompile", + ":py_cc_link_params_info", + ":py_info", + ":reexports", + ":rule_builders", + ":toolchain_types", + ":venv_runfiles", + ":version", "@bazel_skylib//lib:dicts", + "@bazel_skylib//lib:paths", "@bazel_skylib//rules:common_settings", ], ) bzl_library( - name = "py_library_macro_bzl", + name = "py_library_macro", srcs = ["py_library_macro.bzl"], - deps = [":py_library_rule_bzl"], + deps = [":py_library_rule"], ) bzl_library( - name = "py_library_rule_bzl", + name = "py_library_rule", srcs = ["py_library_rule.bzl"], - deps = [ - ":common_labels_bzl", - ":py_library_bzl", - ], + deps = [":py_library"], ) bzl_library( - name = "py_package_bzl", + name = "py_package", srcs = ["py_package.bzl"], - visibility = ["//:__subpackages__"], deps = [ - ":builders_bzl", - ":py_info_bzl", + ":builders", + ":py_info", ], ) bzl_library( - name = "py_runtime_info_bzl", - srcs = ["py_runtime_info.bzl"], -) - -bzl_library( - name = "py_repositories_bzl", + name = "py_repositories", srcs = ["py_repositories.bzl"], deps = [ - ":bazel_tools_bzl", - ":internal_config_repo_bzl", - ":pythons_hub_bzl", - "//python:versions_bzl", - "//python/private/pypi:deps_bzl", + ":internal_config_repo", + ":pythons_hub", + "//python:versions", + "//python/private:bazel_tools", + "//python/private/pypi:deps", ], ) bzl_library( - name = "py_runtime_macro_bzl", + name = "py_runtime_macro", srcs = ["py_runtime_macro.bzl"], - deps = [":py_runtime_rule_bzl"], + deps = [":py_runtime_rule"], ) bzl_library( - name = "py_runtime_rule_bzl", - srcs = ["py_runtime_rule.bzl"], - deps = [ - ":attributes_bzl", - ":common_labels_bzl", - ":flags_bzl", - ":py_internal_bzl", - ":py_runtime_info_bzl", - ":reexports_bzl", - ":rule_builders_bzl", - ":version_bzl", - "@bazel_skylib//lib:dicts", - "@bazel_skylib//lib:paths", - "@bazel_skylib//rules:common_settings", - ], + name = "py_runtime_pair_macro", + srcs = ["py_runtime_pair_macro.bzl"], + deps = [":py_runtime_pair_rule"], ) bzl_library( - name = "py_runtime_pair_macro_bzl", - srcs = ["py_runtime_pair_macro.bzl"], - visibility = ["//:__subpackages__"], - deps = [":py_runtime_pair_rule_bzl"], + name = "py_runtime_pair_rule", + srcs = ["py_runtime_pair_rule.bzl"], + deps = [ + ":common_labels", + ":reexports", + "//python:py_runtime_info", + "@bazel_skylib//rules:common_settings", + ], ) bzl_library( - name = "py_runtime_pair_rule_bzl", - srcs = ["py_runtime_pair_rule.bzl"], + name = "py_runtime_rule", + srcs = ["py_runtime_rule.bzl"], deps = [ - ":common_labels_bzl", - "//python:py_runtime_bzl", - "//python:py_runtime_info_bzl", + ":common_labels", + ":flags", + ":py_internal", + ":py_runtime_info", + ":reexports", + ":version", + "@bazel_skylib//lib:dicts", + "@bazel_skylib//lib:paths", "@bazel_skylib//rules:common_settings", ], ) bzl_library( - name = "py_test_macro_bzl", + name = "py_test_macro", srcs = ["py_test_macro.bzl"], deps = [ - ":py_executable_bzl", - ":py_test_rule_bzl", + ":py_executable", + ":py_test_rule", ], ) bzl_library( - name = "py_test_rule_bzl", + name = "py_test_rule", srcs = ["py_test_rule.bzl"], deps = [ - ":attributes_bzl", - ":common_bzl", - ":py_executable_bzl", - ":rule_builders_bzl", - "@bazel_skylib//lib:dicts", + ":attributes", + ":common", + ":py_executable", ], ) bzl_library( - name = "py_toolchain_suite_bzl", + name = "py_toolchain_suite", srcs = ["py_toolchain_suite.bzl"], deps = [ - ":config_settings_bzl", - ":text_util_bzl", - ":toolchain_types_bzl", + ":text_util", + ":toolchain_types", "@bazel_skylib//lib:selects", + "@platforms//host:constraints_lib", ], ) bzl_library( - name = "py_wheel_bzl", + name = "py_wheel", srcs = ["py_wheel.bzl"], - visibility = ["//:__subpackages__"], deps = [ - ":py_package_bzl", - ":stamp_bzl", - ":transition_labels_bzl", - ":version_bzl", + ":attributes", + ":py_info", + ":py_package", + ":rule_builders", + ":stamp_impl", + ":transition_labels", + ":version", ], ) bzl_library( - name = "reexports_bzl", - srcs = ["reexports.bzl"], - visibility = [ - "//:__subpackages__", + name = "python", + srcs = ["python.bzl"], + deps = [ + ":auth", + ":full_version", + ":pbs_manifest", + ":platform_info", + ":pyproject_utils", + ":python_register_toolchains", + ":pythons_hub", + ":repo_utils", + ":toolchains_repo", + ":version", + "//python:versions", + "@bazel_features//:features", ], +) + +bzl_library( + name = "python_register_multi_toolchains", + srcs = ["python_register_multi_toolchains.bzl"], deps = [ - ":bazel_tools_bzl", - "@rules_python_internal//:rules_python_config_bzl", + ":python_register_toolchains", + ":toolchains_repo", + "//python:versions", ], ) bzl_library( - name = "repo_utils_bzl", - srcs = ["repo_utils.bzl"], + name = "python_register_toolchains", + srcs = ["python_register_toolchains.bzl"], + deps = [ + ":coverage_deps", + ":full_version", + ":python_repository", + ":toolchains_repo", + "//python:versions", + ], ) bzl_library( - name = "rule_builders_bzl", + name = "python_repository", + srcs = ["python_repository.bzl"], + deps = [ + ":auth", + ":repo_utils", + ":text_util", + "//python:versions", + ], +) + +bzl_library( + name = "pythons_hub", + srcs = ["pythons_hub.bzl"], + deps = [ + ":pbs_manifest", + ":text_util", + ":toolchains_repo", + "//python:versions", + ], +) + +bzl_library( + name = "reexports", + srcs = ["reexports.bzl"], + deps = ["@rules_python_internal//:rules_python_config"], +) + +bzl_library( + name = "repl", + srcs = ["repl.bzl"], + deps = ["//python:py_binary"], +) + +bzl_library( + name = "rule_builders", srcs = ["rule_builders.bzl"], deps = [ - ":builders_bzl", - ":builders_util_bzl", + ":builders_util", "@bazel_skylib//lib:types", ], ) +# Keep this target because Gazelle does not automatically generate or +# maintain composite bzl_library targets. We need this to group external +# dependency sources. +# keep bzl_library( - name = "sentinel_bzl", - srcs = ["sentinel.bzl"], + name = "rules_cc_srcs", + srcs = [ + # rules_cc 0.0.13 and earlier load cc_proto_libary (and thus protobuf@), + # but their bzl srcs targets don't transitively refer to protobuf. + "@com_google_protobuf//:bzl_srcs", + # NOTE: As of rules_cc 0.10, cc:bzl_srcs no longer contains + # everything and sub-targets must be used instead + "@rules_cc//cc:bzl_srcs", + "@rules_cc//cc/common", + "@rules_cc//cc/toolchains:toolchain_rules", + ], + deps = [ + ":bazel_tools", + "@rules_cc//cc/common", + ], ) bzl_library( - name = "stamp_bzl", - srcs = ["stamp.bzl"], - visibility = ["//:__subpackages__"], + name = "runtime_env_repo", + srcs = ["runtime_env_repo.bzl"], + deps = [":repo_utils"], ) bzl_library( - name = "text_util_bzl", - srcs = ["text_util.bzl"], + name = "runtime_env_toolchain", + srcs = ["runtime_env_toolchain.bzl"], + deps = [ + ":config_settings", + ":py_exec_tools_toolchain", + ":toolchain_types", + "//python:py_runtime", + "//python:py_runtime_pair", + "//python/cc:py_cc_toolchain", + "@rules_cc//cc:core_rules", + ], ) bzl_library( - name = "toolchains_repo_bzl", - srcs = ["toolchains_repo.bzl"], + name = "toolchain_aliases", + srcs = ["toolchain_aliases.bzl"], deps = [ - ":repo_utils_bzl", - ":text_util_bzl", - "//python:versions_bzl", + "//python:versions", + "@bazel_skylib//lib:selects", ], ) bzl_library( - name = "toolchain_types_bzl", - srcs = ["toolchain_types.bzl"], + name = "toolchains_repo", + srcs = ["toolchains_repo.bzl"], + deps = [ + ":repo_utils", + ":text_util", + "//python:versions", + ], ) bzl_library( - name = "transition_labels_bzl", + name = "transition_labels", srcs = ["transition_labels.bzl"], deps = [ - ":common_labels_bzl", + ":common_labels", "@bazel_skylib//lib:collections", - "@rules_python_internal//:extra_transition_settings_bzl", + "@rules_python_internal//:extra_transition_settings", ], ) bzl_library( - name = "util_bzl", + name = "util", srcs = ["util.bzl"], deps = [ - ":py_internal_bzl", + ":py_internal", "@bazel_skylib//lib:types", ], ) -define_uncachable_version_file( - name = "uncachable_version_file", +bzl_library( + name = "venv_runfiles", + srcs = ["venv_runfiles.bzl"], + deps = [ + ":common", + ":py_info", + ":util", + "@bazel_skylib//lib:paths", + ], ) bzl_library( - name = "version_bzl", - srcs = ["version.bzl"], + name = "pyproject_utils", + srcs = ["pyproject_utils.bzl"], + deps = [ + ":version", + "@toml.bzl//:toml", + ], ) bzl_library( - name = "version_label_bzl", - srcs = ["version_label.bzl"], + name = "stamp_impl", + srcs = ["stamp_impl.bzl"], + deps = [":visibility"], ) -# @bazel_tools can't define bzl_library itself, so we just put a wrapper around it. bzl_library( - name = "bazel_tools_bzl", - srcs = [ - # This set of sources is overly broad, but it's the only public - # target available across Bazel versions that has all the necessary - # sources. - "@bazel_tools//tools:bzl_srcs", - ], + name = "bzlmod_enabled", + srcs = ["bzlmod_enabled.bzl"], ) bzl_library( - name = "rules_cc_srcs_bzl", - srcs = [ - # rules_cc 0.0.13 and earlier load cc_proto_libary (and thus protobuf@), - # but their bzl srcs targets don't transitively refer to protobuf. - "@com_google_protobuf//:bzl_srcs", - # NOTE: As of rules_cc 0.10, cc:bzl_srcs no longer contains - # everything and sub-targets must be used instead - "@rules_cc//cc:bzl_srcs", - "@rules_cc//cc/common", - "@rules_cc//cc/toolchains:toolchain_rules", - ], - deps = [ - ":bazel_tools_bzl", - "@rules_cc//cc/common", - ], + name = "common_labels", + srcs = ["common_labels.bzl"], ) bzl_library( - name = "venv_runfiles_bzl", - srcs = ["venv_runfiles.bzl"], - deps = [ - ":common_bzl", - ":py_info_bzl", - ":py_internal_bzl", - "@bazel_skylib//lib:paths", - ], + name = "enum", + srcs = ["enum.bzl"], ) -# Needed to define bzl_library targets for docgen. (We don't define the -# bzl_library target here because it'd give our users a transitive dependency -# on Skylib.) -exports_files( - [ - "coverage.patch", - "repack_whl.py", - "py_package.bzl", - "py_wheel.bzl", - "version.bzl", - "reexports.bzl", - "stamp.bzl", - "util.bzl", - ], - visibility = ["//:__subpackages__"], +bzl_library( + name = "envsubst", + srcs = ["envsubst.bzl"], ) -exports_files( - ["python_bootstrap_template.txt"], - # Not actually public. Only public because it's an implicit dependency of - # py_runtime. - visibility = ["//visibility:public"], +bzl_library( + name = "full_version", + srcs = ["full_version.bzl"], ) -filegroup( - name = "stage1_bootstrap_template", - srcs = ["stage1_bootstrap_template.sh"], - # Not actually public. Only public because it's an implicit dependency of - # py_runtime. - visibility = ["//visibility:public"], +bzl_library( + name = "normalize_name", + srcs = ["normalize_name.bzl"], ) -filegroup( - name = "stage2_bootstrap_template", - srcs = ["stage2_bootstrap_template.py"], - # Not actually public. Only public because it's an implicit dependency of - # py_runtime. - visibility = ["//visibility:public"], +bzl_library( + name = "pbs_manifest", + srcs = ["pbs_manifest.bzl"], ) -filegroup( - name = "site_init_template", - srcs = ["site_init_template.py"], - # Not actually public. Only public because it's an implicit dependency of - # py_runtime. - visibility = ["//visibility:public"], +bzl_library( + name = "platform_info", + srcs = ["platform_info.bzl"], ) -# NOTE: Windows builds don't use this bootstrap. Instead, a native Windows -# program locates some Python exe and runs `python.exe foo.zip` which -# runs the __main__.py in the zip file. -alias( - name = "bootstrap_template", - actual = select({ - ":is_script_bootstrap_enabled": "stage1_bootstrap_template.sh", - "//conditions:default": "python_bootstrap_template.txt", - }), - # Not actually public. Only public because it's an implicit dependency of - # py_runtime. - visibility = ["//visibility:public"], +bzl_library( + name = "py_cc_toolchain_info", + srcs = ["py_cc_toolchain_info.bzl"], ) -# Used to determine the use of `--stamp` in Starlark rules -stamp_build_setting(name = "stamp") - -config_setting( - name = "is_script_bootstrap_enabled", - flag_values = { - "//python/config_settings:bootstrap_impl": "script", - }, +bzl_library( + name = "py_console_script_gen", + srcs = ["py_console_script_gen.bzl"], ) -config_setting( - name = "is_bazel_config_mode_target", - flag_values = { - "//python/private:bazel_config_mode": "target", - }, +bzl_library( + name = "py_exec_tools_info", + srcs = ["py_exec_tools_info.bzl"], ) -alias( - name = "debugger_if_target_config", - actual = select({ - ":is_bazel_config_mode_target": "//python/config_settings:debugger", - "//conditions:default": "//python/private:empty", - }), +bzl_library( + name = "py_executable_info", + srcs = ["py_executable_info.bzl"], ) -bazel_config_mode(name = "bazel_config_mode") +bzl_library( + name = "py_runtime_info", + srcs = ["py_runtime_info.bzl"], +) -# This should only be set by analysis tests to expose additional metadata to -# aid testing, so a setting instead of a flag. -bool_flag( - name = "visible_for_testing", - build_setting_default = False, - # This is only because it is an implicit dependency by the toolchains. - visibility = ["//visibility:public"], +bzl_library( + name = "repo_utils", + srcs = ["repo_utils.bzl"], ) -print_toolchains_checksums(name = "print_toolchains_checksums") +bzl_library( + name = "runtimes_manifest_workspace", + srcs = ["runtimes_manifest_workspace.bzl"], +) -# Used for py_console_script_gen rule -py_binary( - name = "py_console_script_gen_py", - srcs = ["py_console_script_gen.py"], - main = "py_console_script_gen.py", - visibility = [ - "//visibility:public", - ], +bzl_library( + name = "sentinel_impl", + srcs = ["sentinel_impl.bzl"], ) -py_binary( - name = "py_wheel_dist", - srcs = ["py_wheel_dist.py"], - visibility = ["//visibility:public"], +bzl_library( + name = "text_util", + srcs = ["text_util.bzl"], ) -py_library( - name = "py_console_script_gen_lib", - srcs = ["py_console_script_gen.py"], - imports = ["../.."], - visibility = [ - "//tests/entry_points:__pkg__", - ], +bzl_library( + name = "toolchain_types", + srcs = ["toolchain_types.bzl"], ) -# The current toolchain's interpreter as an excutable, usable with -# executable=True attributes. -current_interpreter_executable( - name = "current_interpreter_executable", - # Not actually public. Only public because it's an implicit dependency of - # py_exec_tools_toolchain. - visibility = ["//visibility:public"], +bzl_library( + name = "version", + srcs = ["version.bzl"], ) -py_library( - name = "empty", +bzl_library( + name = "version_label", + srcs = ["version_label.bzl"], ) -sentinel( - name = "sentinel", +bzl_library( + name = "visibility", + srcs = ["visibility.bzl"], ) diff --git a/python/private/api/BUILD.bazel b/python/private/api/BUILD.bazel index 0826b85d9b..cd7eda1bd2 100644 --- a/python/private/api/BUILD.bazel +++ b/python/private/api/BUILD.bazel @@ -13,6 +13,7 @@ # limitations under the License. load("@bazel_skylib//:bzl_library.bzl", "bzl_library") +load("//python/private:visibility.bzl", "NOT_ACTUALLY_PUBLIC") load(":py_common_api.bzl", "py_common_api") package( @@ -25,24 +26,21 @@ filegroup( ) py_common_api( - name = "py_common_api", + name = "py_common_api_impl", # NOTE: Not actually public. Implicit dependency of public rules. - visibility = ["//visibility:public"], + visibility = NOT_ACTUALLY_PUBLIC, ) bzl_library( - name = "api_bzl", - srcs = ["api.bzl"], + name = "py_common_api", + srcs = ["py_common_api.bzl"], deps = [ - "//python/private:py_info_bzl", + ":api", + "//python/private:py_info", ], ) bzl_library( - name = "py_common_api_bzl", - srcs = ["py_common_api.bzl"], - deps = [ - ":api_bzl", - "//python/private:py_info_bzl", - ], + name = "api", + srcs = ["api.bzl"], ) diff --git a/python/private/api/api.bzl b/python/private/api/api.bzl index 44f9ab4e77..2a1f79fdf5 100644 --- a/python/private/api/api.bzl +++ b/python/private/api/api.bzl @@ -13,7 +13,7 @@ # limitations under the License. """Implementation of py_api.""" -_PY_COMMON_API_LABEL = Label("//python/private/api:py_common_api") +_PY_COMMON_API_LABEL = Label("//python/private/api:py_common_api_impl") ApiImplInfo = provider( doc = "Provider to hold an API implementation", diff --git a/python/private/attributes.bzl b/python/private/attributes.bzl index beaa7a6a73..e1e77cba03 100644 --- a/python/private/attributes.bzl +++ b/python/private/attributes.bzl @@ -271,7 +271,7 @@ source files. Possible values are: the source file. This is most useful when the code won't be modified. For more information on pyc invalidation modes, see -https://docs.python.org/3/library/py_compile.html#py_compile.PycInvalidationMode + """, default = PrecompileInvalidationModeAttr.AUTO, values = sorted(PrecompileInvalidationModeAttr.__members__.values()), @@ -281,7 +281,7 @@ https://docs.python.org/3/library/py_compile.html#py_compile.PycInvalidationMode The optimization level for precompiled files. For more information about optimization levels, see the `compile()` function's -`optimize` arg docs at https://docs.python.org/3/library/functions.html#compile +`optimize` arg docs at NOTE: The value `-1` means "current interpreter", which will be the interpreter used _at build time when pycs are generated_, not the interpreter used at @@ -422,7 +422,7 @@ These values are transitioned on, so will affect the analysis graph and the associated memory overhead. The more unique configurations in your overall build, the more memory and (often unnecessary) re-analysis and re-building can occur. See -https://bazel.build/extending/config#memory-performance-considerations for + for more information about risks and considerations. ::: @@ -446,10 +446,15 @@ def apply_config_settings_attr(settings, attr): if key.package == "command_line_option": if value == "INHERIT": continue - else: - str_key = "//command_line_option:" + key.name + str_key = "//command_line_option:" + key.name + if key.name == "extra_toolchains": + if value == "": + value = [] + else: + value = [v.strip() for v in value.split(",") if v.strip()] else: str_key = str(key) + settings[str_key] = value return settings @@ -530,6 +535,14 @@ environment when the test is executed by bazel test. "@platforms//os:watchos", ], ), + "_validate_test_main": lambda: attrb.Label( + default = "//python/private:py_test_main_validator", + cfg = "exec", + ), + "_validate_test_main_flag": lambda: attrb.Label( + default = labels.VALIDATE_TEST_MAIN, + providers = [BuildSettingInfo], + ), }) # Attributes specific to Python test-equivalent executable rules. Such rules may @@ -542,6 +555,14 @@ AGNOSTIC_TEST_ATTRS = _init_agnostic_test_attrs() # but still accept Python source-agnostic settings. AGNOSTIC_BINARY_ATTRS = dicts.add(AGNOSTIC_EXECUTABLE_ATTRS) +WINDOWS_CONSTRAINTS_PLAIN_ATTRS = { + "_windows_constraints": attr.label_list( + default = [ + "@platforms//os:windows", + ], + ), +} + WINDOWS_CONSTRAINTS_ATTRS = { "_windows_constraints": lambda: attrb.LabelList( default = [ diff --git a/python/private/build_data_writer.ps1 b/python/private/build_data_writer.ps1 index 0074e69d38..05c49fef9c 100644 --- a/python/private/build_data_writer.ps1 +++ b/python/private/build_data_writer.ps1 @@ -21,7 +21,16 @@ $Utf8NoBom = New-Object System.Text.UTF8Encoding $False [System.IO.File]::WriteAllLines($OutputPath, $Lines, $Utf8NoBom) $Acl = Get-Acl $OutputPath -$AccessRule = New-Object System.Security.AccessControl.FileSystemAccessRule("Everyone", "Read", "Allow") +# We use WorldSid because the "Everyone" name is locale-dependent. +$EveryoneSid = New-Object System.Security.Principal.SecurityIdentifier( + [System.Security.Principal.WellKnownSidType]::WorldSid, + $null +) +$AccessRule = New-Object System.Security.AccessControl.FileSystemAccessRule( + $EveryoneSid, + "Read", + "Allow" +) $Acl.SetAccessRule($AccessRule) Set-Acl $OutputPath $Acl diff --git a/python/private/cc/BUILD.bazel b/python/private/cc/BUILD.bazel index 8f4fb468a4..087d406932 100644 --- a/python/private/cc/BUILD.bazel +++ b/python/private/cc/BUILD.bazel @@ -1,3 +1,4 @@ +load("@bazel_skylib//:bzl_library.bzl", "bzl_library") load("@rules_cc//cc:cc_library.bzl", "cc_library") load("//python/private:visibility.bzl", "NOT_ACTUALLY_PUBLIC") @@ -18,3 +19,48 @@ cc_library( name = "empty", visibility = NOT_ACTUALLY_PUBLIC, ) + +# Private alias to avoid "duplicate dependency label" errors when users +# explicitly pass //python/cc:current_py_cc_headers or //python/cc:current_py_cc_libs +# in the deps attribute (including when deps is a select() expression). +alias( + name = "current_py_cc_headers_private_alias", + actual = "//python/cc:current_py_cc_headers", + visibility = NOT_ACTUALLY_PUBLIC, +) + +# Private alias to avoid "duplicate dependency label" errors when users +# explicitly pass //python/cc:current_py_cc_headers or //python/cc:current_py_cc_libs +# in the deps attribute (including when deps is a select() expression). +alias( + name = "current_py_cc_libs_private_alias", + actual = "//python/cc:current_py_cc_libs", + visibility = NOT_ACTUALLY_PUBLIC, +) + +bzl_library( + name = "py_extension_macro", + srcs = ["py_extension_macro.bzl"], + deps = [ + ":py_extension_rule", + "//python/private:common_labels", + "//python/private:util", + "@rules_cc//cc:core_rules", + ], +) + +bzl_library( + name = "py_extension_rule", + srcs = ["py_extension_rule.bzl"], + deps = [ + "//python/private:attr_builders", + "//python/private:attributes", + "//python/private:builders", + "//python/private:common", + "//python/private:py_info", + "//python/private:reexports", + "//python/private:rule_builders", + "//python/private:toolchain_types", + "@bazel_skylib//lib:dicts", + ], +) diff --git a/python/private/cc/py_extension_macro.bzl b/python/private/cc/py_extension_macro.bzl new file mode 100644 index 0000000000..b76685113a --- /dev/null +++ b/python/private/cc/py_extension_macro.bzl @@ -0,0 +1,201 @@ +"""Macro for creating Python extensions. + +:::{include} /_includes/experimental_api.md +::: +""" + +load("@rules_cc//cc:cc_library.bzl", "cc_library") +load("@rules_cc//cc:cc_shared_library.bzl", "cc_shared_library") +load("//python/private:common_labels.bzl", "labels") +load("//python/private:util.bzl", "add_tag", "copy_propagating_kwargs") +load(":py_extension_rule.bzl", "py_extension_libs", "py_extension_wrapper") + +_EMPTY_CANONICAL_TARGET = str(Label("//python/private/cc:empty")) + +_PY_CC_HEADERS_ALIAS_BASE_TARGET = "//python/private/cc:current_py_cc_headers_private_alias" +_PY_CC_HEADERS_ALIAS_CANONICAL_TARGET = str(Label(_PY_CC_HEADERS_ALIAS_BASE_TARGET)) + +_PY_CC_LIBS_ALIAS_BASE_TARGET = "//python/private/cc:current_py_cc_libs_private_alias" +_PY_CC_LIBS_ALIAS_CANONICAL_TARGET = str(Label(_PY_CC_LIBS_ALIAS_BASE_TARGET)) + +def py_extension( + name, + srcs = None, + hdrs = None, + copts = None, + defines = None, + local_defines = None, + includes = None, + linkopts = None, + deps = None, + dynamic_deps = None, + exports_filter = None, + user_link_flags = None, + additional_linker_inputs = None, + module_name = None, + py_limited_api = None, + **kwargs): + """Creates a Python extension module. + + :::{include} /_includes/experimental_api.md + ::: + + By default, extensions are created within their workspace package directory + (e.g., `pkg/ext.so`) and imported using standard Python package paths + (e.g., `from pkg import ext`). + + To customize import path behavior: + - `imports`: Pass `imports = ["..."]` to append custom search directories to + `sys.path` (matching {attr}`py_library.imports`). + - `module_name`: Pass `module_name = "custom_name"` to override the base + module filename. + + :::{versionadded} 2.3.0 + ::: + + Args: + name: {type}`str` Target name. + srcs: {type}`list[Label | str] | None` C/C++ source files to compile + directly for this extension. + hdrs: {type}`list[Label | str] | None` Header files for `srcs`. + copts: {type}`list[str] | None` Compiler flags for `srcs`. + defines: {type}`list[str] | None` Preprocessor defines for `srcs`. + local_defines: {type}`list[str] | None` Preprocessor defines for `srcs` + passed to internal `cc_library`. + includes: {type}`list[str] | None` Header include search paths passed + to internal `cc_library`. + linkopts: {type}`list[str] | None` Link options passed to internal + `cc_library` created for `srcs`/`hdrs`. To pass linker flags to + `cc_shared_library`, use `user_link_flags`. + deps: {type}`list[Label | str] | None` `cc_library` targets to + statically link into the extension. + dynamic_deps: {type}`list[Label | str] | None` `cc_shared_library` + targets to dynamically link. + exports_filter: {type}`list[str] | None` Filter for exported symbols + passed to `cc_shared_library`. + user_link_flags: {type}`list[str] | None` Additional link flags passed + to `cc_shared_library`. To pass linker flags that apply to `srcs`, + use `linkopts`. + additional_linker_inputs: {type}`list[Label | str] | None` Additional + linker inputs passed to `cc_shared_library`. + module_name: {type}`str | None` Custom Python module name. If not set, + defaults to `name`. + py_limited_api: {type}`str | None` Python limited API version string + (e.g., `"3.8"`). + **kwargs: {type}`dict` Additional arguments passed to the underlying + wrapper rule. + """ + add_tag(kwargs, "@rules_python//python/cc:py_extension") + additional_linker_inputs = additional_linker_inputs or [] + copts = copts or [] + deps = deps or [] + user_link_flags = user_link_flags or [] + + csl_deps = [] + + copts = copts + select({ + # -fPIC (Position Independent Code) is required when compiling C/C++ sources into + # dynamic/shared libraries (.so/.dylib/.pyd) so code can be loaded at arbitrary addresses. + # MSVC on Windows does not support or require -fPIC. + labels.PLATFORMS_OS_WINDOWS: [], + "//conditions:default": ["-fPIC"], + }) + + # Private alias targets are appended to avoid "duplicate dependency label" errors + # if a user explicitly passes //python/cc:current_py_cc_headers or //python/cc:current_py_cc_libs + # in their deps attribute (including when deps is a select() expression). + deps = deps + [_PY_CC_HEADERS_ALIAS_CANONICAL_TARGET] + + # 1. If srcs or hdrs are specified, create an implicit cc_library for them + if srcs or hdrs: + impl_lib_name = "_" + name + "_impl" + impl_lib_kwargs = copy_propagating_kwargs(kwargs) + if includes: + impl_lib_kwargs["includes"] = includes + if linkopts: + impl_lib_kwargs["linkopts"] = linkopts + cc_library( + name = impl_lib_name, + srcs = srcs, + hdrs = hdrs, + copts = copts, + defines = defines, + local_defines = local_defines, + deps = deps, + visibility = ["//visibility:private"], + **impl_lib_kwargs + ) + csl_deps.append(":" + impl_lib_name) + + if not csl_deps: + csl_deps = deps + if not csl_deps: + # cc_shared_library requires a dependency, so use an empty library when none are given. + csl_deps = [_EMPTY_CANONICAL_TARGET] + + # 4. Create the underlying cc_shared_library + csl_name = "_" + name + "_csl" + csl_kwargs = copy_propagating_kwargs(kwargs) + + if exports_filter != None: + csl_kwargs["exports_filter"] = exports_filter + + user_link_flags = user_link_flags + select({ + # On macOS, Apple's ld64 linker requires '-undefined dynamic_lookup' so CPython + # C-API symbols (e.g. PyModule_Create) remain unresolved at link time and are + # dynamically resolved at runtime when CPython loads the shared library (.so). + labels.PLATFORMS_OS_MACOS: ["-undefined", "dynamic_lookup"], + "//conditions:default": [], + }) + + win_libs_name = "_" + name + "_win_libs" + + # On Windows, create a private target using toolchain resolution to extract .lib files + # from CcInfo in py_cc_toolchain because system_provided=True in cc_import leaves DefaultInfo empty. + py_extension_libs( + name = win_libs_name, + tags = ["manual"], + visibility = ["//visibility:private"], + ) + + # Windows-specific CPython linking requirements: + # 1. Windows requires .lib files when linking, so they must be added to deps. + # 2. CPython import libraries (python3xx.lib) are declared with system_provided = True + # in cc_import, suppressing automatic propagation of the .lib file path to link.exe. + # We explicitly pass $(locations ...) from win_libs_name to provide the path to MSVC link.exe. + # 3. We pass win_libs_name as an additional linker input to ensure the .lib file is available to the link action. + deps = deps + select({ + labels.PLATFORMS_OS_WINDOWS: [_PY_CC_LIBS_ALIAS_CANONICAL_TARGET], + "//conditions:default": [], + }) + user_link_flags = user_link_flags + select({ + labels.PLATFORMS_OS_WINDOWS: ["$(locations :" + win_libs_name + ")"], + "//conditions:default": [], + }) + additional_linker_inputs = additional_linker_inputs + select({ + labels.PLATFORMS_OS_WINDOWS: [":" + win_libs_name], + "//conditions:default": [], + }) + + cc_shared_library( + name = csl_name, + deps = csl_deps, + additional_linker_inputs = additional_linker_inputs, + dynamic_deps = dynamic_deps, + user_link_flags = user_link_flags, + visibility = ["//visibility:private"], + **csl_kwargs + ) + + # 5. Filter out C++ specific compilation/linking attributes before invoking wrapper rule + for cc_attr in ("includes", "linkopts", "linkshared", "linkstatic", "features"): + kwargs.pop(cc_attr, None) + + # 6. Wrap with py_extension_wrapper for PEP 3149 naming & PyInfo + py_extension_wrapper( + name = name, + src = ":" + csl_name, + module_name = module_name, + py_limited_api = py_limited_api, + **kwargs + ) diff --git a/python/private/cc/py_extension_rule.bzl b/python/private/cc/py_extension_rule.bzl new file mode 100644 index 0000000000..e167969d5b --- /dev/null +++ b/python/private/cc/py_extension_rule.bzl @@ -0,0 +1,135 @@ +"""Implementation of rules supporting py_extension. + +:::{include} /_includes/experimental_api.md +::: +""" + +load("@bazel_skylib//lib:dicts.bzl", "dicts") +load("//python/private:attr_builders.bzl", "attrb") +load("//python/private:attributes.bzl", "COMMON_ATTRS", "IMPORTS_ATTRS", "WINDOWS_CONSTRAINTS_ATTRS") +load("//python/private:builders.bzl", "builders") +load("//python/private:common.bzl", "get_imports", "is_windows_platform") +load("//python/private:py_info.bzl", "PyInfo", "PyInfoBuilder") +load("//python/private:rule_builders.bzl", "ruleb") +load("//python/private:toolchain_types.bzl", "CC_TOOLCHAIN_TYPE", "PY_CC_TOOLCHAIN_TYPE") + +def _py_extension_wrapper_impl(ctx): + module_name = ctx.attr.module_name or ctx.label.name + + ext = _get_extension(ctx) + use_py_limited_api = bool(ctx.attr.py_limited_api) + if use_py_limited_api: + output_filename = "{module_name}.abi3.{ext}".format( + module_name = module_name, + ext = ext, + ) + else: + py_toolchain = ctx.toolchains[PY_CC_TOOLCHAIN_TYPE] + py_cc_toolchain = py_toolchain.py_cc_toolchain + output_filename = "{module_name}.{soabi}.{ext}".format( + module_name = module_name, + soabi = py_cc_toolchain.soabi, + ext = ext, + ) + + py_dso = ctx.actions.declare_file(output_filename) + + # Symlink the cc_shared_library output to the PEP 3149 / abi3 filename + csl_target = ctx.attr.src + csl_file = csl_target[DefaultInfo].files.to_list()[0] + ctx.actions.symlink( + output = py_dso, + target_file = csl_file, + ) + + runfiles_builder = builders.RunfilesBuilder() + runfiles_builder.add(py_dso) + runfiles_builder.add(ctx.files.data) + runfiles_builder.add_targets(ctx.attr.data) + runfiles_builder.add(csl_target[DefaultInfo].default_runfiles) + runfiles = runfiles_builder.build(ctx) + + py_info_builder = PyInfoBuilder.new() + py_info_builder.transitive_sources.add(py_dso) + py_info_builder.imports.add(get_imports(ctx)) + + return [ + DefaultInfo( + files = depset([py_dso]), + runfiles = runfiles, + ), + py_info_builder.build(), + ] + +PY_EXTENSION_WRAPPER_ATTRS = dicts.add( + COMMON_ATTRS, + IMPORTS_ATTRS, + WINDOWS_CONSTRAINTS_ATTRS, + { + "module_name": lambda: attrb.String( + doc = "Custom Python module name. If not set, defaults to name.", + ), + "py_limited_api": lambda: attrb.String( + default = "", + doc = "Python limited API version string (e.g., '3.8').", + ), + "src": lambda: attrb.Label( + mandatory = True, + doc = "The cc_shared_library target to wrap.", + ), + }, +) + +def create_py_extension_wrapper_rule_builder(): + """Create a rule builder for the private internal wrapper rule.""" + return ruleb.Rule( + doc = "Private internal helper rule for py_extension targets.", + implementation = _py_extension_wrapper_impl, + attrs = PY_EXTENSION_WRAPPER_ATTRS, + provides = [PyInfo], + toolchains = [ + ruleb.ToolchainType(PY_CC_TOOLCHAIN_TYPE), + ruleb.ToolchainType(CC_TOOLCHAIN_TYPE), + ], + fragments = ["cpp"], + ) + +py_extension_wrapper = create_py_extension_wrapper_rule_builder().build() + +def _get_extension(ctx): + """Derives the appropriate file extension for C extensions from target platform. + + Note: On macOS, CPython C extensions use .so (PEP 3149), not .dylib. + Windows uses .pyd. + + Args: + ctx: The rule context. + + Returns: + The extension, e.g. "so" or "pyd" + """ + return "pyd" if is_windows_platform(ctx) else "so" + +def _py_extension_libs_impl(ctx): + py_toolchain = ctx.toolchains[PY_CC_TOOLCHAIN_TYPE] + py_cc_toolchain = py_toolchain.py_cc_toolchain + cc_info = py_cc_toolchain.libs.providers_map["CcInfo"] + files = [] + for input in cc_info.linking_context.linker_inputs.to_list(): + for lib in input.libraries: + if lib.interface_library: + files.append(lib.interface_library) + elif lib.static_library: + files.append(lib.static_library) + elif lib.dynamic_library: + files.append(lib.dynamic_library) + link_files = [f for f in files if not f.path.endswith(".dll")] + return [DefaultInfo(files = depset(link_files))] + +py_extension_libs = rule( + implementation = _py_extension_libs_impl, + toolchains = [PY_CC_TOOLCHAIN_TYPE], + doc = """\ +Private internal helper rule for extracting Windows C/C++ library files from toolchain. +""", +) diff --git a/python/private/common_labels.bzl b/python/private/common_labels.bzl index ff4e7e1dad..db4a00ba0a 100644 --- a/python/private/common_labels.bzl +++ b/python/private/common_labels.bzl @@ -14,9 +14,11 @@ labels = struct( # NOTE: Special target; see definition for details. ENABLE_RUNFILES = str(Label("//command_line_option:enable_runfiles")), EXEC_TOOLS_TOOLCHAIN = str(Label("//python/config_settings:exec_tools_toolchain")), + EXTRA_TOOLCHAINS = str(Label("//command_line_option:extra_toolchains")), NONE = str(Label("//python:none")), PIP_ENV_MARKER_CONFIG = str(Label("//python/config_settings:pip_env_marker_config")), PIP_WHL_OSX_VERSION = str(Label("//python/config_settings:pip_whl_osx_version")), + PLATFORMS_OS_MACOS = str(Label("@platforms//os:macos")), PLATFORMS_OS_WINDOWS = str(Label("@platforms//os:windows")), PRECOMPILE = str(Label("//python/config_settings:precompile")), PRECOMPILE_SOURCE_RETENTION = str(Label("//python/config_settings:precompile_source_retention")), @@ -28,6 +30,8 @@ labels = struct( PY_FREETHREADED = str(Label("//python/config_settings:py_freethreaded")), PY_LINUX_LIBC = str(Label("//python/config_settings:py_linux_libc")), REPL_DEP = str(Label("//python/bin:repl_dep")), + VALIDATE_TEST_MAIN = str(Label("//python/config_settings:validate_test_main")), + VENV = str(Label("//python/config_settings:venv")), VENVS_SITE_PACKAGES = str(Label("//python/config_settings:venvs_site_packages")), VENVS_USE_DECLARE_SYMLINK = str(Label("//python/config_settings:venvs_use_declare_symlink")), VISIBLE_FOR_TESTING = str(Label("//python/private:visible_for_testing")), diff --git a/python/private/config_settings.bzl b/python/private/config_settings.bzl index 91fbbba8cb..9cd729aff2 100644 --- a/python/private/config_settings.bzl +++ b/python/private/config_settings.bzl @@ -17,8 +17,9 @@ load("@bazel_skylib//lib:selects.bzl", "selects") load("@bazel_skylib//rules:common_settings.bzl", "BuildSettingInfo") -load("//python/private:text_util.bzl", "render") +load(":text_util.bzl", "render") load(":version.bzl", "version") +load(":visibility.bzl", "NOT_ACTUALLY_PUBLIC") _PYTHON_VERSION_FLAG = Label("//python/config_settings:python_version") _PYTHON_VERSION_MAJOR_MINOR_FLAG = Label("//python/config_settings:python_version_major_minor") @@ -31,10 +32,6 @@ If the value is missing, then the default value is being used, see documentation {docs_url}/python/config_settings """ -# Indicates something needs public visibility so that other generated code can -# access it, but it's not intended for general public usage. -_NOT_ACTUALLY_PUBLIC = ["//visibility:public"] - def construct_config_settings( *, name, @@ -93,7 +90,7 @@ def construct_config_settings( native.config_setting( name = "_" + name, flag_values = {":python_version": ver}, - visibility = ["//visibility:public"], + visibility = NOT_ACTUALLY_PUBLIC, ) # An alias pointing to an underscore-prefixed config_setting_group @@ -111,7 +108,7 @@ def construct_config_settings( native.alias( name = name, actual = "_{}_group".format(name), - visibility = ["//visibility:public"], + visibility = NOT_ACTUALLY_PUBLIC, ) # This matches the raw flag value, e.g. --//python/config_settings:python_version=3.8 @@ -156,30 +153,30 @@ def construct_config_settings( # `whl_library` in the hub repo created by `pip.parse`. flag_values = {"current_config": "will-never-match"}, # Only public so that PyPI hub repo can access it - visibility = _NOT_ACTUALLY_PUBLIC, + visibility = NOT_ACTUALLY_PUBLIC, ) libc = Label("//python/config_settings:py_linux_libc") native.config_setting( name = "_is_py_linux_libc_glibc", flag_values = {libc: "glibc"}, - visibility = _NOT_ACTUALLY_PUBLIC, + visibility = NOT_ACTUALLY_PUBLIC, ) native.config_setting( name = "_is_py_linux_libc_musl", flag_values = {libc: "musl"}, - visibility = _NOT_ACTUALLY_PUBLIC, + visibility = NOT_ACTUALLY_PUBLIC, ) freethreaded = Label("//python/config_settings:py_freethreaded") native.config_setting( name = "_is_py_freethreaded_yes", flag_values = {freethreaded: "yes"}, - visibility = _NOT_ACTUALLY_PUBLIC, + visibility = NOT_ACTUALLY_PUBLIC, ) native.config_setting( name = "_is_py_freethreaded_no", flag_values = {freethreaded: "no"}, - visibility = _NOT_ACTUALLY_PUBLIC, + visibility = NOT_ACTUALLY_PUBLIC, ) def _python_version_flag_impl(ctx): diff --git a/python/private/coverage_deps.bzl b/python/private/coverage_deps.bzl index a32b2e3f97..001146c3d4 100644 --- a/python/private/coverage_deps.bzl +++ b/python/private/coverage_deps.bzl @@ -17,10 +17,13 @@ load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive") load("@bazel_tools//tools/build_defs/repo:utils.bzl", "maybe") -load("//python/private:repo_utils.bzl", "repo_utils") load("//python/private:version_label.bzl", "version_label") # START: maintained by 'bazel run //tools/private/update_deps:update_coverage_deps ' +_default = ( + "https://files.pythonhosted.org/packages/ec/16/114df1c291c22cac3b0c127a73e0af5c12ed7bbb6558d310429a0ae24023/coverage-7.10.7-py3-none-any.whl", + "f7941f6f2fe6dd6807a1208737b8a0cbcf1cc6d7b07d24998ad2d63590868260", +) _coverage_deps = { "cp310": { "aarch64-apple-darwin": ( @@ -167,7 +170,7 @@ _coverage_deps = { _coverage_patch = Label("//python/private:coverage.patch") -def coverage_dep(name, python_version, platform, visibility, logger = None): +def coverage_dep(name, python_version, platform, visibility): """Register a single coverage dependency based on the python version and platform. Args: @@ -175,38 +178,20 @@ def coverage_dep(name, python_version, platform, visibility, logger = None): python_version: The full python version. platform: The platform, which can be found in //python:versions.bzl PLATFORMS dict. visibility: The visibility of the coverage tool. - logger: {type}`repo_utils.logger | None` Optional logger used to emit a - warning when no wheel is available for the (python_version, - platform) pair. If not supplied, a default logger is constructed. Returns: The label of the coverage tool if the platform is supported, otherwise - None. """ - if logger == None: - logger = repo_utils.logger( - struct(getenv = lambda _: None), - name = "coverage_dep", - ) - if "windows" in platform: # NOTE @aignas 2023-01-19: currently we do not support windows as the - # upstream coverage wrapper is written in shell. Do not log any warning - # for now as it is not actionable. + # upstream coverage wrapper is written in shell. return None abi = "cp" + version_label(python_version) - url, sha256 = _coverage_deps.get(abi, {}).get(platform, (None, "")) + url, sha256 = _coverage_deps.get(abi, {}).get(platform, _default) - if url == None: - logger.warn(lambda: ( - "rules_python's bundled coverage tool has no wheel for " + - "python_version={}, platform={}. `bazel coverage` will produce " + - "empty lcov for py_test targets in this configuration. Either " + - "pin python_version to a version in the bundled set (see " + - "python/private/coverage_deps.bzl), or configure coverage " + - "manually via py_runtime.coverage_tool. See docs/coverage.md." - ).format(python_version, platform)) - return None + # NOTE @aignas 2026-07-27: if the default is matched, then the same file may be extracted + # multiple times. The wheel is small enough to not matter in most cases. maybe( http_archive, diff --git a/python/private/flags.bzl b/python/private/flags.bzl index d9e3aa41c3..042e4e9838 100644 --- a/python/private/flags.bzl +++ b/python/private/flags.bzl @@ -87,6 +87,27 @@ AddSrcsToRunfilesFlag = FlagEnum( is_enabled = _AddSrcsToRunfilesFlag_is_enabled, ) +def _ValidateTestMainFlag_is_enabled(ctx): + value = ctx.attr._validate_test_main_flag[BuildSettingInfo].value + if value == ValidateTestMainFlag.AUTO: + # Default off; intended to be flipped to enabled in a future major + # version (e.g. rules_python 3.0). + value = ValidateTestMainFlag.DISABLED + return value == ValidateTestMainFlag.ENABLED + +# Determines if py_test runs a validation action that statically checks the +# main module actually runs tests (instead of silently passing). +# buildifier: disable=name-conventions +ValidateTestMainFlag = FlagEnum( + # Automatically decide the effective value; currently resolves to disabled. + AUTO = "auto", + # Run the validation action. + ENABLED = "enabled", + # Don't run the validation action. + DISABLED = "disabled", + is_enabled = _ValidateTestMainFlag_is_enabled, +) + def _string_flag_impl(ctx): if ctx.attr.override: value = ctx.attr.override @@ -226,6 +247,9 @@ FreeThreadedFlag = enum( NO = "no", ) +def _libc_flag_get_value(ctx): + return ctx.attr._py_linux_libc_flag[BuildSettingInfo].value + # Determines which libc flavor is preferred when selecting the toolchain and # linux whl distributions. # @@ -235,4 +259,5 @@ LibcFlag = FlagEnum( GLIBC = "glibc", # Prefer musl wheels (e.g. musllinux_2_17_x86_64) MUSL = "musl", + get_value = _libc_flag_get_value, ) diff --git a/python/private/get_local_runtime_info.py b/python/private/get_local_runtime_info.py index 787fad5635..a4f622700b 100644 --- a/python/private/get_local_runtime_info.py +++ b/python/private/get_local_runtime_info.py @@ -16,6 +16,7 @@ import glob import json import os +import platform import sys import sysconfig from typing import Any @@ -247,6 +248,7 @@ def _unique_basenames(inputs: dict[str, None]) -> list[str]: "abi_dynamic_libraries": _unique_basenames(abi_dynamic_libraries), "abi_interface_libraries": _unique_basenames(abi_interface_libraries), "abi_flags": abi_flags, + "soabi": config_vars.get("SOABI") or "", "shlib_suffix": ".dylib" if _IS_DARWIN else "", "additional_dlls": dlls, "defines": defines, @@ -265,6 +267,10 @@ def _get_base_executable() -> str: "include": sysconfig.get_path("include"), "implementation_name": sys.implementation.name, "base_executable": _get_base_executable(), + "sys_platform": sys.platform, + # Normalize to lowercase: on Windows, platform.machine() returns uppercase + # "AMD64" / "ARM64", whereas PEP 508 and toolchains expect lowercase. + "platform_machine": platform.machine().lower(), } data.update(_get_python_library_info(_get_base_executable())) print(json.dumps(data)) diff --git a/python/private/internal_config_repo.bzl b/python/private/internal_config_repo.bzl index aea1eee773..0630eb5c2f 100644 --- a/python/private/internal_config_repo.bzl +++ b/python/private/internal_config_repo.bzl @@ -30,12 +30,14 @@ config = struct( supports_whl_extraction = {supports_whl_extraction}, enable_pystar = True, enable_deprecation_warnings = {enable_deprecation_warnings}, + extract_needs_chmod = {extract_needs_chmod}, bazel_8_or_later = {bazel_8_or_later}, bazel_9_or_later = {bazel_9_or_later}, bazel_10_or_later = {bazel_10_or_later}, BuiltinPyInfo = getattr(getattr(native, "legacy_globals", None), "PyInfo", {builtin_py_info_symbol}), BuiltinPyRuntimeInfo = getattr(getattr(native, "legacy_globals", None), "PyRuntimeInfo", {builtin_py_runtime_info_symbol}), BuiltinPyCcLinkParamsProvider = getattr(getattr(native, "legacy_globals", None), "PyCcLinkParamsProvider", {builtin_py_cc_link_params_provider}), + modules_using_explicit_initpy = {modules_using_explicit_initpy}, ) """ @@ -49,13 +51,13 @@ package( ) bzl_library( - name = "rules_python_config_bzl", - srcs = ["rules_python_config.bzl"] + name = "extra_transition_settings", + srcs = ["extra_transition_settings.bzl"], ) bzl_library( - name = "extra_transition_settings_bzl", - srcs = ["extra_transition_settings.bzl"], + name = "rules_python_config", + srcs = ["rules_python_config.bzl"], ) """ @@ -86,6 +88,7 @@ def _internal_config_repo_impl(rctx): bazel_minor_version = 99999 supports_whl_extraction = False + extract_needs_chmod = bazel_major_version < 8 or (bazel_major_version == 8 and bazel_minor_version < 6) if bazel_major_version >= 8: # Extracting .whl files requires Bazel 8.3.0 or later. if bazel_major_version > 8 or bazel_minor_version >= 3: @@ -98,6 +101,7 @@ def _internal_config_repo_impl(rctx): builtin_py_info_symbol = "PyInfo" builtin_py_runtime_info_symbol = "PyRuntimeInfo" builtin_py_cc_link_params_provider = "PyCcLinkParamsProvider" + explicit_init_py_modules = {k: str(v) == "True" for k, v in rctx.attr.explicit_init_py_modules.items()} rctx.file("rules_python_config.bzl", _CONFIG_TEMPLATE.format( build_python_zip_default = repo_utils.get_platforms_os_name(rctx) == "windows", @@ -105,7 +109,9 @@ def _internal_config_repo_impl(rctx): builtin_py_info_symbol = builtin_py_info_symbol, builtin_py_runtime_info_symbol = builtin_py_runtime_info_symbol, supports_whl_extraction = str(supports_whl_extraction), + extract_needs_chmod = str(extract_needs_chmod), builtin_py_cc_link_params_provider = builtin_py_cc_link_params_provider, + modules_using_explicit_initpy = str(explicit_init_py_modules), bazel_8_or_later = str(bazel_major_version >= 8), bazel_9_or_later = str(bazel_major_version >= 9), bazel_10_or_later = str(bazel_major_version > 9), @@ -137,6 +143,7 @@ internal_config_repo = repository_rule( configure = True, environ = [], attrs = { + "explicit_init_py_modules": attr.string_dict(), "transition_setting_generators": attr.string_list_dict(), "transition_settings": attr.string_list(), }, diff --git a/python/private/internal_dev_deps.bzl b/python/private/internal_dev_deps.bzl index b0b06c31b2..5bbee232f9 100644 --- a/python/private/internal_dev_deps.bzl +++ b/python/private/internal_dev_deps.bzl @@ -131,6 +131,24 @@ def _internal_dev_deps_impl(mctx): config_load = "@rules_python//tests/pypi/whl_library/testdata:packages.bzl", ) + # Setup for //tests/pypi/patch_whl/patch_whl_patch_test.py + whl_from_dir_repo( + name = "patch_whl_pkg_whl", + root = "//tests/pypi/patch_whl/testdata/pkg:BUILD.bazel", + output = "pkg-1.0-any-none-any.whl", + ) + whl_library( + name = "patch_whl_pkg", + whl_file = "@patch_whl_pkg_whl//:pkg-1.0-any-none-any.whl", + requirement = "pkg", + whl_patches = { + "//tests/pypi/patch_whl/testdata/patches:modify_pkg.patch": json.encode({ + "patch_strip": 0, + "whls": ["pkg-1.0-any-none-any.whl"], + }), + }, + ) + def _whl_library_from_dir(*, name, output, root, **kwargs): whl_from_dir_repo( name = "{}_whl".format(name), diff --git a/python/private/interpreter.bzl b/python/private/interpreter.bzl index c66d3dc21e..b281be9639 100644 --- a/python/private/interpreter.bzl +++ b/python/private/interpreter.bzl @@ -17,7 +17,7 @@ load("@bazel_skylib//lib:paths.bzl", "paths") load("//python:py_runtime_info.bzl", "PyRuntimeInfo") load(":common.bzl", "runfiles_root_path") -load(":sentinel.bzl", "SentinelInfo") +load(":sentinel_impl.bzl", "SentinelInfo") load(":toolchain_types.bzl", "TARGET_TOOLCHAIN_TYPE") def _interpreter_binary_impl(ctx): diff --git a/python/private/local_runtime_repo.bzl b/python/private/local_runtime_repo.bzl index 37b7d2b130..f5597db613 100644 --- a/python/private/local_runtime_repo.bzl +++ b/python/private/local_runtime_repo.bzl @@ -35,6 +35,7 @@ define_local_runtime_toolchain_impl( minor = "{minor}", micro = "{micro}", abi_flags = "{abi_flags}", + soabi = "{soabi}", os = "{os}", implementation_name = "{implementation_name}", interpreter_path = "{interpreter_path}", @@ -44,6 +45,8 @@ define_local_runtime_toolchain_impl( abi3_interface_library = {abi3_interface_library}, abi3_libraries = {abi3_libraries}, additional_dlls = {additional_dlls}, + sys_platform = "{sys_platform}", + platform_machine = "{platform_machine}", ) """ @@ -53,6 +56,7 @@ def _expand_incompatible_template(): minor = "0", micro = "0", abi_flags = "", + soabi = "", os = "@platforms//:incompatible", implementation_name = "incompatible", interpreter_path = "/incompatible", @@ -62,6 +66,8 @@ def _expand_incompatible_template(): abi3_interface_library = "None", abi3_libraries = "[]", additional_dlls = "[]", + sys_platform = "", + platform_machine = "", ) def _norm_path(path): @@ -210,6 +216,7 @@ def _local_runtime_repo_impl(rctx): minor = info["minor"], micro = info["micro"], abi_flags = info["abi_flags"], + soabi = info["soabi"], os = "@platforms//os:{}".format(repo_utils.get_platforms_os_name(rctx)), implementation_name = info["implementation_name"], interpreter_path = _norm_path(interpreter_path), @@ -219,6 +226,8 @@ def _local_runtime_repo_impl(rctx): abi3_interface_library = repr(abi3_interface_library), abi3_libraries = repr(abi3_libraries), additional_dlls = repr(additional_dlls), + sys_platform = info["sys_platform"], + platform_machine = info["platform_machine"], ) logger.debug(lambda: "BUILD.bazel\n{}".format(build_bazel)) diff --git a/python/private/local_runtime_repo_setup.bzl b/python/private/local_runtime_repo_setup.bzl index 5cb7bda200..05ca6228e0 100644 --- a/python/private/local_runtime_repo_setup.bzl +++ b/python/private/local_runtime_repo_setup.bzl @@ -37,7 +37,10 @@ def define_local_runtime_toolchain_impl( defines, abi3_interface_library, abi3_libraries, - additional_dlls): + additional_dlls, + sys_platform = "", + platform_machine = "", + soabi = ""): """Defines a toolchain implementation for a local Python runtime. Generates public targets: @@ -71,6 +74,9 @@ def define_local_runtime_toolchain_impl( e.g. ["lib/python3.dll"] or ["lib/python3.so"] additional_dlls: `list[str]` Path[s] to additional DLLs. e.g. ["lib/msvcrt123.dll"] + sys_platform: `str` The PEP 508 `sys_platform` marker, e.g. 'linux', 'darwin', 'win32'. + platform_machine: `str` The PEP 508 `platform_machine` marker, e.g. 'x86_64', 'aarch64'. + soabi: `str` The SOABI tag for extension modules, e.g. 'cpython-311-x86_64-linux-gnu'. """ major_minor = "{}.{}".format(major, minor) major_minor_micro = "{}.{}".format(major_minor, micro) @@ -131,6 +137,10 @@ def define_local_runtime_toolchain_impl( hdrs = [":includes"], defines = defines, # NOTE: Users should define Py_LIMITED_API=3 srcs = abi3_libraries + additional_dlls, + deps = select({ + "@bazel_tools//src/conditions:windows": [":abi3_interface"], + "//conditions:default": [], + }), ) cc_library( @@ -138,6 +148,10 @@ def define_local_runtime_toolchain_impl( hdrs = [":includes"], defines = defines, srcs = libraries + additional_dlls, + deps = select({ + "@bazel_tools//src/conditions:windows": [":interface"], + "//conditions:default": [], + }), ) # runtime configuration @@ -183,10 +197,14 @@ def define_local_runtime_toolchain_impl( py_cc_toolchain( name = "py_cc_toolchain", + abi_flags = abi_flags, + soabi = soabi, headers = ":python_headers", headers_abi3 = ":python_headers_abi3", libs = ":libpython", + platform_machine = platform_machine, python_version = major_minor_micro, + sys_platform = sys_platform, visibility = ["//visibility:public"], ) diff --git a/python/private/pbs_manifest.bzl b/python/private/pbs_manifest.bzl new file mode 100644 index 0000000000..86434dc0ea --- /dev/null +++ b/python/private/pbs_manifest.bzl @@ -0,0 +1,152 @@ +"""Helper functions to parse python-build-standalone manifests.""" + +def parse_filename(filename): + """Parses a python-build-standalone filename (or URL) into its components. + + See https://gregoryszorc.com/docs/python-build-standalone/main/running.html + + Example: cpython-3.10.20+20260414-x86_64_v2-unknown-linux-musl-lto-full.tar.zst + + Args: + filename: The filename or URL of the python-build-standalone release asset. + + Returns: + A dictionary of parsed components if parsed successfully, else None. + """ + basename = filename.rpartition("/")[-1] + if basename.endswith(".tar.zst"): + name = basename.removesuffix(".tar.zst") + elif basename.endswith(".tar.gz"): + name = basename.removesuffix(".tar.gz") + else: + return None + + if not name.startswith("cpython-"): + return None + name = name.removeprefix("cpython-") + + left, plus, tail = name.partition("+") + if plus: + python_version = left + build_version, sep, rest = tail.partition("-") + if not sep: + return None + else: + python_version, sep, rest = left.partition("-") + if not sep: + return None + build_version = "" + + arch, sep, rest = rest.partition("-") + if not sep: + return None + + microarch = "" + arch_base, sep_v, microarch_num = arch.partition("_v") + if sep_v: + arch = arch_base + microarch = "v" + microarch_num + + vendor, sep, rest = rest.partition("-") + if not sep: + return None + + os, sep, rest = rest.partition("-") + if not sep: + return None + + libc = "" + next_part, _, remaining = rest.partition("-") + if os == "linux" and next_part in ["gnu", "musl"]: + libc = next_part + flavor = remaining + elif os == "windows" and next_part == "msvc": + libc = next_part + flavor = remaining + else: + libc = "" + flavor = rest + + freethreaded = False + if flavor.startswith("freethreaded+"): + freethreaded = True + flavor = flavor.removeprefix("freethreaded+") + elif flavor.startswith("freethreaded-"): + freethreaded = True + flavor = flavor.removeprefix("freethreaded-") + elif flavor == "freethreaded": + freethreaded = True + flavor = "" + + archive_flavor = "" + if flavor.endswith("-full"): + archive_flavor = "full" + flavor = flavor.removesuffix("-full") + elif flavor == "full": + archive_flavor = "full" + flavor = "" + elif flavor.endswith("-install_only_stripped"): + archive_flavor = "install_only_stripped" + flavor = flavor.removesuffix("-install_only_stripped") + elif flavor == "install_only_stripped": + archive_flavor = "install_only_stripped" + flavor = "" + elif flavor.endswith("-install_only"): + archive_flavor = "install_only" + flavor = flavor.removesuffix("-install_only") + elif flavor == "install_only": + archive_flavor = "install_only" + flavor = "" + + return { + "arch": arch, + "archive_flavor": archive_flavor, + "build_flavor": flavor, + "build_version": build_version, + "freethreaded": freethreaded, + "libc": libc, + "location": filename, + "microarch": microarch, + "os": os, + "python_version": python_version, + "vendor": vendor, + } + +def parse_runtime_manifest(content): + """Parses the SHA256SUMS file content into a list of structs. + + Args: + content: The raw content of the manifest file. + + Returns: + A list of structs capturing the parsed components of each valid entry. + Each struct contains the following fields: + - arch: CPU architecture (e.g., "x86_64"). + - archive_flavor: Release asset archive type (e.g., "full", "install_only"). + - build_version: Standalone release date (e.g., "20260414"). + - location: Full package filename or URL (e.g., "cpython-3.11.15..." or "https://..."). + - build_flavor: Build configuration flavor (e.g., "debug", "pgo+lto"). + - freethreaded: Whether the build is free-threaded (boolean). + - libc: C library type (e.g., "gnu", "musl", "msvc", or ""). + - microarch: Microarchitecture level (e.g., "v2", "v3", or ""). + - os: Operating system (e.g., "linux", "darwin", "windows"). + - python_version: Python semver version (e.g., "3.11.15"). + - sha256: SHA256 integrity hash of the release asset. + - vendor: Platform vendor (e.g., "unknown", "apple"). + """ + results = [] + for line in content.split("\n"): + line = line.strip() + if not line or line.startswith("#"): + continue + parts = [p for p in line.split(" ") if p] + if len(parts) != 2: + continue + sha256, filename = parts + parsed = parse_filename(filename) + if parsed: + results.append(struct( + sha256 = sha256, + **parsed + )) + return results diff --git a/python/private/print_toolchain_checksums.bzl b/python/private/print_toolchain_checksums.bzl deleted file mode 100644 index b4fa400221..0000000000 --- a/python/private/print_toolchain_checksums.bzl +++ /dev/null @@ -1,89 +0,0 @@ -"""Print the toolchain versions. -""" - -load("//python:versions.bzl", "TOOL_VERSIONS", "get_release_info") -load("//python/private:text_util.bzl", "render") -load("//python/private:version.bzl", "version") - -def print_toolchains_checksums(name): - """A macro to print checksums for a particular Python interpreter version. - - Args: - name: {type}`str`: the name of the runnable target. - """ - by_version = {} - - for python_version, metadata in TOOL_VERSIONS.items(): - by_version[python_version] = _commands_for_version( - python_version = python_version, - metadata = metadata, - ) - - all_commands = sorted( - by_version.items(), - key = lambda x: version.key(version.parse(x[0], strict = True)), - ) - all_commands = [x[1] for x in all_commands] - - template = """\ -cat > "$@" <<'EOF' -#!/usr/bin/env bash -set -euo pipefail - -set -o errexit -o nounset -o pipefail - -echo "Fetching hashes..." - -{commands} -EOF - """ - - native.genrule( - name = name, - srcs = [], - outs = ["print_toolchains_checksums.sh"], - cmd = select({ - "//python/config_settings:is_python_{}".format(version_str): template.format( - commands = commands, - ) - for version_str, commands in by_version.items() - } | { - "//conditions:default": template.format(commands = "\n".join(all_commands)), - }), - executable = True, - ) - -def _commands_for_version(*, python_version, metadata): - lines = [] - first_platform = metadata["sha256"].keys()[0] - root, _, _ = get_release_info(first_platform, python_version)[1][0].rpartition("/") - sha_url = "{}/{}".format(root, "SHA256SUMS") - prefix = metadata["strip_prefix"] - prefix = render.indent( - render.dict(prefix) if type(prefix) == type({}) else repr(prefix), - indent = " " * 8, - ).lstrip() - - lines += [ - "sha256s=$$(curl --silent --show-error --location --fail {})".format(sha_url), - "cat <": + abi_flags = "" + if ctx.attr._py_freethreaded_flag[BuildSettingInfo].value == FreeThreadedFlag.YES: + abi_flags += "t" + + libc = ctx.attr.libc or LibcFlag.get_value(ctx) + + platform_tag = _get_platform_tag( + sys_platform = ctx.attr.sys_platform, + platform_machine = ctx.attr.platform_machine, + libc = libc, + ) + + soabi = ctx.attr.soabi + if not soabi: + # Derive default SOABI tag according to PEP 3149: + # On POSIX: cpython-XX[t]- + # On Windows: cpXX[t]- (CPython issue #67169) + version_parts = ctx.attr.python_version.split(".") + prefix = "cp" if ctx.attr.sys_platform == "win32" else "cpython-" + soabi = "{}{}{}{}".format(prefix, version_parts[0], version_parts[1], abi_flags) + if platform_tag: + soabi = "{}-{}".format(soabi, platform_tag) + py_cc_toolchain = PyCcToolchainInfo( + abi_flags = abi_flags, headers = struct( providers_map = { "CcInfo": ctx.attr.headers[CcInfo], @@ -54,7 +117,11 @@ def _py_cc_toolchain_impl(ctx): ), headers_abi3 = headers_abi3, libs = libs, + platform_machine = ctx.attr.platform_machine, + platform_tag = platform_tag, python_version = ctx.attr.python_version, + soabi = soabi, + sys_platform = ctx.attr.sys_platform, ) extra_kwargs = {} if ctx.attr._visible_for_testing[BuildSettingInfo].value: @@ -67,6 +134,19 @@ def _py_cc_toolchain_impl(ctx): py_cc_toolchain = rule( implementation = _py_cc_toolchain_impl, attrs = { + "abi_flags": attr.string( + default = "", + doc = """ +The runtime's ABI flags, i.e. `sys.abiflags`. + +If not set, or set to ``, the ABI flags are automatically derived +from `--//python/config_settings:py_freethreaded` (e.g., `'t'` when +free-threaded is enabled, or `''` otherwise). + +:::{versionadded} 2.3.0 +::: +""", + ), "headers": attr.label( doc = ("Target that provides the Python headers. Typically this " + "is a cc_library target."), @@ -87,15 +167,58 @@ attribute is available or not. default = "//python:none", providers = [[SentinelInfo], [CcInfo]], ), + "libc": attr.string( + doc = """\ +Target C library variant, e.g. 'glibc', 'musl'. + +:::{versionadded} 2.3.0 +::: +""", + default = "", + ), "libs": attr.label( doc = ("Target that provides the Python runtime libraries for linking. " + "Typically this is a cc_library target of `.so` files."), providers = [CcInfo], ), + "platform_machine": attr.string( + doc = """ +Target architecture as a PEP 508 `platform_machine` marker, e.g. 'x86_64', 'aarch64', 'x86_32'. + +:::{versionadded} 2.3.0 +::: +""", + default = "", + ), "python_version": attr.string( doc = "The Major.minor Python version, e.g. 3.11", mandatory = True, ), + "soabi": attr.string( + doc = """\ +The SOABI tag for extension modules (see PEP 3149), e.g. +'cpython-311-x86_64-linux-gnu' or 'cp311'. + +:::{versionadded} 2.3.0 +::: +""", + default = "", + ), + "sys_platform": attr.string( + doc = """ +Target OS as a PEP 508 `sys_platform` marker, e.g. 'linux', 'darwin', 'win32'. + +:::{versionadded} 2.2.0 +::: +""", + default = "", + ), + "_py_freethreaded_flag": attr.label( + default = labels.PY_FREETHREADED, + ), + "_py_linux_libc_flag": attr.label( + default = labels.PY_LINUX_LIBC, + ), "_visible_for_testing": attr.label( default = labels.VISIBLE_FOR_TESTING, ), 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_exec_tools_info.bzl b/python/private/py_exec_tools_info.bzl index 470c0c1bf0..43213d5a13 100644 --- a/python/private/py_exec_tools_info.bzl +++ b/python/private/py_exec_tools_info.bzl @@ -41,7 +41,7 @@ toolchain. :::{warning} This does not work correctly with RBE. Use {obj}`exec_runtime` instead. -Once https://github.com/bazelbuild/bazel/issues/23620 is resolved this warning +Once [bazelbuild/bazel#23620](https://github.com/bazelbuild/bazel/issues/23620) is resolved this warning may be removed. ::: """, diff --git a/python/private/py_exec_tools_toolchain.bzl b/python/private/py_exec_tools_toolchain.bzl index ec8d4e53d0..d126262033 100644 --- a/python/private/py_exec_tools_toolchain.bzl +++ b/python/private/py_exec_tools_toolchain.bzl @@ -18,7 +18,7 @@ load("@bazel_skylib//lib:paths.bzl", "paths") load("@bazel_skylib//rules:common_settings.bzl", "BuildSettingInfo") load(":common_labels.bzl", "labels") load(":py_exec_tools_info.bzl", "PyExecToolsInfo") -load(":sentinel.bzl", "SentinelInfo") +load(":sentinel_impl.bzl", "SentinelInfo") load(":toolchain_types.bzl", "TARGET_TOOLCHAIN_TYPE") def _py_exec_tools_toolchain_impl(ctx): diff --git a/python/private/py_executable.bzl b/python/private/py_executable.bzl index 198ff9d548..4ee484dcd0 100644 --- a/python/private/py_executable.bzl +++ b/python/private/py_executable.bzl @@ -38,6 +38,7 @@ load(":cc_helper.bzl", "cc_helper") load( ":common.bzl", "ExplicitSymlink", + "actions_run", "collect_cc_info", "collect_deps", "collect_imports", @@ -57,16 +58,17 @@ load( "runfiles_root_path", ) load(":common_labels.bzl", "labels") -load(":flags.bzl", "BootstrapImplFlag", "VenvsUseDeclareSymlinkFlag", "read_possibly_native_flag") +load(":flags.bzl", "BootstrapImplFlag", "ValidateTestMainFlag", "VenvsUseDeclareSymlinkFlag", "read_possibly_native_flag") load(":precompile.bzl", "maybe_precompile") load(":py_cc_link_params_info.bzl", "PyCcLinkParamsInfo") load(":py_executable_info.bzl", "PyExecutableInfo") load(":py_info.bzl", "PyInfo", "VenvSymlinkKind") load(":py_internal.bzl", "py_internal") +load(":py_interpreter_program.bzl", "PyInterpreterProgramInfo") load(":py_runtime_info.bzl", "DEFAULT_STUB_SHEBANG") load(":reexports.bzl", "BuiltinPyInfo", "BuiltinPyRuntimeInfo") load(":rule_builders.bzl", "ruleb") -load(":toolchain_types.bzl", "EXEC_TOOLS_TOOLCHAIN_TYPE", "LAUNCHER_MAKER_TOOLCHAIN_TYPE", TOOLCHAIN_TYPE = "TARGET_TOOLCHAIN_TYPE") +load(":toolchain_types.bzl", "CC_TOOLCHAIN_TYPE", "EXEC_TOOLS_TOOLCHAIN_TYPE", "LAUNCHER_MAKER_TOOLCHAIN_TYPE", TOOLCHAIN_TYPE = "TARGET_TOOLCHAIN_TYPE") load(":transition_labels.bzl", "TRANSITION_LABELS") load(":venv_runfiles.bzl", "create_venv_app_files") @@ -96,7 +98,7 @@ EXECUTABLE_ATTRS = dicts.add( Arguments that are only applicable to the interpreter. The args an interpreter supports are specific to the interpreter. For -CPython, see https://docs.python.org/3/using/cmdline.html. +CPython, see . :::{note} Only supported for {obj}`--bootstrap_impl=script`. Ignored otherwise. @@ -113,15 +115,20 @@ The {any}`RULES_PYTHON_ADDITIONAL_INTERPRETER_ARGS` environment variable "legacy_create_init": lambda: attrb.Int( default = -1, values = [-1, 0, 1], - doc = """\ + doc = """ Whether to implicitly create empty `__init__.py` files in the runfiles tree. These are created in every directory containing Python source code or shared libraries, and every parent directory of those directories, excluding the repo root directory. The default, `-1` (auto), means true unless -`--incompatible_default_to_explicit_init_py` is used. If false, the user is -responsible for creating (possibly empty) `__init__.py` files and adding them to -the `srcs` of Python targets as required. - """, +`--incompatible_default_to_explicit_init_py` or the `explicit_init_py` +module configuration option are used. If false, the user is responsible for +creating (possibly empty) `__init__.py` files and adding them to the `srcs` of +Python targets as required. + +:::{versionchanged} VERSION_NEXT_FEATURE +Now checks module-level `explicit_init_py` configuration before CLI flags. +::: +""", ), # TODO(b/203567235): In the Java impl, any file is allowed. While marked # label, it is more treated as a string, and doesn't have to refer to @@ -145,7 +152,7 @@ Module name to execute as the main program. When set, `srcs` is not required, and it is assumed the module is provided by a dependency. -See https://docs.python.org/3/using/cmdline.html#cmdoption-m for more +See for more information about running modules as the main program. This is mutually exclusive with {obj}`main`. @@ -218,6 +225,10 @@ accepting arbitrary Python versions. default = "//python/private:debugger_if_target_config", providers = [PyInfo], ), + "_exe_zip_maker": lambda: attrb.Label( + cfg = "exec", + default = "//tools/private/zipapp:exe_zip_maker", + ), "_launcher": lambda: attrb.Label( cfg = "target", # NOTE: This is an executable, but is only used for Windows. It @@ -278,11 +289,23 @@ def create_binary_semantics(): ) def _should_create_init_files(ctx): - if ctx.attr.legacy_create_init == -1: - return not read_possibly_native_flag(ctx, "default_to_explicit_init_py") - else: + # Each target has the first say in this setting. + if ctx.attr.legacy_create_init != -1: return bool(ctx.attr.legacy_create_init) + # Check if it's configured by a module extension. + canonical_name = ctx.label.repo_name + for sep in ("+", "~"): + if canonical_name.startswith(sep): + canonical_name = "" + module_name = canonical_name.rstrip(sep) if sep not in canonical_name else canonical_name.split(sep)[0] + module_configured_explicit_initpy = rp_config.modules_using_explicit_initpy.get(module_name, None) + if module_configured_explicit_initpy != None: + return not module_configured_explicit_initpy + + # Fall back to CLI setting. + return not read_possibly_native_flag(ctx, "default_to_explicit_init_py") + def _create_executable( ctx, *, @@ -306,6 +329,9 @@ def _create_executable( else: base_executable_name = executable.basename + # Venv outputs are package-relative, so preserve the full target name to + # avoid collisions between targets like foo/tool, bar/tool, and foo_tool. + venv_output_prefix = ctx.label.name venv = None # The check for stage2_bootstrap_template is to support legacy @@ -318,7 +344,7 @@ def _create_executable( ): venv = _create_venv( ctx, - output_prefix = base_executable_name, + output_prefix = venv_output_prefix, imports = imports, runtime_details = runtime_details, add_runfiles_root_to_sys_path = ( @@ -1091,15 +1117,16 @@ def _create_executable_zip_file( else: ctx.actions.write(prelude, "#!/usr/bin/env python3\n") - ctx.actions.run_shell( - command = "cat {prelude} {zip} > {output}".format( - prelude = prelude.path, - zip = zip_file.path, - output = output.path, - ), - inputs = [prelude, zip_file], + args = ctx.actions.args() + args.add(prelude) + args.add(zip_file) + args.add(output) + actions_run( + ctx, + executable = ctx.attr._exe_zip_maker, + arguments = [args], + inputs = depset([prelude, zip_file]), outputs = [output], - use_default_shell_env = True, mnemonic = "PyBuildExecutableZip", progress_message = "Build Python zip executable: %{label}", ) @@ -1167,6 +1194,11 @@ def py_executable_base_impl(ctx, *, semantics, is_test, inherited_environment = main_py = determine_main(ctx) else: main_py = None + + # Keep a reference to the main source file (before it may be replaced with a + # precompiled pyc below) so the test-main validation can statically analyze + # the original source. + main_py_source = main_py direct_sources = filter_to_py_srcs(ctx.files.srcs) precompile_result = maybe_precompile(ctx, direct_sources) @@ -1285,10 +1317,84 @@ def py_executable_base_impl(ctx, *, semantics, is_test, inherited_environment = implicit_pyc_source_files = implicit_pyc_source_files, imports = imports, ) - _add_provider_output_group_info(providers, py_info, exec_result.output_groups) + output_groups = dict(exec_result.output_groups) + if is_test: + _maybe_add_test_main_validation(ctx, main_py_source, output_groups) + _add_provider_output_group_info(providers, py_info, output_groups) return providers +def _maybe_add_test_main_validation(ctx, main_py, output_groups): + """Adds a validation action that checks the test main actually runs tests. + + This is a safeguard against the common pitfall of defining test classes or + functions but forgetting to invoke a test runner, which causes the test to + silently pass without running anything. See the + `//python/config_settings:validate_test_main` flag. + + Args: + ctx: Rule ctx. + main_py: File or None; the main entry point source file. None when the + target uses `main_module` (which can't be statically analyzed here). + output_groups: dict[str, depset[File]]; mutated in place to add the + `_validation` output group when a validation action is created. + """ + if not ValidateTestMainFlag.is_enabled(ctx): + return + + # `main_module` targets execute a module by name; there's no single source + # file to statically analyze, so the check doesn't apply. + if main_py == None: + return + + exec_tools_toolchain = ctx.toolchains[EXEC_TOOLS_TOOLCHAIN_TYPE] + if exec_tools_toolchain == None or exec_tools_toolchain.exec_tools.exec_interpreter == None: + fail( + "Validating py_test main modules requires the exec tools toolchain " + + "with an exec interpreter, but none was found. Either register one " + + "or set --@rules_python//python/config_settings:validate_test_main=disabled.", + ) + + exec_tools = exec_tools_toolchain.exec_tools + validator = ctx.attr._validate_test_main + program_info = validator[PyInterpreterProgramInfo] + interpreter = exec_tools.exec_interpreter[DefaultInfo].files_to_run + validator_files_to_run = validator[DefaultInfo].files_to_run + + validation_output = ctx.actions.declare_file(ctx.label.name + "_validate_test_main.txt") + + args = ctx.actions.args() + args.add_all(program_info.interpreter_args) + args.add(validator_files_to_run.executable) + args.add("--src", main_py) + args.add("--src_name", main_py.short_path) + args.add("--label", str(ctx.label)) + args.add("--output", validation_output) + + execution_requirements = {} + if testing.ExecutionInfo in validator: + execution_requirements = validator[testing.ExecutionInfo].requirements + + ctx.actions.run( + executable = interpreter, + arguments = [args], + inputs = [main_py], + outputs = [validation_output], + tools = [validator_files_to_run], + mnemonic = "PyValidateTestMain", + progress_message = "Validating py_test main %{label}", + env = program_info.env | { + "PYTHONNOUSERSITE": "1", + "PYTHONSAFEPATH": "1", + }, + execution_requirements = execution_requirements, + toolchain = EXEC_TOOLS_TOOLCHAIN_TYPE, + ) + if "_validation" in output_groups: + output_groups["_validation"] = depset([validation_output], transitive = [output_groups["_validation"]]) + else: + output_groups["_validation"] = depset([validation_output]) + def _get_build_info(ctx, cc_toolchain): build_info_files = py_internal.cc_toolchain_build_info_files(cc_toolchain) if cc_helper.is_stamping_enabled(ctx): @@ -1485,6 +1591,33 @@ def _get_base_runfiles_for_binary( app_runfiles = app_runfiles.build(ctx) if _should_create_init_files(ctx): + # buildifier: disable=print + print( + """ +====================================================================== +WARNING: Target {} is using implicit __init__.py creation. + This diabolic behavior is deprecated and will be disabled by default in a + future release. + See https://github.com/bazel-contrib/rules_python/issues/2945 + + Ensure all __init__.py files are explicitly created and + added to the srcs or deps of your targets. + + Disable implicit creation for your module in MODULE.bazel: + + rules_python_config = use_extension("@rules_python//python/extensions:config.bzl", "config") + rules_python_config.explicit_init_py(default = True) + + Or for a specific target by setting: + + legacy_create_init = 0 + + Or globally with the following Bazel flag: + + --incompatible_default_to_explicit_init_py +====================================================================== + """.rstrip().format(ctx.label), + ) app_runfiles = _py_builtins.merge_runfiles_with_generated_inits_empty_files_supplier( ctx = ctx, runfiles = app_runfiles, @@ -2096,7 +2229,7 @@ def create_executable_rule_builder(implementation, **kwargs): toolchains = [ ruleb.ToolchainType(TOOLCHAIN_TYPE), ruleb.ToolchainType(EXEC_TOOLS_TOOLCHAIN_TYPE, mandatory = False), - ruleb.ToolchainType("@bazel_tools//tools/cpp:toolchain_type", mandatory = False), + ruleb.ToolchainType(CC_TOOLCHAIN_TYPE, mandatory = False), ] + ([ruleb.ToolchainType(LAUNCHER_MAKER_TOOLCHAIN_TYPE)] if rp_config.bazel_9_or_later else []), cfg = dict( implementation = _transition_executable_impl, diff --git a/python/private/py_executable_info.bzl b/python/private/py_executable_info.bzl index 9e9baea421..7693fa950f 100644 --- a/python/private/py_executable_info.bzl +++ b/python/private/py_executable_info.bzl @@ -88,7 +88,7 @@ Only used with Windows for files that would have used `declare_symlink()` to create relative symlinks. These may overlap with paths in runfiles; it's up to the consumer to determine how to handle such overlaps. -:::{versionadded} VERSION_NEXT_FEATURE +:::{versionadded} 2.1.0 ::: """, "venv_interpreter_runfiles": """ diff --git a/python/private/py_info.bzl b/python/private/py_info.bzl index 551e0bb48c..dac1ddeff3 100644 --- a/python/private/py_info.bzl +++ b/python/private/py_info.bzl @@ -116,6 +116,240 @@ PEP440 standard. }, ) +def _VenvSymlinkEntryBuilder_typedef(): + """Builder for VenvSymlinkEntry. + + To create an instance, use {obj}`PyInfoBuilder.add_venv_symlink()`. + + :::{field} files + :type: DepsetBuilder[File] + ::: + + :::{versionadded} 2.2.0 + ::: + """ + +def _VenvSymlinkEntryBuilder(): + # buildifier: disable=uninitialized + self = struct( + _state = { + "kind": None, + "link_to_file": None, + "link_to_path": None, + "package": None, + "venv_path": None, + "version": None, + }, + files = builders.DepsetBuilder(), + get_kind = lambda: _VenvSymlinkEntryBuilder_get_kind(self), + set_kind = lambda k: _VenvSymlinkEntryBuilder_set_kind(self, k), + get_link_to_file = lambda: _VenvSymlinkEntryBuilder_get_link_to_file(self), + set_link_to_file = lambda f: _VenvSymlinkEntryBuilder_set_link_to_file(self, f), + get_link_to_path = lambda: _VenvSymlinkEntryBuilder_get_link_to_path(self), + set_link_to_path = lambda p: _VenvSymlinkEntryBuilder_set_link_to_path(self, p), + get_package = lambda: _VenvSymlinkEntryBuilder_get_package(self), + set_package = lambda p: _VenvSymlinkEntryBuilder_set_package(self, p), + get_venv_path = lambda: _VenvSymlinkEntryBuilder_get_venv_path(self), + set_venv_path = lambda p: _VenvSymlinkEntryBuilder_set_venv_path(self, p), + get_version = lambda: _VenvSymlinkEntryBuilder_get_version(self), + set_version = lambda v: _VenvSymlinkEntryBuilder_set_version(self, v), + build = lambda: _VenvSymlinkEntryBuilder_build(self), + ) + return self + +def _VenvSymlinkEntryBuilder_get_kind(self): + """Get the kind of the symlink. + + Args: + self: implicitly added. + + Returns: + {type}`string` One of the {obj}`VenvSymlinkKind` values. + """ + return self._state["kind"] + +def _VenvSymlinkEntryBuilder_get_link_to_file(self): + """Get the file that the symlink points to. + + Args: + self: implicitly added. + + Returns: + {type}`File | None` + """ + return self._state["link_to_file"] + +def _VenvSymlinkEntryBuilder_get_link_to_path(self): + """Get the runfiles-root relative path that the symlink points to. + + Args: + self: implicitly added. + + Returns: + {type}`string | None` + """ + return self._state["link_to_path"] + +def _VenvSymlinkEntryBuilder_get_package(self): + """Get the PyPI package name that the code originates from. + + Args: + self: implicitly added. + + Returns: + {type}`string | None` + """ + return self._state["package"] + +def _VenvSymlinkEntryBuilder_get_venv_path(self): + """Get the path relative to the kind directory within the venv. + + Args: + self: implicitly added. + + Returns: + {type}`string` + """ + return self._state["venv_path"] + +def _VenvSymlinkEntryBuilder_get_version(self): + """Get the PyPI package version that the code originates from. + + Args: + self: implicitly added. + + Returns: + {type}`string | None` + """ + return self._state["version"] + +def _VenvSymlinkEntryBuilder_set_kind(self, kind): + """Set the kind of the symlink. + + Args: + self: implicitly added. + kind: {type}`string` One of the {obj}`VenvSymlinkKind` values. + + Returns: + {type}`VenvSymlinkEntryBuilder` self. + """ + _check_arg_type("kind", "string", kind) + self._state["kind"] = kind + return self + +def _VenvSymlinkEntryBuilder_set_link_to_file(self, link_to_file): + """Set the file that the symlink points to. + + Args: + self: implicitly added. + link_to_file: {type}`File | None` + + Returns: + {type}`VenvSymlinkEntryBuilder` self. + """ + if link_to_file != None: + _check_arg_type("link_to_file", "File", link_to_file) + self._state["link_to_file"] = link_to_file + return self + +def _VenvSymlinkEntryBuilder_set_link_to_path(self, link_to_path): + """Set the runfiles-root relative path that the symlink points to. + + Args: + self: implicitly added. + link_to_path: {type}`string | None` + + Returns: + {type}`VenvSymlinkEntryBuilder` self. + """ + if link_to_path != None: + _check_arg_type("link_to_path", "string", link_to_path) + self._state["link_to_path"] = link_to_path + return self + +def _VenvSymlinkEntryBuilder_set_package(self, package): + """Set the PyPI package name that the code originates from. + + Args: + self: implicitly added. + package: {type}`string | None` + + Returns: + {type}`VenvSymlinkEntryBuilder` self. + """ + if package != None: + _check_arg_type("package", "string", package) + self._state["package"] = package + return self + +def _VenvSymlinkEntryBuilder_set_venv_path(self, venv_path): + """Set the path relative to the kind directory within the venv. + + Args: + self: implicitly added. + venv_path: {type}`string` + + Returns: + {type}`VenvSymlinkEntryBuilder` self. + """ + _check_arg_type("venv_path", "string", venv_path) + self._state["venv_path"] = venv_path + return self + +def _VenvSymlinkEntryBuilder_set_version(self, version): + """Set the PyPI package version that the code originates from. + + Args: + self: implicitly added. + version: {type}`string | None` + + Returns: + {type}`VenvSymlinkEntryBuilder` self. + """ + if version != None: + _check_arg_type("version", "string", version) + self._state["version"] = version + return self + +def _VenvSymlinkEntryBuilder_build(self): + """Builds into a {obj}`VenvSymlinkEntry` object. + + Args: + self: implicitly added. + + Returns: + {type}`VenvSymlinkEntry` + """ + if not self._state["venv_path"]: + fail("venv_path must be set") + return VenvSymlinkEntry( + files = self.files.build(), + kind = self._state["kind"], + link_to_file = self._state["link_to_file"], + link_to_path = self._state["link_to_path"], + package = self._state["package"], + venv_path = self._state["venv_path"], + version = self._state["version"], + ) + +# buildifier: disable=name-conventions +VenvSymlinkEntryBuilder = struct( + TYPEDEF = _VenvSymlinkEntryBuilder_typedef, + build = _VenvSymlinkEntryBuilder_build, + get_kind = _VenvSymlinkEntryBuilder_get_kind, + set_kind = _VenvSymlinkEntryBuilder_set_kind, + get_link_to_file = _VenvSymlinkEntryBuilder_get_link_to_file, + set_link_to_file = _VenvSymlinkEntryBuilder_set_link_to_file, + get_link_to_path = _VenvSymlinkEntryBuilder_get_link_to_path, + set_link_to_path = _VenvSymlinkEntryBuilder_set_link_to_path, + get_package = _VenvSymlinkEntryBuilder_get_package, + set_package = _VenvSymlinkEntryBuilder_set_package, + get_venv_path = _VenvSymlinkEntryBuilder_get_venv_path, + set_venv_path = _VenvSymlinkEntryBuilder_set_venv_path, + get_version = _VenvSymlinkEntryBuilder_get_version, + set_version = _VenvSymlinkEntryBuilder_set_version, +) + def _check_arg_type(name, required_type, value): """Check that a value is of an expected type.""" value_type = type(value) @@ -409,6 +643,8 @@ def _PyInfoBuilder_new(): _has_py2_only_sources = [False], _has_py3_only_sources = [False], _uses_shared_libraries = [False], + _venv_symlink_builders = [], + add_venv_symlink = lambda *a, **k: _PyInfoBuilder_add_venv_symlink(self, *a, **k), build = lambda *a, **k: _PyInfoBuilder_build(self, *a, **k), build_builtin_py_info = lambda *a, **k: _PyInfoBuilder_build_builtin_py_info(self, *a, **k), direct_original_sources = builders.DepsetBuilder(), @@ -438,6 +674,22 @@ def _PyInfoBuilder_new(): ) return self +def _PyInfoBuilder_add_venv_symlink(self): + """Create and return a new VenvSymlinkEntryBuilder. + + :::{versionadded} 2.2.0 + ::: + + Args: + self: implicitly added. + + Returns: + {type}`VenvSymlinkEntryBuilder` + """ + entry_builder = _VenvSymlinkEntryBuilder() + self._venv_symlink_builders.append(entry_builder) + return entry_builder + def _PyInfoBuilder_get_has_py3_only_sources(self): """Get the `has_py3_only_sources` value. @@ -649,6 +901,11 @@ def _PyInfoBuilder_build(self): Returns: {type}`PyInfo` """ + venv_symlinks = depset( + direct = [b.build() for b in self._venv_symlink_builders], + transitive = [self.venv_symlinks.build()], + ) + return _EffectivePyInfo( has_py2_only_sources = self._has_py2_only_sources[0], has_py3_only_sources = self._has_py3_only_sources[0], @@ -663,7 +920,7 @@ def _PyInfoBuilder_build(self): transitive_original_sources = self.transitive_original_sources.build(), transitive_pyc_files = self.transitive_pyc_files.build(), transitive_pyi_files = self.transitive_pyi_files.build(), - venv_symlinks = self.venv_symlinks.build(), + venv_symlinks = venv_symlinks, ) def _PyInfoBuilder_build_builtin_py_info(self): @@ -692,6 +949,7 @@ def _PyInfoBuilder_build_builtin_py_info(self): PyInfoBuilder = struct( TYPEDEF = _PyInfoBuilder_typedef, new = _PyInfoBuilder_new, + add_venv_symlink = _PyInfoBuilder_add_venv_symlink, build = _PyInfoBuilder_build, build_builtin_py_info = _PyInfoBuilder_build_builtin_py_info, get_has_py2_only_sources = _PyInfoBuilder_get_has_py2_only_sources, diff --git a/python/private/py_interpreter_program.bzl b/python/private/py_interpreter_program.bzl index 7eb3e28bd9..1f1abb1bfe 100644 --- a/python/private/py_interpreter_program.bzl +++ b/python/private/py_interpreter_program.bzl @@ -15,7 +15,7 @@ """Internal only bootstrap level binary-like rule.""" load("@bazel_skylib//rules:common_settings.bzl", "BuildSettingInfo") -load("//python/private:sentinel.bzl", "SentinelInfo") +load("//python/private:sentinel_impl.bzl", "SentinelInfo") PyInterpreterProgramInfo = provider( doc = "Information about how to run a program with an external interpreter.", diff --git a/python/private/py_repositories.bzl b/python/private/py_repositories.bzl index 9c4051ede9..3489613a6c 100644 --- a/python/private/py_repositories.bzl +++ b/python/private/py_repositories.bzl @@ -72,6 +72,12 @@ def py_repositories(transition_settings = []): strip_prefix = "rules_cc-0.1.5", urls = ["https://github.com/bazelbuild/rules_cc/releases/download/0.1.5/rules_cc-0.1.5.tar.gz"], ) + http_archive( + name = "toml.bzl", + sha256 = "63633c762bdb4d836add5a9a81deeeae93c4b2edbd62ac032733020b5652b90a", + strip_prefix = "toml.bzl-0.4.0", + url = "https://github.com/jvolkman/toml.bzl/releases/download/v0.4.0/toml.bzl-v0.4.0.tar.gz", + ) http_archive( name = "package_metadata", sha256 = "8f27dc7393e3f3bdc793bdc4ba36d67a63c22cc9d38cc65d3204654974ea4563", diff --git a/python/private/py_runtime_info.bzl b/python/private/py_runtime_info.bzl index d94f469fc0..daf01e6c7e 100644 --- a/python/private/py_runtime_info.bzl +++ b/python/private/py_runtime_info.bzl @@ -55,6 +55,7 @@ def _PyRuntimeInfo_init( interpreter_path = None, interpreter = None, files = None, + interpreter_files_to_run = None, coverage_tool = None, coverage_files = None, pyc_tag = None, @@ -74,6 +75,15 @@ def _PyRuntimeInfo_init( if interpreter_path and files != None: fail("cannot specify 'files' if 'interpreter_path' is given") + if interpreter_path and interpreter_files_to_run: + fail("cannot specify 'interpreter_files_to_run' if 'interpreter_path' is given") + + if interpreter_files_to_run: + if not interpreter_files_to_run.executable: + fail("'interpreter_files_to_run' must have an executable") + if interpreter_files_to_run.executable != interpreter: + fail("'interpreter_files_to_run.executable' must match 'interpreter'") + if (coverage_tool and not coverage_files) or (not coverage_tool and coverage_files): fail( "coverage_tool and coverage_files must both be set or neither must be set, " + @@ -112,6 +122,7 @@ def _PyRuntimeInfo_init( "files": files, "implementation_name": implementation_name, "interpreter": interpreter, + "interpreter_files_to_run": interpreter_files_to_run, "interpreter_path": interpreter_path, "interpreter_version_info": interpreter_version_info_struct_from_dict(interpreter_version_info), "pyc_tag": pyc_tag, @@ -239,6 +250,19 @@ The Python implementation name (`sys.implementation.name`) If this is an in-build runtime, this field is a `File` representing the interpreter. Otherwise, this is `None`. Note that an in-build runtime can use either a prebuilt, checked-in interpreter or an interpreter built from source. +""", + "interpreter_files_to_run": """ +:type: None | FilesToRunProvider + +The `FilesToRunProvider` for the interpreter target when this runtime was +created from an executable target. This includes the interpreter executable and +the runfiles metadata needed to use it as an action tool. Rules that execute the +interpreter in an action should use this field so Bazel can stage the +interpreter together with its runfiles. This is `None` for platform runtimes +using `interpreter_path` and for file-only interpreter targets. + +:::{versionadded} 2.1.0 +::: """, "interpreter_path": """ :type: str | None diff --git a/python/private/py_runtime_rule.bzl b/python/private/py_runtime_rule.bzl index dfc915f463..4f450c4c9b 100644 --- a/python/private/py_runtime_rule.bzl +++ b/python/private/py_runtime_rule.bzl @@ -25,6 +25,41 @@ load(":version.bzl", "version") _py_builtins = py_internal +def coverage_tool_missing_message(*, coverage_enabled, coverage_tool, label): + """Build the warning for a selected runtime that cannot produce coverage. + + Kept separate from the rule implementation so the decision is unit testable + without having to capture analysis-phase output. + + Args: + coverage_enabled: {type}`bool` whether the build is collecting coverage. + coverage_tool: {type}`File | None` the runtime's coverage entry point. + label: {type}`Label` the `py_runtime` being analyzed. + + Returns: + {type}`str | None` the message to print, or `None` when no warning is + warranted. + """ + if not coverage_enabled or coverage_tool: + return None + + return """ +====================================================================== +WARNING: Python runtime {label} has no coverage_tool. + `bazel coverage` will produce empty lcov data for py_test targets that + resolve to this runtime. + + For rules_python's hermetic toolchains, enable the bundled coverage.py: + python.toolchain(configure_coverage_tool = True) # bzlmod + python_register_toolchains(register_coverage_tool = True) # WORKSPACE + A bundled wheel must exist for this interpreter's version and platform; + python/private/coverage_deps.bzl lists what ships with rules_python. + + Otherwise, set py_runtime.coverage_tool directly. See + https://rules-python.readthedocs.io/en/latest/coverage.html +====================================================================== +""".format(label = label) + def _py_runtime_impl(ctx): interpreter_path = ctx.attr.interpreter_path or None # Convert empty string to None interpreter = ctx.attr.interpreter @@ -39,6 +74,7 @@ def _py_runtime_impl(ctx): runfiles = ctx.runfiles() hermetic = bool(interpreter) + interpreter_files_to_run = None if not hermetic: if runtime_files: fail("if 'interpreter_path' is given then 'files' must be empty") @@ -46,9 +82,35 @@ def _py_runtime_impl(ctx): fail("interpreter_path must be an absolute path") else: interpreter_di = interpreter[DefaultInfo] + interpreter_file = None + + if _is_singleton_depset(interpreter_di.files): + interpreter_file = interpreter_di.files.to_list()[0] + + is_executable_source_file = ( + interpreter_file and + interpreter_file.is_source and + interpreter_di.files_to_run and + interpreter_di.files_to_run.executable == interpreter_file + ) - if interpreter_di.files_to_run and interpreter_di.files_to_run.executable: + if is_executable_source_file: + # Source files Bazel treats as executable, e.g. direct file labels + # and filegroups: preserve historical runtime-file expansion, but + # do not expose a FilesToRunProvider. + interpreter = interpreter_file + runfiles = runfiles.merge(interpreter_di.default_runfiles) + + runtime_files = depset(transitive = [ + interpreter_di.files, + interpreter_di.default_runfiles.files, + runtime_files, + ]) + elif interpreter_di.files_to_run and interpreter_di.files_to_run.executable: + # Executable rule target: use the executable and preserve the full + # FilesToRunProvider so action consumers can stage its runfiles. interpreter = interpreter_di.files_to_run.executable + interpreter_files_to_run = interpreter_di.files_to_run runfiles = runfiles.merge(interpreter_di.default_runfiles) runtime_files = depset(transitive = [ @@ -56,8 +118,10 @@ def _py_runtime_impl(ctx): interpreter_di.default_runfiles.files, runtime_files, ]) - elif _is_singleton_depset(interpreter_di.files): - interpreter = interpreter_di.files.to_list()[0] + elif interpreter_file: + # Non-executable rule with exactly one output: preserve the + # historical file-only interpreter behavior. + interpreter = interpreter_file else: fail("interpreter must be an executable target or must produce exactly one file.") @@ -79,6 +143,19 @@ def _py_runtime_impl(ctx): coverage_tool = None coverage_files = None + # Reported here rather than where the toolchains are registered: this rule is + # analyzed once per configuration, and only for the runtime that toolchain + # resolution actually selected, so the empty-lcov outcome is real rather than + # hypothetical. See https://github.com/bazel-contrib/rules_python/issues/3950. + coverage_warning = coverage_tool_missing_message( + coverage_enabled = ctx.configuration.coverage_enabled, + coverage_tool = coverage_tool, + label = ctx.label, + ) + if coverage_warning: + # buildifier: disable=print + print(coverage_warning) + python_version = ctx.attr.python_version interpreter_version_info = ctx.attr.interpreter_version_info @@ -124,6 +201,7 @@ def _py_runtime_impl(ctx): py_runtime_info_kwargs.update(dict( implementation_name = ctx.attr.implementation_name, interpreter_version_info = interpreter_version_info, + interpreter_files_to_run = interpreter_files_to_run, pyc_tag = pyc_tag, stage2_bootstrap_template = ctx.file.stage2_bootstrap_template, zip_main_template = ctx.file.zip_main_template, @@ -344,7 +422,7 @@ The template to use when two stage bootstrapping is enabled "Shebang" expression prepended to the bootstrapping Python stub script used when executing {rule}`py_binary` targets. -See https://github.com/bazelbuild/bazel/issues/8685 for +See [bazelbuild/bazel#8685](https://github.com/bazelbuild/bazel/issues/8685) for motivation. Does not apply to Windows. diff --git a/python/private/py_test_main_validator.py b/python/private/py_test_main_validator.py new file mode 100644 index 0000000000..e66bf6849e --- /dev/null +++ b/python/private/py_test_main_validator.py @@ -0,0 +1,227 @@ +"""Static check that a py_test main module actually runs something. + +A common ``py_test`` pitfall is to define test classes or functions but forget +to add any code that actually executes them (for example, assuming that +``py_test`` automatically invokes ``unittest`` or ``pytest``). When that +happens, running the test does nothing and the target silently passes. + +This validator parses the main module with :mod:`ast` and fails if the +module body is "inert", i.e. every top-level statement is one that does not +run anything (definitions, imports, assignments, docstrings, ``pass``). A +single active statement -- a bare call, a loop, or the conventional +``if __name__ == "__main__":`` guard -- is enough to consider the module able +to run tests. Top-level ``if`` and ``try`` blocks are inspected recursively, so +common guards like ``if TYPE_CHECKING:`` or ``try: import foo except +ImportError:`` (whose branches are themselves inert) don't bypass the check. + +As an exception, a module that defines no classes or functions at all (for +example, one that only imports other modules) is always allowed: it isn't the +"defined some tests but forgot to run them" case this check targets, and it +may legitimately rely on import side effects. +""" + +import argparse +import ast +import sys + + +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,) + +# Expression node types whose evaluation runs code (and thus may run tests). +# `await`/`yield` can't appear at the module top level, but are included for +# completeness when walking nested expressions. +_ACTIVE_EXPR_NODE_TYPES = (ast.Call, ast.Await, ast.Yield, ast.YieldFrom) + + +def _expression_runs_code(value) -> bool: + """Returns True if evaluating the expression would invoke/await/yield. + + Note this walks the expression as written; it does not try to model what + actually executes (e.g. a call inside a `lambda` body won't run until the + lambda is called). Erring toward "runs code" keeps the safeguard from + flagging valid tests. + """ + if value is None: + return False + return any(isinstance(child, _ACTIVE_EXPR_NODE_TYPES) for child in ast.walk(value)) + + +def _all_inert(nodes) -> bool: + return all(_is_inert_statement(node) for node in nodes) + + +def _is_inert_statement(node: ast.stmt) -> bool: + """Returns True if the top-level statement does not run any test code.""" + if isinstance(node, _INERT_NODE_TYPES): + return True + + # Assignments are inert unless their value runs code, e.g. + # `exit_code = unittest.main()` actually runs the tests. + if isinstance(node, (ast.Assign, ast.AnnAssign, ast.AugAssign)): + return not _expression_runs_code(node.value) + + # Bare expression statements: docstrings and other no-op expressions are + # inert; a call/await/yield (e.g. `unittest.main()`) runs code. + # Assert statements are inert unless their condition or message runs code. + if isinstance(node, ast.Assert): + return not (_expression_runs_code(node.test) or _expression_runs_code(node.msg)) + + if isinstance(node, ast.Expr): + return not _expression_runs_code(node.value) + + # An `if` whose condition and branches are entirely inert, e.g. the very + # common `if TYPE_CHECKING:` guard around typing-only imports. A call in the + # condition (e.g. `if feature_enabled():`) or a runner call in a branch body + # makes it active. + if isinstance(node, ast.If): + return ( + not _expression_runs_code(node.test) + and _all_inert(node.body) + and _all_inert(node.orelse) + ) + + # A `try` (or `try/except*`) whose body, handlers, else, and finally are all + # inert, e.g. the common `try: import foo except ImportError: ...` optional + # import pattern. + if isinstance(node, _TRY_NODE_TYPES): + if not ( + _all_inert(node.body) + and _all_inert(node.orelse) + and _all_inert(node.finalbody) + ): + return False + return all(_all_inert(handler.body) for handler in node.handlers) + + return False + + +def module_runs_something(tree: ast.Module) -> bool: + """Returns True if the module body has at least one active statement.""" + return any(not _is_inert_statement(node) for node in tree.body) + + +def _defines_test_code(tree: ast.Module) -> bool: + """Returns True if the module defines any top-level class or function.""" + return any( + isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)) + for node in tree.body + ) + + +def module_runs_tests(tree: ast.Module) -> bool: + """Returns True if the module appears able to run tests (or isn't checked). + + The check targets the specific pitfall of defining test classes/functions + but never running them. A module that defines no classes or functions at + all (for example, one that only imports other modules) is not subject to + the check and is always allowed, since it isn't the "defined but never run" + case and may legitimately rely on import side effects. + """ + if module_runs_something(tree): + return True + return not _defines_test_code(tree) + + +def _format_error(label: str, src_name: str) -> str: + target = label or src_name + return ( + "py_test target {target} will not run any tests.\n" + "\n" + "The main module '{src_name}' only contains inert top-level statements " + "(class/function definitions, imports, assignments). Running it does " + "nothing, so the test silently passes without executing any test " + "code.\n" + "\n" + "py_test runs the main module directly; it does not automatically " + "invoke a test runner such as unittest or pytest. Add code that runs " + "your tests, for example:\n" + "\n" + ' if __name__ == "__main__":\n' + " unittest.main()\n" + "\n" + "or use a main module that invokes a runner (e.g. pytest.main()).\n" + "\n" + "This check can be disabled by setting " + "--@rules_python//python/config_settings:validate_test_main=disabled." + ).format(target=target, src_name=src_name) + + +def main(args) -> int: + parser = argparse.ArgumentParser() + parser.add_argument( + "--src", + required=True, + help="Path to the main .py source file to analyze.", + ) + parser.add_argument( + "--src_name", + default="", + help="Human-friendly name of the source file, used in error messages.", + ) + parser.add_argument( + "--label", + default="", + help="The py_test target label, used in error messages.", + ) + parser.add_argument( + "--output", + required=True, + help="Path to the validation marker file to write on success.", + ) + options = parser.parse_args(args) + + src_name = options.src_name or options.src + + with open(options.src, "rb") as f: + source = f.read() + + try: + tree = ast.parse(source, filename=src_name) + except SyntaxError as e: + # A syntax error is surfaced by other actions (compilation/execution). + # The validator can't analyze the file, so don't fail here; treat it as + # passing to avoid duplicate or confusing errors. + sys.stderr.write( + "WARNING: py_test main validator could not parse {}: {}\n".format( + src_name, e + ) + ) + with open(options.output, "w") as out: + out.write("") + return 0 + + if not module_runs_tests(tree): + sys.stderr.write(_format_error(options.label, src_name) + "\n") + return 1 + + # Validation actions must produce their declared outputs on success. + with open(options.output, "w") as out: + out.write("") + return 0 + + +if __name__ == "__main__": + sys.exit(main(sys.argv[1:])) diff --git a/python/private/py_wheel.bzl b/python/private/py_wheel.bzl index 1ca344c086..c28aefff48 100644 --- a/python/private/py_wheel.bzl +++ b/python/private/py_wheel.bzl @@ -18,7 +18,7 @@ load(":attributes.bzl", "CONFIG_SETTINGS_ATTR", "apply_config_settings_attr") load(":py_info.bzl", "PyInfo") load(":py_package.bzl", "py_package_lib") load(":rule_builders.bzl", "ruleb") -load(":stamp.bzl", "is_stamping_enabled") +load(":stamp_impl.bzl", "is_stamping_enabled") load(":transition_labels.bzl", "TRANSITION_LABELS") load(":version.bzl", "version") @@ -54,7 +54,7 @@ Workspace status keys are expanded using `{NAME}` format, for example: - `distribution = "package.{CLASSIFIER}"` - `distribution = "{DISTRIBUTION}"` -For the available keys, see https://bazel.build/docs/user-manual#workspace-status +For the available keys, see """, ), "platform": attr.string( @@ -196,7 +196,7 @@ The {attr}`add_path_prefix` attribute was added. default = "", ), "classifiers": attr.string_list( - doc = "A list of strings describing the categories for the package. For valid classifiers see https://pypi.org/classifiers", + doc = "A list of strings describing the categories for the package. For valid classifiers see ", ), "data_files": attr.label_keyed_string_dict( doc = (""" @@ -233,7 +233,7 @@ be moved under that directory. "description_content_type": attr.string( doc = ("The type of contents in description_file. " + "If not provided, the type will be inferred from the extension of description_file. " + - "Also see https://packaging.python.org/en/latest/specifications/core-metadata/#description-content-type"), + "Also see "), ), "description_file": attr.label( doc = "A file containing text describing the package.", @@ -332,7 +332,7 @@ def _escape_filename_distribution_name(name): def _escape_filename_segment(segment): """Escape a segment of the wheel filename. - See https://www.python.org/dev/peps/pep-0427/#escaping-and-unicode + See {pep}`PEP 427` """ # TODO: this is wrong, isalnum replaces non-ascii letters, while we should diff --git a/python/private/pypi/BUILD.bazel b/python/private/pypi/BUILD.bazel index 7569eccac4..3c07135d40 100644 --- a/python/private/pypi/BUILD.bazel +++ b/python/private/pypi/BUILD.bazel @@ -13,7 +13,6 @@ # limitations under the License. load("@bazel_skylib//:bzl_library.bzl", "bzl_library") -load("//python:py_library.bzl", "py_library") package(default_visibility = ["//:__subpackages__"]) @@ -42,6 +41,15 @@ alias( visibility = ["//visibility:public"], ) +alias( + name = "wheel_record_rewriter", + actual = select({ + "@platforms//os:windows": "wheel_record_rewriter.ps1", + "//conditions:default": "wheel_record_rewriter.sh", + }), + visibility = ["//visibility:public"], +) + exports_files( srcs = ["deps.bzl"], visibility = ["//tools/private/update_deps:__pkg__"], @@ -75,477 +83,531 @@ filegroup( visibility = ["//tools/private/update_deps:__pkg__"], ) -# Keep sorted by library name and keep the files named by the main symbol they export - bzl_library( - name = "argparse_bzl", - srcs = ["argparse.bzl"], -) - -bzl_library( - name = "attrs_bzl", - srcs = ["attrs.bzl"], -) - -bzl_library( - name = "config_settings_bzl", + name = "config_settings", srcs = ["config_settings.bzl"], - deps = [ - ":flags_bzl", - "//python/private:common_labels_bzl", - "//python/private:flags_bzl", - "@bazel_skylib//lib:selects", - ], + deps = ["@bazel_skylib//lib:selects"], ) bzl_library( - name = "deps_bzl", + name = "deps", srcs = ["deps.bzl"], - deps = [ - "//python/private:bazel_tools_bzl", - ], -) - -bzl_library( - name = "env_marker_info_bzl", - srcs = ["env_marker_info.bzl"], + deps = ["//python/private:bazel_tools"], ) bzl_library( - name = "env_marker_setting_bzl", + name = "env_marker_setting", srcs = ["env_marker_setting.bzl"], deps = [ - ":env_marker_info_bzl", - ":pep508_env_bzl", - ":pep508_evaluate_bzl", - "//python/private:common_labels_bzl", - "//python/private:toolchain_types_bzl", + ":env_marker_info", + ":pep508_env", + ":pep508_evaluate", + "//python/private:common_labels", + "//python/private:toolchain_types", "@bazel_skylib//rules:common_settings", ], ) bzl_library( - name = "evaluate_markers_bzl", + name = "evaluate_markers", srcs = ["evaluate_markers.bzl"], deps = [ - ":deps_bzl", - ":pep508_evaluate_bzl", - ":pep508_requirement_bzl", + ":pep508_evaluate", + ":pep508_requirement", ], ) +# keep bzl_library( - name = "extension_bzl", + name = "extension", srcs = ["extension.bzl"], deps = [ - ":hub_builder_bzl", - ":hub_repository_bzl", - ":parse_whl_name_bzl", - ":pep508_env_bzl", - ":pip_repository_attrs_bzl", - ":platform_bzl", - ":pypi_cache_bzl", - ":simpleapi_download_bzl", - ":whl_library_bzl", - "//python/private:auth_bzl", - "//python/private:normalize_name_bzl", - "//python/private:repo_utils_bzl", - "@bazel_features//:features", - "@pythons_hub//:interpreters_bzl", - "@pythons_hub//:versions_bzl", - "@rules_python_internal//:rules_python_config_bzl", - ], -) - -bzl_library( - name = "flags_bzl", + ":hub_builder", + ":hub_repository", + ":parse_whl_name", + ":pep508_env", + ":pip_repository_attrs", + ":platform", + ":pypi_cache", + ":simpleapi_download", + ":unified_hub_repo", + ":whl_library", + "//python/private:auth", + "//python/private:normalize_name", + "//python/private:pyproject_utils", + "//python/private:repo_utils", + "@pythons_hub//:interpreters", + "@pythons_hub//:versions", + "@rules_python_internal//:rules_python_config", + "@toml.bzl//:toml", + ], +) + +bzl_library( + name = "flags", srcs = ["flags.bzl"], deps = [ - ":env_marker_info.bzl", - ":pep508_env_bzl", + ":env_marker_info", + ":pep508_env", + "//python/private:common_labels", "@bazel_skylib//rules:common_settings", ], ) bzl_library( - name = "generate_whl_library_build_bazel_bzl", - srcs = ["generate_whl_library_build_bazel.bzl"], + name = "generate_group_library_build_bazel", + srcs = ["generate_group_library_build_bazel.bzl"], deps = [ - "//python/private:text_util_bzl", + ":labels", + "//python/private:normalize_name", + "//python/private:text_util", ], ) bzl_library( - name = "generate_group_library_build_bazel_bzl", - srcs = ["generate_group_library_build_bazel.bzl"], - deps = [ - ":labels_bzl", - "//python/private:normalize_name_bzl", - ], + name = "generate_whl_library_build_bazel", + srcs = ["generate_whl_library_build_bazel.bzl"], + deps = ["//python/private:text_util"], ) bzl_library( - name = "hub_builder_bzl", + name = "hub_builder", srcs = ["hub_builder.bzl"], - visibility = ["//:__subpackages__"], deps = [ - ":attrs_bzl", - ":parse_requirements_bzl", - ":pep508_env_bzl", - ":pep508_evaluate_bzl", - ":python_tag_bzl", - ":requirements_files_by_platform_bzl", - ":whl_config_setting_bzl", - ":whl_repo_name_bzl", - "//python/private:full_version_bzl", - "//python/private:normalize_name_bzl", - "//python/private:text_util_bzl", - "//python/private:version_bzl", - "//python/private:version_label_bzl", + ":attrs", + ":hash", + ":parse_requirements", + ":pep508_env", + ":pep508_evaluate", + ":python_tag", + ":requirements_files_by_platform", + ":whl_config_setting", + ":whl_repo_name", + "//python/private:envsubst", + "//python/private:full_version", + "//python/private:normalize_name", + "//python/private:repo_utils", + "//python/private:text_util", + "//python/private:version", + "//python/private:version_label", ], ) bzl_library( - name = "hub_repository_bzl", + name = "hub_repository", srcs = ["hub_repository.bzl"], - visibility = ["//:__subpackages__"], deps = [ - ":render_pkg_aliases_bzl", - "//python/private:text_util_bzl", + ":render_pkg_aliases", + ":whl_config_setting", + "//python/private:text_util", ], ) bzl_library( - name = "index_sources_bzl", - srcs = ["index_sources.bzl"], -) - -bzl_library( - name = "labels_bzl", - srcs = ["labels.bzl"], + name = "missing_package", + srcs = ["missing_package.bzl"], + deps = [ + "//python/private:py_info", + "//python/private:reexports", + ], ) bzl_library( - name = "multi_pip_parse_bzl", + name = "multi_pip_parse", srcs = ["multi_pip_parse.bzl"], deps = [ - ":pip_repository_bzl", - "//python/private:text_util_bzl", + ":pip_repository", + "//python/private:text_util", ], ) bzl_library( - name = "package_annotation_bzl", - srcs = ["package_annotation.bzl"], + name = "namespace_pkgs", + srcs = ["namespace_pkgs.bzl"], + deps = ["@bazel_skylib//rules:copy_file"], ) bzl_library( - name = "parse_requirements_bzl", + name = "parse_requirements", srcs = ["parse_requirements.bzl"], deps = [ - ":argparse_bzl", - ":index_sources_bzl", - ":parse_requirements_txt_bzl", - ":pep508_evaluate_bzl", - ":pep508_requirement_bzl", - ":pypi_repo_utils_bzl", - ":requirements_files_by_platform_bzl", - ":select_whl_bzl", - "//python/private:normalize_name_bzl", - "//python/private:repo_utils_bzl", + ":argparse", + ":hash", + ":index_sources", + ":parse_requirements_txt", + ":pep508_evaluate", + ":pep508_requirement", + ":select_whl", + "//python/private:normalize_name", + "//python/private:repo_utils", + "//python/uv/private:uv_lock_to_requirements", ], ) bzl_library( - name = "parse_requirements_txt_bzl", - srcs = ["parse_requirements_txt.bzl"], -) - -bzl_library( - name = "parse_simpleapi_html_bzl", + name = "parse_simpleapi_html", srcs = ["parse_simpleapi_html.bzl"], deps = [ - ":version_from_filename_bzl", - "//python/private:normalize_name_bzl", + ":hash", + ":version_from_filename", + "//python/private:normalize_name", ], ) bzl_library( - name = "parse_whl_name_bzl", - srcs = ["parse_whl_name.bzl"], -) - -bzl_library( - name = "patch_whl_bzl", + name = "patch_whl", srcs = ["patch_whl.bzl"], deps = [ - ":parse_whl_name_bzl", - "//python/private:repo_utils_bzl", - "@rules_python_internal//:rules_python_config_bzl", + ":parse_whl_name", + "//python/private:repo_utils", + "@rules_python_internal//:rules_python_config", ], ) bzl_library( - name = "pep508_deps_bzl", + name = "pep508_deps", srcs = ["pep508_deps.bzl"], deps = [ - ":pep508_env_bzl", - ":pep508_evaluate_bzl", - ":pep508_requirement_bzl", - "//python/private:normalize_name_bzl", + ":pep508_env", + ":pep508_evaluate", + ":pep508_requirement", + "//python/private:normalize_name", ], ) bzl_library( - name = "pep508_env_bzl", + name = "pep508_env", srcs = ["pep508_env.bzl"], deps = [ - "//python/private:normalize_name_bzl", - "//python/private:version_bzl", + "//python/private:normalize_name", + "//python/private:version", ], ) bzl_library( - name = "pep508_evaluate_bzl", + name = "pep508_evaluate", srcs = ["pep508_evaluate.bzl"], deps = [ - "//python/private:enum_bzl", - "//python/private:version_bzl", + "//python/private:enum", + "//python/private:version", ], ) bzl_library( - name = "pep508_requirement_bzl", + name = "pep508_requirement", srcs = ["pep508_requirement.bzl"], - deps = [ - "//python/private:normalize_name_bzl", - ], + deps = ["//python/private:normalize_name"], ) bzl_library( - name = "pip_bzl", + name = "pip", srcs = ["pip.bzl"], - deps = [ - ":extension_bzl", - ], + deps = [":extension"], ) bzl_library( - name = "pip_compile_bzl", + name = "pip_compile", srcs = ["pip_compile.bzl"], deps = [ - ":deps_bzl", - "//python:py_binary_bzl", - "//python:py_test_bzl", + "//python:py_binary", + "//python:py_test", ], ) bzl_library( - name = "pip_repository_bzl", + name = "pip_repository", srcs = ["pip_repository.bzl"], deps = [ - ":attrs_bzl", - ":parse_requirements_bzl", - ":pep508_env_bzl", - ":pip_repository_attrs_bzl", - ":pypi_repo_utils_bzl", - ":render_pkg_aliases_bzl", - ":whl_config_setting_bzl", - "//python/private:normalize_name_bzl", - "//python/private:repo_utils_bzl", - "//python/private:text_util_bzl", + ":parse_requirements", + ":pep508_env", + ":pip_repository_attrs", + ":pypi_repo_utils", + ":render_pkg_aliases", + ":requirements_files_by_platform", + "//python/private:normalize_name", + "//python/private:repo_utils", + "//python/private:text_util", "@bazel_skylib//lib:sets", ], ) bzl_library( - name = "pip_repository_attrs_bzl", + name = "pip_repository_attrs", srcs = ["pip_repository_attrs.bzl"], + deps = [":attrs"], ) bzl_library( - name = "pkg_aliases_bzl", + name = "pkg_aliases", srcs = ["pkg_aliases.bzl"], deps = [ - ":labels_bzl", - "//python/private:common_labels_bzl", - "//python/private:text_util_bzl", + ":labels", + "//python/private:common_labels", + "//python/private:text_util", "@bazel_skylib//lib:selects", ], ) bzl_library( - name = "platform_bzl", - srcs = ["platform.bzl"], -) - -bzl_library( - name = "pypi_cache_bzl", + name = "pypi_cache", srcs = ["pypi_cache.bzl"], - deps = [ - ":version_from_filename_bzl", - ], + deps = [":version_from_filename"], ) bzl_library( - name = "pypi_repo_utils_bzl", + name = "pypi_repo_utils", srcs = ["pypi_repo_utils.bzl"], deps = [ - "//python/private:repo_utils_bzl", + "//python/private:repo_utils", + "//python/private:util", "@bazel_skylib//lib:types", ], ) bzl_library( - name = "python_tag_bzl", + name = "python_tag", srcs = ["python_tag.bzl"], - deps = [ - "//python/private:version_bzl", - ], + deps = ["//python/private:version"], ) bzl_library( - name = "render_pkg_aliases_bzl", + name = "render_pkg_aliases", srcs = ["render_pkg_aliases.bzl"], deps = [ - ":generate_group_library_build_bazel_bzl", - ":whl_config_setting_bzl", - "//python/private:normalize_name_bzl", - "//python/private:text_util_bzl", + ":generate_group_library_build_bazel", + "//python/private:normalize_name", + "//python/private:text_util", ], ) -py_library( - name = "repack_whl", - srcs = ["repack_whl.py"], - deps = ["//tools:wheelmaker"], -) - bzl_library( - name = "requirements_files_by_platform_bzl", + name = "requirements_files_by_platform", srcs = ["requirements_files_by_platform.bzl"], deps = [ - ":argparse_bzl", - ":whl_target_platforms_bzl", + ":argparse", + ":whl_target_platforms", ], ) bzl_library( - name = "select_whl_bzl", + name = "select_whl", srcs = ["select_whl.bzl"], deps = [ - ":parse_whl_name_bzl", - ":python_tag_bzl", - "//python/private:version_bzl", + ":parse_whl_name", + ":python_tag", + "//python/private:version", ], ) bzl_library( - name = "simpleapi_download_bzl", + name = "simpleapi_download", srcs = ["simpleapi_download.bzl"], deps = [ - ":parse_simpleapi_html_bzl", - ":urllib_bzl", - "//python/private:auth_bzl", - "//python/private:normalize_name_bzl", + ":parse_simpleapi_html", + ":urllib", + "//python/private:auth", + "//python/private:envsubst", + "//python/private:normalize_name", ], ) bzl_library( - name = "urllib_bzl", - srcs = ["urllib.bzl"], + name = "unified_hub_repo", + srcs = ["unified_hub_repo.bzl"], + deps = [ + "//python/private:normalize_name", + "//python/private:text_util", + ], ) bzl_library( - name = "version_from_filename_bzl", - srcs = ["version_from_filename.bzl"], + name = "unified_hub_setup", + srcs = ["unified_hub_setup.bzl"], + deps = [ + "@rules_python//python/private/pypi:labels", + "@rules_python//python/private/pypi:missing_package", + ], ) bzl_library( - name = "whl_config_repo_bzl", - srcs = ["whl_config_repo.bzl"], + name = "venv_entry_point", + srcs = ["venv_entry_point.bzl"], deps = [ - ":generate_group_library_build_bazel_bzl", - "//python/private:text_util_bzl", + "//python/private:attributes", + "//python/private:common", + "//python/private:rule_builders", ], ) bzl_library( - name = "whl_config_setting_bzl", - srcs = ["whl_config_setting.bzl"], + name = "venv_rewrite_shebang", + srcs = ["venv_rewrite_shebang.bzl"], + deps = [ + "//python/private:attributes", + "//python/private:common", + "//python/private:py_info", + "//python/private:rule_builders", + ], ) bzl_library( - name = "whl_extract_bzl", + name = "whl_config_repo", + srcs = ["whl_config_repo.bzl"], + deps = [ + ":generate_group_library_build_bazel", + "//python/private:text_util", + ], +) + +bzl_library( + name = "whl_extract", srcs = ["whl_extract.bzl"], deps = [ - ":whl_metadata_bzl", - "//python/private:repo_utils_bzl", - "@rules_python_internal//:rules_python_config_bzl", + ":whl_metadata", + "//python/private:repo_utils", + "@rules_python_internal//:rules_python_config", ], ) bzl_library( - name = "whl_library_alias_bzl", + name = "whl_library", + srcs = ["whl_library.bzl"], + deps = [ + ":attrs", + ":deps", + ":generate_whl_library_build_bazel", + ":patch_whl", + ":pep508_requirement", + ":pypi_repo_utils", + ":urllib", + ":whl_extract", + ":whl_metadata", + "//python/private:auth", + "//python/private:envsubst", + "//python/private:is_standalone_interpreter", + "//python/private:normalize_name", + "//python/private:repo_utils", + ], +) + +bzl_library( + name = "whl_library_alias", srcs = ["whl_library_alias.bzl"], deps = [ - ":render_pkg_aliases_bzl", - "//python/private:full_version_bzl", + ":render_pkg_aliases", + "//python/private:full_version", ], ) bzl_library( - name = "whl_library_bzl", - srcs = ["whl_library.bzl"], + name = "gen_wheel_record", + srcs = ["gen_wheel_record.bzl"], deps = [ - ":attrs_bzl", - ":deps_bzl", - ":generate_whl_library_build_bazel_bzl", - ":parse_whl_name_bzl", - ":patch_whl_bzl", - ":pep508_requirement_bzl", - ":pypi_repo_utils_bzl", - ":urllib_bzl", - ":whl_extract_bzl", - ":whl_metadata_bzl", - ":whl_target_platforms_bzl", - "//python/private:auth_bzl", - "//python/private:envsubst_bzl", - "//python/private:is_standalone_interpreter_bzl", - "//python/private:normalize_name_bzl", - "//python/private:repo_utils_bzl", - "//python/private:util_bzl", - "@rules_python_internal//:rules_python_config_bzl", - ], -) - -bzl_library( - name = "whl_metadata_bzl", - srcs = ["whl_metadata.bzl"], + "//python/private:attributes", + "//python/private:common", + ], +) + +bzl_library( + name = "whl_library_targets", + srcs = ["whl_library_targets.bzl"], + deps = [ + ":env_marker_setting", + ":gen_wheel_record", + ":labels", + ":namespace_pkgs", + ":pep508_deps", + ":venv_entry_point", + ":venv_rewrite_shebang", + "//python:py_binary", + "//python:py_library", + "//python/private:normalize_name", + "@bazel_skylib//rules:copy_file", + ], ) bzl_library( - name = "whl_repo_name_bzl", + name = "whl_repo_name", srcs = ["whl_repo_name.bzl"], deps = [ - ":parse_whl_name_bzl", - "//python/private:normalize_name_bzl", + ":parse_whl_name", + "//python/private:normalize_name", ], ) bzl_library( - name = "whl_target_platforms_bzl", - srcs = ["whl_target_platforms.bzl"], + name = "urllib", + srcs = ["urllib.bzl"], + deps = ["//python/private:envsubst"], ) bzl_library( - name = "venv_entry_point_bzl", - srcs = ["venv_entry_point.bzl"], - visibility = ["//visibility:public"], + name = "index_sources", + srcs = ["index_sources.bzl"], + deps = [":hash"], ) bzl_library( - name = "venv_rewrite_shebang_bzl", - srcs = ["venv_rewrite_shebang.bzl"], - visibility = ["//visibility:public"], + name = "argparse", + srcs = ["argparse.bzl"], +) + +bzl_library( + name = "attrs", + srcs = ["attrs.bzl"], +) + +bzl_library( + name = "env_marker_info", + srcs = ["env_marker_info.bzl"], +) + +bzl_library( + name = "hash", + srcs = ["hash.bzl"], +) + +bzl_library( + name = "labels", + srcs = ["labels.bzl"], +) + +bzl_library( + name = "package_annotation", + srcs = ["package_annotation.bzl"], +) + +bzl_library( + name = "parse_requirements_txt", + srcs = ["parse_requirements_txt.bzl"], +) + +bzl_library( + name = "parse_whl_name", + srcs = ["parse_whl_name.bzl"], +) + +bzl_library( + name = "platform", + srcs = ["platform.bzl"], +) + +bzl_library( + name = "version_from_filename", + srcs = ["version_from_filename.bzl"], +) + +bzl_library( + name = "whl_config_setting", + srcs = ["whl_config_setting.bzl"], +) + +bzl_library( + name = "whl_metadata", + srcs = ["whl_metadata.bzl"], +) + +bzl_library( + name = "whl_target_platforms", + srcs = ["whl_target_platforms.bzl"], ) diff --git a/python/private/pypi/attrs.bzl b/python/private/pypi/attrs.bzl index 57bd93f40a..271c17e35c 100644 --- a/python/private/pypi/attrs.bzl +++ b/python/private/pypi/attrs.bzl @@ -64,79 +64,6 @@ here do not cause packages to be re-fetched. Don't fetch different things based on the value of these variables. """, ), - "experimental_requirement_cycles": attr.string_list_dict( - default = {}, - doc = """\ -A mapping of dependency cycle names to a list of requirements which form that cycle. - -Requirements which form cycles will be installed together and taken as -dependencies together in order to ensure that the cycle is always satisified. - -Example: - `sphinx` depends on `sphinxcontrib-serializinghtml` - When listing both as requirements, ala - - ``` - py_binary( - name = "doctool", - ... - deps = [ - "@pypi//sphinx:pkg", - "@pypi//sphinxcontrib_serializinghtml", - ] - ) - ``` - - Will produce a Bazel error such as - - ``` - ERROR: .../external/pypi_sphinxcontrib_serializinghtml/BUILD.bazel:44:6: in alias rule @pypi_sphinxcontrib_serializinghtml//:pkg: cycle in dependency graph: - //:doctool (...) - @pypi//sphinxcontrib_serializinghtml:pkg (...) - .-> @pypi_sphinxcontrib_serializinghtml//:pkg (...) - | @pypi_sphinxcontrib_serializinghtml//:_pkg (...) - | @pypi_sphinx//:pkg (...) - | @pypi_sphinx//:_pkg (...) - `-- @pypi_sphinxcontrib_serializinghtml//:pkg (...) - ``` - - Which we can resolve by configuring these two requirements to be installed together as a cycle - - ``` - pip_parse( - ... - experimental_requirement_cycles = { - "sphinx": [ - "sphinx", - "sphinxcontrib-serializinghtml", - ] - }, - ) - ``` - -Warning: - If a dependency participates in multiple cycles, all of those cycles must be - collapsed down to one. For instance `a <-> b` and `a <-> c` cannot be listed - as two separate cycles. -""", - ), - "extra_hub_aliases": attr.string_list_dict( - doc = """\ -Extra aliases to make for specific wheels in the hub repo. This is useful when -paired with the {attr}`whl_modifications`. - -:::{versionadded} 0.38.0 - -For `pip.parse` with bzlmod -::: - -:::{versionadded} 1.0.0 - -For `pip_parse` with workspace. -::: -""", - mandatory = False, - ), "extra_pip_args": attr.string_list( doc = """Extra arguments to pass on to pip. Must not contain spaces. 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/extension.bzl b/python/private/pypi/extension.bzl index a0b59e9ed1..ed1fa28664 100644 --- a/python/private/pypi/extension.bzl +++ b/python/private/pypi/extension.bzl @@ -17,8 +17,10 @@ load("@pythons_hub//:interpreters.bzl", "INTERPRETER_LABELS") load("@pythons_hub//:versions.bzl", "MINOR_MAPPING") load("@rules_python_internal//:rules_python_config.bzl", rp_config = "config") +load("@toml.bzl", "toml") load("//python/private:auth.bzl", "AUTH_ATTRS") load("//python/private:normalize_name.bzl", "normalize_name") +load("//python/private:pyproject_utils.bzl", "read_pyproject", "version_from_requires_python") load("//python/private:repo_utils.bzl", "repo_utils") load(":hub_builder.bzl", "hub_builder") load(":hub_repository.bzl", "hub_repository", "whl_config_settings_to_json") @@ -28,13 +30,14 @@ load(":pip_repository_attrs.bzl", "ATTRS") load(":platform.bzl", _plat = "platform") load(":pypi_cache.bzl", "pypi_cache") load(":simpleapi_download.bzl", "simpleapi_download") +load(":unified_hub_repo.bzl", "unified_hub_repo") load(":whl_library.bzl", "whl_library") def _whl_mods_impl(whl_mods_dict): """Implementation of the pip.whl_mods tag class. This creates the JSON files used to modify the creation of different wheels. -""" + """ for hub_name, whl_maps in whl_mods_dict.items(): whl_mods = {} @@ -203,14 +206,27 @@ def build_config( Returns: A struct with the configuration. """ + default_hub = None defaults = { "platforms": default_platforms(), + "python_version": None, } for mod in module_ctx.modules: if not (mod.is_root or mod.name == "rules_python"): continue for tag in mod.tags.default: + if tag.default_hub: + if mod.is_root: + if default_hub: + fail("Duplicate pip.default tag: only one explicit default PyPI hub is allowed.") + default_hub = tag.default_hub + pyproject_toml = tag.pyproject_toml + if pyproject_toml: + pyproject = read_pyproject(module_ctx, pyproject_toml) + if pyproject.requires_python: + defaults["python_version"] = version_from_requires_python(pyproject.requires_python) + platform = tag.platform if platform: specific_config = defaults["platforms"].setdefault(platform, {}) @@ -244,13 +260,16 @@ def build_config( return struct( auth_patterns = defaults.get("auth_patterns", {}), + default_hub = default_hub, index_url = defaults.get("index_url", "https://pypi.org/simple").rstrip("/"), netrc = defaults.get("netrc", None), + python_version = defaults.get("python_version", None), platforms = { name: _plat(**values) for name, values in defaults["platforms"].items() }, enable_pipstar_extract = enable_pipstar_extract, + toml_decode = toml.decode, ) def parse_modules( @@ -343,9 +362,49 @@ You cannot use both the additive_build_content and additive_build_content_file a pip_hub_map = {} simpleapi_cache = pypi_cache(mctx = module_ctx) + is_pypi_hub_reserved = module_ctx.getenv("RULES_PYTHON_PYPI_HUB_RESERVED", "0") == "1" + renamed_default_hub = None + for mod in module_ctx.modules: for pip_attr in mod.tags.parse: + python_version = pip_attr.python_version + if not python_version and pip_attr.pyproject_toml: + pyproject = read_pyproject(module_ctx, pip_attr.pyproject_toml) + if pyproject.requires_python: + python_version = version_from_requires_python(pyproject.requires_python) + python_version = python_version or config.python_version + if not python_version: + _fail("pip.parse() requires one of `python_version`, `pyproject_toml`, or `pip.default(pyproject_toml=...)` to be set") + hub_name = pip_attr.hub_name + if hub_name == "pypi": + if is_pypi_hub_reserved: + renamed_name = mod.name + "_pypi" + print( + ( + "WARNING: The PyPI hub name 'pypi' is reserved " + + "(module '{}'). The hub was renamed to '{}'. " + + "Please rename your hub." + ).format( + mod.name, + renamed_name, + ), + ) # buildifier: disable=print + hub_name = renamed_name + if not renamed_default_hub: + renamed_default_hub = hub_name + else: + print( + ( + "WARNING: The PyPI hub name 'pypi' is reserved " + + "(module '{}'). Please rename your hub, otherwise " + + "a future release will rename it to '{}_pypi'." + ).format( + mod.name, + mod.name, + ), + ) # buildifier: disable=print + if hub_name not in pip_hub_map: builder = hub_builder( name = hub_name, @@ -359,7 +418,7 @@ You cannot use both the additive_build_content and additive_build_content_file a available_interpreters = kwargs.get("available_interpreters", INTERPRETER_LABELS), logger = repo_utils.logger(module_ctx, "pypi:hub:" + hub_name, mod = mod), ) - pip_hub_map[pip_attr.hub_name] = builder + pip_hub_map[hub_name] = builder elif pip_hub_map[hub_name].module_name != mod.name: # We cannot have two hubs with the same name in different # modules. @@ -375,13 +434,23 @@ You cannot use both the additive_build_content and additive_build_content_file a )) else: - builder = pip_hub_map[pip_attr.hub_name] + builder = pip_hub_map[hub_name] builder.pip_parse( module_ctx, pip_attr = pip_attr, + python_version = python_version, ) + # dict[str package, dict[str, None] extra_targets] + declared_deps = {} + for mod in module_ctx.modules: + for dep_attr in mod.tags.dep: + name = normalize_name(dep_attr.name) + targets = declared_deps.setdefault(name, {}) + for target in dep_attr.extra_targets: + targets[target] = None + # Keeps track of all the hub's whl repos across the different versions. # dict[hub, dict[whl, dict[version, str pip]]] # Where hub, whl, and pip are the repo names @@ -406,6 +475,8 @@ You cannot use both the additive_build_content and additive_build_content_file a return struct( config = config, + declared_deps = declared_deps, + default_hub = config.default_hub or renamed_default_hub, exposed_packages = exposed_packages, extra_aliases = extra_aliases, facts = simpleapi_cache.get_facts(), @@ -422,6 +493,50 @@ You cannot use both the additive_build_content and additive_build_content_file a }, ) +def _create_unified_hub_repo(mods): + if "pypi" in mods.hub_whl_map: + return + + hubs = sorted(mods.hub_whl_map.keys()) + if mods.default_hub and mods.default_hub not in hubs: + fail("default_hub '%s' is not a defined PyPI hub. Available hubs: %s" % (mods.default_hub, ", ".join(hubs))) + + packages = {} + extra_aliases = {} + + for hub_name in hubs: + for pkg_name in mods.exposed_packages.get(hub_name, []): + norm_pkg = normalize_name(pkg_name) + if norm_pkg not in packages: + packages[norm_pkg] = [] + if hub_name not in packages[norm_pkg]: + packages[norm_pkg].append(hub_name) + + extra = mods.extra_aliases.get(hub_name, {}).get(norm_pkg, []) + for alias_name in extra: + qual_alias = "%s:%s" % (norm_pkg, alias_name) + if qual_alias not in extra_aliases: + extra_aliases[qual_alias] = [] + if hub_name not in extra_aliases[qual_alias]: + extra_aliases[qual_alias].append(hub_name) + + for norm_pkg, extra_targets in mods.declared_deps.items(): + if norm_pkg not in packages: + packages[norm_pkg] = [] + + for target_name in extra_targets: + qual_alias = "%s:%s" % (norm_pkg, target_name) + if qual_alias not in extra_aliases: + extra_aliases[qual_alias] = [] + + unified_hub_repo( + name = "pypi", + default_hub = mods.default_hub or (hubs[0] if hubs else ""), + extra_aliases = extra_aliases, + hubs = hubs, + packages = packages, + ) + def _pip_impl(module_ctx): """Implementation of a class tag that creates the pip hub and corresponding pip spoke whl repositories. @@ -513,6 +628,8 @@ def _pip_impl(module_ctx): groups = mods.hub_group_map.get(hub_name), ) + _create_unified_hub_repo(mods) + # The code is smart to not return facts if we don't support the mechanism for that. # Hence we should not pass it to the metadata if mods.facts: @@ -535,12 +652,14 @@ Either this or {attr}`env` `platform_machine` key should be specified. """, ), "config_settings": attr.label_list( - mandatory = True, doc = """\ The list of labels to `config_setting` targets that need to be matched for the platform to be -selected. +selected. Mandatory if platform is specified. """, ), + "default_hub": attr.string( + doc = "The name of the concrete PyPI hub to use by default when {flag}`--venv=auto`.", + ), "env": attr.string_dict( doc = """\ The values to use for environment markers when evaluating an expression. @@ -571,10 +690,10 @@ This value is going to be subject to `envsubst` substitutions if necessary, look {attr}`pip.parse.envsubst` documentation for more information.. The indexes must support Simple API as described here: -https://packaging.python.org/en/latest/specifications/simple-repository-api/ + -Index metadata will be used to get `sha256` values for packages even if the -`sha256` values are not present in the requirements.txt lock file. +Index metadata will be used to get the hash digest values for packages even +if the `--hash` values are not present in the requirements.txt lock file. Defaults to `https://pypi.org/simple`. @@ -617,6 +736,23 @@ If you are defining custom platforms in your project and don't want things to cl [isolation] feature. [isolation]: https://bazel.build/rules/lib/globals/module#use_extension.isolate +""", + ), + "pyproject_toml": attr.label( + mandatory = False, + doc = """\ +Label pointing to pyproject.toml file to read the default Python version from. +When specified, reads the `requires-python` field from pyproject.toml and uses +it as the default python_version for all `pip.parse()` calls that don't +explicitly specify one. + +:::{note} +The version must be specified as `==X.Y.Z` (exact version with full semver). +This is designed to work with dependency management tools like Renovate. +::: + +:::{versionadded} 2.3.0 +::: """, ), "whl_abi_tags": attr.string_list( @@ -731,10 +867,10 @@ index URL. This design pattern has been chosen in order to be fully deterministic about which packages come from which source. We want to avoid issues similar to what happened in -https://pytorch.org/blog/compromised-nightly-dependency/. +. The indexes must support Simple API as described here: -https://packaging.python.org/en/latest/specifications/simple-repository-api/ + """, ), "hub_name": attr.string( @@ -742,6 +878,11 @@ https://packaging.python.org/en/latest/specifications/simple-repository-api/ doc = """ The name of the repo pip dependencies will be accessible from. +The hub name `"pypi"` is reserved for the automatically generated +[Unified @pypi Hub](unified-pypi-hub) repository. Please choose a different name +for your concrete hubs. See [Unified @pypi Hub](unified-pypi-hub) for how to +handle collisions. + This name must be unique between modules; unless your module is guaranteed to always be the root module, it's highly recommended to include your module name in the hub name. Repo mapping, `use_repo(..., pip="my_modules_pip_deps")`, can @@ -757,6 +898,12 @@ is not required. Each hub is a separate resolution of pip dependencies. This means if different programs need different versions of some library, separate hubs can be created, and each program can use its respective hub's targets. Targets from different hubs should not be used together. + +:::{versionchanged} 2.2.0 +Using the hub name `"pypi"` is deprecated and is changed to +`{module_name}_pypi` depending on the +{envvar}`RULES_PYTHON_PYPI_HUB_RESERVED` environment variable. +::: """, ), "parallel_download": attr.bool( @@ -776,8 +923,23 @@ find in case extra indexes are specified. """, default = True, ), + "pyproject_toml": attr.label( + mandatory = False, + doc = """\ +Label pointing to a pyproject.toml file to read the Python version from. +When specified, the `requires-python` field is used as the `python_version` +for this `pip.parse()` call, unless `python_version` is set explicitly. + +:::{note} +The version must be specified as `==X.Y.Z` (exact version with full semver). +::: + +:::{versionadded} 2.3.0 +::: +""", + ), "python_version": attr.string( - mandatory = True, + mandatory = False, doc = """ The Python version the dependencies are targetting, in Major.Minor format (e.g., "3.11") or patch level granularity (e.g. "3.11.1"). @@ -785,6 +947,15 @@ The Python version the dependencies are targetting, in Major.Minor format If an interpreter isn't explicitly provided (using `python_interpreter` or `python_interpreter_target`), then the version specified here must have a corresponding `python.toolchain()` configured. + +:::{seealso} +The {obj}`pyproject_toml` attribute for getting the version from a project file. +::: + +:::{versionchanged} 2.3.0 +No longer mandatory if the {obj}`pyproject_toml` attribute or +{obj}`pip.default.pyproject_toml` is specified. +::: """, ), "simpleapi_skip": attr.string_list( @@ -817,6 +988,15 @@ a string `"{os}_{arch}"` as the value here. You could also use `"{os}_{arch}_fre :::{versionadded} 1.8.0 ::: +""", + ), + "uv_lock": attr.label( + doc = """ +(label, optional): A label pointing to the uv.lock file. If provided, +the uv.lock file will be used as the primary source for package metadata. + +:::{versionadded} 2.2.0 +::: """, ), "whl_modifications": attr.label_keyed_string_dict( @@ -916,7 +1096,33 @@ Apply any overrides (e.g. patches) to a given Python distribution defined by other tags in this extension.""", ) +_dep_tag = tag_class( + attrs = { + "extra_targets": attr.string_list( + doc = """\ +A list of extra target names in the package that are expected to be available. +See {obj}`pip.parse.extra_hub_aliases`. +""", + default = [], + ), + "name": attr.string( + doc = "The name of a pypi package. Note that the name is normalized.", + mandatory = True, + ), + }, + doc = """\ +Declare an abstract PyPI dependency to ensure its target structure exists in the unified hub. + +This is useful for targets or rules that need to depend on a package (e.g., `@pypi//numpy`) +but do not want to force a specific version or concrete requirements lock file on their +consumers. The concrete version and implementation must be provided by downstreams calling +`pip.parse`. If they are not, the target will still be defined, but it will result in an +execution-phase error when built. +""", +) + pypi = module_extension( + environ = ["RULES_PYTHON_PYPI_HUB_RESERVED"], doc = """\ This extension is used to make dependencies from pip available. @@ -930,6 +1136,14 @@ can be made to configure different Python versions, and will be grouped by the `hub_name` argument. This allows the same logical name, e.g. `@pip//numpy` to automatically resolve to different, Python version-specific, libraries. +A unified `@pypi` proxy repository is always generated (unless a hub is +explicitly named "pypi") to route dependencies dynamically. See +[Unified @pypi Hub](unified-pypi-hub) for details. + +Environment Variables: +- `RULES_PYTHON_PYPI_HUB_RESERVED`: Enable fallback renaming for reserved hub name collisions. + See the {envvar}`RULES_PYTHON_PYPI_HUB_RESERVED` documentation for details. + pip.whl_mods: This tag class is used to help create JSON files to describe modifications to the BUILD files for wheels. @@ -941,6 +1155,10 @@ the BUILD files for wheels. doc = """\ This tag class allows for more customization of how the configuration for the hub repositories is built. +It can also be used to designate the default hub for the automatically +generated [Unified @pypi Hub](unified-pypi-hub) using the `default_hub` +attribute. + :::{seealso} The [environment markers][environment_markers] specification for the explanation of the @@ -953,6 +1171,7 @@ terms used in this extension. ::: """, ), + "dep": _dep_tag, "override": _override_tag, "parse": tag_class( attrs = _pip_parse_ext_attrs(), @@ -961,6 +1180,9 @@ This tag class is used to create a pip hub and all of the spokes that are part o This tag class reuses most of the attributes found in {bzl:obj}`pip_parse`. The exception is it does not use the arg 'repo_prefix'. We set the repository prefix for the user and the alias arg is always True in bzlmod. + +You can use the automatically generated [Unified @pypi Hub](unified-pypi-hub) +repository to route package dependencies dynamically at build time. """, ), "whl_mods": tag_class( diff --git a/python/private/pypi/gen_wheel_record.bzl b/python/private/pypi/gen_wheel_record.bzl new file mode 100644 index 0000000000..6dd211f776 --- /dev/null +++ b/python/private/pypi/gen_wheel_record.bzl @@ -0,0 +1,79 @@ +"""Rule for generating platform-specific RECORD files.""" + +load("//python/private:attributes.bzl", "WINDOWS_CONSTRAINTS_PLAIN_ATTRS") +load("//python/private:common.bzl", "is_windows_platform") + +def _gen_wheel_record_impl(ctx): + is_windows = is_windows_platform(ctx) + rewriter_file = ctx.files._wheel_record_rewriter[0] + out_files = [] + + for in_file in ctx.files.srcs: + dist_info_name = in_file.dirname.rpartition("/")[2] + if dist_info_name: + if dist_info_name.endswith(".dist-info"): + data_dir_basename = ( + dist_info_name[:-len(".dist-info")] + ".data" + ) + else: + data_dir_basename = dist_info_name + ".data" + out_file = ctx.actions.declare_file( + "site-packages/{}/RECORD".format(dist_info_name), + ) + else: + data_dir_basename = "data" + out_file = ctx.actions.declare_file("site-packages/RECORD") + + out_files.append(out_file) + + action_args = ctx.actions.args() + inputs = depset([in_file, rewriter_file]) + + if rewriter_file.path.endswith(".ps1"): + action_exe = "powershell.exe" + action_args.add_all([ + "-ExecutionPolicy", + "Bypass", + "-NoProfile", + "-File", + rewriter_file, + ]) + else: + action_exe = ( + ctx.attr._wheel_record_rewriter[DefaultInfo].files_to_run + ) + + action_args.add(in_file) + action_args.add(out_file) + action_args.add("windows" if is_windows else "unix") + action_args.add(data_dir_basename) + + ctx.actions.run( + inputs = inputs, + outputs = [out_file], + executable = action_exe, + arguments = [action_args], + mnemonic = "PyRewriteWheelRecord", + progress_message = "Rewriting wheel RECORD %{output}", + toolchain = None, + ) + + return [ + DefaultInfo(files = depset(out_files)), + ] + +gen_wheel_record = rule( + implementation = _gen_wheel_record_impl, + attrs = WINDOWS_CONSTRAINTS_PLAIN_ATTRS | { + "srcs": attr.label_list( + doc = "The original RECORD files to rewrite.", + mandatory = True, + allow_files = True, + ), + "_wheel_record_rewriter": attr.label( + default = "//python/private/pypi:wheel_record_rewriter", + allow_files = True, + cfg = "exec", + ), + }, +) diff --git a/python/private/pypi/generate_whl_library_build_bazel.bzl b/python/private/pypi/generate_whl_library_build_bazel.bzl index a9a29081f7..ceee319cd7 100644 --- a/python/private/pypi/generate_whl_library_build_bazel.bzl +++ b/python/private/pypi/generate_whl_library_build_bazel.bzl @@ -21,16 +21,14 @@ _RENDER = { "copy_files": render.dict, "data": render.list, "data_exclude": render.list, - "dependencies": render.list, - "dependencies_by_platform": lambda x: render.dict(x, value_repr = render.list), "entry_points": render.dict_dict, "extras": render.list, "group_deps": render.list, "include": str, + "repo": lambda maybe_label: repr(str(maybe_label)), "requires_dist": render.list, "srcs_exclude": render.list, "tags": render.list, - "target_platforms": render.list, } # NOTE @aignas 2024-10-25: We have to keep this so that files in @@ -41,29 +39,35 @@ _TEMPLATE = """\ package(default_visibility = ["//visibility:public"]) +{fn}( +{kwargs} +) +""" + +_PURL = """\ package_metadata( name = "package_metadata", purl = {purl}, visibility = ["//:__subpackages__"], ) - -{fn}( -{kwargs} -) """ def generate_whl_library_build_bazel( *, + metadata_version, annotation = None, - default_python_version = None, + config_load, purl = None, + requires_dist = [], **kwargs): """Generate a BUILD file for an unzipped Wheel Args: + metadata_version: The version to use for tag generation. annotation: The annotation for the build file. - default_python_version: The python version to use to parse the METADATA. + config_load: {type}`str` The location from where to load the config. purl: The purl. + requires_dist: {type}`list[str]` The list of dependencies from the METADATA file. **kwargs: Extra args serialized to be passed to the {obj}`whl_library_targets`. @@ -75,36 +79,26 @@ def generate_whl_library_build_bazel( """load("@package_metadata//rules:package_metadata.bzl", "package_metadata")""", ] - if kwargs.get("tags"): + if kwargs.get("repo"): + fn = "whl_library_deps_targets" + else: fn = "whl_library_targets" - # legacy path - unsupported_args = [ - "requires", - "metadata_name", - "metadata_version", - "packages", - "include", - ] + tags = [ + "pypi_name={}".format(kwargs.get("metadata_name")), + "pypi_version={}".format(metadata_version), + ] + kwargs["tags"] = tags + + if not requires_dist: + # no deps, we can leave the extra loads out + pass + elif config_load: + loads.append("""load("{}", "{}")""".format(config_load, "packages")) + kwargs["include"] = "packages" + kwargs["requires_dist"] = requires_dist else: - fn = "whl_library_targets_from_requires" - unsupported_args = [ - "dependencies", - "dependencies_by_platform", - "target_platforms", - "default_python_version", - ] - packages_load = kwargs.pop("config_load") - if not kwargs.get("requires_dist"): - # no deps, we can leave the extra loads out - pass - else: - loads.append("""load("{}", "{}")""".format(packages_load, "packages")) - kwargs["include"] = "packages" - - for arg in unsupported_args: - if kwargs.get(arg): - fail("BUG, unsupported arg: '{}'".format(arg)) + kwargs["requires_dist"] = requires_dist loads.extend([ """load("@rules_python//python/private/pypi:whl_library_targets.bzl", "{}")""".format(fn), @@ -119,8 +113,6 @@ def generate_whl_library_build_bazel( kwargs["srcs_exclude"] = annotation.srcs_exclude_glob if annotation.additive_build_content: additional_content.append(annotation.additive_build_content) - if default_python_version: - kwargs["default_python_version"] = default_python_version contents = "\n".join( [ @@ -131,9 +123,8 @@ def generate_whl_library_build_bazel( "{} = {},".format(k, _RENDER.get(k, repr)(v)) for k, v in sorted(kwargs.items()) ])), - purl = repr(purl), ), - ] + additional_content, + ] + ([_PURL.format(purl = repr(purl))] if purl else []) + additional_content, ) # NOTE: Ensure that we terminate with a new line diff --git a/python/private/pypi/hash.bzl b/python/private/pypi/hash.bzl new file mode 100644 index 0000000000..bcdfaecf00 --- /dev/null +++ b/python/private/pypi/hash.bzl @@ -0,0 +1,189 @@ +# Copyright 2026 The Bazel Authors. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Helpers for working with distribution hash digests. + +Both the requirement `--hash=:` option (PEP 508 tooling) and the +Simple API `#=` URL fragments (PEP 503) or `hashes` dicts +(PEP 691) may use any algorithm from `hashlib.algorithms_guaranteed`, even +though `sha256` is the most common one. + +Internally a single digest is always represented as the canonical +`:` string (the same shape as the pip `--hash` values and +the uv.lock `hash` values) and only converted to the Subresource Integrity +format at the `ctx.download(integrity = ...)` boundary. +""" + +# The names from `hashlib.algorithms_guaranteed`, which PEP 691 uses as the +# set of valid hash names and PEP 503 strongly recommends fragments to come +# from. +_ALGOS = [ + "blake2b", + "blake2s", + "md5", + "sha1", + "sha224", + "sha256", + "sha384", + "sha3_224", + "sha3_256", + "sha3_384", + "sha3_512", + "sha512", + "shake_128", + "shake_256", +] + +# The algorithms supported by the Subresource Integrity format understood by +# `ctx.download(integrity = ...)`, strongest first. +_SRI_ALGOS = ["sha512", "sha384", "sha256"] + +# The order used to pick the digest identifying an artifact when several are +# available: `sha256` first so that repo names stay stable for the common +# case, then the remaining algorithms that the bazel downloader can verify. +_PREFERRED_ALGOS = ["sha256", "sha512", "sha384"] + +_HEX = "0123456789abcdef" +_B64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/" + +def _hex_to_b64(hex_digest): + if len(hex_digest) % 2: + return None + + data = [] + for i in range(0, len(hex_digest), 2): + hi = _HEX.find(hex_digest[i]) + lo = _HEX.find(hex_digest[i + 1]) + if hi < 0 or lo < 0: + return None + data.append(hi * 16 + lo) + + out = [] + for i in range(0, len(data) // 3 * 3, 3): + n = (data[i] << 16) | (data[i + 1] << 8) | data[i + 2] + out.append(_B64[n >> 18]) + out.append(_B64[(n >> 12) & 63]) + out.append(_B64[(n >> 6) & 63]) + out.append(_B64[n & 63]) + + rem = len(data) % 3 + if rem == 1: + n = data[-1] << 16 + out.append(_B64[n >> 18]) + out.append(_B64[(n >> 12) & 63]) + out.append("==") + elif rem == 2: + n = (data[-2] << 16) | (data[-1] << 8) + out.append(_B64[n >> 18]) + out.append(_B64[(n >> 12) & 63]) + out.append(_B64[(n >> 6) & 63]) + out.append("=") + + return "".join(out) + +def _digest(algo, hex_digest): + """Construct a canonical `:` digest string. + + Args: + algo: {type}`str` the hash algorithm name, e.g. `sha256`. + hex_digest: {type}`str` the hex encoded digest. + + Returns: + {type}`str` the canonical digest string or an empty string if the + algorithm is not a known `hashlib` algorithm or the digest is empty. + """ + algo = algo.lower() + if not hex_digest or algo not in _ALGOS: + return "" + + return "{}:{}".format(algo, hex_digest) + +def _hex_to_sri(algo, hex_digest): + """Convert a hex digest to a Subresource Integrity value. + + Args: + algo: {type}`str` the hash algorithm name, e.g. `sha512`. + hex_digest: {type}`str` the hex encoded digest. + + Returns: + {type}`str` the SRI value (e.g. `sha512-...`) that can be passed to + `ctx.download(integrity = ...)` or an empty string if the algorithm + cannot be expressed as SRI or the digest is not valid hex. + """ + if algo not in _SRI_ALGOS or not hex_digest: + return "" + + b64 = _hex_to_b64(hex_digest.lower()) + if b64 == None: + return "" + + return "{}-{}".format(algo, b64) + +def _integrity(digest): + """Get the SRI value for a canonical `:` string. + + Args: + digest: {type}`str` the canonical digest string. + + Returns: + {type}`str` the SRI value (e.g. `sha256-...`) that can be passed to + `ctx.download(integrity = ...)` or an empty string if the algorithm + cannot be expressed as SRI. Fails if the algorithm is SRI supported + but the digest is not valid hex, because silently returning an empty + string would disable the download verification for an artifact that + should be verifiable. + """ + algo, _, hex_digest = digest.partition(":") + if algo not in _SRI_ALGOS or not hex_digest: + return "" + + sri = _hex_to_sri(algo, hex_digest) + if not sri: + fail("Invalid {} digest: {}".format(algo, hex_digest)) + + return sri + +def _preferred_digest(digests): + """Pick the digest that identifies an artifact, e.g. for repo naming. + + Args: + digests: {type}`list[str]` canonical `:` strings. + + Returns: + {type}`str` the canonical digest string of the most preferred + algorithm present or an empty string if there are no digests. + """ + by_algo = {} + for digest in digests: + if not digest: + continue + algo, _, _ = digest.partition(":") + by_algo.setdefault(algo, digest) + + if not by_algo: + return "" + + for algo in _PREFERRED_ALGOS: + if algo in by_algo: + return by_algo[algo] + + return by_algo[sorted(by_algo.keys())[0]] + +hash = struct( + ALGOS = _ALGOS, + digest = _digest, + hex_to_sri = _hex_to_sri, + integrity = _integrity, + preferred_digest = _preferred_digest, +) diff --git a/python/private/pypi/hub_builder.bzl b/python/private/pypi/hub_builder.bzl index d3be266bdf..596cfb2cbe 100644 --- a/python/private/pypi/hub_builder.bzl +++ b/python/private/pypi/hub_builder.bzl @@ -1,5 +1,6 @@ """A hub repository builder for incrementally building the hub configuration.""" +load("//python/private:envsubst.bzl", "envsubst") load("//python/private:full_version.bzl", "full_version") load("//python/private:normalize_name.bzl", "normalize_name") load("//python/private:repo_utils.bzl", "repo_utils") @@ -7,6 +8,7 @@ load("//python/private:text_util.bzl", "render") load("//python/private:version.bzl", "version") load("//python/private:version_label.bzl", "version_label") load(":attrs.bzl", "use_isolated") +load(":hash.bzl", "hash") load(":parse_requirements.bzl", "parse_requirements") load(":pep508_env.bzl", "env") load(":pep508_evaluate.bzl", "evaluate") @@ -143,8 +145,8 @@ def _build(self): whl_libraries = self._whl_libraries, ) -def _pip_parse(self, module_ctx, pip_attr): - python_version = pip_attr.python_version +def _pip_parse(self, module_ctx, pip_attr, python_version = None): + python_version = python_version or pip_attr.python_version if python_version in self._platforms: fail(( "Duplicate pip python version '{version}' for hub " + @@ -177,7 +179,7 @@ def _pip_parse(self, module_ctx, pip_attr): )) return - _set_get_index_urls(self, pip_attr) + _set_get_index_urls(self, module_ctx, pip_attr) self._platforms[python_version] = _platforms( module_ctx, python_version = full_python_version, @@ -190,7 +192,8 @@ def _pip_parse(self, module_ctx, pip_attr): self, module_ctx, pip_attr = pip_attr, - enable_pipstar_extract = bool(self._config.enable_pipstar_extract or self._get_index_urls.get(pip_attr.python_version)), + python_version = python_version, + enable_pipstar_extract = bool(self._config.enable_pipstar_extract or self._get_index_urls.get(python_version)), ) ### end of PUBLIC methods @@ -345,8 +348,17 @@ def _add_whl_library(self, *, python_version, whl, repo): ### end of setters, below we have various functions to implement the public methods -def _set_get_index_urls(self, pip_attr): - default_index_url = pip_attr.experimental_index_url or self._config.index_url +def _set_get_index_urls(self, mctx, pip_attr): + # Resolve the index URL through envsubst so the ``$VAR`` / ``${VAR:-default}`` + # form is honored when deciding whether the experimental index-url mode is + # active. Without this, an unsubstituted template like ``$RULES_PYTHON_PIP_INDEX_URL`` + # is treated as truthy and the mode is forced on, even when the env var + # would expand to the empty string. + default_index_url = envsubst( + pip_attr.experimental_index_url, + pip_attr.envsubst, + mctx.getenv, + ) or self._config.index_url default_extra_index_urls = pip_attr.experimental_extra_index_urls or [] if not default_index_url: @@ -383,11 +395,11 @@ def _set_get_index_urls(self, pip_attr): ) return True -def _detect_interpreter(self, pip_attr): +def _detect_interpreter(self, pip_attr, python_version): python_interpreter_target = pip_attr.python_interpreter_target if python_interpreter_target == None and not pip_attr.python_interpreter: python_name = "python_{}_host".format( - pip_attr.python_version.replace(".", "_"), + python_version.replace(".", "_"), ) if python_name not in self._available_interpreters: fail(( @@ -397,7 +409,7 @@ def _detect_interpreter(self, pip_attr): "Expected to find {python_name} among registered versions:\n {labels}" ).format( hub_name = self.name, - version = pip_attr.python_version, + version = python_version, python_name = python_name, labels = " \n".join(self._available_interpreters), )) @@ -466,6 +478,7 @@ def _create_whl_repos( module_ctx, *, pip_attr, + python_version, enable_pipstar_extract = False): """create all of the whl repositories @@ -473,10 +486,11 @@ def _create_whl_repos( self: the builder. module_ctx: {type}`module_ctx`. pip_attr: {type}`struct` - the struct that comes from the tag class iteration. + python_version: {type}`str` - the resolved python version for this pip.parse call. enable_pipstar_extract: {type}`bool` - enable the pipstar extraction or not. """ logger = self._logger - platforms = self._platforms[pip_attr.python_version] + platforms = self._platforms[python_version] requirements_by_platform = parse_requirements( module_ctx, requirements_by_platform = requirements_files_by_platform( @@ -485,17 +499,20 @@ def _create_whl_repos( requirements_lock = pip_attr.requirements_lock, requirements_osx = pip_attr.requirements_darwin, requirements_windows = pip_attr.requirements_windows, + uv_lock = pip_attr.uv_lock, extra_pip_args = pip_attr.extra_pip_args, platforms = sorted(platforms), # here we only need keys python_version = full_version( - version = pip_attr.python_version, + version = python_version, minor_mapping = self._minor_mapping, ), logger = logger, ), + uv_lock = pip_attr.uv_lock, platforms = platforms, extra_pip_args = pip_attr.extra_pip_args, get_index_urls = self._get_index_urls.get(pip_attr.python_version), + toml_decode = getattr(self._config, "toml_decode", None), logger = logger, ) @@ -516,7 +533,7 @@ def _create_whl_repos( pip_attr = pip_attr, ) - interpreter = _detect_interpreter(self, pip_attr) + interpreter = _detect_interpreter(self, pip_attr, python_version) for whl in requirements_by_platform: whl_library_args = common_args | _whl_library_args( @@ -531,16 +548,16 @@ def _create_whl_repos( whl_library_args = whl_library_args, download_only = pip_attr.download_only, netrc = self._config.netrc or pip_attr.netrc, - use_downloader = src.url and _use_downloader(self, pip_attr.python_version, whl.name), + use_downloader = src.url and _use_downloader(self, python_version, whl.name), auth_patterns = self._config.auth_patterns or pip_attr.auth_patterns, - python_version = _major_minor_version(pip_attr.python_version), + python_version = _major_minor_version(python_version), is_multiple_versions = whl.is_multiple_versions, interpreter = interpreter, enable_pipstar_extract = enable_pipstar_extract, ) _add_whl_library( self, - python_version = pip_attr.python_version, + python_version = python_version, whl = whl, repo = repo, ) @@ -623,7 +640,7 @@ def _whl_repo( # need to pass the extra args there, so only pop this for whls args["extra_pip_args"] = src.extra_pip_args - if "whl_patches" in args or not (enable_pipstar_extract and is_whl): + if not (enable_pipstar_extract and is_whl): if interpreter.path: args["python_interpreter"] = interpreter.path if interpreter.target: @@ -660,7 +677,12 @@ def _whl_repo( args["index_url"] = index_url args["urls"] = [src.url] - args["sha256"] = src.sha256 + + # The digest is always passed in the SRI format that the bazel downloader + # supports. It may be empty if the digest uses a hash algorithm that + # cannot be expressed as SRI (e.g. `md5`), in which case `whl_library` + # will download without verification and warn. + args["integrity"] = hash.integrity(src.digest) args["filename"] = src.filename # TODO @aignas 2025-11-02: once we have pipstar enabled we can add extra @@ -669,7 +691,7 @@ def _whl_repo( target_platforms = src.target_platforms if is_multiple_versions else [] return struct( - repo_name = whl_repo_name(src.filename, src.sha256, *target_platforms), + repo_name = whl_repo_name(src.filename, src.digest, *target_platforms), args = args, config_setting = whl_config_setting( version = python_version, diff --git a/python/private/pypi/index_sources.bzl b/python/private/pypi/index_sources.bzl index 1998e4fb33..4353f5051a 100644 --- a/python/private/pypi/index_sources.bzl +++ b/python/private/pypi/index_sources.bzl @@ -16,6 +16,8 @@ A file that houses private functions used in the `bzlmod` extension with the same name. """ +load(":hash.bzl", "hash") + # Just list them here and me super conservative _KNOWN_EXTS = [ # Note, the following source in pip has more extensions @@ -43,8 +45,10 @@ def index_sources(line): line(str): The requirements.txt entry. Returns: - A struct with shas attribute containing: - * `shas` - list[str]; shas to download from pypi_index. + A struct with hashes attribute containing: + * `hashes` - list[str]; `:` hashes of the artifacts + to download from pypi_index. Note that any hash algorithm from + {obj}`hash.ALGOS` is accepted, not only `sha256`. * `version` - str; version of the package. * `marker` - str; the marker expression, as per PEP508 spec. * `requirement` - str; a requirement line without the marker. This can @@ -58,10 +62,12 @@ def index_sources(line): marker, _, _ = maybe_hashes.partition("--hash=") maybe_hashes = maybe_hashes or line - shas = [ - sha.strip() - for sha in maybe_hashes.split("--hash=sha256:")[1:] - ] + hashes = [] + for h in maybe_hashes.split("--hash=")[1:]: + algo, _, hex_digest = h.strip().partition(" ")[0].partition(":") + digest = hash.digest(algo, hex_digest) + if digest: + hashes.append(digest) marker = marker.strip() if head == line: @@ -71,7 +77,7 @@ def index_sources(line): requirement_line = "{} {}".format( requirement, - " ".join(["--hash=sha256:{}".format(sha) for sha in shas]), + " ".join(["--hash={}".format(h) for h in hashes]), ).strip() url = "" @@ -80,9 +86,14 @@ def index_sources(line): maybe_requirement, _, url_and_rest = requirement.partition("@") url = url_and_rest.strip().partition(" ")[0].strip() - url, _, sha256 = url.partition("#sha256=") - if sha256: - shas.append(sha256) + url, _, fragment = url.partition("#") + algo, _, hex_digest = fragment.partition("=") + digest = hash.digest(algo, hex_digest) + if digest: + hashes.append(digest) + elif fragment: + # Not a hash fragment (e.g. `#egg=`), keep it as part of the URL. + url = "{}#{}".format(url, fragment) _, _, filename = url.rpartition("/") # Replace URL encoded characters and luckily there is only one case @@ -104,7 +115,7 @@ def index_sources(line): requirement = requirement, requirement_line = requirement_line, version = version, - shas = sorted(shas), + hashes = sorted(hashes), marker = marker, url = url, filename = filename, diff --git a/python/private/pypi/labels.bzl b/python/private/pypi/labels.bzl index 8f91a03b4c..c77167b593 100644 --- a/python/private/pypi/labels.bzl +++ b/python/private/pypi/labels.bzl @@ -17,8 +17,10 @@ EXTRACTED_WHEEL_FILES = "extracted_whl_files" WHEEL_FILE_PUBLIC_LABEL = "whl" WHEEL_FILE_IMPL_LABEL = "_whl" +WHEEL_FILE = "whl_file" PY_LIBRARY_PUBLIC_LABEL = "pkg" PY_LIBRARY_IMPL_LABEL = "_pkg" +PY_SRCS_LABEL = "srcs" DATA_LABEL = "data" DIST_INFO_LABEL = "dist_info" NODEPS_LABEL = "no_deps" diff --git a/python/private/pypi/missing_package.bzl b/python/private/pypi/missing_package.bzl new file mode 100644 index 0000000000..46823e813b --- /dev/null +++ b/python/private/pypi/missing_package.bzl @@ -0,0 +1,43 @@ +"""Rule for generating an execution-phase action failure when a PyPI package is missing.""" + +load("//python/private:py_info.bzl", "PyInfo") +load("//python/private:reexports.bzl", "BuiltinPyInfo") + +def _missing_package_error_impl(ctx): + out = ctx.actions.declare_file(ctx.label.name + ".error") + + if ctx.attr.hub_name: + hub_clause = ' when building under PyPI hub "{hub}". Try adding it to the requirements of this hub (e.g. requirements_lock or requirements_by_platform in pip.parse)'.format( + hub = ctx.attr.hub_name, + ) + else: + hub_clause = ' because no default PyPI hub was configured. Try designating a default hub via pip.default(default_hub = "...") or select a hub using --@rules_python//python/config_settings:venv' + + # Register an action that fails when Bazel attempts to stage/build this file + ctx.actions.run_shell( + outputs = [out], + command = "echo 'ERROR: PyPI package \"{pkg}\" is not available{hub_clause}.' >&2 && exit 1".format( + pkg = ctx.attr.package_name, + hub_clause = hub_clause, + ), + ) + + maybe_builtin = [BuiltinPyInfo(transitive_sources = depset([out]))] if BuiltinPyInfo != None else [] + + return [ + DefaultInfo( + files = depset([out]), + data_runfiles = ctx.runfiles([out]), + ), + PyInfo( + transitive_sources = depset([out]), + ), + ] + maybe_builtin + +missing_package_error = rule( + implementation = _missing_package_error_impl, + attrs = { + "hub_name": attr.string(mandatory = True), + "package_name": attr.string(mandatory = True), + }, +) diff --git a/python/private/pypi/parse_requirements.bzl b/python/private/pypi/parse_requirements.bzl index c976e74bca..4874c7ac79 100644 --- a/python/private/pypi/parse_requirements.bzl +++ b/python/private/pypi/parse_requirements.bzl @@ -28,7 +28,9 @@ behavior. load("//python/private:normalize_name.bzl", "normalize_name") load("//python/private:repo_utils.bzl", "repo_utils") +load("//python/uv/private:uv_lock_to_requirements.bzl", "uv_lock_extras_map") # buildifier: disable=bzl-visibility load(":argparse.bzl", "argparse") +load(":hash.bzl", "hash") load(":index_sources.bzl", "index_sources") load(":parse_requirements_txt.bzl", "parse_requirements_txt") load(":pep508_evaluate.bzl", "evaluate") @@ -40,15 +42,18 @@ def parse_requirements( *, requirements_by_platform = {}, extra_pip_args = [], - platforms = {}, + platforms, get_index_urls = None, extract_url_srcs = True, + uv_lock = None, + toml_decode = None, logger): """Get the requirements with platforms that the requirements apply to. Args: ctx: A context that has .read function that would read contents from a label. - platforms: The target platform descriptions. + platforms: The target platform descriptions. Cannot be empty and needs to have + at least the host platform and the definitions. requirements_by_platform (label_keyed_string_dict): a way to have different package versions (or different packages) for different os, arch combinations. @@ -59,28 +64,270 @@ def parse_requirements( distribution names to query. extract_url_srcs: A boolean to enable extracting URLs from requirement lines to enable using bazel downloader. + uv_lock: {type}`str | None` an optional label/file path to the uv.lock + file. The ctx.read function will be used to read the contents. + If provided, the function will use the uv.lock file as the primary + source for package metadata and perform a consistency check against + requirements files if both are provided. + toml_decode: {type}`callable | None` A function to decode TOML + content (e.g. `toml.decode`). Required when `uv_lock` is provided. logger: repo_utils.logger, a simple struct to log diagnostic messages. Returns: - {type}`dict[str, list[struct]]` where the key is the distribution name and the struct - contains the following attributes: - * `distribution`: {type}`str` The non-normalized distribution name. - * `srcs`: {type}`struct` The parsed requirement line for easier Simple - API downloading (see `index_sources` return value). - * `target_platforms`: {type}`list[str]` Target platforms that this package is for. - The format is `cp3{minor}_{os}_{arch}`. + {type}`list[struct]` where each struct contains the following attributes: + * `name`: {type}`str` The normalized distribution name. * `is_exposed`: {type}`bool` `True` if the package should be exposed via the hub repository. - * `extra_pip_args`: {type}`list[str]` pip args to use in case we are - not using the bazel downloader to download the archives. This should - be passed to {obj}`whl_library`. - * `whls`: {type}`list[struct]` The list of whl entries that can be - downloaded using the bazel downloader. - * `sdist`: {type}`list[struct]` The sdist that can be downloaded using - the bazel downloader. - - The second element is extra_pip_args should be passed to `whl_library`. + * `is_multiple_versions`: {type}`bool` `True` if multiple versions have been + specified for this package. + * `index_url`: {type}`str` The index URL used to download the package. + * `srcs`: {type}`list[struct]` A list of per-distribution source entries, each + containing: `distribution`, `extra_pip_args`, `requirement_line`, + `target_platforms`, `filename`, `digest`, `url`, `yanked`. The `digest` + is a `:` string or empty if the artifact digest is unknown. + """ + if uv_lock and toml_decode: + uv_lock = toml_decode(ctx.read(uv_lock)) + return _parse_requirements_with_uv_lock( + ctx = ctx, + requirements_by_platform = requirements_by_platform, + extra_pip_args = extra_pip_args, + platforms = platforms, + get_index_urls = get_index_urls, + extract_url_srcs = extract_url_srcs, + uv_lock = uv_lock, + logger = logger, + ) + + return _parse_requirements_from_req_files( + ctx = ctx, + requirements_by_platform = requirements_by_platform, + extra_pip_args = extra_pip_args, + platforms = platforms, + get_index_urls = get_index_urls, + extract_url_srcs = extract_url_srcs, + logger = logger, + ) + +def _get_all_platforms(requirements_by_platform): + """Get the set of all platform names from requirements_by_platform.""" + all_platforms = {} + for plats in requirements_by_platform.values(): + for p in plats: + all_platforms[p] = None + return sorted(all_platforms) + +def _parse_requirements_with_uv_lock( + ctx, + *, + requirements_by_platform, + extra_pip_args, + platforms, + get_index_urls, + extract_url_srcs, + uv_lock, + logger): + """Parse requirements using uv.lock as the primary source.""" + if uv_lock: + return _parse_uv_lock_json( + uv_lock = uv_lock, + all_platforms = _get_all_platforms(requirements_by_platform) if requirements_by_platform else sorted(platforms.keys()), + platforms = platforms, + extra_pip_args = extra_pip_args, + logger = logger, + ) + + return _parse_requirements_from_req_files( + ctx = ctx, + requirements_by_platform = requirements_by_platform, + extra_pip_args = extra_pip_args, + platforms = platforms, + get_index_urls = get_index_urls, + extract_url_srcs = extract_url_srcs, + logger = logger, + ) + +def _parse_uv_lock_json(uv_lock, all_platforms, logger, extra_pip_args = None, platforms = {}): + """Parse uv.lock JSON and build the same return structs as parse_requirements. + + Args: + uv_lock: {type}`dict` The decoded uv.lock contents. + all_platforms: {type}`list[str]` The list of all platform names. + logger: {type}`struct` A logger for diagnostic messages. + extra_pip_args: {type}`list[str] | None` Extra pip arguments to pass through. + platforms: {type}`dict[str, struct]` A dict of platform name to platform info + (containing `.env` with PEP 508 marker environment). + + Returns: + {type}`list[struct]` The same format as {func}`parse_requirements`. + """ + extras_map = uv_lock_extras_map(uv_lock) + + uv_packages = {} + + if not platforms: + fail("BUG: platforms must be configured") + + for pkg in uv_lock["package"]: + name = pkg["name"] + version = pkg["version"] + norm_name = normalize_name(name) + entry = uv_packages.setdefault(norm_name, { + "distribution": name, + "resolved_srcs": [], + "versions": {}, + }) + entry["versions"][version] = None + source = pkg.get("source") or {} + registry = (source.get("registry") or "").rstrip("/") + if registry.startswith("http://") or registry.startswith("https://"): + index_url = "{}/{}".format(registry, norm_name.replace("_", "-")) + else: + index_url = "" + entry["index_url"] = index_url + + pkg_extras = sorted(extras_map.get(name, [])) + extra_str = "[{}]".format(",".join(pkg_extras)) if pkg_extras else "" + + markers = pkg.get("resolution-markers", []) + if markers: + marker_expr = " or ".join(markers) + pkg_platforms = [ + p + for p in all_platforms + if evaluate(marker_expr, env = platforms[p].env) + ] + else: + pkg_platforms = list(all_platforms) + + # Prepare candidates + candidates = [] + for wheel in pkg.get("wheels", []): + url = wheel["url"] + _, _, filename = url.rpartition("/") + candidates.append(struct( + filename = filename, + url = url, + digest = _parse_uv_lock_hash(wheel.get("hash", "")), + kind = "wheel", + )) + + sdist_struct = None + sdist = pkg.get("sdist", None) + if sdist: + url = sdist["url"] + _, _, filename = url.rpartition("/") + sdist_struct = struct( + filename = filename, + url = url, + digest = _parse_uv_lock_hash(sdist.get("hash", "")), + kind = "sdist", + ) + + git_struct = None + if pkg.get("source", {}).get("git"): + url = pkg["source"]["git"] + _, _, filename = url.rpartition("/") + git_struct = struct( + filename = filename, + url = url, + digest = "", + kind = "git", + source = pkg["source"], + ) + + plat_to_src = {} + for p in pkg_platforms: + platform = platforms.get(p) + if not platform: + continue + + best_wheel = None + if candidates: + best_wheel = select_whl( + whls = candidates, + python_version = platform.env.get("python_full_version", "3"), + whl_platform_tags = platform.whl_platform_tags, + whl_abi_tags = platform.whl_abi_tags, + implementation_name = platform.env.get("implementation_name", "cpython"), + limit = 1, + logger = logger, + ) + + if best_wheel: + plat_to_src[p] = best_wheel + elif sdist_struct: + plat_to_src[p] = sdist_struct + elif git_struct: + plat_to_src[p] = git_struct + + # Group platforms by resolved source + src_to_plats = {} + for p, src in plat_to_src.items(): + key = src.filename + src.digest + src_to_plats.setdefault(key, struct(src = src, plats = [])).plats.append(p) + + # Build resolved_srcs + for key, val in src_to_plats.items(): + src = val.src + plats = sorted(val.plats) + requirement_line = "{name}{extras}=={version}".format( + name = name, + extras = extra_str, + version = version, + ) + entry["resolved_srcs"].append(struct( + distribution = name, + extra_pip_args = extra_pip_args or [], + requirement_line = requirement_line, + target_platforms = plats, + filename = src.filename, + digest = src.digest, + url = src.url, + yanked = None, + )) + + ret = [] + for norm_name, info in sorted(uv_packages.items()): + versions = sorted(info["versions"].keys()) + item = struct( + name = norm_name, + # Only expose packages that resolved to at least one source. uv + # workspace/root members (e.g. `source = { virtual = "." }` or + # editable installs) resolve to no wheel/sdist, so exposing them + # would add a dangling entry to the hub's `all_requirements` / + # `all_whl_requirements` and create an alias to a subpackage that + # doesn't exist. This mirrors the requirements path, which also + # gates `is_exposed`. + is_exposed = bool(info["resolved_srcs"]), + is_multiple_versions = len(versions) > 1, + index_url = info["index_url"], + srcs = info["resolved_srcs"], + ) + ret.append(item) + + logger.debug(lambda: "Parsed {} packages from uv.lock".format(len(ret))) + return ret + +def _parse_uv_lock_hash(hash_str): + """Parse a uv.lock `hash` value of the form `:`. + + The algorithm is not necessarily `sha256` because uv records the strongest + digest that the index offers. """ + algo, _, hex_digest = hash_str.partition(":") + return hash.digest(algo, hex_digest) + +def _parse_requirements_from_req_files( + ctx, + *, + requirements_by_platform, + extra_pip_args, + platforms, + get_index_urls, + extract_url_srcs, + logger): + """Parse requirements from requirements.txt files (existing behavior).""" options = {} requirements = {} all_files_parsed = {} @@ -90,14 +337,8 @@ def parse_requirements( logger.trace(lambda: "Using {} for {}".format(file, plats)) contents = ctx.read(file) - # Parse the requirements file directly in starlark to get the information - # needed for the whl_library declarations later. parse_result = parse_requirements_txt(contents) - # Save parsed results from ALL files, even those with no matching - # platforms. This ensures the distributions dict (used for index URL - # queries) includes packages from all platform files, making the - # lockfile facts platform-independent. if file not in all_files_parsed: all_files_parsed[file] = parse_result.requirements @@ -133,23 +374,18 @@ def parse_requirements( options[plat] = pip_args + index_url = argparse.index_url(pip_args, index_url) + extra_index_urls = argparse.extra_index_url(pip_args, []) + platform = argparse.platform(pip_args, []) + if platform: + get_index_urls = None + + reqs_by_name = {} requirements_by_platform = {} for plat, parse_results in requirements.items(): - # Replicate a surprising behavior that WORKSPACE builds allowed: - # Defining a repo with the same name multiple times, but only the last - # definition is respected. - # The requirement lines might have duplicate names because lines for extras - # are returned as just the base package name. e.g., `foo[bar]` results - # in an entry like `("foo", "foo[bar] == 1.0 ...")`. requirements_dict = {} for entry in sorted( parse_results, - # Get the longest match and fallback to original WORKSPACE sorting, - # which should get us the entry with most extras. - # - # FIXME @aignas 2024-05-13: The correct behaviour might be to get an - # entry with all aggregated extras, but it is unclear if we - # should do this now. key = lambda x: (len(x[1].partition("==")[0]), x), ): req_line = entry[1] @@ -157,32 +393,28 @@ def parse_requirements( requirements_dict[req.name] = entry - extra_pip_args = options[plat] + extra_pip_args_for_plat = options[plat] for distribution, requirement_line in requirements_dict.values(): - for_whl = requirements_by_platform.setdefault( + for_whl = reqs_by_name.setdefault( normalize_name(distribution), {}, ) for_req = for_whl.setdefault( - (requirement_line, ",".join(extra_pip_args)), + (requirement_line, ",".join(extra_pip_args_for_plat)), struct( distribution = distribution, srcs = index_sources(requirement_line), requirement_line = requirement_line, target_platforms = [], - extra_pip_args = extra_pip_args, + extra_pip_args = extra_pip_args_for_plat, ), ) for_req.target_platforms.append(plat) index_urls = {} if get_index_urls: - # Collect all distributions from all requirements files irrespective - # of python_version and platform markers. This ensures that the index - # is queried for all packages, not just those matching the current - # platform's markers. distributions = {} for entries in all_files_parsed.values(): for entry in entries: @@ -203,7 +435,7 @@ def parse_requirements( ) ret = [] - for name, reqs in sorted(requirements_by_platform.items()): + for name, reqs in sorted(reqs_by_name.items()): requirement_target_platforms = {} for r in reqs.values(): for p in r.target_platforms: @@ -219,22 +451,7 @@ def parse_requirements( logger = logger, ) - # FIXME @aignas 2025-11-24: we can get the list of target platforms here - # - # However it is likely that we may stop exposing packages like torch in here - # which do not have wheels for all osx platforms. - # - # If users specify the target platforms accurately, then it is a different - # (better) story, but we may not be able to guarantee this - # - # target_platforms = [ - # p - # for dist in package_srcs - # for p in dist.target_platforms - # ] - item = struct( - # Return normalized names name = normalize_name(name), is_exposed = len(requirement_target_platforms) == len(requirements), is_multiple_versions = len(reqs.values()) > 1, @@ -287,7 +504,7 @@ def _package_srcs( dist = struct( url = "", filename = "", - sha256 = "", + digest = "", yanked = None, ) req_line = r.srcs.requirement_line @@ -307,7 +524,7 @@ def _package_srcs( requirement_line = req_line, target_platforms = [], filename = dist.filename, - sha256 = dist.sha256, + digest = dist.digest, url = dist.url, yanked = dist.yanked, ), @@ -399,7 +616,7 @@ def _add_dists(*, requirement, index_urls, target_platform, logger = None): dist = struct( url = requirement.srcs.url, filename = requirement.srcs.filename, - sha256 = requirement.srcs.shas[0] if requirement.srcs.shas else "", + digest = hash.preferred_digest(requirement.srcs.hashes), yanked = None, ) @@ -411,29 +628,29 @@ def _add_dists(*, requirement, index_urls, target_platform, logger = None): whls = [] sdist = None - # First try to find distributions by SHA256 if provided - shas_to_use = requirement.srcs.shas - if not shas_to_use: + # First try to find distributions by the hashes if provided + hashes_to_use = requirement.srcs.hashes + if not hashes_to_use: version = requirement.srcs.version - shas_to_use = index_urls.sha256s_by_version.get(version, []) - logger.warn(lambda: "requirement file has been generated without hashes, will use all hashes for the given version {} that could find on the index:\n {}".format(version, shas_to_use)) + hashes_to_use = index_urls.hashes_by_version.get(version, []) + logger.warn(lambda: "requirement file has been generated without hashes, will use all hashes for the given version {} that could find on the index:\n {}".format(version, hashes_to_use)) - for sha256 in shas_to_use: + for digest in hashes_to_use: # For now if the artifact is marked as yanked we just ignore it. # # See https://packaging.python.org/en/latest/specifications/simple-repository-api/#adding-yank-support-to-the-simple-api - maybe_whl = index_urls.whls.get(sha256) + maybe_whl = index_urls.whls.get(digest) if maybe_whl and maybe_whl.yanked == None: whls.append(maybe_whl) continue - maybe_sdist = index_urls.sdists.get(sha256) + maybe_sdist = index_urls.sdists.get(digest) if maybe_sdist and maybe_sdist.yanked == None: sdist = maybe_sdist continue - logger.warn(lambda: "Could not find a whl or an sdist with sha256={}".format(sha256)) + logger.warn(lambda: "Could not find a whl or an sdist with hash {}; note that the index may be advertising digests calculated with a different hash algorithm".format(digest)) yanked = {} for dist in whls + [sdist]: @@ -454,8 +671,8 @@ def _add_dists(*, requirement, index_urls, target_platform, logger = None): if not whls and not sdist: # If there are no suitable wheels to handle for now allow fallback to pip, it # may be a little bit more helpful when debugging? Most likely something is - # going a bit wrong here, should we raise an error because the sha256 have most - # likely mismatched? We are already printing a warning above. + # going a bit wrong here, should we raise an error because the digests have + # most likely mismatched? We are already printing a warning above. return None, True # Select a single wheel that can work on the target_platform diff --git a/python/private/pypi/parse_simpleapi_html.bzl b/python/private/pypi/parse_simpleapi_html.bzl index 7f0d2776d7..e919c8ca55 100644 --- a/python/private/pypi/parse_simpleapi_html.bzl +++ b/python/private/pypi/parse_simpleapi_html.bzl @@ -17,10 +17,11 @@ Parse SimpleAPI HTML in Starlark. """ load("//python/private:normalize_name.bzl", "normalize_name") +load(":hash.bzl", "hash") load(":version_from_filename.bzl", "version_from_filename") def parse_simpleapi_html(*, content, parse_index = False): - """Get the package URLs for given shas by parsing the Simple API HTML. + """Get the package URLs for given digests by parsing the Simple API HTML. Args: content: {type}`str` The Simple API HTML content. @@ -29,11 +30,15 @@ def parse_simpleapi_html(*, content, parse_index = False): Returns: If it is the index page, return the map of package to URL it can be queried from. - Otherwise, a list of structs with: + Otherwise, a struct with `whls` and `sdists` dicts keyed by the `:` + value advertised in the URL fragment (PEP 503, most commonly `sha256`, but any + algorithm from {obj}`hash.ALGOS` is accepted) and `hashes_by_version` mapping each + version to the digests of its artifacts. The dict values are structs with: * filename: {type}`str` The filename of the artifact. * version: {type}`str` The version of the artifact. * url: {type}`str` The URL to download the artifact. - * sha256: {type}`str` The sha256 of the artifact. + * digest: {type}`str` The `:` value of the artifact, may be empty + if the index does not advertise any digest. * metadata_sha256: {type}`str` The whl METADATA sha256 if we can download it. If this is present, then the 'metadata_url' is also present. Defaults to "". * metadata_url: {type}`str` The URL for the METADATA if we can download it. Defaults to "". @@ -43,7 +48,7 @@ def parse_simpleapi_html(*, content, parse_index = False): """ sdists = {} whls = {} - sha256s_by_version = {} + hashes_by_version = {} # 1. Faster Version Extraction # Search only the first 2KB for versioning metadata instead of splitting everything @@ -101,7 +106,14 @@ def parse_simpleapi_html(*, content, parse_index = False): continue # 3. Efficient Attribute Parsing - dist_url, _, sha256 = href.partition("#sha256=") + # PEP 503 says the URL fragment SHOULD be `#=` where the + # hash name may be any algorithm from `hashlib`, not only `sha256`. + dist_url, _, fragment = href.partition("#") + algo, _, hex_digest = fragment.partition("=") + digest = hash.digest(algo, hex_digest) + if not digest and fragment: + # Not a hash fragment, keep it as part of the URL. + dist_url = "{}#{}".format(dist_url, fragment) # Handle Yanked status yanked = None @@ -109,7 +121,7 @@ def parse_simpleapi_html(*, content, parse_index = False): yanked = _unescape_pypi_html(attrs["data-yanked"]) version = version_from_filename(filename) - sha256s_by_version.setdefault(version, []).append(sha256) + hashes_by_version.setdefault(version, []).append(digest) # 4. Optimized Metadata Check (PEP 714) metadata_sha256 = "" @@ -126,16 +138,16 @@ def parse_simpleapi_html(*, content, parse_index = False): filename = filename, version = version, url = dist_url, - sha256 = sha256, + digest = digest, metadata_sha256 = metadata_sha256, metadata_url = metadata_url, yanked = yanked, ) if filename.endswith(".whl"): - whls[sha256] = dist + whls[digest] = dist else: - sdists[sha256] = dist + sdists[digest] = dist if parse_index: return packages @@ -143,7 +155,7 @@ def parse_simpleapi_html(*, content, parse_index = False): return struct( sdists = sdists, whls = whls, - sha256s_by_version = sha256s_by_version, + hashes_by_version = hashes_by_version, ) def _parse_attrs(attr_string): diff --git a/python/private/pypi/patch_whl.bzl b/python/private/pypi/patch_whl.bzl index 71b46f6e7c..e8d76eeba5 100644 --- a/python/private/pypi/patch_whl.bzl +++ b/python/private/pypi/patch_whl.bzl @@ -12,27 +12,26 @@ # See the License for the specific language governing permissions and # limitations under the License. -"""A small utility to patch a file in the repository context and repackage it using a Python interpreter - -Note, because we are patching a wheel file and we need a new RECORD file, this -function will print a diff of the RECORD and will ask the user to include a -RECORD patch in their patches that they maintain. This is to ensure that we can -satisfy the following usecases: -* Patch an invalid RECORD file. -* Patch files within a wheel. - -If we were silently regenerating the RECORD file, we may be vulnerable to supply chain -attacks (it is a very small chance) and keeping the RECORD patches next to the -other patches ensures that the users have overview on exactly what has changed -within the wheel. +"""A small utility to patch a file in the repository context natively. + +This replaces the previous approach that depended on running Python to +repack patched wheels. Instead, after extracting and patching, we fix +the RECORD file natively in Starlark and repack using system zip. """ load("@rules_python_internal//:rules_python_config.bzl", rp_config = "config") load("//python/private:repo_utils.bzl", "repo_utils") load(":parse_whl_name.bzl", "parse_whl_name") -load(":pypi_repo_utils.bzl", "pypi_repo_utils") -_rules_python_root = Label("//:BUILD.bazel") +_DUMMY_SHA256 = "sha256=0" + +_RECORD_EXCLUDES = { + "INSTALLER": None, + "RECORD": None, + "RECORD.jws": None, + "RECORD.p7s": None, + "REQUESTED": None, +} def patched_whl_name(original_whl_name): """Return the new filename to output the patched wheel. @@ -47,14 +46,6 @@ def patched_whl_name(original_whl_name): version = parsed_whl.version suffix = "patched" if "+" in version: - # This already has some local version, so we just append one more - # identifier here. We comply with the spec and mark the file as patched - # by adding a local version identifier at the end. - # - # By doing this we can still install the package using most of the package - # managers - # - # See https://packaging.python.org/en/latest/specifications/version-specifiers/#local-version-identifiers version = "{}.{}".format(version, suffix) else: version = "{}+{}".format(version, suffix) @@ -67,29 +58,29 @@ def patched_whl_name(original_whl_name): platform_tag = parsed_whl.platform_tag, ) -def patch_whl(rctx, *, python_interpreter, whl_path, patches, **kwargs): - """Patch a whl file and repack it to ensure that the RECORD metadata stays correct. +def patch_whl(rctx, *, whl_path, patches): + """Patch a whl file and repack it natively. + + The wheel is extracted, patched, missing RECORD entries are added + with dummy sha256 and size values, and finally the wheel is + repacked using system zip. Args: rctx: repository_ctx - python_interpreter: the python interpreter to use. whl_path: The whl file name to be patched. patches: a label-keyed-int dict that has the patch files as keys and the patch_strip as the value. - **kwargs: extras passed to repo_utils.execute_checked. Returns: value of the repackaging action. """ - - # extract files into the current directory for patching as rctx.patch - # does not support patching in another directory. whl_input = rctx.path(whl_path) repo_utils.extract( rctx, archive = whl_input, supports_whl_extraction = rp_config.supports_whl_extraction, + extract_needs_chmod = rp_config.extract_needs_chmod, ) if not patches: @@ -98,43 +89,159 @@ def patch_whl(rctx, *, python_interpreter, whl_path, patches, **kwargs): for patch_file, patch_strip in patches.items(): rctx.patch(patch_file, strip = patch_strip) - record_patch = rctx.path("RECORD.patch") + _fix_record(rctx) + whl_patched = patched_whl_name(whl_input.basename) - pypi_repo_utils.execute_checked( + rctx.delete(whl_input) + _repack_whl(rctx, output = whl_patched) + + return rctx.path(whl_patched) + +def _fix_record(rctx): + """Add missing file entries to RECORD with dummy sha256 and file size.""" + for entry in rctx.path(".").readdir(): + if not entry.basename.endswith(".dist-info"): + continue + + record_path = entry.get_child("RECORD") + if not record_path.exists: + continue + + all_files = _collect_files(rctx) + record_rel = repo_utils.repo_root_relative_path(rctx, record_path) + + new_content = fix_record_content( + record_content = rctx.read(record_path), + all_files = all_files, + record_rel = record_rel, + ) + if new_content != None: + rctx.file(record_path, new_content) + +def fix_record_content(record_content, all_files, record_rel): + """Add missing file entries to RECORD content with dummy sha256 and size. + + Args: + record_content: {type}`str` The existing RECORD file content. + all_files: {type}`dict[str, bool]` All files in the directory, keys + are repo-root-relative paths. + record_rel: {type}`str` The repo-root-relative path to the RECORD file. + + Returns: + {type}`str | None` The new RECORD content if entries were added, + or None if no changes were needed. + """ + has_trailing_newline = record_content.endswith("\n") + lines = record_content.split("\n") + + is_all_quoted = True + has_content = False + existing = {} + for line in lines: + stripped = line.strip() + if not stripped: + continue + has_content = True + if not stripped.startswith('"'): + is_all_quoted = False + parts = stripped.split(",") + if len(parts) >= 1: + fname = parts[0].strip('"') + existing[fname] = True + + if not has_content: + is_all_quoted = False + + existing[record_rel] = True + + added = [] + for fpath in sorted(all_files.keys()): + if fpath in existing: + continue + basename = fpath.split("/")[-1] if "/" in fpath else fpath + if basename in _RECORD_EXCLUDES: + continue + if fpath.endswith(".whl"): + continue + added.append(fpath) + + if not added: + return None + + new_lines = list(lines) + if not has_trailing_newline: + new_lines.append("") + if is_all_quoted: + for fpath in added: + new_lines.append('"{file}",{sha256},0'.format( + file = fpath, + sha256 = _DUMMY_SHA256, + )) + else: + for fpath in added: + new_lines.append("{file},{sha256},0".format( + file = fpath, + sha256 = _DUMMY_SHA256, + )) + new_lines.append("") + + return "\n".join(new_lines) + +def _collect_files(rctx): + """Collect all file paths relative to the repo root (iterative).""" + result = {} + paths = [(rctx.path("."), "")] + for _ in range(10000000): + if not paths: + break + path, prefix = paths.pop() + for entry in path.readdir(): + rel = prefix + "/" + entry.basename if prefix else entry.basename + if entry.is_dir: + paths.append((entry, rel)) + else: + result[rel] = True + return result + +def _repack_whl(rctx, *, output): + """Repack the current directory into a wheel file using system zip.""" + os_name = repo_utils.get_platforms_os_name(rctx) + if os_name == "windows": + _repack_whl_windows(rctx, output = output) + else: + _repack_whl_unix(rctx, output = output) + +def _repack_whl_unix(rctx, *, output): + repo_utils.execute_checked( rctx, - python = python_interpreter, - srcs = [ - Label("//python/private/pypi:repack_whl.py"), - Label("//tools:wheelmaker.py"), - ], + op = "PatchWhl", arguments = [ - "-m", - "python.private.pypi.repack_whl", - "--record-patch", - record_patch, - whl_input, - whl_patched, + "zip", + "-0", + "-X", + str(output), + "-r", + ".", ], - environment = { - "PYTHONPATH": str(rctx.path(_rules_python_root).dirname), - }, - **kwargs ) - if record_patch.exists: - record_patch_contents = rctx.read(record_patch) - warning_msg = """WARNING: the resultant RECORD file of the patch wheel is different +def _repack_whl_windows(rctx, *, output): + powershell_exe = rctx.which("powershell.exe") or rctx.which("powershell") + if not powershell_exe: + fail("powershell not found on PATH") - If you are patching on Windows, you may see this warning because of - a known issue (bazel-contrib/rules_python#1639) with file endings. + script_path = rctx.path(Label("//python/private/pypi:repack_whl.ps1")) - If you would like to silence the warning, you can apply the patch that is stored in - {record_patch}. The contents of the file are below: -{record_patch_contents}""".format( - record_patch = record_patch, - record_patch_contents = record_patch_contents, - ) - print(warning_msg) # buildifier: disable=print - - return rctx.path(whl_patched) + repo_utils.execute_checked( + rctx, + op = "PatchWhl", + arguments = [ + powershell_exe, + "-NoProfile", + "-File", + str(script_path), + "-Output", + str(output), + ], + ) diff --git a/python/private/pypi/pep508_deps.bzl b/python/private/pypi/pep508_deps.bzl index c004334d96..fd6d961bf5 100644 --- a/python/private/pypi/pep508_deps.bzl +++ b/python/private/pypi/pep508_deps.bzl @@ -44,6 +44,12 @@ def deps( * deps_select: {type}`dict[str, list[str]]` dependencies to include on particular subset of target platforms. """ + if not requires_dist: + return struct( + deps = [], + deps_select = {}, + ) + reqs = sorted( [requirement(r) for r in requires_dist], key = lambda x: "{}:{}:".format(x.name, sorted(x.extras), x.marker), diff --git a/python/private/pypi/pip_compile.bzl b/python/private/pypi/pip_compile.bzl index 3ef2cdb39c..1bec3435a4 100644 --- a/python/private/pypi/pip_compile.bzl +++ b/python/private/pypi/pip_compile.bzl @@ -22,6 +22,15 @@ make it possible to have multiple tools inside the `pypi` directory load("//python:py_binary.bzl", _py_binary = "py_binary") load("//python:py_test.bzl", _py_test = "py_test") +# The `data` attribute does not allow duplicate labels, but user-provided `data` +# can overlap with labels added from attributes like `src`. +def _dedupe_data(data): + res = [] + for d in data: + if d not in res: + res.append(d) + return res + def pip_compile( name, srcs = None, @@ -39,6 +48,7 @@ def pip_compile( visibility = ["//visibility:private"], tags = None, constraints = [], + data = [], **kwargs): """Generates targets for managing pip dependencies with pip-compile (piptools). @@ -59,12 +69,12 @@ def pip_compile( defaults to `["pyproject.toml"]`. Supported formats are: * a requirements text file, usually named `requirements.in` * A `.toml` file, where the `project.dependencies` list is used as per - [PEP621](https://peps.python.org/pep-0621/). + {pep}`PEP 621`. src: file containing inputs to dependency resolution. If not specified, defaults to `pyproject.toml`. Supported formats are: * a requirements text file, usually named `requirements.in` * A `.toml` file, where the `project.dependencies` list is used as per - [PEP621](https://peps.python.org/pep-0621/). + {pep}`PEP 621`. extra_args: passed to pip-compile (aka `piptools`). See the [pip-compile docs](https://pip-tools.readthedocs.io/en/latest/cli/pip-compile) for args and meaning (passing `-h` and/or `--version` can help @@ -81,6 +91,7 @@ def pip_compile( tags: tagging attribute common to all build rules, passed to both the _test and .update rules. visibility: passed to both the _test and .update rules. constraints: a list of files containing constraints to pass to pip-compile with `--constraint`. + data: A list of labels to include as part of the `data` attribute in the generated `py_binary`. **kwargs: other bazel attributes passed to the "_test" rule. """ if len([x for x in [srcs, src, requirements_in] if x != None]) > 1: @@ -95,16 +106,18 @@ def pip_compile( requirements_txt = name + ".txt" if requirements_txt == None else requirements_txt + data = data or [] + # "Default" target produced by this macro # Allow a compile_pip_requirements rule to include another one in the data # for a requirements file that does `-r ../other/requirements.txt` native.filegroup( name = name, - srcs = kwargs.pop("data", []) + [requirements_txt], + srcs = data + [requirements_txt], visibility = visibility, ) - data = [name, requirements_txt] + srcs + [f for f in (requirements_linux, requirements_darwin, requirements_windows) if f != None] + constraints + data = _dedupe_data(data + [name, requirements_txt] + srcs + [f for f in (requirements_linux, requirements_darwin, requirements_windows) if f != None] + constraints) # Use the Label constructor so this is expanded in the context of the file # where it appears, which is to say, in @rules_python diff --git a/python/private/pypi/pip_repository.bzl b/python/private/pypi/pip_repository.bzl index 5fcd351958..436af73c8d 100644 --- a/python/private/pypi/pip_repository.bzl +++ b/python/private/pypi/pip_repository.bzl @@ -360,13 +360,13 @@ In some cases you may not want to generate the requirements.bzl file as a reposi while Bazel is fetching dependencies. For example, if you produce a reusable Bazel module such as a ruleset, you may want to include the requirements.bzl file rather than make your users install the WORKSPACE setup to generate it. -See https://github.com/bazel-contrib/rules_python/issues/608 +See {gh-issue}`608` This is the same workflow as Gazelle, which creates `go_repository` rules with [`update-repos`](https://github.com/bazelbuild/bazel-gazelle#update-repos) To do this, use the "write to source file" pattern documented in -https://blog.aspect.dev/bazel-can-write-to-the-source-folder + to put a copy of the generated requirements.bzl into your project. Then load the requirements.bzl file directly rather than from the generated repository. See the example in rules_python/examples/pip_parse_vendored. diff --git a/python/private/pypi/pip_repository_attrs.bzl b/python/private/pypi/pip_repository_attrs.bzl index 23000869e9..4f0b26448b 100644 --- a/python/private/pypi/pip_repository_attrs.bzl +++ b/python/private/pypi/pip_repository_attrs.bzl @@ -21,6 +21,79 @@ repositories.""" load(":attrs.bzl", COMMON_ATTRS = "ATTRS") ATTRS = { + "experimental_requirement_cycles": attr.string_list_dict( + default = {}, + doc = """\ +A mapping of dependency cycle names to a list of requirements which form that cycle. + +Requirements which form cycles will be installed together and taken as +dependencies together in order to ensure that the cycle is always satisified. + +Example: + `sphinx` depends on `sphinxcontrib-serializinghtml` + When listing both as requirements, ala + + ``` + py_binary( + name = "doctool", + ... + deps = [ + "@pypi//sphinx:pkg", + "@pypi//sphinxcontrib_serializinghtml", + ] + ) + ``` + + Will produce a Bazel error such as + + ``` + ERROR: .../external/pypi_sphinxcontrib_serializinghtml/BUILD.bazel:44:6: in alias rule @pypi_sphinxcontrib_serializinghtml//:pkg: cycle in dependency graph: + //:doctool (...) + @pypi//sphinxcontrib_serializinghtml:pkg (...) + .-> @pypi_sphinxcontrib_serializinghtml//:pkg (...) + | @pypi_sphinxcontrib_serializinghtml//:_pkg (...) + | @pypi_sphinx//:pkg (...) + | @pypi_sphinx//:_pkg (...) + `-- @pypi_sphinxcontrib_serializinghtml//:pkg (...) + ``` + + Which we can resolve by configuring these two requirements to be installed together as a cycle + + ``` + pip_parse( + ... + experimental_requirement_cycles = { + "sphinx": [ + "sphinx", + "sphinxcontrib-serializinghtml", + ] + }, + ) + ``` + +Warning: + If a dependency participates in multiple cycles, all of those cycles must be + collapsed down to one. For instance `a <-> b` and `a <-> c` cannot be listed + as two separate cycles. +""", + ), + "extra_hub_aliases": attr.string_list_dict( + doc = """\ +Extra aliases to make for specific wheels in the hub repo. This is useful when +paired with the {attr}`whl_modifications`. + +:::{versionadded} 0.38.0 + +For `pip.parse` with bzlmod +::: + +:::{versionadded} 1.0.0 + +For `pip_parse` with workspace. +::: +""", + mandatory = False, + ), "requirements_by_platform": attr.label_keyed_string_dict( doc = """\ The requirements files and the comma delimited list of target platforms as values. diff --git a/python/private/pypi/pkg_aliases.bzl b/python/private/pypi/pkg_aliases.bzl index 111a49d3c6..b1c29c95ee 100644 --- a/python/private/pypi/pkg_aliases.bzl +++ b/python/private/pypi/pkg_aliases.bzl @@ -201,6 +201,7 @@ def multiplatform_whl_aliases( ret[alias] = repo continue + # This is if we are using `whl_config_setting` struct config_settings = get_config_settings( target_platforms = alias.target_platforms, python_version = alias.version, diff --git a/python/private/pypi/pypi_cache.bzl b/python/private/pypi/pypi_cache.bzl index d3a3034a79..4f6fa9ac37 100644 --- a/python/private/pypi/pypi_cache.bzl +++ b/python/private/pypi/pypi_cache.bzl @@ -13,7 +13,10 @@ load(":version_from_filename.bzl", "version_from_filename") # This value should be changed whenever the storage format changes. # Changing it simply means the information cached in the lockfile has to be # recomputed. -_FACT_VERSION = "v1" +# +# v2: the `dist_hashes` values and the `dist_yanked` keys are canonical +# `:` strings instead of bare sha256 hex digests. +_FACT_VERSION = "v2" def pypi_cache(mctx = None, store = None): """The cache for PyPI index queries. @@ -135,23 +138,23 @@ def _filter_packages(dists, requested_versions): } return result if result else None - sha256s_by_version = {} + hashes_by_version = {} whls = {} sdists = {} - for sha256, d in dists.sdists.items(): + for digest, d in dists.sdists.items(): if d.version not in requested_versions: continue - sdists[sha256] = d - sha256s_by_version.setdefault(d.version, []).append(sha256) + sdists[digest] = d + hashes_by_version.setdefault(d.version, []).append(digest) - for sha256, d in dists.whls.items(): + for digest, d in dists.whls.items(): if d.version not in requested_versions: continue - whls[sha256] = d - sha256s_by_version.setdefault(d.version, []).append(sha256) + whls[digest] = d + hashes_by_version.setdefault(d.version, []).append(digest) if not whls and not sdists: # TODO @aignas 2026-03-08: add logging @@ -161,9 +164,9 @@ def _filter_packages(dists, requested_versions): return struct( whls = whls, sdists = sdists, - sha256s_by_version = { + hashes_by_version = { k: sorted(v) - for k, v in sha256s_by_version.items() + for k, v in hashes_by_version.items() }, ) @@ -232,14 +235,14 @@ def _get_from_facts(facts, known_facts, index_url, requested_versions, facts_ver retrieved_versions = {} - for url, sha256 in known_facts.get("dist_hashes", {}).get(root_url, {}).get(distribution, {}).items(): - filename = known_facts.get("dist_filenames", {}).get(root_url, {}).get(distribution, {}).get(sha256) + for url, digest in known_facts.get("dist_hashes", {}).get(root_url, {}).get(distribution, {}).items(): + filename = known_facts.get("dist_filenames", {}).get(root_url, {}).get(distribution, {}).get(url) if not filename: _, _, filename = url.rpartition("/") version = version_from_filename(filename) if version not in requested_versions: - # TODO @aignas 2026-01-21: do the check by requested shas at some point + # TODO @aignas 2026-01-21: do the check by requested digests at some point # We don't have sufficient info in the lock file, need to call the API # continue @@ -251,16 +254,16 @@ def _get_from_facts(facts, known_facts, index_url, requested_versions, facts_ver else: dists = known_sources.setdefault("sdists", {}) - known_sources.setdefault("sha256s_by_version", {}).setdefault(version, []).append(sha256) + known_sources.setdefault("hashes_by_version", {}).setdefault(version, []).append(digest) - dists.setdefault(sha256, struct( - sha256 = sha256, + dists.setdefault(digest, struct( + digest = digest, filename = filename, version = version, metadata_url = "", metadata_sha256 = "", url = url, - yanked = known_facts.get("dist_yanked", {}).get(root_url, {}).get(distribution, {}).get(sha256), + yanked = known_facts.get("dist_yanked", {}).get(root_url, {}).get(distribution, {}).get(digest), )) if not known_sources: @@ -275,9 +278,9 @@ def _get_from_facts(facts, known_facts, index_url, requested_versions, facts_ver output = struct( whls = known_sources.get("whls", {}), sdists = known_sources.get("sdists", {}), - sha256s_by_version = { + hashes_by_version = { k: sorted(v) - for k, v in known_sources.get("sha256s_by_version", {}).items() + for k, v in known_sources.get("hashes_by_version", {}).items() }, ) @@ -318,7 +321,8 @@ def _store_facts(facts, fact_version, index_url, value): # "dist_hashes": { # "": { # "": { - # "": "", + # # An empty string if the index does not advertise any digest. + # "": ":", # }, # }, # }, @@ -332,16 +336,16 @@ def _store_facts(facts, fact_version, index_url, value): # "dist_yanked": { # "": { # "": { - # "": "", # if the package is yanked + # ":": "", # if the package is yanked # }, # }, # }, # }, - for sha256, d in (value.sdists | value.whls).items(): - facts.setdefault("dist_hashes", {}).setdefault(root_url, {}).setdefault(distribution, {}).setdefault(d.url, sha256) + for digest, d in (value.sdists | value.whls).items(): + facts.setdefault("dist_hashes", {}).setdefault(root_url, {}).setdefault(distribution, {}).setdefault(d.url, digest) if not d.url.endswith(d.filename): facts.setdefault("dist_filenames", {}).setdefault(root_url, {}).setdefault(distribution, {}).setdefault(d.url, d.filename) if d.yanked != None: - facts.setdefault("dist_yanked", {}).setdefault(root_url, {}).setdefault(distribution, {}).setdefault(sha256, d.yanked) + facts.setdefault("dist_yanked", {}).setdefault(root_url, {}).setdefault(distribution, {}).setdefault(digest, d.yanked) return value diff --git a/python/private/pypi/repack_whl.ps1 b/python/private/pypi/repack_whl.ps1 new file mode 100644 index 0000000000..2f3b4ada0f --- /dev/null +++ b/python/private/pypi/repack_whl.ps1 @@ -0,0 +1,7 @@ +param( + [string]$Output +) + +$files = Get-ChildItem -Path . -Exclude 'tmp.zip', $Output +Compress-Archive -Path $files -DestinationPath 'tmp.zip' -Force +Move-Item -Path 'tmp.zip' -Destination $Output -Force diff --git a/python/private/pypi/repack_whl.py b/python/private/pypi/repack_whl.py deleted file mode 100644 index 4bf554cdc9..0000000000 --- a/python/private/pypi/repack_whl.py +++ /dev/null @@ -1,200 +0,0 @@ -# Copyright 2023 The Bazel Authors. All rights reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -""" -Regenerate a whl file after patching and cleanup the patched contents. - -This script will take contents of the current directory and create a new wheel -out of it and will remove all files that were written to the wheel. -""" - -from __future__ import annotations - -import argparse -import csv -import difflib -import logging -import pathlib -import sys -import tempfile - -from tools.wheelmaker import _WhlFile - -# NOTE: Implement the following matching of what goes into the RECORD -# https://peps.python.org/pep-0491/#the-dist-info-directory -_EXCLUDES = [ - "RECORD", - "INSTALLER", - "RECORD.jws", - "RECORD.p7s", - "REQUESTED", -] - -_DISTINFO = "dist-info" - - -def _has_all_quoted_filenames(record_contents: str) -> bool: - """Check if all filenames in the RECORD are quoted. - - Some wheels (like torch) have all filenames quoted in their RECORD file. - We detect this to preserve the quoting style when repacking. - """ - lines = record_contents.splitlines() - return all(line.startswith('"') for line in lines) - - -def _unidiff_output(expected, actual, record): - """ - Helper function. Returns a string containing the unified diff of two - multiline strings. - """ - - expected = expected.splitlines(1) - actual = actual.splitlines(1) - - diff = difflib.unified_diff( - expected, actual, fromfile=f"a/{record}", tofile=f"b/{record}" - ) - - return "".join(diff) - - -def _files_to_pack(dir: pathlib.Path, want_record: str) -> list[pathlib.Path]: - """Check that the RECORD file entries are correct and print a unified diff on failure.""" - - # First get existing files by using the RECORD file - got_files = [] - got_distinfos = [] - for row in csv.reader(want_record.splitlines()): - rec = row[0] - path = dir / rec - - if not path.exists(): - # skip files that do not exist as they won't be present in the final - # RECORD file. - continue - - if not path.parent.name.endswith(_DISTINFO): - got_files.append(path) - elif path.name not in _EXCLUDES: - got_distinfos.append(path) - - # Then get extra files present in the directory but not in the RECORD file - extra_files = [] - extra_distinfos = [] - for path in dir.rglob("*"): - if path.is_dir(): - continue - - elif path.parent.name.endswith(_DISTINFO): - if path.name in _EXCLUDES: - # NOTE: we implement the following matching of what goes into the RECORD - # https://peps.python.org/pep-0491/#the-dist-info-directory - continue - elif path not in got_distinfos: - extra_distinfos.append(path) - - elif path not in got_files: - extra_files.append(path) - - # sort the extra files for reproducibility - extra_files.sort() - extra_distinfos.sort() - - # This order ensures that the structure of the RECORD file is always the - # same and ensures smaller patchsets to the RECORD file in general - return got_files + extra_files + got_distinfos + extra_distinfos - - -def main(sys_argv): - parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument( - "whl_path", - type=pathlib.Path, - help="The original wheel file that we have patched.", - ) - parser.add_argument( - "--record-patch", - type=pathlib.Path, - help="The output path that we are going to write the RECORD file patch to.", - ) - parser.add_argument( - "output", - type=pathlib.Path, - help="The output path that we are going to write a new file to.", - ) - args = parser.parse_args(sys_argv) - - cwd = pathlib.Path.cwd() - logging.debug("=" * 80) - logging.debug("Repackaging the wheel") - logging.debug("=" * 80) - - with tempfile.TemporaryDirectory(dir=cwd) as tmpdir: - patched_wheel_dir = cwd / tmpdir - logging.debug(f"Created a tmpdir: {patched_wheel_dir}") - - excludes = [args.whl_path, patched_wheel_dir] - - logging.debug("Moving whl contents to the newly created tmpdir") - for p in cwd.glob("*"): - if p in excludes: - logging.debug(f"Ignoring: {p}") - continue - - rel_path = p.relative_to(cwd) - dst = p.rename(patched_wheel_dir / rel_path) - logging.debug(f"mv {p} -> {dst}") - - distinfo_dir = next(iter(patched_wheel_dir.glob("*dist-info"))) - logging.debug(f"Found dist-info dir: {distinfo_dir}") - record_path = distinfo_dir / "RECORD" - record_contents = record_path.read_text() if record_path.exists() else "" - quote_files = _has_all_quoted_filenames(record_contents) - distribution_prefix = distinfo_dir.with_suffix("").name - - with _WhlFile( - args.output, - mode="w", - distribution_prefix=distribution_prefix, - quote_all_filenames=quote_files, - ) as out: - for p in _files_to_pack(patched_wheel_dir, record_contents): - rel_path = p.relative_to(patched_wheel_dir) - out.add_file(str(rel_path), p) - - logging.debug("Writing RECORD file") - got_record = out.add_recordfile() - - if got_record == record_contents: - logging.info(f"Created a whl file: {args.output}") - return - - record_diff = _unidiff_output( - record_contents, - got_record, - out.distinfo_path("RECORD"), - ) - args.record_patch.write_text(record_diff) - logging.warning( - f"Please apply patch to the RECORD file ({args.record_patch}):\n{record_diff}" - ) - - -if __name__ == "__main__": - logging.basicConfig( - format="%(module)s: %(levelname)s: %(message)s", level=logging.DEBUG - ) - - sys.exit(main(sys.argv[1:])) diff --git a/python/private/pypi/requirements_files_by_platform.bzl b/python/private/pypi/requirements_files_by_platform.bzl index dcdb6128a7..c6f53ae611 100644 --- a/python/private/pypi/requirements_files_by_platform.bzl +++ b/python/private/pypi/requirements_files_by_platform.bzl @@ -73,6 +73,7 @@ def requirements_files_by_platform( requirements_linux = None, requirements_lock = None, requirements_windows = None, + uv_lock = None, platforms, extra_pip_args = None, python_version = None, @@ -88,6 +89,7 @@ def requirements_files_by_platform( requirements_linux (label): The requirements file for the linux OS. requirements_lock (label): The requirements file for all OSes, or used as a fallback. requirements_windows (label): The requirements file for windows OS. + uv_lock (label): The uv.lock file, or used as primary source. extra_pip_args (string list): Extra pip arguments to perform extra validations and to be joined with args fined in files. python_version: str or None. This is needed when the get_index_urls is @@ -106,10 +108,11 @@ def requirements_files_by_platform( requirements_linux or requirements_osx or requirements_windows or - requirements_by_platform + requirements_by_platform or + uv_lock ): fail_fn( - "A 'requirements_lock' attribute must be specified, a platform-specific lockfiles " + + "A 'requirements_lock' or 'uv_lock' attribute must be specified, a platform-specific lockfiles " + "via 'requirements_by_platform' or an os-specific lockfiles must be specified " + "via 'requirements_*' attributes", ) @@ -143,9 +146,12 @@ def requirements_files_by_platform( fail_fn("only a single 'requirements_lock' file can be used when using '--platform' pip argument, consider specifying it via 'requirements_lock' attribute") return None - files_by_platform = [ - (lock_files[0], platforms_from_args), - ] + if not lock_files: + files_by_platform = [] + else: + files_by_platform = [ + (lock_files[0], platforms_from_args), + ] if logger: logger.debug(lambda: "Files by platform with the platform set in the args: {}".format(files_by_platform)) else: diff --git a/python/private/pypi/simpleapi_download.bzl b/python/private/pypi/simpleapi_download.bzl index 5377a08093..b5f7032b0a 100644 --- a/python/private/pypi/simpleapi_download.bzl +++ b/python/private/pypi/simpleapi_download.bzl @@ -34,7 +34,7 @@ def simpleapi_download( """Download Simple API HTML. First it queries all of the indexes for available packages and then it downloads the contents of - the per-package URLs and sha256 values. This is to enable us to use bazel_downloader with + the per-package URLs and hash digest values. This is to enable us to use bazel_downloader with `requirements.txt` files. As a side effect we also are able to "cross-compile" by fetching the right wheel for the right target platform through the information that we retrieve here. @@ -294,6 +294,6 @@ def _with_index_url(index_url, values): return struct( sdists = values.sdists, whls = values.whls, - sha256s_by_version = values.sha256s_by_version, + hashes_by_version = values.hashes_by_version, index_url = index_url, ) diff --git a/python/private/pypi/unified_hub_repo.bzl b/python/private/pypi/unified_hub_repo.bzl new file mode 100644 index 0000000000..f88db75f19 --- /dev/null +++ b/python/private/pypi/unified_hub_repo.bzl @@ -0,0 +1,111 @@ +"""Repository rule for creating the Unified PyPI Hub.""" + +load("//python/private:normalize_name.bzl", "normalize_name") +load("//python/private:text_util.bzl", "render") + +_ROOT_BUILD_TMPL = """\ +load("@rules_python//python/private/pypi:unified_hub_setup.bzl", "define_venv_flag_config_settings") + +package(default_visibility = ["//visibility:public"]) + +define_venv_flag_config_settings( + name = "venv_config_settings", + hubs = {hubs}, +) +""" + +_PKG_BUILD_TMPL = """\ +load("@rules_python//python/private/pypi:unified_hub_setup.bzl", "define_pypi_package_targets") + +package(default_visibility = ["//visibility:public"]) + +define_pypi_package_targets( + name = "{pkg_name}", + default_hub = {default_hub}, + extra_aliases = {extra_aliases}, + hubs = {hubs}, + pkg_hubs = {pkg_hubs}, +) +""" + +def _unified_hub_repo_impl(rctx): + hubs = rctx.attr.hubs + default_hub = rctx.attr.default_hub or None + + # 1. Generate Root BUILD.bazel with shared config settings + rctx.file( + "BUILD.bazel", + _ROOT_BUILD_TMPL.format(hubs = hubs), + ) + + # 2. Organize extra aliases by package + extra_aliases_by_pkg = {} + for qual_alias, alias_hubs in rctx.attr.extra_aliases.items(): + if ":" not in qual_alias: + fail("extra_aliases keys must be in 'pkg:alias' format.") + pkg, alias = qual_alias.split(":", 1) + extra_aliases_by_pkg.setdefault(pkg, {})[alias] = alias_hubs + + # 3. Generate package subpackages + for pkg_name, pkg_hubs in rctx.attr.packages.items(): + extra_aliases = extra_aliases_by_pkg.get(pkg_name, {}) + rctx.file( + pkg_name + "/BUILD.bazel", + _PKG_BUILD_TMPL.format( + default_hub = render.str(default_hub), + extra_aliases = extra_aliases, + hubs = hubs, + pkg_hubs = pkg_hubs, + pkg_name = pkg_name, + ), + ) + +unified_hub_repo = repository_rule( + implementation = _unified_hub_repo_impl, + attrs = { + "default_hub": attr.string( + doc = "The PyPI hub to use when no other hub's conditions match.", + ), + "extra_aliases": attr.string_list_dict( + doc = "Dictionary mapping 'package:alias' to a list of hubs that support it.", + ), + "hubs": attr.string_list( + mandatory = True, + doc = "List of all concrete PyPI hub names.", + ), + "packages": attr.string_list_dict( + mandatory = True, + doc = "Dictionary mapping package names to a list of hubs that contain them.", + ), + }, + doc = "Private repository rule creating the automatic Unified PyPI Hub.", +) + +def unified_workspace_hub_repo(name, hubs, default_hub = None, extra_aliases = {}): + """Creates a Unified PyPI Hub repository for WORKSPACE mode by loading requirements from hubs. + + Args: + name: Name of the repository rule (e.g. "pypi"). + hubs: Dict mapping hub name to its `all_requirements` list or dict. + e.g. {"dev_pip": dev_pip_requirements, "pypi_alpha": pypi_alpha_requirements} + default_hub: Optional default hub name. + extra_aliases: Dictionary mapping 'package:alias' to a list of hubs that support it. + """ + packages = {} + for hub_name, req_map in hubs.items(): + req_list = req_map.keys() if type(req_map) == type({}) else req_map + for req in req_list: + pkg_name = req.split("//")[-1].split(":")[0] + norm_pkg = normalize_name(pkg_name) + if norm_pkg not in packages: + packages[norm_pkg] = [] + if hub_name not in packages[norm_pkg]: + packages[norm_pkg].append(hub_name) + + unified_hub_repo( + name = name, + default_hub = default_hub, + extra_aliases = extra_aliases, + hubs = sorted(hubs.keys()), + packages = packages, + ) diff --git a/python/private/pypi/unified_hub_setup.bzl b/python/private/pypi/unified_hub_setup.bzl new file mode 100644 index 0000000000..b19410fd4b --- /dev/null +++ b/python/private/pypi/unified_hub_setup.bzl @@ -0,0 +1,110 @@ +"""Helper functions for setting up targets within the Unified PyPI Hub repository.""" + +load( + "@rules_python//python/private/pypi:labels.bzl", + "DATA_LABEL", + "DIST_INFO_LABEL", + "EXTRACTED_WHEEL_FILES", + "PY_LIBRARY_PUBLIC_LABEL", + "WHEEL_FILE_PUBLIC_LABEL", +) +load("@rules_python//python/private/pypi:missing_package.bzl", "missing_package_error") + +def define_venv_flag_config_settings(name, hubs): + """Defines the root config_settings for each PyPI spoke hub. + + Args: + name: unused macro name required by buildifier. + hubs: list of concrete hub names. + """ + for hub in hubs: + native.config_setting( + name = "_is_venv_" + hub, + flag_values = {"@rules_python//python/config_settings:venv": hub}, + ) + +_STANDARD_ALIASES = [ + PY_LIBRARY_PUBLIC_LABEL, + WHEEL_FILE_PUBLIC_LABEL, + DATA_LABEL, + DIST_INFO_LABEL, + EXTRACTED_WHEEL_FILES, +] + +def define_pypi_package_targets(name, pkg_hubs, extra_aliases, hubs, default_hub = None): + """Define the targets for a PyPI package in the unified PyPI hub. + + Args: + name: normalized PyPI package name, serving as the main target name. + pkg_hubs: list of hubs that contain this package. + extra_aliases: dict mapping extra alias names to lists of hubs that support them. + hubs: list of all concrete hub names. + default_hub: the hub to use by default. + """ + pkg_name = name + + # Main apparent package target delegates to :pkg + native.alias( + name = pkg_name, + actual = ":pkg", + ) + + all_aliases = _STANDARD_ALIASES + sorted(extra_aliases.keys()) + missing_errors = {} + + for alias_name in all_aliases: + select_map = {} + for hub in hubs: + is_supported = ( + (alias_name in _STANDARD_ALIASES and hub in pkg_hubs) or + (alias_name not in _STANDARD_ALIASES and hub in extra_aliases.get(alias_name, [])) + ) + + if is_supported: + select_map["//:_is_venv_" + hub] = "@{hub}//{pkg}:{alias}".format( + hub = hub, + pkg = pkg_name, + alias = alias_name, + ) + else: + err_target = "_missing_{alias}_in_{hub}".format(alias = alias_name, hub = hub) + if err_target not in missing_errors: + missing_errors[err_target] = { + "hub_name": hub, + "package_name": pkg_name if alias_name in _STANDARD_ALIASES else (pkg_name + ":" + alias_name), + } + select_map["//:_is_venv_" + hub] = ":{}".format(err_target) + + # //conditions:default fallback + default_supported = ( + default_hub and + ((alias_name in _STANDARD_ALIASES and default_hub in pkg_hubs) or + (alias_name not in _STANDARD_ALIASES and default_hub in extra_aliases.get(alias_name, []))) + ) + + if default_supported: + select_map["//conditions:default"] = "@{hub}//{pkg}:{alias}".format( + hub = default_hub, + pkg = pkg_name, + alias = alias_name, + ) + else: + err_target = "_missing_{alias}_in_default".format(alias = alias_name) + if err_target not in missing_errors: + missing_errors[err_target] = { + "hub_name": default_hub or "", + "package_name": pkg_name if alias_name in _STANDARD_ALIASES else (pkg_name + ":" + alias_name), + } + select_map["//conditions:default"] = ":{}".format(err_target) + + native.alias( + name = alias_name, + actual = select(select_map), + ) + + # Generate missing package error targets + for err_name, err_args in missing_errors.items(): + missing_package_error( + name = err_name, + **err_args + ) diff --git a/python/private/pypi/urllib.bzl b/python/private/pypi/urllib.bzl index ea4cd32cc9..0754334de9 100644 --- a/python/private/pypi/urllib.bzl +++ b/python/private/pypi/urllib.bzl @@ -1,5 +1,7 @@ """Utilities for getting an absolute URL from index_url and the URL we find on PyPI index.""" +load("//python/private:envsubst.bzl", _envsubst = "envsubst") + def _get_root_directory(url): scheme_end = url.find("://") if scheme_end == -1: @@ -53,6 +55,12 @@ def _absolute_url(index_url, candidate): # relative path without up-references return "{}/{}".format(index_url.rstrip("/"), candidate) +def _with_envsubst(url, envsubst = None, getenv = None): + if not url or not envsubst or not getenv: + return url + + return _envsubst(url, envsubst, getenv) + def _strip_empty_path_segments(url): """Removes empty path segments from a URL. Does nothing for urls with no scheme. @@ -65,18 +73,23 @@ def _strip_empty_path_segments(url): The url with empty path segments removed and any trailing slash preserved. If the url had no scheme it is returned unchanged. """ - scheme, _, rest = url.partition("://") + sep = "://" + scheme, _, rest = url.partition(sep) + if scheme.lower() == "file" and rest.startswith("/"): + sep = sep + "/" + rest = rest[1:] + if rest == "": return url stripped = "/".join([p for p in rest.split("/") if p]) if url.endswith("/"): - return "{}://{}/".format(scheme, stripped) + return "{}{}{}/".format(scheme, sep, stripped) else: - return "{}://{}".format(scheme, stripped) + return "{}{}{}".format(scheme, sep, stripped) urllib = struct( is_absolute = _is_downloadable, # Ensure that we strip empty path segments when making an absolute URL - absolute_url = lambda index_url, candidate: _strip_empty_path_segments(_absolute_url(index_url, candidate)), + absolute_url = lambda index_url, candidate, *, envsubst = None, getenv = None: _strip_empty_path_segments(_with_envsubst(_absolute_url(index_url, candidate), envsubst, getenv)), strip_empty_path_segments = _strip_empty_path_segments, ) diff --git a/python/private/pypi/wheel_record_rewriter.ps1 b/python/private/pypi/wheel_record_rewriter.ps1 new file mode 100644 index 0000000000..60dd0eca11 --- /dev/null +++ b/python/private/pypi/wheel_record_rewriter.ps1 @@ -0,0 +1,66 @@ +[CmdletBinding()] +param( + [Parameter(Position=0, Mandatory=$true)] + [string]$InFile, + + [Parameter(Position=1, Mandatory=$true)] + [string]$OutFile, + + [Parameter(Position=2, Mandatory=$true)] + [string]$TargetOs, + + [Parameter(Position=3, Mandatory=$true)] + [string]$DataDirBasename +) + +$ErrorActionPreference = "Stop" + +$dataPrefix = "$DataDirBasename/" +$quotedDataPrefix = "`"$DataDirBasename/" + +if ($TargetOs -eq "windows") { + $dataRepl = "../../" + $headersRepl = "../../Include/" + $platlibRepl = "" + $purelibRepl = "" + $scriptsRepl = "../../Scripts/" +} else { + $dataRepl = "../../../" + $headersRepl = "../../../include/" + $platlibRepl = "" + $purelibRepl = "" + $scriptsRepl = "../../../bin/" +} + +$lines = Get-Content -Path $InFile +$outLines = [System.Collections.Generic.List[string]]::new() +$Utf8NoBom = New-Object System.Text.UTF8Encoding $False + +foreach ($line in $lines) { + if ($line.StartsWith($quotedDataPrefix)) { + $quote = "`"" + $rest = $line.Substring($quotedDataPrefix.Length) + } elseif ($line.StartsWith($dataPrefix)) { + $quote = "" + $rest = $line.Substring($dataPrefix.Length) + } else { + $outLines.Add($line) + continue + } + + if ($rest.StartsWith("purelib/")) { + $outLines.Add($quote + $purelibRepl + $rest.Substring(8)) + } elseif ($rest.StartsWith("platlib/")) { + $outLines.Add($quote + $platlibRepl + $rest.Substring(8)) + } elseif ($rest.StartsWith("scripts/")) { + $outLines.Add($quote + $scriptsRepl + $rest.Substring(8)) + } elseif ($rest.StartsWith("headers/")) { + $outLines.Add($quote + $headersRepl + $rest.Substring(8)) + } elseif ($rest.StartsWith("data/")) { + $outLines.Add($quote + $dataRepl + $rest.Substring(5)) + } else { + $outLines.Add($line) + } +} + +[System.IO.File]::WriteAllText($OutFile, ($outLines -join "`n") + "`n", $Utf8NoBom) diff --git a/python/private/pypi/wheel_record_rewriter.sh b/python/private/pypi/wheel_record_rewriter.sh new file mode 100755 index 0000000000..10189435e1 --- /dev/null +++ b/python/private/pypi/wheel_record_rewriter.sh @@ -0,0 +1,60 @@ +#!/bin/sh +set -eu + +IN="$1" +OUT="$2" +TARGET_OS="$3" +DATA_DIR_BASENAME="$4" + +DATA_PREFIX="${DATA_DIR_BASENAME}/" +QUOTED_DATA_PREFIX="\"${DATA_DIR_BASENAME}/" + +if [ "$TARGET_OS" = "windows" ]; then + DATA_REPL="../../" + HEADERS_REPL="../../Include/" + PLATLIB_REPL="" + PURELIB_REPL="" + SCRIPTS_REPL="../../Scripts/" +else + DATA_REPL="../../../" + HEADERS_REPL="../../../include/" + PLATLIB_REPL="" + PURELIB_REPL="" + SCRIPTS_REPL="../../../bin/" +fi + +awk -v data_prefix="$DATA_PREFIX" \ + -v quoted_data_prefix="$QUOTED_DATA_PREFIX" \ + -v data_repl="$DATA_REPL" \ + -v headers_repl="$HEADERS_REPL" \ + -v platlib_repl="$PLATLIB_REPL" \ + -v purelib_repl="$PURELIB_REPL" \ + -v scripts_repl="$SCRIPTS_REPL" ' +{ + line = $0 + quote = "" + if (substr(line, 1, length(quoted_data_prefix)) == quoted_data_prefix) { + quote = "\"" + rest = substr(line, length(quoted_data_prefix) + 1) + } else if (substr(line, 1, length(data_prefix)) == data_prefix) { + rest = substr(line, length(data_prefix) + 1) + } else { + print line + next + } + + if (substr(rest, 1, 8) == "purelib/") { + print quote purelib_repl substr(rest, 9) + } else if (substr(rest, 1, 8) == "platlib/") { + print quote platlib_repl substr(rest, 9) + } else if (substr(rest, 1, 8) == "scripts/") { + print quote scripts_repl substr(rest, 9) + } else if (substr(rest, 1, 8) == "headers/") { + print quote headers_repl substr(rest, 9) + } else if (substr(rest, 1, 5) == "data/") { + print quote data_repl substr(rest, 6) + } else { + print line + } +} +' "$IN" > "$OUT" diff --git a/python/private/pypi/whl_extract.bzl b/python/private/pypi/whl_extract.bzl index 2ebb61a83a..d7195fd681 100644 --- a/python/private/pypi/whl_extract.bzl +++ b/python/private/pypi/whl_extract.bzl @@ -4,6 +4,21 @@ load("@rules_python_internal//:rules_python_config.bzl", rp_config = "config") load("//python/private:repo_utils.bzl", "repo_utils") load(":whl_metadata.bzl", "find_whl_metadata") +# Mapping of wheel .data categories to their extraction destination (relative to +# repository root). +_DATA_CATEGORIES = { + # category: repo_dest_dir + "data": "data", + "headers": "include", + # In theory there may be directory collisions in platlib/purelib, so it is + # best to merge the paths here. What is more, this code has to be reasonably + # efficient because some packages like to explicitly indicate if something + # is in `platlib` or `purelib` (e.g. libclang wheel). + "platlib": "site-packages", + "purelib": "site-packages", + "scripts": "bin", +} + def whl_extract(rctx, *, whl_path, logger): """Extract whls in Starlark. @@ -18,10 +33,9 @@ def whl_extract(rctx, *, whl_path, logger): archive = whl_path, output = install_dir_path, supports_whl_extraction = rp_config.supports_whl_extraction, + extract_needs_chmod = rp_config.extract_needs_chmod, ) - _maybe_fix_permissions(rctx, whl_path = whl_path, logger = logger) - metadata_file = find_whl_metadata( install_dir = install_dir_path, logger = logger, @@ -35,22 +49,11 @@ def whl_extract(rctx, *, whl_path, logger): ) # Get the .dist_info dir name - data_dir = dist_info_dir.dirname.get_child(dist_info_dir.basename[:-len(".dist-info")] + ".data") + data_dir = dist_info_dir.dirname.get_child( + dist_info_dir.basename[:-len(".dist-info")] + ".data", + ) if data_dir.exists: - for prefix, dest_prefix in { - # https://docs.python.org/3/library/sysconfig.html#posix-prefix - # We are taking this from the legacy whl installer config - "data": "data", - "headers": "include", - # In theory there may be directory collisions here, so it would be best to - # merge the paths here. We are doing for quite a few levels deep. What is - # more, this code has to be reasonably efficient because some packages like - # to not put everything to the top level, but to indicate explicitly if - # something is in `platlib` or `purelib` (e.g. libclang wheel). - "platlib": "site-packages", - "purelib": "site-packages", - "scripts": "bin", - }.items(): + for prefix, dest_prefix in _DATA_CATEGORIES.items(): src = data_dir.get_child(prefix) if not src.exists: # The prefix does not exist in the wheel, we can continue @@ -62,30 +65,23 @@ def whl_extract(rctx, *, whl_path, logger): logger.debug(lambda: "Renaming: {} -> {}".format(src, dest)) repo_utils.rename(rctx, src, dest) + # Move RECORD to rewrite-record so gen_wheel_record can generate + # the platform-specific RECORD file at build time. + record_file = dist_info_dir.get_child("RECORD") + if record_file.exists: + rewrite_record_dir = rctx.path( + "rewrite-record/" + dist_info_dir.basename, + ) + repo_utils.mkdir(rctx, rewrite_record_dir) + repo_utils.rename( + rctx, + record_file, + rewrite_record_dir.get_child("RECORD"), + ) + # Ensure that there is no data dir left rctx.delete(data_dir) -# TODO: This can be removed when Bazel 8.6+ is the minimum supported version. -def _maybe_fix_permissions(rctx, *, whl_path, logger): - # Fix permissions on extracted files. Some wheels have files without read permissions set, - # which causes errors when trying to read them later. - # We apply this to the root directory to ensure that everything in bin/, site-packages/, - # etc. is readable and executable where appropriate. - os_name = repo_utils.get_platforms_os_name(rctx) - if os_name != "windows": - # On Unix-like systems, recursively add read permissions to all files - # and ensure directories are traversable (need execute permission) - result = repo_utils.execute_unchecked( - rctx, - op = "Fixing wheel permissions {}".format(whl_path), - arguments = ["chmod", "-R", "a+rX", "."], - logger = logger, - ) - if result.return_code != 0: - # It's possible chmod is not available or the filesystem doesn't support it. - # This is fine, we just want to try to fix permissions if possible. - logger.warn(lambda: "Failed to fix file permissions: {}".format(result.stderr)) - def merge_trees(src, dest): """Merge src into the destination path. 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/pypi/whl_library.bzl b/python/private/pypi/whl_library.bzl index 966d25d04d..47b05468ef 100644 --- a/python/private/pypi/whl_library.bzl +++ b/python/private/pypi/whl_library.bzl @@ -308,108 +308,8 @@ def _to_purl(*, index, metadata, filename): return "pkg:pypi/{}@{}?{}".format(name, metadata.version, "&".join(["{}={}".format(key, val) for key, val in qualifiers.items()])) -def _whl_library_impl(rctx): - logger = repo_utils.logger(rctx) - - whl_path = None - sdist_filename = None - extra_pip_args = [] - extra_pip_args.extend(rctx.attr.extra_pip_args) - if rctx.attr.whl_file: - rctx.watch(rctx.attr.whl_file) - whl_path = rctx.path(rctx.attr.whl_file) - - # Simulate the behaviour where the whl is present in the current directory. - rctx.symlink(whl_path, whl_path.basename) - whl_path = rctx.path(whl_path.basename) - elif rctx.attr.urls and rctx.attr.filename: - filename = rctx.attr.filename - urls = rctx.attr.urls - urls = [ - urllib.absolute_url( - envsubst(rctx.attr.index_url, rctx.attr.envsubst, rctx.getenv), - url, - ) - for url in urls - ] - result = rctx.download( - url = urls, - output = filename, - sha256 = rctx.attr.sha256, - auth = get_auth(rctx, urls), - ) - if not rctx.attr.sha256: - # this is only seen when there is a direct URL reference without sha256 - logger.warn("Please update the requirement line to include the hash:\n{} \\\n --hash=sha256:{}".format( - rctx.attr.requirement, - result.sha256, - )) - - if not result.success: - fail("could not download the '{}' from {}:\n{}".format(filename, urls, result)) - - if filename.endswith(".whl"): - whl_path = rctx.path(filename) - else: - sdist_filename = filename - - # It is an sdist and we need to tell PyPI to use a file in this directory - # and, allow getting build dependencies from PYTHONPATH, which we - # setup in this repository rule, but still download any necessary - # build deps from PyPI (e.g. `flit_core`) if they are missing. - extra_pip_args.extend(["--find-links", "."]) - - # When we already have a wheel and there are no patches, Python isn't used, - # so there's no need to setup env vars to run Python, unless we need to - # build an sdist or resolve a requirement. - if whl_path and not rctx.attr.whl_patches: - environment = {} - args = [] - python_interpreter = None - else: - python_interpreter = pypi_repo_utils.resolve_python_interpreter( - rctx, - python_interpreter = rctx.attr.python_interpreter, - python_interpreter_target = rctx.attr.python_interpreter_target, - ) - args = [ - "-m", - "python.private.pypi.whl_installer.wheel_installer", - "--requirement", - rctx.attr.requirement, - ] - args = _parse_optional_attrs(rctx, args, extra_pip_args) - - # Manually construct the PYTHONPATH since we cannot use the toolchain here - environment = _create_repository_execution_environment(rctx, python_interpreter, logger = logger) - - if not whl_path: - if rctx.attr.urls: - op_tmpl = "whl_library.BuildWheelFromSource({name}, {requirement})" - elif rctx.attr.download_only: - op_tmpl = "whl_library.DownloadWheel({name}, {requirement})" - else: - op_tmpl = "whl_library.ResolveRequirement({name}, {requirement})" - - pypi_repo_utils.execute_checked( - rctx, - # truncate the requirement value when logging it / reporting - # progress since it may contain several ' --hash=sha256:... - # --hash=sha256:...' substrings that fill up the console - python = python_interpreter, - op = op_tmpl.format(name = rctx.attr.name, requirement = rctx.attr.requirement.split(" ", 1)[0]), - arguments = args, - environment = environment, - srcs = rctx.attr._python_srcs, - quiet = rctx.attr.quiet, - timeout = rctx.attr.timeout, - logger = logger, - ) - - whl_path = rctx.path(json.decode(rctx.read("whl_file.json"))["whl_file"]) - if not rctx.delete("whl_file.json"): - fail("failed to delete the whl_file.json file") - +def _whl_extract(rctx, *, whl_path, logger, sdist_filename = None): + """Extract the wheel, apply patches and generate BUILD.bazel files.""" if rctx.attr.whl_patches: patches = {} for patch_file, json_args in rctx.attr.whl_patches.items(): @@ -420,12 +320,8 @@ def _whl_library_impl(rctx): if patches: whl_path = patch_whl( rctx, - op = "whl_library.PatchWhl({}, {})".format(rctx.attr.name, rctx.attr.requirement), - python_interpreter = python_interpreter, whl_path = whl_path, patches = patches, - quiet = rctx.attr.quiet, - timeout = rctx.attr.timeout, ) whl_extract(rctx, whl_path = whl_path, logger = logger) @@ -436,6 +332,12 @@ def _whl_library_impl(rctx): read_fn = rctx.read, logger = logger, ) + rctx.file("metadata.json", json.encode_indent({ + "name": metadata.name, + "provides_extra": metadata.provides_extra, + "requires_dist": metadata.requires_dist, + "version": metadata.version, + })) namespace_package_files = pypi_repo_utils.find_namespace_package_files(rctx, install_dir_path) entry_points = _get_entry_points(rctx, install_dir_path, metadata) @@ -443,10 +345,10 @@ def _whl_library_impl(rctx): build_file_contents = generate_whl_library_build_bazel( name = whl_path.basename, - sdist_filename = sdist_filename, dep_template = rctx.attr.dep_template or "@{}{{name}}//:{{target}}".format( rctx.attr.repo_prefix, ), + sdist_filename = sdist_filename, config_load = rctx.attr.config_load, metadata_name = metadata.name, metadata_version = metadata.version, @@ -490,6 +392,148 @@ repo( return None +def _whl_archive_impl(rctx): + logger = repo_utils.logger(rctx) + + whl_path = None + if rctx.attr.whl_file: + rctx.watch(rctx.attr.whl_file) + whl_path = rctx.path(rctx.attr.whl_file) + + # Simulate the behaviour where the whl is present in the current directory. + rctx.symlink(whl_path, whl_path.basename) + whl_path = rctx.path(whl_path.basename) + elif rctx.attr.urls and rctx.attr.filename: + filename = rctx.attr.filename + urls = rctx.attr.urls + urls = [ + urllib.absolute_url( + rctx.attr.index_url, + url, + envsubst = rctx.attr.envsubst, + getenv = rctx.getenv, + ) + for url in urls + ] + result = rctx.download( + url = urls, + output = filename, + sha256 = rctx.attr.sha256, + integrity = rctx.attr.integrity if not rctx.attr.sha256 else "", + auth = get_auth(rctx, urls), + ) + if not rctx.attr.sha256 and not rctx.attr.integrity: + # this is only seen when there is a direct URL reference without a hash + logger.warn("Please update the requirement line to include the hash:\n{} \\\n --hash=sha256:{}".format( + rctx.attr.requirement, + result.sha256, + )) + + if not result.success: + fail("could not download the '{}' from {}:\n{}".format(filename, urls, result)) + + if filename.endswith(".whl"): + whl_path = rctx.path(filename) + else: + fail("Only wheels are supported") + else: + fail("Either 'whl_file' or 'urls' and 'filename' needs to be specified") + + return _whl_extract(rctx, whl_path = whl_path, logger = logger) + +def _pip_archive_impl(rctx): + logger = repo_utils.logger(rctx) + + sdist_filename = None + extra_pip_args = [] + extra_pip_args.extend(rctx.attr.extra_pip_args) + if rctx.attr.urls and rctx.attr.filename: + filename = rctx.attr.filename + urls = rctx.attr.urls + urls = [ + urllib.absolute_url( + rctx.attr.index_url, + url, + envsubst = rctx.attr.envsubst, + getenv = rctx.getenv, + ) + for url in urls + ] + result = rctx.download( + url = urls, + output = filename, + sha256 = rctx.attr.sha256, + integrity = rctx.attr.integrity if not rctx.attr.sha256 else "", + auth = get_auth(rctx, urls), + ) + if not rctx.attr.sha256 and not rctx.attr.integrity: + # this is only seen when there is a direct URL reference without a hash + logger.warn("Please update the requirement line to include the hash:\n{} \\\n --hash=sha256:{}".format( + rctx.attr.requirement, + result.sha256, + )) + + if not result.success: + fail("could not download the '{}' from {}:\n{}".format(filename, urls, result)) + + if filename.endswith(".whl"): + fail("Only sdists are supported") + else: + sdist_filename = filename + + # It is an sdist and we need to tell PyPI to use a file in this directory + # and, allow getting build dependencies from PYTHONPATH, which we + # setup in this repository rule, but still download any necessary + # build deps from PyPI (e.g. `flit_core`) if they are missing. + extra_pip_args.extend(["--find-links", "."]) + + # When we already have a wheel, Python isn't used, + # so there's no need to setup env vars to run Python, unless we need to + # build an sdist or resolve a requirement. + python_interpreter = pypi_repo_utils.resolve_python_interpreter( + rctx, + python_interpreter = rctx.attr.python_interpreter, + python_interpreter_target = rctx.attr.python_interpreter_target, + ) + args = [ + "-m", + "python.private.pypi.whl_installer.wheel_installer", + "--requirement", + rctx.attr.requirement, + ] + args = _parse_optional_attrs(rctx, args, extra_pip_args) + + # Manually construct the PYTHONPATH since we cannot use the toolchain here + environment = _create_repository_execution_environment(rctx, python_interpreter, logger = logger) + + if rctx.attr.urls: + op_tmpl = "whl_library.BuildWheelFromSource({name}, {requirement})" + elif rctx.attr.download_only: + op_tmpl = "whl_library.DownloadWheel({name}, {requirement})" + else: + op_tmpl = "whl_library.ResolveRequirement({name}, {requirement})" + + pypi_repo_utils.execute_checked( + rctx, + # truncate the requirement value when logging it / reporting + # progress since it may contain several ' --hash=sha256:... + # --hash=sha256:...' substrings that fill up the console + python = python_interpreter, + op = op_tmpl.format(name = rctx.attr.name, requirement = rctx.attr.requirement.split(" ", 1)[0]), + arguments = args, + environment = environment, + srcs = rctx.attr._python_srcs, + quiet = rctx.attr.quiet, + timeout = rctx.attr.timeout, + logger = logger, + ) + + whl_path = rctx.path(json.decode(rctx.read("whl_file.json"))["whl_file"]) + if not rctx.delete("whl_file.json"): + fail("failed to delete the whl_file.json file") + + return _whl_extract(rctx, whl_path = whl_path, logger = logger, sdist_filename = sdist_filename) + def _remove_files(rctx, *basenames): paths = list(rctx.path(".").readdir()) for _ in range(10000000): @@ -503,7 +547,7 @@ def _remove_files(rctx, *basenames): paths.extend(path.readdir()) # NOTE @aignas 2024-03-21: The usage of dict({}, **common) ensures that all args to `dict` are unique -whl_library_attrs = dict({ +_pip_archive_attrs = dict({ "annotation": attr.label( doc = ( "Optional json encoded file containing annotation to apply to the extracted wheel. " + @@ -537,6 +581,16 @@ For example if your whl depends on `numpy` and your Python package repo is named "index_url": attr.string( doc = "The index_url that the package will be downloaded from.", ), + "integrity": attr.string( + doc = """\ +The expected checksum of the downloaded whl in Subresource Integrity format +(e.g. `sha256-...` or `sha512-...`). Only used when `urls` is passed. If +`sha256` is also set, it takes precedence over this attribute. + +:::{versionadded} 2.3.0 +::: +""", + ), "repo": attr.string( doc = "Pointer to parent repo name. Used to make these rules rerun if the parent repo changes.", ), @@ -559,9 +613,6 @@ DEPRECATED. Only left for people who vendor requirements.bzl. The list of urls of the whl to be downloaded using bazel downloader. Using this attr makes `extra_pip_args` and `download_only` ignored.""", ), - "whl_file": attr.label( - doc = "The whl file that should be used instead of downloading or building the whl.", - ), "whl_patches": attr.label_keyed_string_dict( doc = """ A label-keyed-string dict with patch files as keys and json-strings as values. @@ -609,10 +660,10 @@ way to define whl_library and move whl patching to a separate place. INTERNAL US ), "_rule_name": attr.string(default = "whl_library"), }, **ATTRS) -whl_library_attrs.update(AUTH_ATTRS) +_pip_archive_attrs.update(AUTH_ATTRS) -whl_library = repository_rule( - attrs = whl_library_attrs, +pip_archive = repository_rule( + attrs = _pip_archive_attrs, doc = """ Download and extracts a single wheel based into a bazel repo based on the requirement string passed in. Instantiated from pip_repository and inherits config options from there. @@ -621,10 +672,144 @@ Instantiated from pip_repository and inherits config options from there. The `whl_library` is marked as reproducible if using starlark to extract and parse the wheel contents without building an `sdist` first. ::: + +:::{versionchanged} 2.3.0 +The whl-only pure Starlark operations have been refactored into {obj}`whl_archive` and the +previously named {obj}`whl_library` repository became renamed to `pip_archive`. +::: """, - implementation = _whl_library_impl, + implementation = _pip_archive_impl, environ = [ "RULES_PYTHON_PIP_ISOLATED", REPO_DEBUG_ENV_VAR, ], ) + +whl_archive = repository_rule( + attrs = { + k: _pip_archive_attrs[k] + for k in [ + "annotation", + "config_load", + "dep_template", + "filename", + "group_deps", + "group_name", + "index_url", + "integrity", + "repo_prefix", + "requirement", + "sha256", + "urls", + "whl_patches", + "enable_implicit_namespace_pkgs", + "envsubst", + "pip_data_exclude", + ] + } | { + "whl_file": attr.label( + doc = "The whl file that should be used instead of downloading or building the whl.", + ), + } | AUTH_ATTRS, + doc = """ +Download and extracts a single wheel based into a bazel repo based on the requirement string passed in. + +Does not depend on any python. +""", + implementation = _whl_archive_impl, + environ = [ + REPO_DEBUG_ENV_VAR, + ], +) + +def _whl_deps_library_impl(rctx): + logger = repo_utils.logger(rctx) + + if rctx.attr.metadata_file and rctx.attr.metadata: + logger.fail("Only one of 'metadata_file' and 'metadata' can be specified") + return + if not (rctx.attr.metadata_file or rctx.attr.metadata): + logger.fail("At least one of 'metadata_file' and 'metadata' must be specified") + return + + if rctx.attr.metadata_file: + metadata_contents = rctx.read(rctx.attr.metadata_file) + else: + metadata_contents = rctx.attr.metadata + + metadata = struct(**json.decode(metadata_contents)) + + build_file_contents = generate_whl_library_build_bazel( + dep_template = rctx.attr.dep_template or "@{}{{name}}//:{{target}}".format( + rctx.attr.repo_prefix, + ), + config_load = rctx.attr.config_load, + metadata_name = metadata.name, + metadata_version = metadata.version, + requires_dist = metadata.requires_dist, + group_deps = rctx.attr.group_deps, + group_name = rctx.attr.group_name, + repo = rctx.attr.repo or ( + str(rctx.attr.metadata_file) if rctx.attr.metadata_file else None + ), + extras = requirement(rctx.attr.requirement).extras, + ) + rctx.file("BUILD.bazel", build_file_contents) + +whl_deps_library = repository_rule( + attrs = { + k: _pip_archive_attrs[k] + for k in [ + "config_load", + "dep_template", + "group_deps", + "group_name", + "requirement", + ] + } | { + "metadata": attr.string( + doc = """ +The subset of the METADATA contents that is needed for generation of the dependencies. +* name: {type}`str` +* version: {type}`str` +* provides_extra: {type}`list[str]` +* requires_dist: {type}`list[str]` +""", + ), + "metadata_file": attr.label(doc = "An alternative way to pass {attr}`metadata` but as a file."), + "repo": attr.label(doc = "A label at the root of the repo to get stuff from."), + }, + doc = """ +A repo rule that reuses the sources from a different place and then creates the necessary targets +so that this can be used in the repo. + +Does not depend on any python. +""", + implementation = _whl_deps_library_impl, + environ = [REPO_DEBUG_ENV_VAR], +) + +def whl_library(name, repo = None, **kwargs): + """Create a whl_library. + + This proxies to one of the underlying implementations: + * {obj}`whl_archive` + * {obj}`pip_archive` + + Args: + name: {type}`str` The name of the repo. + repo: Unused, will be dropped in the next major release. + **kwargs: The args passed to the underlying implementation. + + Returns: + the repo metadata. + """ + _ = repo # buildifier: disable=unused-variable + + whl_file = kwargs.get("whl_file") + urls = kwargs.get("urls", []) + filename = kwargs.get("filename") + if whl_file or (urls and filename and filename.endswith(".whl")): + whl_archive(name = name, **kwargs) + else: + pip_archive(name = name, **kwargs) diff --git a/python/private/pypi/whl_library_targets.bzl b/python/private/pypi/whl_library_targets.bzl index 01a89aadcc..b7fdbd55e9 100644 --- a/python/private/pypi/whl_library_targets.bzl +++ b/python/private/pypi/whl_library_targets.bzl @@ -19,6 +19,7 @@ load("//python:py_binary.bzl", "py_binary") load("//python:py_library.bzl", "py_library") load("//python/private:normalize_name.bzl", "normalize_name") load(":env_marker_setting.bzl", "env_marker_setting") +load(":gen_wheel_record.bzl", "gen_wheel_record") load( ":labels.bzl", "DATA_LABEL", @@ -26,6 +27,8 @@ load( "EXTRACTED_WHEEL_FILES", "PY_LIBRARY_IMPL_LABEL", "PY_LIBRARY_PUBLIC_LABEL", + "PY_SRCS_LABEL", + "WHEEL_FILE", "WHEEL_FILE_IMPL_LABEL", "WHEEL_FILE_PUBLIC_LABEL", ) @@ -47,23 +50,33 @@ _BAZEL_REPO_FILE_GLOBS = [ _IS_VENV_SITE_PACKAGES_YES = Label("//python/config_settings:_is_venvs_site_packages_yes") _VENV_SITE_PACKAGES_FLAG = Label("//python/config_settings:venvs_site_packages") -def whl_library_targets_from_requires( +def whl_library_targets( *, name, metadata_name = "", - metadata_version = "", requires_dist = [], extras = [], entry_points = {}, include = [], group_deps = [], + group_name = None, + dep_template = None, + data_exclude = [], + enable_implicit_namespace_pkgs = False, + sdist_filename = None, + namespace_package_files = [], + filegroups = None, + copy_files = {}, + copy_executables = {}, + srcs_exclude = [], + data = [], + visibility = ["//visibility:public"], **kwargs): """The macro to create whl targets from the METADATA. Args: name: {type}`str` The wheel filename metadata_name: {type}`str` The package name as written in wheel `METADATA`. - metadata_version: {type}`str` The package version as written in wheel `METADATA`. group_deps: {type}`list[str]` names of fellow members of the group (if any). These will be excluded from generated deps lists so as to avoid direct cycles. These dependencies will be provided at runtime by the @@ -73,70 +86,81 @@ def whl_library_targets_from_requires( extras: {type}`list[str]` The list of requested extras. This essentially includes extra transitive dependencies in the final targets depending on the wheel `METADATA`. entry_points: {type}`list[dict]` A list of parsed entry point definitions. include: {type}`list[str]` The list of packages to include. - **kwargs: Extra args passed to the {obj}`whl_library_targets` + group_name: {type}`str | None` name of the dependency group (if any). + dep_template: {type}`str | None` The dep_template to use. + data_exclude: {type}`list[str]` The globs for data attribute exclusion. + enable_implicit_namespace_pkgs: {type}`boolean` generate __init__.py files for namespace pkgs. + sdist_filename: {type}`str | None` The filename of the sdist. + namespace_package_files: {type}`list[str]` A list of labels of files whose directories are namespace packages. + filegroups: {type}`dict[str, list[str]] | None` A dictionary of the target names and the glob matches. + copy_files: {type}`dict[str, str]` The mapping between src and dest locations. + copy_executables: {type}`dict[str, str]` The mapping between src and dest locations for executables. + srcs_exclude: {type}`list[str]` The globs for srcs attribute exclusion. + data: {type}`list[str]` A list of labels to include as part of the `data` attribute. + visibility: {type}`list[str]` The visibility of the targets. + **kwargs: Extra args passed to the {obj}`whl_library_deps_targets` and {obj}`whl_library_srcs`. """ - package_deps = _parse_requires_dist( - name = metadata_name, - requires_dist = requires_dist, - excludes = group_deps, - extras = extras, - include = include, - ) - - whl_library_targets( + create_extra_targets = bool(requires_dist or group_name) and dep_template + whl_library_srcs( name = name, - dependencies = package_deps.deps, - dependencies_with_markers = package_deps.deps_select, + sdist_filename = sdist_filename, + data_exclude = data_exclude, + srcs_exclude = srcs_exclude, + filegroups = filegroups, entry_points = entry_points, - tags = [ - "pypi_name={}".format(metadata_name), - "pypi_version={}".format(metadata_version), - ], + visibility = visibility, + data = data, + copy_files = copy_files, + copy_executables = copy_executables, + enable_implicit_namespace_pkgs = enable_implicit_namespace_pkgs, + namespace_package_files = namespace_package_files, + # If there are no dependencies, then let's create the targets with public labels. + # Note, we are not supporting grouping the packages in this case, but that is fine. + whl_name = WHEEL_FILE if create_extra_targets else WHEEL_FILE_PUBLIC_LABEL, + pkg_name = PY_SRCS_LABEL if create_extra_targets else PY_LIBRARY_PUBLIC_LABEL, **kwargs ) -def _parse_requires_dist( - *, - name, - requires_dist, - excludes, - include, - extras): - return deps( - name = normalize_name(name), - requires_dist = requires_dist, - excludes = excludes, - include = include, - extras = extras, - ) + if create_extra_targets: + whl_library_deps_targets( + name = name, + metadata_name = metadata_name, + requires_dist = requires_dist, + group_deps = group_deps, # only needed if requires_dist is present + extras = extras, # only needed if requires_dist is present + include = include, # only needed if requires_dist is present + group_name = group_name, # only needed if requires_dist is present + dep_template = dep_template, # only needed if requires_dist is present + repo = None, # set aliases in the same repo + aliases = {}, + **kwargs + ) -def whl_library_targets( +def whl_library_srcs( *, name, - dep_template, sdist_filename = None, data_exclude = [], srcs_exclude = [], tags = [], - dependencies = [], filegroups = None, - dependencies_by_platform = {}, - dependencies_with_markers = {}, entry_points = {}, - group_deps = [], - group_name = "", data = [], copy_files = {}, copy_executables = {}, native = native, enable_implicit_namespace_pkgs = False, namespace_package_files = [], + whl_name = WHEEL_FILE, + pkg_name = PY_SRCS_LABEL, + visibility = ["//visibility:public"], rules = struct( copy_file = copy_file, py_binary = py_binary, py_library = py_library, venv_entry_point = venv_entry_point, venv_rewrite_shebang = venv_rewrite_shebang, + gen_wheel_record = gen_wheel_record, env_marker_setting = env_marker_setting, create_inits = _create_inits, )): @@ -145,27 +169,13 @@ def whl_library_targets( Args: name: {type}`str` The file to match for including it into the `whl` filegroup. This may be also parsed to generate extra metadata. - dep_template: {type}`str` The dep_template to use for dependency - interpolation. sdist_filename: {type}`str | None` If the wheel was built from an sdist, the filename of the sdist. + visibility: {type}`list[str]` The visibility of the source targets. tags: {type}`list[str]` The tags set on the `py_library`. - dependencies: {type}`list[str]` A list of dependencies. - dependencies_by_platform: {type}`dict[str, list[str]]` A list of - dependencies by platform key. - dependencies_with_markers: {type}`dict[str, str]` A marker to evaluate - in order for the dep to be included. entry_points: {type}`list[dict]` A list of parsed entry point definitions. filegroups: {type}`dict[str, list[str]] | None` A dictionary of the target names and the glob matches. If `None`, defaults will be used. - group_name: {type}`str` name of the dependency group (if any) which - contains this library. If set, this library will behave as a shim - to group implementation rules which will provide simultaneously - installed dependencies which would otherwise form a cycle. - group_deps: {type}`list[str]` names of fellow members of the group (if - any). These will be excluded from generated deps lists so as to avoid - direct cycles. These dependencies will be provided at runtime by the - group rules which wrap this library and its fellows together. copy_executables: {type}`dict[str, str]` The mapping between src and dest locations for the targets. copy_files: {type}`dict[str, str]` The mapping between src and @@ -177,16 +187,13 @@ def whl_library_targets( data: {type}`list[str]` A list of labels to include as part of the `data` attribute in `py_library`. enable_implicit_namespace_pkgs: {type}`boolean` generate __init__.py files for namespace pkgs. - native: {type}`native` The native struct for overriding in tests. namespace_package_files: {type}`list[str]` A list of labels of files whose directories are namespace packages. + whl_name: {type}`str` The label name to use for the wheel filegroup target. + pkg_name: {type}`str` The label name to use for the py_library target. + native: {type}`native` The native struct for overriding in tests. rules: {type}`struct` A struct with references to rules for creating targets. """ - dependencies = sorted([normalize_name(d) for d in dependencies]) - dependencies_by_platform = { - platform: sorted([normalize_name(d) for d in deps]) - for platform, deps in dependencies_by_platform.items() - } tags = sorted(tags) data = [] + data @@ -220,12 +227,23 @@ def whl_library_targets( bins_for_data_label.append(rewrite_target_name) data.append(rewrite_target_name) + record_srcs = native.glob(["rewrite-record/*/RECORD"], allow_empty = True) + record_target_name = "record" + if record_srcs: + rules.gen_wheel_record( + name = record_target_name, + srcs = record_srcs, + tags = ["manual"], + ) + data.append(record_target_name) + if filegroups == None: filegroups = { EXTRACTED_WHEEL_FILES: dict( include = ["**"], - exclude = ( - _BAZEL_REPO_FILE_GLOBS + + # The Bazel repo files are always excluded; only the sdist + # filename is conditional on `sdist_filename`. + exclude = _BAZEL_REPO_FILE_GLOBS + ( [sdist_filename] if sdist_filename else [] ), ), @@ -242,10 +260,12 @@ def whl_library_targets( srcs = native.glob(**glob_kwargs) if filegroup_name == DATA_LABEL: srcs = srcs + bins_for_data_label + if filegroup_name == DIST_INFO_LABEL and record_srcs: + srcs = srcs + [record_target_name] native.filegroup( name = filegroup_name, srcs = srcs, - visibility = ["//visibility:public"], + visibility = visibility, ) for src, dest in copy_files.items(): @@ -253,7 +273,7 @@ def whl_library_targets( name = dest + ".copy", src = src, out = dest, - visibility = ["//visibility:public"], + visibility = visibility, ) data.append(dest) for src, dest in copy_executables.items(): @@ -262,100 +282,15 @@ def whl_library_targets( src = src, out = dest, is_executable = True, - visibility = ["//visibility:public"], + visibility = visibility, ) data.append(dest) - _config_settings( - dependencies_by_platform = dependencies_by_platform.keys(), - dependencies_with_markers = dependencies_with_markers, - native = native, - rules = rules, - visibility = ["//visibility:private"], - ) - deps_conditional = { - d: "is_include_{}_true".format(d) - for d in dependencies_with_markers - } - - # Ensure this list is normalized - # Note: mapping used as set - group_deps = { - normalize_name(d): True - for d in group_deps - } - - dependencies = [ - d - for d in dependencies - if d not in group_deps - ] - dependencies_by_platform = { - p: deps - for p, deps in dependencies_by_platform.items() - for deps in [[d for d in deps if d not in group_deps]] - if deps - } - - # If this library is a member of a group, its public label aliases need to - # point to the group implementation rule not the implementation rules. We - # also need to mark the implementation rules as visible to the group - # implementation. - if group_name and "//:" in dep_template: - # This is the legacy behaviour where the group library is outside the hub repo - # - # It is expected to disappear when we drop WORKSPACE or drop the vendoring of - # pip_parse `requirements.bzl` in WORKSPACE. The alternative would be to add - # another argument to the macro, but it is already full of arguments. - label_tmpl = dep_template.format( - name = "_config", - target = normalize_name(group_name) + "_{}", - ).replace( - "//:", - "//_groups:", - ) - impl_vis = [dep_template.format( - name = "_config", - target = "__pkg__", - ).replace( - "//:", - "//_groups:", - )] - - native.alias( - name = PY_LIBRARY_PUBLIC_LABEL, - actual = label_tmpl.format(PY_LIBRARY_PUBLIC_LABEL), - visibility = ["//visibility:public"], - ) - native.alias( - name = WHEEL_FILE_PUBLIC_LABEL, - actual = label_tmpl.format(WHEEL_FILE_PUBLIC_LABEL), - visibility = ["//visibility:public"], - ) - py_library_label = PY_LIBRARY_IMPL_LABEL - whl_file_label = WHEEL_FILE_IMPL_LABEL - - elif group_name: - py_library_label = PY_LIBRARY_PUBLIC_LABEL - whl_file_label = WHEEL_FILE_PUBLIC_LABEL - impl_vis = [dep_template.format(name = "", target = "__subpackages__")] - - else: - py_library_label = PY_LIBRARY_PUBLIC_LABEL - whl_file_label = WHEEL_FILE_PUBLIC_LABEL - impl_vis = ["//visibility:public"] - if hasattr(native, "filegroup"): native.filegroup( - name = whl_file_label, + name = whl_name, srcs = [name], - data = _deps( - deps = dependencies, - deps_by_platform = dependencies_by_platform, - deps_conditional = deps_conditional, - tmpl = dep_template.format(name = "{}", target = WHEEL_FILE_PUBLIC_LABEL), - ), - visibility = impl_vis, + visibility = visibility, ) if hasattr(rules, "py_library"): @@ -409,41 +344,201 @@ def whl_library_targets( data = data + [DATA_LABEL] rules.py_library( - name = py_library_label, + name = pkg_name, srcs = srcs, pyi_srcs = pyi_srcs, data = data, # This makes this directory a top-level in the python import # search path for anything that depends on this. imports = ["site-packages"], + tags = tags, + visibility = visibility, + experimental_venvs_site_packages = _VENV_SITE_PACKAGES_FLAG, + namespace_package_files = namespace_package_files, + ) + +def whl_library_deps_targets( + *, + name = None, + repo, + aliases = None, + metadata_name, + requires_dist, + extras, + include = [], + group_deps = [], + group_name = None, + dep_template, + tags = [], + visibility = ["//visibility:public"], + native = native, + rules = struct( + copy_file = copy_file, + py_binary = py_binary, + py_library = py_library, + venv_entry_point = venv_entry_point, + venv_rewrite_shebang = venv_rewrite_shebang, + env_marker_setting = env_marker_setting, + create_inits = _create_inits, + )): + """Create all of the whl_library targets. + + Args: + name: {type}`str` The wheel filename + metadata_name: {type}`str` The package name as written in wheel `METADATA`. + group_deps: {type}`list[str]` names of fellow members of the group (if + any). These will be excluded from generated deps lists so as to avoid + direct cycles. These dependencies will be provided at runtime by the + group rules which wrap this library and its fellows together. + requires_dist: {type}`list[str]` The list of `Requires-Dist` values from + the whl `METADATA`. + extras: {type}`list[str]` The list of requested extras. This essentially includes extra transitive dependencies in the final targets depending on the wheel `METADATA`. + include: {type}`list[str]` The list of packages to include. + group_name: {type}`str | None` name of the dependency group (if any). + dep_template: {type}`str | None` The dep_template to use. + tags: {type}`list[str]` The tags set on the targets. + repo: {type}`str | Label | None` The BUILD.bazel label to the parent repo that has the + sources. If none, then will take the targets from the current dir. + aliases: {type}`dict[str, str] | None` The list of aliases to create in the parent repo. If None, will create + the default values. Empty list means no aliases. + visibility: {type}`list[str]` The visibility of the targets. + native: {type}`native` The native struct for overriding in tests. + rules: {type}`struct` A struct with references to rules for creating targets. + """ + repo_label = Label(repo).same_package_label if repo else (lambda x: x) + if aliases == None: + aliases = { + EXTRACTED_WHEEL_FILES: repo_label(EXTRACTED_WHEEL_FILES), + DIST_INFO_LABEL: repo_label(DIST_INFO_LABEL), + DATA_LABEL: repo_label(DATA_LABEL), + } + + # If this library is a member of a group, its public label aliases need to + # point to the group implementation rule not the implementation rules. We + # also need to mark the implementation rules as visible to the group + # implementation. + if group_name and "//:" in dep_template: + # This is the legacy behaviour where the group library is outside the hub repo + # + # It is expected to disappear when we drop WORKSPACE or drop the vendoring of + # pip_parse `requirements.bzl` in WORKSPACE. The alternative would be to add + # another argument to the macro, but it is already full of arguments. + label_tmpl = dep_template.format( + name = "_config", + target = normalize_name(group_name) + "_{}", + ).replace( + "//:", + "//_groups:", + ) + aliases = aliases | { + PY_LIBRARY_PUBLIC_LABEL: label_tmpl.format(PY_LIBRARY_PUBLIC_LABEL), + WHEEL_FILE_PUBLIC_LABEL: label_tmpl.format(WHEEL_FILE_PUBLIC_LABEL), + } + impl_vis = [dep_template.format( + name = "_config", + target = "__pkg__", + ).replace( + "//:", + "//_groups:", + )] + + py_library_label = PY_LIBRARY_IMPL_LABEL + whl_file_label = WHEEL_FILE_IMPL_LABEL + else: + py_library_label = PY_LIBRARY_PUBLIC_LABEL + whl_file_label = WHEEL_FILE_PUBLIC_LABEL + if group_name: + impl_vis = [dep_template.format(name = "", target = "__subpackages__")] + else: + impl_vis = visibility + + if not requires_dist: + # If the package is in a group but has no deps, we still need the public labels to + # point at the srcs targets so that the group implementation can use them. We don't + # need any of the extra targets, so just create the aliases. + aliases = aliases | { + py_library_label: repo_label(PY_SRCS_LABEL), + whl_file_label: repo_label(WHEEL_FILE), + } + + for alias, actual in aliases.items(): + native.alias( + name = alias, + actual = actual, + visibility = visibility, + ) + + if not requires_dist: + # If there are extras, then they will be visible in requires_dist. + return + + package_deps = _parse_requires_dist( + name = metadata_name, + requires_dist = requires_dist, + excludes = group_deps, + extras = extras, + include = include, + ) + + _config_settings( + dependencies_with_markers = package_deps.deps_select, + rules = rules, + visibility = ["//visibility:private"], + ) + + if hasattr(native, "filegroup"): + # We include the whl file as srcs so that `$(location :whl)` expands to the whl file. + # The transitive dependencies are available via the `data` attribute. + native.filegroup( + name = whl_file_label, + srcs = [repo_label(WHEEL_FILE)], + data = _deps( + deps = [], + package_deps = package_deps, + tmpl = dep_template.format(name = "{}", target = WHEEL_FILE_PUBLIC_LABEL), + ), + visibility = impl_vis, + ) + + if hasattr(rules, "py_library"): + rules.py_library( + name = py_library_label, + # We include as srcs to ensure that the (locations :pkg) works as expected. + srcs = [repo_label(PY_SRCS_LABEL)], deps = _deps( - deps = dependencies, - deps_by_platform = dependencies_by_platform, - deps_conditional = deps_conditional, + # We include as deps, so that `PyInfo` and friends (e.g. `pyi_srcs`) get + # propagated. Just passing the target as `srcs` is not enough to propagate + # `pyi_srcs`, see `tests/base_rules/py_library`. + deps = [repo_label(PY_SRCS_LABEL)], + package_deps = package_deps, tmpl = dep_template.format(name = "{}", target = PY_LIBRARY_PUBLIC_LABEL), ), tags = tags, visibility = impl_vis, - experimental_venvs_site_packages = _VENV_SITE_PACKAGES_FLAG, - namespace_package_files = namespace_package_files, ) -def _config_settings(dependencies_by_platform, dependencies_with_markers, rules, native = native, **kwargs): +def _parse_requires_dist( + *, + name, + requires_dist, + excludes, + include, + extras): + return deps( + name = normalize_name(name), + requires_dist = requires_dist, + excludes = excludes, + include = include, + extras = extras, + ) + +def _config_settings(dependencies_with_markers, rules, **kwargs): """Generate config settings for the targets. Args: - dependencies_by_platform: {type}`list[str]` platform keys, can be - one of the following formats: - * `//conditions:default` - * `@platforms//os:{value}` - * `@platforms//cpu:{value}` - * `@//python/config_settings:is_python_3.{minor_version}` - * `{os}_{cpu}` - * `cp3{minor_version}_{os}_{cpu}` dependencies_with_markers: {type}`dict[str, str]` The markers to evaluate by each dep. rules: used for testing - native: {type}`native` The native struct for overriding in tests. **kwargs: Extra kwargs to pass to the rule. """ for dep, expression in dependencies_with_markers.items(): @@ -453,70 +548,13 @@ def _config_settings(dependencies_by_platform, dependencies_with_markers, rules, **kwargs ) - for p in dependencies_by_platform: - if p.startswith("@") or p.endswith("default"): - continue +def _deps(deps, package_deps, tmpl): + deps = [] + deps + [tmpl.format(d) for d in sorted(package_deps.deps)] - # TODO @aignas 2025-04-20: add tests here - abi, _, tail = p.partition("_") - if not abi.startswith("cp"): - tail = p - abi = "" - os, _, arch = tail.partition("_") - - _kwargs = dict(kwargs) - _kwargs["constraint_values"] = [ - "@platforms//cpu:{}".format(arch), - "@platforms//os:{}".format(os), - ] - - if abi: - _kwargs["flag_values"] = { - Label("//python/config_settings:python_version"): "3.{}".format(abi[len("cp3"):]), - } - - native.config_setting( - name = "is_{name}".format( - name = p.replace("cp3", "python_3."), - ), - **_kwargs - ) - -def _plat_label(plat): - if plat.endswith("default"): - return plat - elif plat.startswith("@//"): - return Label(plat.strip("@")) - elif plat.startswith("@"): - return plat - else: - return ":is_" + plat.replace("cp3", "python_3.") - -def _deps(deps, deps_by_platform, deps_conditional, tmpl): - deps = [tmpl.format(d) for d in sorted(deps)] - - for dep, setting in deps_conditional.items(): + for dep in package_deps.deps_select: deps = deps + select({ - ":{}".format(setting): [tmpl.format(dep)], + ":is_include_{}_true".format(dep): [tmpl.format(dep)], "//conditions:default": [], }) - if not deps_by_platform: - return deps - - deps_by_platform = { - _plat_label(p): [ - tmpl.format(d) - for d in sorted(deps) - ] - for p, deps in sorted(deps_by_platform.items()) - } - - # Add the default, which means that we will be just using the dependencies in - # `deps` for platforms that are not handled in a special way by the packages - deps_by_platform.setdefault("//conditions:default", []) - - if not deps: - return select(deps_by_platform) - else: - return deps + select(deps_by_platform) + return deps diff --git a/python/private/pypi/whl_repo_name.bzl b/python/private/pypi/whl_repo_name.bzl index 29d774c361..739ff2a4a1 100644 --- a/python/private/pypi/whl_repo_name.bzl +++ b/python/private/pypi/whl_repo_name.bzl @@ -18,25 +18,31 @@ load("//python/private:normalize_name.bzl", "normalize_name") load(":parse_whl_name.bzl", "parse_whl_name") -def whl_repo_name(filename, sha256, *target_platforms): +def whl_repo_name(filename, digest, *target_platforms): """Return a valid whl_library repo name given a distribution filename. Args: filename: {type}`str` the filename of the distribution. - sha256: {type}`str` the sha256 of the distribution. + digest: {type}`str` the digest of the distribution, either as a + canonical `:` string or a bare hex digest. *target_platforms: {type}`list[str]` the extra suffixes to append. Only used when we need to support different extras per version. Returns: a string that can be used in {obj}`whl_library`. """ + + # Strip the `:` prefix so that the name only contains the hex digest + # and stays the same as it has always been for bare sha256 values. + digest = digest.rpartition(":")[2] + parts = [] if not filename.endswith(".whl"): # Then the filename is basically foo-3.2.1. name, _, tail = filename.rpartition("-") parts.append(normalize_name(name)) - if sha256: + if digest: parts.append("sdist") version = "" else: @@ -56,8 +62,8 @@ def whl_repo_name(filename, sha256, *target_platforms): parts.append(abi_tag) parts.append(platform_tag) - if sha256: - parts.append(sha256[:8]) + if digest: + parts.append(digest[:8]) elif version: parts.insert(1, version) diff --git a/python/private/pyproject_utils.bzl b/python/private/pyproject_utils.bzl new file mode 100644 index 0000000000..e96ffd2023 --- /dev/null +++ b/python/private/pyproject_utils.bzl @@ -0,0 +1,51 @@ +"""Utilities for reading values from pyproject.toml.""" + +load("@toml.bzl", "toml") +load(":version.bzl", "version") + +def read_pyproject(module_ctx, pyproject): + """Read a pyproject.toml file and return the relevant fields. + + The file is parsed with a pure-Starlark TOML decoder; no Python + interpreter is required. The raw `requires-python` value is returned + as-is so that callers can decide how to interpret it. + + Args: + module_ctx: the module extension context (needs `path` and `read`). + pyproject: {type}`Label` pointing at the pyproject.toml file. + + Returns: + {type}`struct` with the attributes: + * `requires_python`: {type}`str | None` the raw `requires-python` + value (e.g. `"==3.13.9"`), or `None` if it is not set. + """ + data = toml.decode(module_ctx.read(module_ctx.path(pyproject), watch = "yes")) + return struct( + requires_python = data.get("project", {}).get("requires-python"), + ) + +def version_from_requires_python(requires_python): + """Derive a concrete Python version from a `requires-python` value. + + Currently only an exact `==X.Y.Z` specifier is supported. The value is + validated and normalized via {obj}`//python/private:version.bzl` so that + malformed input fails in a consistent way. Broader specifier support + (e.g. `>=`, `X.Y`) can be layered on here in the future. + + Args: + requires_python: {type}`str` the raw `requires-python` value. + + Returns: + {type}`str` the normalized version string (e.g. `"3.13.9"`). + """ + if not requires_python: + fail("`requires-python` must be specified") + + if not requires_python.startswith("=="): + fail("`requires-python` must pin an exact version with `==`, got: {}".format(requires_python)) + + bare_version = requires_python[len("=="):].strip() + + # Parse strictly so malformed versions fail cleanly, then normalize. + version.parse(bare_version, strict = True) + return version.normalize(bare_version) diff --git a/python/private/python.bzl b/python/private/python.bzl index 6abc81e3d2..8932d00e1b 100644 --- a/python/private/python.bzl +++ b/python/private/python.bzl @@ -15,10 +15,12 @@ "Python toolchain module extensions for use with bzlmod." load("@bazel_features//:features.bzl", "bazel_features") -load("//python:versions.bzl", "DEFAULT_RELEASE_BASE_URL", "PLATFORMS", "TOOL_VERSIONS") +load("//python:versions.bzl", "DEFAULT_RELEASE_BASE_URLS", "PLATFORMS") load(":auth.bzl", "AUTH_ATTRS") load(":full_version.bzl", "full_version") +load(":pbs_manifest.bzl", "parse_runtime_manifest") load(":platform_info.bzl", "platform_info") +load(":pyproject_utils.bzl", "read_pyproject", "version_from_requires_python") load(":python_register_toolchains.bzl", "python_register_toolchains") load(":pythons_hub.bzl", "hub_repo") load(":repo_utils.bzl", "repo_utils") @@ -76,7 +78,7 @@ def parse_modules(*, module_ctx, logger = None, _fail = fail): # Map of string Major.Minor or Major.Minor.Patch to the toolchain_info struct global_toolchain_versions = {} - config = _get_toolchain_config(modules = module_ctx.modules, _fail = _fail) + config = _get_toolchain_config(mctx = module_ctx, modules = module_ctx.modules, _fail = _fail) default_python_version = _compute_default_python_version(module_ctx) @@ -87,6 +89,7 @@ def parse_modules(*, module_ctx, logger = None, _fail = fail): mod = mod, seen_versions = seen_versions, config = config, + default_python_version = default_python_version, ) for toolchain_attr in toolchain_attr_structs: @@ -275,7 +278,6 @@ def _python_impl(module_ctx): register_result = python_register_toolchains( name = toolchain_info.name, _internal_bzlmod_toolchain_call = True, - _internal_module_ctx = module_ctx, **kwargs ) if not register_result.impl_repos: @@ -713,7 +715,7 @@ def _process_global_overrides(*, tag, default, _fail = fail): default["add_target_settings"] = list(tag.add_target_settings) forwarded_attrs = sorted(AUTH_ATTRS) + [ - "base_url", + "base_urls", "register_all_versions", ] for key in forwarded_attrs: @@ -741,10 +743,100 @@ def _override_defaults(*overrides, modules, _fail = fail, default): override.fn(tag = tag, _fail = _fail, default = default) -def _get_toolchain_config(*, modules, _fail = fail): +def _manifest_entry_sort_key(entry): + flavor_rank = {"full": 3, "install_only": 1, "install_only_stripped": 2}.get(entry.archive_flavor, 4) + microarch = entry.microarch + if not microarch: + microarch_rank = 0 + elif microarch.startswith("v") and microarch[1:].isdigit(): + microarch_rank = int(microarch[1:]) + else: + microarch_rank = 999 + return (flavor_rank, microarch_rank) + +def _populate_from_pbs_manifest( + *, + mctx, + add_runtime_manifest_urls = [], + add_runtime_manifest_files = [], + runtime_manifest_sha = "", + base_urls = [], + available_versions, + _fail): + manifest_contents = [] + + if add_runtime_manifest_urls: + manifest_path = mctx.path("runtime_manifest") + result = mctx.download( + url = add_runtime_manifest_urls, + output = manifest_path, + sha256 = runtime_manifest_sha, + ) + if not result.success: + _fail("Failed to download manifest from {}: {}".format(add_runtime_manifest_urls, result)) + return + manifest_contents.append(mctx.read(manifest_path)) + + for manifest_file in add_runtime_manifest_files: + manifest_contents.append(mctx.read(manifest_file, watch = "yes")) + + if not manifest_contents: + return + + base_download_urls = [url.rpartition("/")[0] for url in add_runtime_manifest_urls] + if not base_download_urls and base_urls: + base_download_urls = list(base_urls) + + entries = [] + for content in manifest_contents: + entries.extend(parse_runtime_manifest(content)) + + # We don't model archive_flavor via flags yet, so have to pick one. + # Preference is given to install_only because its smaller + entries = sorted( + entries, + key = _manifest_entry_sort_key, + ) + + for entry in entries: + location = entry.location + sha256 = entry.sha256 + py_version = entry.python_version + + # Fallback to matching against PLATFORMS keys as before to ensure compatibility + # with rules_python expected platform keys. + matched_platform = "{}-{}-{}".format(entry.arch, entry.vendor, entry.os) + if entry.libc: + matched_platform += "-" + entry.libc + if entry.freethreaded: + matched_platform += "-freethreaded" + + if matched_platform not in PLATFORMS: + continue + + if entry.archive_flavor not in ["install_only", "install_only_stripped", "full"]: + continue + + v_dict = available_versions.setdefault(py_version, {}) + if matched_platform in v_dict.get("sha256", {}): + continue + + if "://" in location: + urls = [location] + else: + urls = ["{}/{}".format(b_url, location) for b_url in base_download_urls] + + strip_prefix = "python/install" if entry.archive_flavor == "full" else "python" + + v_dict.setdefault("sha256", {})[matched_platform] = sha256 + v_dict.setdefault("url", {})[matched_platform] = urls + v_dict.setdefault("strip_prefix", {})[matched_platform] = strip_prefix + +def _get_toolchain_config(*, mctx, modules, _fail = fail): """Computes the configs for toolchains. Args: + mctx: The module context. modules: The modules from module_ctx _fail: Function to call for failing; only used for testing. @@ -764,30 +856,46 @@ def _get_toolchain_config(*, modules, _fail = fail): # Items that can be overridden available_versions = {} - for py_version, item in TOOL_VERSIONS.items(): - available_versions[py_version] = {} - available_versions[py_version]["sha256"] = dict(item["sha256"]) - platforms = item["sha256"].keys() - - strip_prefix = item["strip_prefix"] - if type(strip_prefix) == type(""): - available_versions[py_version]["strip_prefix"] = { - platform: strip_prefix - for platform in platforms - } - else: - available_versions[py_version]["strip_prefix"] = dict(strip_prefix) - url = item["url"] - if type(url) == type(""): - available_versions[py_version]["url"] = { - platform: url - for platform in platforms - } - else: - available_versions[py_version]["url"] = dict(url) + _populate_from_pbs_manifest( + mctx = mctx, + add_runtime_manifest_files = [Label("//python/private:runtimes_manifest.txt")], + base_urls = DEFAULT_RELEASE_BASE_URLS, + available_versions = available_versions, + _fail = _fail, + ) + + # Check for add_runtime_manifest_urls or add_runtime_manifest_files in override tags in root module + root_module = modules[0] if modules else None + if root_module and root_module.is_root: + for tag in root_module.tags.override: + if tag.add_runtime_manifest_urls or tag.add_runtime_manifest_files: + _populate_from_pbs_manifest( + mctx = mctx, + add_runtime_manifest_urls = tag.add_runtime_manifest_urls, + add_runtime_manifest_files = tag.add_runtime_manifest_files, + runtime_manifest_sha = tag.runtime_manifest_sha, + base_urls = tag.base_urls, + available_versions = available_versions, + _fail = _fail, + ) + + # Check for add_runtime_manifest_urls or add_runtime_manifest_files in override tags in root module + root_module = modules[0] if modules else None + if root_module and root_module.is_root: + for tag in root_module.tags.override: + if tag.add_runtime_manifest_urls or tag.add_runtime_manifest_files: + _populate_from_pbs_manifest( + mctx = mctx, + add_runtime_manifest_urls = tag.add_runtime_manifest_urls, + add_runtime_manifest_files = tag.add_runtime_manifest_files, + runtime_manifest_sha = tag.runtime_manifest_sha, + base_urls = tag.base_urls, + available_versions = available_versions, + _fail = _fail, + ) default = { - "base_url": DEFAULT_RELEASE_BASE_URL, + "base_urls": DEFAULT_RELEASE_BASE_URLS, "platforms": dict(PLATFORMS), # Copy so it's mutable. "tool_versions": available_versions, } @@ -864,8 +972,15 @@ def _compute_default_python_version(mctx): defaults_attr_structs = _create_defaults_attr_structs(mod = mod) default_python_version_env = None default_python_version_file = None + pyproject_toml_label = None for defaults_attr in defaults_attr_structs: + pyproject_toml_label = _one_or_the_same( + pyproject_toml_label, + defaults_attr.pyproject_toml, + onerror = lambda: fail("Multiple pyproject.toml files specified in defaults"), + ) + default_python_version = _one_or_the_same( default_python_version, defaults_attr.python_version, @@ -881,11 +996,17 @@ def _compute_default_python_version(mctx): defaults_attr.python_version_file, onerror = _fail_multiple_defaults_python_version_file, ) + + # Priority order: ENV > pyproject_toml > python_version_file > python_version if default_python_version_file: default_python_version = _one_or_the_same( default_python_version, mctx.read(default_python_version_file, watch = "yes").strip(), ) + if pyproject_toml_label: + pyproject = read_pyproject(mctx, pyproject_toml_label) + if pyproject.requires_python: + default_python_version = version_from_requires_python(pyproject.requires_python) if default_python_version_env: default_python_version = mctx.getenv( default_python_version_env, @@ -927,11 +1048,29 @@ def _create_defaults_attr_struct(*, tag): python_version = getattr(tag, "python_version", None), python_version_env = getattr(tag, "python_version_env", None), python_version_file = getattr(tag, "python_version_file", None), + pyproject_toml = getattr(tag, "pyproject_toml", None), ) -def _create_toolchain_attr_structs(*, mod, config, seen_versions): +def _create_toolchain_attr_structs(*, mod, config, seen_versions, default_python_version): arg_structs = [] + # Auto-register a toolchain for the default version if not already + # registered via an explicit python.toolchain() call. + # This works for any default source: pyproject_toml, python_version_file, + # python_version_env, or python_version. + has_explicit_toolchain = default_python_version and any([ + tag.python_version == default_python_version + for tag in mod.tags.toolchain + ]) + if (default_python_version and + default_python_version not in seen_versions and + mod.is_root and not has_explicit_toolchain): + arg_structs.append(_create_toolchain_attrs_struct( + python_version = default_python_version, + toolchain_tag_count = 1, + )) + seen_versions[default_python_version] = True + for tag in mod.tags.toolchain: arg_structs.append(_create_toolchain_attrs_struct( tag = tag, @@ -971,6 +1110,17 @@ def _create_toolchain_attrs_struct( _defaults = tag_class( doc = """Tag class to specify the default Python version.""", attrs = { + "pyproject_toml": attr.label( + mandatory = False, + doc = """\ +Label pointing to pyproject.toml file to read the default Python version from. +When specified, reads the `requires-python` field from pyproject.toml. +The version must be specified as `==X.Y.Z` (exact version with full semver). + +:::{versionadded} 2.3.0 +::: +""", + ), "python_version": attr.string( mandatory = False, doc = """\ @@ -1111,6 +1261,42 @@ _override = tag_class( ::: """, attrs = { + "add_runtime_manifest_files": attr.label_list( + mandatory = False, + allow_files = True, + doc = """ +Labels pointing to local python-build-standalone manifest files (e.g., `SHA256SUMS`). + +Example: +`//my/custom/manifest:SHA256SUMS` + +:::{seealso} +[Manifest file format documentation](https://rules-python.readthedocs.io/en/latest/toolchains.html#manifest-file-format) +::: + +:::{versionadded} 2.1.0 +::: +""", + ), + "add_runtime_manifest_urls": attr.string_list( + mandatory = False, + doc = """ +URLs pointing to python-build-standalone manifest files (e.g., SHA256SUMS). + +Example: +`https://github.com/astral-sh/python-build-standalone/releases/download/20260414/SHA256SUMS` + +Note that `/latest/` can be used in place of a specific release date (e.g., `20260414`) to automatically use the latest release: +`https://github.com/astral-sh/python-build-standalone/releases/latest/download/SHA256SUMS` + +:::{seealso} +[Manifest file format documentation](https://rules-python.readthedocs.io/en/latest/toolchains.html#manifest-file-format) +::: + +:::{versionadded} 2.1.0 +::: +""", + ), "add_target_settings": attr.string_list( mandatory = False, doc = """\ @@ -1131,7 +1317,7 @@ These settings are appended to the `target_settings` of all toolchains registered by the extension, including any that already have settings from `python.single_version_platform_override`. -:::{versionadded} VERSION_NEXT_FEATURE +:::{versionadded} 2.1.0 ::: """, ), @@ -1149,10 +1335,10 @@ This attribute is usually used in order to ensure that no unexpected transitive dependencies are introduced. """, ), - "base_url": attr.string( + "base_urls": attr.string_list( mandatory = False, - doc = "The base URL to be used when downloading toolchains.", - default = DEFAULT_RELEASE_BASE_URL, + doc = "The base URLs to be used when downloading toolchains.", + default = DEFAULT_RELEASE_BASE_URLS, ), "ignore_root_user_error": attr.bool( default = True, @@ -1180,6 +1366,15 @@ The values in this mapping override the default values and do not replace them. default = {}, ), "register_all_versions": attr.bool(default = False, doc = "Add all versions"), + "runtime_manifest_sha": attr.string( + mandatory = False, + doc = """ +SHA256 hash for the add_runtime_manifest_urls. + +:::{versionadded} 2.1.0 +::: +""", + ), } | AUTH_ATTRS, ) diff --git a/python/private/python_bootstrap_template.txt b/python/private/python_bootstrap_template.txt index 38c93ec5b5..482918c038 100644 --- a/python/private/python_bootstrap_template.txt +++ b/python/private/python_bootstrap_template.txt @@ -5,13 +5,31 @@ from __future__ import absolute_import from __future__ import division from __future__ import print_function +import sys + +# By default, Python prepends the directory containing this script to +# sys.path. The stage 1 bootstrap only needs stdlib modules before it +# re-execs into the configured runtime, so avoid resolving imports from the +# target's output or runfiles package directory. This matters when that +# directory contains files with stdlib names, such as shutil.py or types.py. +# +# Python 3.11 introduced PYTHONSAFEPATH (-P), which disables the unsafe prepend. +# Isolated mode (-I) also disables it, including on older interpreters without +# safe_path. In either case, sys.path[0] is not the script directory and should +# be preserved. +if ( + not getattr(sys.flags, "safe_path", False) and + not getattr(sys.flags, "isolated", False) and + sys.path +): + del sys.path[0] + # Generated file from @rules_python//python/private:python_bootstrap_template.txt from os.path import abspath, dirname, join, basename, normpath import os import shutil import subprocess -import sys # NOTE: The sentinel strings are split (e.g., "%stage2" + "_bootstrap%") so that # the substitution logic won't replace them. This allows runtime detection of diff --git a/python/private/python_register_toolchains.bzl b/python/private/python_register_toolchains.bzl index 3b92902c7e..c6f827be22 100644 --- a/python/private/python_register_toolchains.bzl +++ b/python/private/python_register_toolchains.bzl @@ -17,7 +17,7 @@ load( "//python:versions.bzl", - "DEFAULT_RELEASE_BASE_URL", + "DEFAULT_RELEASE_BASE_URLS", "MINOR_MAPPING", "PLATFORMS", "TOOL_VERSIONS", @@ -26,7 +26,6 @@ load( load(":coverage_deps.bzl", "coverage_dep") load(":full_version.bzl", "full_version") load(":python_repository.bzl", "python_repository") -load(":repo_utils.bzl", "repo_utils") load( ":toolchains_repo.bzl", "host_compatible_python_repo", @@ -90,20 +89,7 @@ def python_register_toolchains( if bzlmod_toolchain_call: register_toolchains = False - # When invoked from the bzlmod python extension, a module_ctx is plumbed in - # so the coverage_dep logger can attribute warnings to the right module and - # honor module-root filtering. In the WORKSPACE/macro path no module_ctx is - # available; a minimal stand-in struct gives the logger what it needs. - module_ctx = kwargs.pop("_internal_module_ctx", None) - if module_ctx != None: - coverage_logger = repo_utils.logger(module_ctx, name = "coverage_dep") - else: - coverage_logger = repo_utils.logger( - struct(getenv = lambda _: None), - name = "coverage_dep", - ) - - base_url = kwargs.pop("base_url", DEFAULT_RELEASE_BASE_URL) + base_urls = kwargs.pop("base_urls", DEFAULT_RELEASE_BASE_URLS) tool_versions = tool_versions or TOOL_VERSIONS minor_mapping = minor_mapping or MINOR_MAPPING @@ -122,7 +108,12 @@ def python_register_toolchains( continue loaded_platforms.append(platform) - (release_filename, urls, strip_prefix, patches, patch_strip) = get_release_info(platform, python_version, base_url, tool_versions) + (release_filename, urls, strip_prefix, patches, patch_strip) = get_release_info( + platform, + python_version, + base_urls = base_urls, + tool_versions = tool_versions, + ) # allow passing in a tool version coverage_tool = None @@ -135,7 +126,6 @@ def python_register_toolchains( ), python_version = python_version, platform = platform, - logger = coverage_logger, visibility = ["@{name}_{platform}//:__subpackages__".format( name = name, platform = platform, diff --git a/python/private/pythons_hub.bzl b/python/private/pythons_hub.bzl index cc25b4ba1d..c64abf9887 100644 --- a/python/private/pythons_hub.bzl +++ b/python/private/pythons_hub.bzl @@ -15,6 +15,7 @@ "Repo rule used by bzlmod extension to create a repo that has a map of Python interpreters and their labels" load("//python:versions.bzl", "PLATFORMS") +load(":pbs_manifest.bzl", "parse_runtime_manifest") load(":text_util.bzl", "render") load(":toolchains_repo.bzl", "toolchain_suite_content") @@ -30,13 +31,13 @@ load("@@{rules_python}//python/private:py_toolchain_suite.bzl", "py_toolchain_su load("@bazel_skylib//:bzl_library.bzl", "bzl_library") bzl_library( - name = "interpreters_bzl", + name = "interpreters", srcs = ["interpreters.bzl"], visibility = ["@rules_python//:__subpackages__"], ) bzl_library( - name = "versions_bzl", + name = "versions", srcs = ["versions.bzl"], visibility = ["@rules_python//:__subpackages__"], ) @@ -118,15 +119,24 @@ def _hub_repo_impl(rctx): executable = False, ) + python_versions = rctx.attr.python_versions + if not python_versions and not rctx.attr.toolchain_python_versions: + content = rctx.read(rctx.path(Label("//python/private:runtimes_manifest.txt"))) + entries = parse_runtime_manifest(content) + python_versions_str = render.list(sorted({getattr(e, "python_version", ""): None for e in entries if getattr(e, "python_version", "")})) + + else: + python_versions_str = render.list(python_versions) if python_versions else render.list(sorted({ + v: None + for v in rctx.attr.toolchain_python_versions + })) + rctx.file( "versions.bzl", _versions_bzl_template.format( default_python_version = rctx.attr.default_python_version, minor_mapping = render.dict(rctx.attr.minor_mapping), - python_versions = rctx.attr.python_versions or render.list(sorted({ - v: None - for v in rctx.attr.toolchain_python_versions - })), + python_versions = python_versions_str, ), executable = False, ) 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/private/repo_utils.bzl b/python/private/repo_utils.bzl index 6fa851b7b3..0e83fda28c 100644 --- a/python/private/repo_utils.bzl +++ b/python/private/repo_utils.bzl @@ -202,7 +202,7 @@ def _execute_internal( output = _outputs_to_str(result, log_stdout = log_stdout, log_stderr = log_stderr), )) - result_kwargs = {k: getattr(result, k) for k in dir(result)} + result_kwargs = {k: getattr(result, k) for k in dir(result) if k not in ["to_json", "to_proto"]} return struct( describe_failure = lambda: _execute_describe_failure( op = op, @@ -511,7 +511,7 @@ def _get_platforms_cpu_name(mrctx): return "riscv64" return arch -def _extract(mrctx, *, archive, supports_whl_extraction = False, **kwargs): +def _extract(mrctx, *, archive, supports_whl_extraction = False, extract_needs_chmod = False, **kwargs): """Extract an archive TODO: remove when the earliest supported bazel version is at least 8.3. @@ -533,6 +533,26 @@ def _extract(mrctx, *, archive, supports_whl_extraction = False, **kwargs): if not mrctx.delete(archive): fail("Failed to remove the symlink after extracting") + if extract_needs_chmod: + _maybe_fix_permissions(mrctx, whl_path = archive) + +def _maybe_fix_permissions(mrctx, *, whl_path, logger = None): + if not logger and hasattr(mrctx, "attr"): + logger = _logger(mrctx) + elif not logger: + fail("logger must be specified when using 'module_ctx'") + + os_name = _get_platforms_os_name(mrctx) + if os_name != "windows": + result = _execute_unchecked( + mrctx, + op = "Fixing wheel permissions {}".format(whl_path), + arguments = ["chmod", "-R", "a+rX", "."], + logger = logger, + ) + if result.return_code != 0: + logger.warn(lambda: "Failed to fix file permissions: {}".format(result.stderr)) + def _rename(mrctx, src, dest): """Rename a file or directory. @@ -575,6 +595,7 @@ repo_utils = struct( get_platforms_os_name = _get_platforms_os_name, is_repo_debug_enabled = _is_repo_debug_enabled, logger = _logger, + maybe_fix_permissions = _maybe_fix_permissions, mkdir = _mkdir, norm_path = _norm_path, relative_to = _relative_to, diff --git a/python/private/runtime_env_toolchain_interpreter.sh b/python/private/runtime_env_toolchain_interpreter.sh index c78cfe1a9b..57d9838919 100755 --- a/python/private/runtime_env_toolchain_interpreter.sh +++ b/python/private/runtime_env_toolchain_interpreter.sh @@ -77,7 +77,7 @@ if [ -e "$self_dir/pyvenv.cfg" ] || [ -e "$self_dir/../pyvenv.cfg" ]; then # binary, not the actual one invoked. # NOTE: exec -a would be simpler, but isn't posix-compatible, and dash shell # (Ubuntu/debian default) doesn't support it; see #3009. - exec sh -c "$PYTHON_BIN \$@" "$venv_bin" "$@" + exec sh -c 'exec "$@"' "$venv_bin" "$PYTHON_BIN" "$@" else exec "$PYTHON_BIN" "$@" fi diff --git a/python/private/runtimes_manifest.txt b/python/private/runtimes_manifest.txt new file mode 100755 index 0000000000..df02a3049c --- /dev/null +++ b/python/private/runtimes_manifest.txt @@ -0,0 +1,621 @@ +# Manifest of runtimes to make available +# Originally generated circa 2026-06 from the runtimes in TOOL_VERSIONS from the original python/versions.bzl +# To sort this file, execute: ./python/private/tools/sort_manifest.py python/private/runtimes_manifest.txt + +1409acd9a506e2d1d3b65c1488db4e40d8f19d09a7df099667c87a506f71c0ef 20220227/cpython-3.10.2+20220227-aarch64-apple-darwin-install_only.tar.gz +8f351a8cc348bb45c0f95b8634c8345ec6e749e483384188ad865b7428342703 20220227/cpython-3.10.2+20220227-aarch64-unknown-linux-gnu-install_only.tar.gz +8146ad4390710ec69b316a5649912df0247d35f4a42e2aa9615bffd87b3e235a 20220227/cpython-3.10.2+20220227-x86_64-apple-darwin-install_only.tar.gz +a1d9a594cd3103baa24937ad9150c1a389544b4350e859200b3e5c036ac352bd 20220227/cpython-3.10.2+20220227-x86_64-pc-windows-msvc-shared-install_only.tar.gz +9b64eca2a94f7aff9409ad70bdaa7fbbf8148692662e764401883957943620dd 20220227/cpython-3.10.2+20220227-x86_64-unknown-linux-gnu-install_only.tar.gz +2c99983d1e83e4b6e7411ed9334019f193fba626344a50c36fba6c25d4de78a2 20220502/cpython-3.10.4+20220502-aarch64-apple-darwin-install_only.tar.gz +d8098c0c54546637e7516f93b13403b11f9db285def8d7abd825c31407a13d7e 20220502/cpython-3.10.4+20220502-aarch64-unknown-linux-gnu-install_only.tar.gz +f2711eaffff3477826a401d09a013c6802f11c04c63ab3686aa72664f1216a05 20220502/cpython-3.10.4+20220502-x86_64-apple-darwin-install_only.tar.gz +bee24a3a5c83325215521d261d73a5207ab7060ef3481f76f69b4366744eb81d 20220502/cpython-3.10.4+20220502-x86_64-pc-windows-msvc-shared-install_only.tar.gz +f6f871e53a7b1469c13f9bd7920ad98c4589e549acad8e5a1e14760fff3dd5c9 20220502/cpython-3.10.4+20220502-x86_64-unknown-linux-gnu-install_only.tar.gz +efaf66acdb9a4eb33d57702607d2e667b1a319d58c167a43c96896b97419b8b7 20220802/cpython-3.10.6+20220802-aarch64-apple-darwin-install_only.tar.gz +81625f5c97f61e2e3d7e9f62c484b1aa5311f21bd6545451714b949a29da5435 20220802/cpython-3.10.6+20220802-aarch64-unknown-linux-gnu-install_only.tar.gz +7718411adf3ea1480f3f018a643eb0550282aefe39e5ecb3f363a4a566a9398c 20220802/cpython-3.10.6+20220802-x86_64-apple-darwin-install_only.tar.gz +91889a7dbdceea585ff4d3b7856a6bb8f8a4eca83a0ff52a73542c2e67220eaa 20220802/cpython-3.10.6+20220802-x86_64-pc-windows-msvc-shared-install_only.tar.gz +55aa2190d28dcfdf414d96dc5dcea9fe048fadcd583dc3981fec020869826111 20220802/cpython-3.10.6+20220802-x86_64-unknown-linux-gnu-install_only.tar.gz +d52b03817bd245d28e0a8b2f715716cd0fcd112820ccff745636932c76afa20a 20221106/cpython-3.10.8+20221106-aarch64-apple-darwin-install_only.tar.gz +33170bef18c811906b738be530f934640491b065bf16c4d276c6515321918132 20221106/cpython-3.10.8+20221106-aarch64-unknown-linux-gnu-install_only.tar.gz +525b79c7ce5de90ab66bd07b0ac1008bafa147ddc8a41bef15ffb7c9c1e9e7c5 20221106/cpython-3.10.8+20221106-x86_64-apple-darwin-install_only.tar.gz +f2b6d2f77118f06dd2ca04dae1175e44aaa5077a5ed8ddc63333c15347182bfe 20221106/cpython-3.10.8+20221106-x86_64-pc-windows-msvc-shared-install_only.tar.gz +6c8db44ae0e18e320320bbaaafd2d69cde8bfea171ae2d651b7993d1396260b7 20221106/cpython-3.10.8+20221106-x86_64-unknown-linux-gnu-install_only.tar.gz +018d05a779b2de7a476f3b3ff2d10f503d69d14efcedd0774e6dab8c22ef84ff 20230116/cpython-3.10.9+20230116-aarch64-apple-darwin-install_only.tar.gz +2003750f40cd09d4bf7a850342613992f8d9454f03b3c067989911fb37e7a4d1 20230116/cpython-3.10.9+20230116-aarch64-unknown-linux-gnu-install_only.tar.gz +0e685f98dce0e5bc8da93c7081f4e6c10219792e223e4b5886730fd73a7ba4c6 20230116/cpython-3.10.9+20230116-x86_64-apple-darwin-install_only.tar.gz +59c6970cecb357dc1d8554bd0540eb81ee7f6d16a07acf3d14ed294ece02c035 20230116/cpython-3.10.9+20230116-x86_64-pc-windows-msvc-shared-install_only.tar.gz +d196347aeb701a53fe2bb2b095abec38d27d0fa0443f8a1c2023a1bed6e18cdf 20230116/cpython-3.10.9+20230116-x86_64-unknown-linux-gnu-install_only.tar.gz +4918cdf1cab742a90f85318f88b8122aeaa2d04705803c7b6e78e81a3dd40f80 20230116/cpython-3.11.1+20230116-aarch64-apple-darwin-install_only.tar.gz +debf15783bdcb5530504f533d33fda75a7b905cec5361ae8f33da5ba6599f8b4 20230116/cpython-3.11.1+20230116-aarch64-unknown-linux-gnu-install_only.tar.gz +20a4203d069dc9b710f70b09e7da2ce6f473d6b1110f9535fb6f4c469ed54733 20230116/cpython-3.11.1+20230116-x86_64-apple-darwin-install_only.tar.gz +edc08979cb0666a597466176511529c049a6f0bba8adf70df441708f766de5bf 20230116/cpython-3.11.1+20230116-x86_64-pc-windows-msvc-shared-install_only.tar.gz +02a551fefab3750effd0e156c25446547c238688a32fabde2995c941c03a6423 20230116/cpython-3.11.1+20230116-x86_64-unknown-linux-gnu-install_only.tar.gz +8348bc3c2311f94ec63751fb71bd0108174be1c4def002773cf519ee1506f96f 20230507/cpython-3.10.11+20230507-aarch64-apple-darwin-install_only.tar.gz +c7573fdb00239f86b22ea0e8e926ca881d24fde5e5890851339911d76110bc35 20230507/cpython-3.10.11+20230507-aarch64-unknown-linux-gnu-install_only.tar.gz +73a9d4c89ed51be39dd2de4e235078281087283e9fdedef65bec02f503e906ee 20230507/cpython-3.10.11+20230507-ppc64le-unknown-linux-gnu-install_only.tar.gz +bd3fc6e4da6f4033ebf19d66704e73b0804c22641ddae10bbe347c48f82374ad 20230507/cpython-3.10.11+20230507-x86_64-apple-darwin-install_only.tar.gz +9c2d3604a06fcd422289df73015cd00e7271d90de28d2c910f0e2309a7f73a68 20230507/cpython-3.10.11+20230507-x86_64-pc-windows-msvc-shared-install_only.tar.gz +c5bcaac91bc80bfc29cf510669ecad12d506035ecb3ad85ef213416d54aecd79 20230507/cpython-3.10.11+20230507-x86_64-unknown-linux-gnu-install_only.tar.gz +09e412506a8d63edbb6901742b54da9aa7faf120b8dbdce56c57b303fc892c86 20230507/cpython-3.11.3+20230507-aarch64-apple-darwin-install_only.tar.gz +8190accbbbbcf7620f1ff6d668e4dd090c639665d11188ce864b62554d40e5ab 20230507/cpython-3.11.3+20230507-aarch64-unknown-linux-gnu-install_only.tar.gz +767d24f3570b35fedb945f5ac66224c8983f2d556ab83c5cfaa5f3666e9c212c 20230507/cpython-3.11.3+20230507-ppc64le-unknown-linux-gnu-install_only.tar.gz +f710b8d60621308149c100d5175fec39274ed0b9c99645484fd93d1716ef4310 20230507/cpython-3.11.3+20230507-x86_64-apple-darwin-install_only.tar.gz +24741066da6f35a7ff67bee65ce82eae870d84e1181843e64a7076d1571e95af 20230507/cpython-3.11.3+20230507-x86_64-pc-windows-msvc-shared-install_only.tar.gz +da50b87d1ec42b3cb577dfd22a3655e43a53150f4f98a4bfb40757c9d7839ab5 20230507/cpython-3.11.3+20230507-x86_64-unknown-linux-gnu-install_only.tar.gz +bc66c706ea8c5fc891635fda8f9da971a1a901d41342f6798c20ad0b2a25d1d6 20230726/cpython-3.10.12+20230726-aarch64-apple-darwin-install_only.tar.gz +fee80e221663eca5174bd794cb5047e40d3910dbeadcdf1f09d405a4c1c15fe4 20230726/cpython-3.10.12+20230726-aarch64-unknown-linux-gnu-install_only.tar.gz +bb5e8cb0d2e44241725fa9b342238245503e7849917660006b0246a9c97b1d6c 20230726/cpython-3.10.12+20230726-ppc64le-unknown-linux-gnu-install_only.tar.gz +8d33d435ae6fb93ded7fc26798cc0a1a4f546a4e527012a1e2909cc314b332df 20230726/cpython-3.10.12+20230726-s390x-unknown-linux-gnu-install_only.tar.gz +8a6e3ed973a671de468d9c691ed9cb2c3a4858c5defffcf0b08969fba9c1dd04 20230726/cpython-3.10.12+20230726-x86_64-apple-darwin-install_only.tar.gz +c1a31c353ca44de7d1b1a3b6c55a823e9c1eed0423d4f9f66e617bdb1b608685 20230726/cpython-3.10.12+20230726-x86_64-pc-windows-msvc-shared-install_only.tar.gz +a476dbca9184df9fc69fe6309cda5ebaf031d27ca9e529852437c94ec1bc43d3 20230726/cpython-3.10.12+20230726-x86_64-unknown-linux-gnu-install_only.tar.gz +cb6d2948384a857321f2aa40fa67744cd9676a330f08b6dad7070bda0b6120a4 20230726/cpython-3.11.4+20230726-aarch64-apple-darwin-install_only.tar.gz +2e84fc53f4e90e11963281c5c871f593abcb24fc796a50337fa516be99af02fb 20230726/cpython-3.11.4+20230726-aarch64-unknown-linux-gnu-install_only.tar.gz +df7b92ed9cec96b3bb658fb586be947722ecd8e420fb23cee13d2e90abcfcf25 20230726/cpython-3.11.4+20230726-ppc64le-unknown-linux-gnu-install_only.tar.gz +e477f0749161f9aa7887964f089d9460a539f6b4a8fdab5166f898210e1a87a4 20230726/cpython-3.11.4+20230726-s390x-unknown-linux-gnu-install_only.tar.gz +47e1557d93a42585972772e82661047ca5f608293158acb2778dccf120eabb00 20230726/cpython-3.11.4+20230726-x86_64-apple-darwin-install_only.tar.gz +878614c03ea38538ae2f758e36c85d2c0eb1eaaca86cd400ff8c76693ee0b3e1 20230726/cpython-3.11.4+20230726-x86_64-pc-windows-msvc-shared-install_only.tar.gz +e26247302bc8e9083a43ce9e8dd94905b40d464745b1603041f7bc9a93c65d05 20230726/cpython-3.11.4+20230726-x86_64-unknown-linux-gnu-install_only.tar.gz +dab64b3580118ad2073babd7c29fd2053b616479df5c107d31fe2af1f45e948b 20230826/cpython-3.11.5+20230826-aarch64-apple-darwin-install_only.tar.gz +bb5c5d1ea0f199fe2d3f0996fff4b48ca6ddc415a3dbd98f50bff7fce48aac80 20230826/cpython-3.11.5+20230826-aarch64-unknown-linux-gnu-install_only.tar.gz +14121b53e9c8c6d0741f911ae00102a35adbcf5c3cdf732687ef7617b7d7304d 20230826/cpython-3.11.5+20230826-ppc64le-unknown-linux-gnu-install_only.tar.gz +fe459da39874443579d6fe88c68777c6d3e331038e1fb92a0451879fb6beb16d 20230826/cpython-3.11.5+20230826-s390x-unknown-linux-gnu-install_only.tar.gz +4a4efa7378c72f1dd8ebcce1afb99b24c01b07023aa6b8fea50eaedb50bf2bfc 20230826/cpython-3.11.5+20230826-x86_64-apple-darwin-install_only.tar.gz +00f002263efc8aea896bcfaaf906b1f4dab3e5cd3db53e2b69ab9a10ba220b97 20230826/cpython-3.11.5+20230826-x86_64-pc-windows-msvc-shared-install_only.tar.gz +fbed6f7694b2faae5d7c401a856219c945397f772eea5ca50c6eb825cbc9d1e1 20230826/cpython-3.11.5+20230826-x86_64-unknown-linux-gnu-install_only.tar.gz +916c35125b5d8323a21526d7a9154ca626453f63d0878e95b9f613a95006c990 20231002/cpython-3.11.6+20231002-aarch64-apple-darwin-install_only.tar.gz +3e26a672df17708c4dc928475a5974c3fb3a34a9b45c65fb4bd1e50504cc84ec 20231002/cpython-3.11.6+20231002-aarch64-unknown-linux-gnu-install_only.tar.gz +7937035f690a624dba4d014ffd20c342e843dd46f89b0b0a1e5726b85deb8eaf 20231002/cpython-3.11.6+20231002-ppc64le-unknown-linux-gnu-install_only.tar.gz +f9f19823dba3209cedc4647b00f46ed0177242917db20fb7fb539970e384531c 20231002/cpython-3.11.6+20231002-s390x-unknown-linux-gnu-install_only.tar.gz +178cb1716c2abc25cb56ae915096c1a083e60abeba57af001996e8bc6ce1a371 20231002/cpython-3.11.6+20231002-x86_64-apple-darwin-install_only.tar.gz +3933545e6d41462dd6a47e44133ea40995bc6efeed8c2e4cbdf1a699303e95ea 20231002/cpython-3.11.6+20231002-x86_64-pc-windows-msvc-shared-install_only.tar.gz +ee37a7eae6e80148c7e3abc56e48a397c1664f044920463ad0df0fc706eacea8 20231002/cpython-3.11.6+20231002-x86_64-unknown-linux-gnu-install_only.tar.gz +4734a2be2becb813830112c780c9879ac3aff111a0b0cd590e65ec7465774d02 20231002/cpython-3.12.0+20231002-aarch64-apple-darwin-install_only.tar.gz +bccfe67cf5465a3dfb0336f053966e2613a9bc85a6588c2fcf1366ef930c4f88 20231002/cpython-3.12.0+20231002-aarch64-unknown-linux-gnu-install_only.tar.gz +b5dae075467ace32c594c7877fe6ebe0837681f814601d5d90ba4c0dfd87a1f2 20231002/cpython-3.12.0+20231002-ppc64le-unknown-linux-gnu-install_only.tar.gz +5681621349dd85d9726d1b67c84a9686ce78f72e73a6f9e4cc4119911655759e 20231002/cpython-3.12.0+20231002-s390x-unknown-linux-gnu-install_only.tar.gz +5a9e88c8aa52b609d556777b52ebde464ae4b4f77e4aac4eb693af57395c9abf 20231002/cpython-3.12.0+20231002-x86_64-apple-darwin-install_only.tar.gz +facfaa1fbc8653f95057f3c4a0f8aa833dab0e0b316e24ee8686bc761d4b4f8d 20231002/cpython-3.12.0+20231002-x86_64-pc-windows-msvc-shared-install_only.tar.gz +e51a5293f214053ddb4645b2c9f84542e2ef86870b8655704367bd4b29d39fe9 20231002/cpython-3.12.0+20231002-x86_64-unknown-linux-gnu-install_only.tar.gz +b042c966920cf8465385ca3522986b12d745151a72c060991088977ca36d3883 20240107/cpython-3.11.7+20240107-aarch64-apple-darwin-install_only.tar.gz +b102eaf865eb715aa98a8a2ef19037b6cc3ae7dfd4a632802650f29de635aa13 20240107/cpython-3.11.7+20240107-aarch64-unknown-linux-gnu-install_only.tar.gz +b44e1b74afe75c7b19143413632c4386708ae229117f8f950c2094e9681d34c7 20240107/cpython-3.11.7+20240107-ppc64le-unknown-linux-gnu-install_only.tar.gz +49520e3ff494708020f306e30b0964f079170be83e956be4504f850557378a22 20240107/cpython-3.11.7+20240107-s390x-unknown-linux-gnu-install_only.tar.gz +a0e615eef1fafdc742da0008425a9030b7ea68a4ae4e73ac557ef27b112836d4 20240107/cpython-3.11.7+20240107-x86_64-apple-darwin-install_only.tar.gz +67077e6fa918e4f4fd60ba169820b00be7c390c497bf9bc9cab2c255ea8e6f3e 20240107/cpython-3.11.7+20240107-x86_64-pc-windows-msvc-shared-install_only.tar.gz +4a51ce60007a6facf64e5495f4cf322e311ba9f39a8cd3f3e4c026eae488e140 20240107/cpython-3.11.7+20240107-x86_64-unknown-linux-gnu-install_only.tar.gz +f93f8375ca6ac0a35d58ff007043cbd3a88d9609113f1cb59cf7c8d215f064af 20240107/cpython-3.12.1+20240107-aarch64-apple-darwin-install_only.tar.gz +236533ef20e665007a111c2f36efb59c87ae195ad7dca223b6dc03fb07064f0b 20240107/cpython-3.12.1+20240107-aarch64-unknown-linux-gnu-install_only.tar.gz +78051f0d1411ee62bc2af5edfccf6e8400ac4ef82887a2affc19a7ace6a05267 20240107/cpython-3.12.1+20240107-ppc64le-unknown-linux-gnu-install_only.tar.gz +60631211c701f8d2c56e5dd7b154e68868128a019b9db1d53a264f56c0d4aee2 20240107/cpython-3.12.1+20240107-s390x-unknown-linux-gnu-install_only.tar.gz +eca96158c1568dedd9a0b3425375637a83764d1fa74446438293089a8bfac1f8 20240107/cpython-3.12.1+20240107-x86_64-apple-darwin-install_only.tar.gz +fd5a9e0f41959d0341246d3643f2b8794f638adc0cec8dd5e1b6465198eae08a 20240107/cpython-3.12.1+20240107-x86_64-pc-windows-msvc-shared-install_only.tar.gz +74e330b8212ca22fd4d9a2003b9eec14892155566738febc8e5e572f267b9472 20240107/cpython-3.12.1+20240107-x86_64-unknown-linux-gnu-install_only.tar.gz +5fdc0f6a5b5a90fd3c528e8b1da8e3aac931ea8690126c2fdb4254c84a3ff04a 20240224/cpython-3.10.13+20240224-aarch64-apple-darwin-install_only.tar.gz +a898a88705611b372297bb8fe4d23cc16b8603ce5f24494c3a8cfa65d83787f9 20240224/cpython-3.10.13+20240224-aarch64-unknown-linux-gnu-install_only.tar.gz +c23706e138a0351fc1e9def2974af7b8206bac7ecbbb98a78f5aa9e7535fee42 20240224/cpython-3.10.13+20240224-ppc64le-unknown-linux-gnu-install_only.tar.gz +09be8fb2cdfbb4a93d555f268f244dbe4d8ff1854b2658e8043aa4ec08aede3e 20240224/cpython-3.10.13+20240224-s390x-unknown-linux-gnu-install_only.tar.gz +6378dfd22f58bb553ddb02be28304d739cd730c1f95c15c74955c923a1bc3d6a 20240224/cpython-3.10.13+20240224-x86_64-apple-darwin-install_only.tar.gz +086f7fe9156b897bb401273db8359017104168ac36f60f3af4e31ac7acd6634e 20240224/cpython-3.10.13+20240224-x86_64-pc-windows-msvc-shared-install_only.tar.gz +d995d032ca702afd2fc3a689c1f84a6c64972ecd82bba76a61d525f08eb0e195 20240224/cpython-3.10.13+20240224-x86_64-unknown-linux-gnu-install_only.tar.gz +389a51139f5abe071a0d70091ca5df3e7a3dfcfcbe3e0ba6ad85fb4c5638421e 20240224/cpython-3.11.8+20240224-aarch64-apple-darwin-install_only.tar.gz +389b9005fb78dd5a6f68df5ea45ab7b30d9a4b3222af96999e94fd20d4ad0c6a 20240224/cpython-3.11.8+20240224-aarch64-unknown-linux-gnu-install_only.tar.gz +eb2b31f8e50309aae493c6a359c32b723a676f07c641f5e8fe4b6aa4dbb50946 20240224/cpython-3.11.8+20240224-ppc64le-unknown-linux-gnu-install_only.tar.gz +844f64f4c16e24965778281da61d1e0e6cd1358a581df1662da814b1eed096b9 20240224/cpython-3.11.8+20240224-s390x-unknown-linux-gnu-install_only.tar.gz +097f467b0c36706bfec13f199a2eaf924e668f70c6e2bd1f1366806962f7e86e 20240224/cpython-3.11.8+20240224-x86_64-apple-darwin-install_only.tar.gz +b618f1f047349770ee1ef11d1b05899840abd53884b820fd25c7dfe2ec1664d4 20240224/cpython-3.11.8+20240224-x86_64-pc-windows-msvc-shared-install_only.tar.gz +94e13d0e5ad417035b80580f3e893a72e094b0900d5d64e7e34ab08e95439987 20240224/cpython-3.11.8+20240224-x86_64-unknown-linux-gnu-install_only.tar.gz +01c064c00013b0175c7858b159989819ead53f4746d40580b5b0b35b6e80fba6 20240224/cpython-3.12.2+20240224-aarch64-apple-darwin-install_only.tar.gz +e52550379e7c4ac27a87de832d172658bc04150e4e27d4e858e6d8cbb96fd709 20240224/cpython-3.12.2+20240224-aarch64-unknown-linux-gnu-install_only.tar.gz +74bc02c4bbbd26245c37b29b9e12d0a9c1b7ab93477fed8b651c988b6a9a6251 20240224/cpython-3.12.2+20240224-ppc64le-unknown-linux-gnu-install_only.tar.gz +ecd6b0285e5eef94deb784b588b4b425a15a43ae671bf206556659dc141a9825 20240224/cpython-3.12.2+20240224-s390x-unknown-linux-gnu-install_only.tar.gz +a53a6670a202c96fec0b8c55ccc780ea3af5307eb89268d5b41a9775b109c094 20240224/cpython-3.12.2+20240224-x86_64-apple-darwin-install_only.tar.gz +1e5655a6ccb1a64a78460e4e3ee21036c70246800f176a6c91043a3fe3654a3b 20240224/cpython-3.12.2+20240224-x86_64-pc-windows-msvc-shared-install_only.tar.gz +57a37b57f8243caa4cdac016176189573ad7620f0b6da5941c5e40660f9468ab 20240224/cpython-3.12.2+20240224-x86_64-unknown-linux-gnu-install_only.tar.gz +ccc40e5af329ef2af81350db2a88bbd6c17b56676e82d62048c15d548401519e 20240415/cpython-3.12.3+20240415-aarch64-apple-darwin-install_only.tar.gz +ec8126de97945e629cca9aedc80a29c4ae2992c9d69f2655e27ae73906ba187d 20240415/cpython-3.12.3+20240415-aarch64-unknown-linux-gnu-install_only.tar.gz +c5dcf08b8077e617d949bda23027c49712f583120b3ed744f9b143da1d580572 20240415/cpython-3.12.3+20240415-ppc64le-unknown-linux-gnu-install_only.tar.gz +872fc321363b8cdd826fd2cb1adfd1ceb813bc1281f9d410c1c2c4e177e8df86 20240415/cpython-3.12.3+20240415-s390x-unknown-linux-gnu-install_only.tar.gz +c37a22fca8f57d4471e3708de6d13097668c5f160067f264bb2b18f524c890c8 20240415/cpython-3.12.3+20240415-x86_64-apple-darwin-install_only.tar.gz +f7cfa4ad072feb4578c8afca5ba9a54ad591d665a441dd0d63aa366edbe19279 20240415/cpython-3.12.3+20240415-x86_64-pc-windows-msvc-shared-install_only.tar.gz +a73ba777b5d55ca89edef709e6b8521e3f3d4289581f174c8699adfb608d09d6 20240415/cpython-3.12.3+20240415-x86_64-unknown-linux-gnu-install_only.tar.gz +164d89f0df2feb689981864ecc1dffb19e6aa3696c8880166de555494fe92607 20240726/cpython-3.10.14+20240726-aarch64-apple-darwin-install_only.tar.gz +39bcd46b4d70e40da177c55259be16d5c2be7a3f7f93f1e3bde47e71b4833f29 20240726/cpython-3.10.14+20240726-aarch64-unknown-linux-gnu-install_only.tar.gz +549d38b9ef59cba9ab2990025255231bfa1cb32b4bc5eac321667640fdee19d1 20240726/cpython-3.10.14+20240726-ppc64le-unknown-linux-gnu-install_only.tar.gz +de4bc878a8666c734f983db971610980870148f333bda8b0c34abfaeae88d7ec 20240726/cpython-3.10.14+20240726-s390x-unknown-linux-gnu-install_only.tar.gz +1a1455838cd1e8ed0da14a152a2d559a2fd3a6047ba7013e841db4a35a228c1d 20240726/cpython-3.10.14+20240726-x86_64-apple-darwin-install_only.tar.gz +7f68821a8b5445267eca480660364ebd06ec84632b336770c6e39de07ac0f6c3 20240726/cpython-3.10.14+20240726-x86_64-pc-windows-msvc-shared-install_only.tar.gz +32b34cd13d9d745b3db3f3b8398ab2c07de74544829915dbebd8dce39bdc405e 20240726/cpython-3.10.14+20240726-x86_64-unknown-linux-gnu-install_only.tar.gz +cbdac9462bab9671c8e84650e425d3f43b775752a930a2ef954a0d457d5c00c3 20240726/cpython-3.11.9+20240726-aarch64-apple-darwin-install_only.tar.gz +4d17cf988abe24449d649aad3ef974091ab76807904d41839907061925b4c9e3 20240726/cpython-3.11.9+20240726-aarch64-unknown-linux-gnu-install_only.tar.gz +fc4f3c9ef9bfac2ed0282126ff376e544697ad04a5408d6429d46899d7d3bf21 20240726/cpython-3.11.9+20240726-ppc64le-unknown-linux-gnu-install_only.tar.gz +e69b66e53e926460df044f44846eef3fea642f630e829719e1a4112fc370dc56 20240726/cpython-3.11.9+20240726-s390x-unknown-linux-gnu-install_only.tar.gz +dc3174666a30f4c38d04e79a80c3159b4b3aa69597c4676701c8386696811611 20240726/cpython-3.11.9+20240726-x86_64-apple-darwin-install_only.tar.gz +f694be48bdfec1dace6d69a19906b6083f4dd7c7c61f1138ba520e433e5598f8 20240726/cpython-3.11.9+20240726-x86_64-pc-windows-msvc-shared-install_only.tar.gz +f6e955dc9ddfcad74e77abe6f439dac48ebca14b101ed7c85a5bf3206ed2c53d 20240726/cpython-3.11.9+20240726-x86_64-unknown-linux-gnu-install_only.tar.gz +1801025e825c04b3907e4ef6220a13607bc0397628c9485897073110ef7fde15 20240726/cpython-3.12.4+20240726-aarch64-apple-darwin-install_only.tar.gz +a098b18b7e9fea0c66867b76c0124fce9465765017572b2e7b522154c87c78d7 20240726/cpython-3.12.4+20240726-aarch64-unknown-linux-gnu-install_only.tar.gz +04011c4c5b7fe34b0b895edf4ad8748e410686c1d69aaee11d6688d481023bcb 20240726/cpython-3.12.4+20240726-ppc64le-unknown-linux-gnu-install_only.tar.gz +8f8f3e29cf0c2facdbcfee70660939fda7667ac24fee8656d3388fc72f3acc7c 20240726/cpython-3.12.4+20240726-s390x-unknown-linux-gnu-install_only.tar.gz +4c325838c1b0ed13698506fcd515be25c73dcbe195f8522cf98f9148a97601ed 20240726/cpython-3.12.4+20240726-x86_64-apple-darwin-install_only.tar.gz +74309b0f322716409883d38c621743ea7fa0376eb00927b8ee1e1671d3aff450 20240726/cpython-3.12.4+20240726-x86_64-pc-windows-msvc-shared-install_only.tar.gz +e133dd6fc6a2d0033e2658637cc22e9c95f9d7073b80115037ee1f16417a54ac 20240726/cpython-3.12.4+20240726-x86_64-unknown-linux-gnu-install_only.tar.gz +f64776f455a44c24d50f947c813738cfb7b9ac43732c44891bc831fa7940a33c 20241016/cpython-3.10.15+20241016-aarch64-apple-darwin-install_only.tar.gz +eb58581f85fde83d1f3e8e1f8c6f5a15c7ae4fdbe3b1d1083931f9167fdd8dbc 20241016/cpython-3.10.15+20241016-aarch64-unknown-linux-gnu-install_only.tar.gz +0c45af4e7525e2db59901606db32b2896ac1e9830c6f95551402207f537c2ce4 20241016/cpython-3.10.15+20241016-ppc64le-unknown-linux-gnu-install_only.tar.gz +de205896b070e6f5259ac0f2b3379eead875ea84e6a6ef533b89886fcbb46a4c 20241016/cpython-3.10.15+20241016-s390x-unknown-linux-gnu-install_only.tar.gz +90b46dfb1abd98d45663c7a2a8c45d3047a59391d8586d71b459cec7b75f662b 20241016/cpython-3.10.15+20241016-x86_64-apple-darwin-install_only.tar.gz +e48952619796c66ec9719867b87be97edca791c2ef7fbf87d42c417c3331609e 20241016/cpython-3.10.15+20241016-x86_64-pc-windows-msvc-shared-install_only.tar.gz +3db2171e03c1a7acdc599fba583c1b92306d3788b375c9323077367af1e9d9de 20241016/cpython-3.10.15+20241016-x86_64-unknown-linux-gnu-install_only.tar.gz +ed519c47d9620eb916a6f95ec2875396e7b1a9ab993ee40b2f31b837733f318c 20241016/cpython-3.10.15+20241016-x86_64-unknown-linux-musl-install_only.tar.gz +5a69382da99c4620690643517ca1f1f53772331b347e75f536088c42a4cf6620 20241016/cpython-3.11.10+20241016-aarch64-apple-darwin-install_only.tar.gz +803e49259280af0f5466d32829cd9d65a302b0226e424b3f0b261f9daf6aee8f 20241016/cpython-3.11.10+20241016-aarch64-unknown-linux-gnu-install_only.tar.gz +92b666d103902001322f42badbd68da92adc5cebb826af9c1c906c33166e2f34 20241016/cpython-3.11.10+20241016-ppc64le-unknown-linux-gnu-install_only.tar.gz +6d584317651c1ad4a857cb32d1999707e8bb3046fcb2f156d80381814fa19fde 20241016/cpython-3.11.10+20241016-s390x-unknown-linux-gnu-install_only.tar.gz +1e23ffe5bc473e1323ab8f51464da62d77399afb423babf67f8e13c82b69c674 20241016/cpython-3.11.10+20241016-x86_64-apple-darwin-install_only.tar.gz +647b66ff4552e70aec3bf634dd470891b4a2b291e8e8715b3bdb162f577d4c55 20241016/cpython-3.11.10+20241016-x86_64-pc-windows-msvc-shared-install_only.tar.gz +8b50a442b04724a24c1eebb65a36a0c0e833d35374dbdf9c9470d8a97b164cd9 20241016/cpython-3.11.10+20241016-x86_64-unknown-linux-gnu-install_only.tar.gz +d36fc77a8dd76155a7530f6235999a693b9e7c48aa11afeb5610a091cae5aa6f 20241016/cpython-3.11.10+20241016-x86_64-unknown-linux-musl-install_only.tar.gz +4c18852bf9c1a11b56f21bcf0df1946f7e98ee43e9e4c0c5374b2b3765cf9508 20241016/cpython-3.12.7+20241016-aarch64-apple-darwin-install_only.tar.gz +bba3c6be6153f715f2941da34f3a6a69c2d0035c9c5396bc5bb68c6d2bd1065a 20241016/cpython-3.12.7+20241016-aarch64-unknown-linux-gnu-install_only.tar.gz +0a1d1d92e33a969bd2f40a80af53c97b6c0cc1060d384ceff50ff801593bf9d6 20241016/cpython-3.12.7+20241016-ppc64le-unknown-linux-gnu-install_only.tar.gz +935676a0c960b552f95e9ac2e1e385de5de4b34038ff65ffdc688838f1189c17 20241016/cpython-3.12.7+20241016-s390x-unknown-linux-gnu-install_only.tar.gz +60c5271e7edc3c2ab47440b7abf4ed50fbc693880b474f74f05768f5b657045a 20241016/cpython-3.12.7+20241016-x86_64-apple-darwin-install_only.tar.gz +f05531bff16fa77b53be0776587b97b466070e768e6d5920894de988bdcd547a 20241016/cpython-3.12.7+20241016-x86_64-pc-windows-msvc-shared-install_only.tar.gz +43576f7db1033dd57b900307f09c2e86f371152ac8a2607133afa51cbfc36064 20241016/cpython-3.12.7+20241016-x86_64-unknown-linux-gnu-install_only.tar.gz +5ed4a4078db3cbac563af66403aaa156cd6e48831d90382a1820db2b120627b5 20241016/cpython-3.12.7+20241016-x86_64-unknown-linux-musl-install_only.tar.gz +efc2e71c0e05bc5bedb7a846e05f28dd26491b1744ded35ed82f8b49ccfa684b 20241016/cpython-3.13.0+20241016-aarch64-apple-darwin-freethreaded+pgo+lto-full.tar.zst +31397953849d275aa2506580f3fa1cb5a85b6a3d392e495f8030e8b6412f5556 20241016/cpython-3.13.0+20241016-aarch64-apple-darwin-install_only.tar.gz +59b50df9826475d24bb7eff781fa3949112b5e9c92adb29e96a09cdf1216d5bd 20241016/cpython-3.13.0+20241016-aarch64-unknown-linux-gnu-freethreaded+lto-full.tar.zst +e8378c0162b2e0e4cc1f62b29443a3305d116d09583304dbb0149fecaff6347b 20241016/cpython-3.13.0+20241016-aarch64-unknown-linux-gnu-install_only.tar.gz +1217efa5f4ce67fcc9f7eb64165b1bd0912b2a21bc25c1a7e2cb174a21a5df7e 20241016/cpython-3.13.0+20241016-ppc64le-unknown-linux-gnu-freethreaded+lto-full.tar.zst +fc4b7f27c4e84c78f3c8e6c7f8e4023e4638d11f1b36b6b5ce457b1926cebb53 20241016/cpython-3.13.0+20241016-ppc64le-unknown-linux-gnu-install_only.tar.gz +6c3e1e4f19d2b018b65a7e3ef4cd4225c5b9adfbc490218628466e636d5c4b8c 20241016/cpython-3.13.0+20241016-s390x-unknown-linux-gnu-freethreaded+lto-full.tar.zst +66b19e6a07717f6cfcd3a8ca953f0a2eaa232291142f3d26a8d17c979ec0f467 20241016/cpython-3.13.0+20241016-s390x-unknown-linux-gnu-install_only.tar.gz +2e07dfea62fe2215738551a179c87dbed1cc79d1b3654f4d7559889a6d5ce4eb 20241016/cpython-3.13.0+20241016-x86_64-apple-darwin-freethreaded+pgo+lto-full.tar.zst +cff1b7e7cd26f2d47acac1ad6590e27d29829776f77e8afa067e9419f2f6ce77 20241016/cpython-3.13.0+20241016-x86_64-apple-darwin-install_only.tar.gz +bfd89f9acf866463bc4baf01733da5e767d13f5d0112175a4f57ba91f1541310 20241016/cpython-3.13.0+20241016-x86_64-pc-windows-msvc-shared-freethreaded+pgo-full.tar.zst +b25926e8ce4164cf103bacc4f4d154894ea53e07dd3fdd5ebb16fb1a82a7b1a0 20241016/cpython-3.13.0+20241016-x86_64-pc-windows-msvc-shared-install_only.tar.gz +a73adeda301ad843cce05f31a2d3e76222b656984535a7b87696a24a098b216c 20241016/cpython-3.13.0+20241016-x86_64-unknown-linux-gnu-freethreaded+pgo+lto-full.tar.zst +2c8cb15c6a2caadaa98af51df6fe78a8155b8471cb3dd7b9836038e0d3657fb4 20241016/cpython-3.13.0+20241016-x86_64-unknown-linux-gnu-install_only.tar.gz +2f61ee3b628a56aceea63b46c7afe2df3e22a61da706606b0c8efda57f953cf4 20241016/cpython-3.13.0+20241016-x86_64-unknown-linux-musl-install_only.tar.gz +08f05618bdcf8064a7960b25d9ba92155447c9b08e0cf2f46a981e4c6a1bb5a5 20241205/cpython-3.13.1+20241205-aarch64-apple-darwin-freethreaded+pgo+lto-full.tar.zst +88b88b609129c12f4b3841845aca13230f61e97ba97bd0fb28ee64b0e442a34f 20241205/cpython-3.13.1+20241205-aarch64-apple-darwin-install_only.tar.gz +9f2fcb809f9ba6c7c014a8803073a88786701a98971135bce684355062e4bb35 20241205/cpython-3.13.1+20241205-aarch64-unknown-linux-gnu-freethreaded+lto-full.tar.zst +fdfa86c2746d2ae700042c461846e6c37f70c249925b58de8cd02eb8d1423d4e 20241205/cpython-3.13.1+20241205-aarch64-unknown-linux-gnu-install_only.tar.gz +15ceea78dff78ca8ccaac8d9c54b808af30daaa126f1f561e920a6896e098634 20241205/cpython-3.13.1+20241205-ppc64le-unknown-linux-gnu-freethreaded+lto-full.tar.zst +27b20b3237c55430ca1304e687d021f88373f906249f9cd272c5ff2803d5e5c3 20241205/cpython-3.13.1+20241205-ppc64le-unknown-linux-gnu-install_only.tar.gz +ed3c6118d1d12603309c930e93421ac7a30a69045ffd43006f63ecf71d72c317 20241205/cpython-3.13.1+20241205-s390x-unknown-linux-gnu-freethreaded+lto-full.tar.zst +7d0187e20cb5e36c689eec27e4d3de56d8b7f1c50dc5523550fc47377801521f 20241205/cpython-3.13.1+20241205-s390x-unknown-linux-gnu-install_only.tar.gz +dc780fecd215d2cc9e573abf1e13a175fcfa8f6efd100ef888494a248a16cda8 20241205/cpython-3.13.1+20241205-x86_64-apple-darwin-freethreaded+pgo+lto-full.tar.zst +47eef6efb8664e2d1d23a7cdaf56262d784f8ace48f3bfca1b183e95a49888d6 20241205/cpython-3.13.1+20241205-x86_64-apple-darwin-install_only.tar.gz +7537b2ab361c0eabc0eabfca9ffd9862d7f5f6576eda13b97e98aceb5eea4fd3 20241205/cpython-3.13.1+20241205-x86_64-pc-windows-msvc-shared-freethreaded+pgo-full.tar.zst +f51f0493a5f979ff0b8d8c598a8d74f2a4d86a190c2729c85e0af65c36a9cbbe 20241205/cpython-3.13.1+20241205-x86_64-pc-windows-msvc-shared-install_only.tar.gz +9ec1b81213f849d91f5ebe6a16196e85cd6ff7c05ca823ce0ab7ba5b0e9fee84 20241205/cpython-3.13.1+20241205-x86_64-unknown-linux-gnu-freethreaded+pgo+lto-full.tar.zst +242b2727df6c1e00de6a9f0f0dcb4562e168d27f428c785b0eb41a6aeb34d69a 20241205/cpython-3.13.1+20241205-x86_64-unknown-linux-gnu-install_only.tar.gz +76b30c6373b9c0aa2ba610e07da02f384aa210ac79643da38c66d3e6171c6ef5 20241205/cpython-3.13.1+20241205-x86_64-unknown-linux-musl-install_only.tar.gz +e3c4aa607717b23903ca2650d5c3ee24f89b97543e2db2b0f463bddc7a9e92f3 20241206/cpython-3.12.8+20241206-aarch64-apple-darwin-install_only.tar.gz +ce674b55442b732973afb2932c281bb1ded4ad7e22bcf9b07071165770758c7e 20241206/cpython-3.12.8+20241206-aarch64-unknown-linux-gnu-install_only.tar.gz +b7214790b273de9ed0532420054b72ba1393d62d2fc844ec55ade193771bd90c 20241206/cpython-3.12.8+20241206-ppc64le-unknown-linux-gnu-install_only.tar.gz +73102f5dbd7d1e7e9c2f2c80aedf2893d99a7fa407f6674ec8b2f57ba07daee5 20241206/cpython-3.12.8+20241206-s390x-unknown-linux-gnu-install_only.tar.gz +3ba35c706577d755e8e52a4c161a042464577c0e695e2a605362fa469e26de10 20241206/cpython-3.12.8+20241206-x86_64-apple-darwin-install_only.tar.gz +767b4be3ddf6b99e5ade519789c1615c191d8cf99d5aff4685cc18b48931f1e6 20241206/cpython-3.12.8+20241206-x86_64-pc-windows-msvc-shared-install_only.tar.gz +b9d6ee5ddac1198e72d53112698773fc8bb597de095592eb849ca794306699ba 20241206/cpython-3.12.8+20241206-x86_64-unknown-linux-gnu-install_only.tar.gz +6f305888703691dd04cfff85284d23ea0b0146ed7c4415e472f1fb72b3f32cdf 20241206/cpython-3.12.8+20241206-x86_64-unknown-linux-musl-install_only.tar.gz +e99f8457d9c79592c036489c5cfa78df76e4762d170665e499833e045d82608f 20250317/cpython-3.10.16+20250317-aarch64-apple-darwin-install_only.tar.gz +76d0f04d2444e77200fdc70d1c57480e29cca78cb7420d713bc1c523709c198d 20250317/cpython-3.10.16+20250317-aarch64-unknown-linux-gnu-install_only.tar.gz +39c9b3486de984fe1d72d90278229c70d6b08bcf69cd55796881b2d75077b603 20250317/cpython-3.10.16+20250317-ppc64le-unknown-linux-gnu-install_only.tar.gz +ebe949ada9293581c17d9bcdaa8f645f67d95f73eac65def760a71ef9dd6600d 20250317/cpython-3.10.16+20250317-riscv64-unknown-linux-gnu-install_only.tar.gz +9b2fc0b7f1c75b48e799b6fa14f7e24f5c61f2db82e3c65d13ed25e08f7f0857 20250317/cpython-3.10.16+20250317-s390x-unknown-linux-gnu-install_only.tar.gz +e03e62dbe95afa2f56b7344ff3bd061b180a0b690ff77f9a1d7e6601935e05ca 20250317/cpython-3.10.16+20250317-x86_64-apple-darwin-install_only.tar.gz +c7e0eb0ff5b36758b7a8cacd42eb223c056b9c4d36eded9bf5b9fe0c0b9aeb08 20250317/cpython-3.10.16+20250317-x86_64-pc-windows-msvc-install_only.tar.gz +b350c7e63956ca8edb856b91316328e0fd003a840cbd63d08253af43b2c63643 20250317/cpython-3.10.16+20250317-x86_64-unknown-linux-gnu-install_only.tar.gz +6ed64923ee4fbea4c5780f1a5a66651d239191ac10bd23420db4f5e4e0bf79c4 20250317/cpython-3.10.16+20250317-x86_64-unknown-linux-musl-install_only.tar.gz +7c7fd9809da0382a601a79287b5d62d61ce0b15f5a5ee836233727a516e85381 20250317/cpython-3.12.9+20250317-aarch64-apple-darwin-install_only.tar.gz +00c6bf9acef21ac741fea24dc449d0149834d30e9113429e50a95cce4b00bb80 20250317/cpython-3.12.9+20250317-aarch64-unknown-linux-gnu-install_only.tar.gz +25d77599dfd5849f17391d92da0da99079e4e94f19a881f763f5cc62530ef7e1 20250317/cpython-3.12.9+20250317-ppc64le-unknown-linux-gnu-install_only.tar.gz +e97ab0fdf443b302c56a52b4fd08f513bf3be66aa47263f0f9df3c6e60e05f2e 20250317/cpython-3.12.9+20250317-riscv64-unknown-linux-gnu-install_only.tar.gz +7492d079ffa8425c8f6c58e43b237c37e3fb7b31e2e14635927bb4d3397ba21e 20250317/cpython-3.12.9+20250317-s390x-unknown-linux-gnu-install_only.tar.gz +1ee1b1bb9fbce5c145c4bec9a3c98d7a4fa22543e09a7c1d932bc8599283c2dc 20250317/cpython-3.12.9+20250317-x86_64-apple-darwin-install_only.tar.gz +d15361fd202dd74ae9c3eece1abdab7655f1eba90bf6255cad1d7c53d463ed4d 20250317/cpython-3.12.9+20250317-x86_64-pc-windows-msvc-install_only.tar.gz +ef382fb88cbb41a3b0801690bd716b8a1aec07a6c6471010bcc6bd14cd575226 20250317/cpython-3.12.9+20250317-x86_64-unknown-linux-gnu-install_only.tar.gz +94e3837da1adf9964aab2d6047b33f70167de3096d1f9a2d1fa9340b1bbf537d 20250317/cpython-3.12.9+20250317-x86_64-unknown-linux-musl-install_only.tar.gz +c98c9c977e6fa05c3813bd49f3553904d89d60fed27e2e36468da7afa1d6d5e2 20250317/cpython-3.13.2+20250317-aarch64-apple-darwin-freethreaded+pgo+lto-full.tar.zst +faa44274a331eb39786362818b21b3a4e74514e8805000b20b0e55c590cecb94 20250317/cpython-3.13.2+20250317-aarch64-apple-darwin-install_only.tar.gz +b8635e59e3143fd17f19a3dfe8ccc246ee6587c87da359bd1bcab35eefbb5f19 20250317/cpython-3.13.2+20250317-aarch64-unknown-linux-gnu-freethreaded+lto-full.tar.zst +9c67260446fee6ea706dad577a0b32936c63f449c25d66e4383d5846b2ab2e36 20250317/cpython-3.13.2+20250317-aarch64-unknown-linux-gnu-install_only.tar.gz +6ae8fa44cb2edf4ab49cff1820b53c40c10349c0f39e11b8cd76ce7f3e7e1def 20250317/cpython-3.13.2+20250317-ppc64le-unknown-linux-gnu-freethreaded+lto-full.tar.zst +345b53d2f86c9dbd7f1320657cb227ff9a42ef63ff21f129abbbc8c82a375147 20250317/cpython-3.13.2+20250317-ppc64le-unknown-linux-gnu-install_only.tar.gz +2af1b8850c52801fb6189e7a17a51e0c93d9e46ddefcca72247b76329c97d02a 20250317/cpython-3.13.2+20250317-riscv64-unknown-linux-gnu-freethreaded+lto-full.tar.zst +172d22b2330737f3a028ea538ffe497c39a066a8d3200b22dd4d177a3332ad85 20250317/cpython-3.13.2+20250317-riscv64-unknown-linux-gnu-install_only.tar.gz +c074144cc80c2af32c420b79a9df26e8db405212619990c1fbdd308bd75afe3f 20250317/cpython-3.13.2+20250317-s390x-unknown-linux-gnu-freethreaded+lto-full.tar.zst +ec3b16ea8a97e3138acec72bc5ff35949950c62c8994a8ec8e213fd93f0e806b 20250317/cpython-3.13.2+20250317-s390x-unknown-linux-gnu-install_only.tar.gz +0d73e4348d8d4b5159058609d2303705190405b485dd09ad05d870d7e0f36e0f 20250317/cpython-3.13.2+20250317-x86_64-apple-darwin-freethreaded+pgo+lto-full.tar.zst +ee4526e84b5ce5b11141c50060b385320f2773616249a741f90c96d460ce8e8f 20250317/cpython-3.13.2+20250317-x86_64-apple-darwin-install_only.tar.gz +c51b4845fda5421e044067c111192f645234081d704313f74ee77fa013a186ea 20250317/cpython-3.13.2+20250317-x86_64-pc-windows-msvc-freethreaded+pgo-full.tar.zst +84d7b52f3558c8e35c670a4fa14080c75e3ec584adfae49fec8b51008b75b21e 20250317/cpython-3.13.2+20250317-x86_64-pc-windows-msvc-install_only.tar.gz +1aea5062614c036904b55c1cc2fb4b500b7f6f7a4cacc263f4888889d355eef8 20250317/cpython-3.13.2+20250317-x86_64-unknown-linux-gnu-freethreaded+pgo+lto-full.tar.zst +db011f0cd29cab2291584958f4e2eb001b0e6051848d89b38a2dc23c5c54e512 20250317/cpython-3.13.2+20250317-x86_64-unknown-linux-gnu-install_only.tar.gz +00bb2d629f7eacbb5c6b44dc04af26d1f1da64cee3425b0d8eb5135a93830296 20250317/cpython-3.13.2+20250317-x86_64-unknown-linux-musl-install_only.tar.gz +278dccade56b4bbeecb9a613b77012cf5c1433a5e9b8ef99230d5e61f31d9e02 20250610/cpython-3.13.4+20250610-aarch64-apple-darwin-freethreaded+pgo+lto-full.tar.zst +c2ce6601b2668c7bd1f799986af5ddfbff36e88795741864aba6e578cb02ed7f 20250610/cpython-3.13.4+20250610-aarch64-apple-darwin-install_only.tar.gz +b1c1bd6ab9ef95b464d92a6a911cef1a8d9f0b0f6a192f694ef18ed15d882edf 20250610/cpython-3.13.4+20250610-aarch64-unknown-linux-gnu-freethreaded+lto-full.tar.zst +3c2596ece08ffe17e11bc1f27aeb4ce1195d2490a83d695d36ef4933d5c5ca53 20250610/cpython-3.13.4+20250610-aarch64-unknown-linux-gnu-install_only.tar.gz +ed66ae213a62b286b9b7338b816ccd2815f5248b7a28a185dc8159fe004149ae 20250610/cpython-3.13.4+20250610-ppc64le-unknown-linux-gnu-freethreaded+lto-full.tar.zst +b3cc13ee177b8db1d3e9b2eac413484e3c6a356f97d91dc59de8d3fd8cf79d6b 20250610/cpython-3.13.4+20250610-ppc64le-unknown-linux-gnu-install_only.tar.gz +913264545215236660e4178bc3e5b57a20a444a8deb5c11680c95afc960b4016 20250610/cpython-3.13.4+20250610-riscv64-unknown-linux-gnu-freethreaded+lto-full.tar.zst +d1b989e57a9ce29f6c945eeffe0e9750c222fdd09e99d2f8d6b0d8532a523053 20250610/cpython-3.13.4+20250610-riscv64-unknown-linux-gnu-install_only.tar.gz +7556a38ab5e507c1ec22bc38f9859982bc956cab7f4de05a2faac114feb306db 20250610/cpython-3.13.4+20250610-s390x-unknown-linux-gnu-freethreaded+lto-full.tar.zst +d1d19fb01961ac6476712fdd6c5031f74c83666f6f11aa066207e9a158f7e3d8 20250610/cpython-3.13.4+20250610-s390x-unknown-linux-gnu-install_only.tar.gz +64ab7ac8c88002d9ba20a92f72945bfa350268e944a7922500af75d20330574d 20250610/cpython-3.13.4+20250610-x86_64-apple-darwin-freethreaded+pgo+lto-full.tar.zst +79feb6ca68f3921d07af52d9db06cf134e6f36916941ea850ab0bc20f5ff638b 20250610/cpython-3.13.4+20250610-x86_64-apple-darwin-install_only.tar.gz +9457504547edb2e0156bf76b53c7e4941c7f61c0eff9fd5f4d816d3df51c58e3 20250610/cpython-3.13.4+20250610-x86_64-pc-windows-msvc-freethreaded+pgo-full.tar.zst +29ac3585cc2dcfd79e3fe380c272d00e9d34351fc456e149403c86d3fea34057 20250610/cpython-3.13.4+20250610-x86_64-pc-windows-msvc-install_only.tar.gz +864df6e6819e8f8e855ce30f34410fdc5867d0616e904daeb9a40e5806e970d7 20250610/cpython-3.13.4+20250610-x86_64-unknown-linux-gnu-freethreaded+pgo+lto-full.tar.zst +44e5477333ebca298a7a0a316985c6c3533b8645f92a83f7f73c44033832bf32 20250610/cpython-3.13.4+20250610-x86_64-unknown-linux-gnu-install_only.tar.gz +a3afbfa94b9ff4d9fc426b47eb3c8446cada535075b8d51b7bdc9d9ab9911fc2 20250610/cpython-3.13.4+20250610-x86_64-unknown-linux-musl-install_only.tar.gz +a94c02b2d597cd6b075a713fe4e9a909cc97ca6a3b2b2ce86eda21be2062d48e 20250808/cpython-3.10.18+20250808-aarch64-apple-darwin-install_only.tar.gz +ef7de3b715d519e246d98ff7856247f7f7b357068705f09c6f300b7e7b76c701 20250808/cpython-3.10.18+20250808-aarch64-unknown-linux-gnu-install_only.tar.gz +f580efed11cc54e1a221c052e8bc88bfbc12844d3ca8949da828351a1232386e 20250808/cpython-3.10.18+20250808-ppc64le-unknown-linux-gnu-install_only.tar.gz +0d7e460e30203a9225b6f417ae972f66415a1cc0e32b37ebc48d195816282669 20250808/cpython-3.10.18+20250808-riscv64-unknown-linux-gnu-install_only.tar.gz +d4ada974daadb08a0184c19232ee3b03b3137aa70609760e1a94aaf7b12989ef 20250808/cpython-3.10.18+20250808-s390x-unknown-linux-gnu-install_only.tar.gz +da96fe2ba841640215788ddb9f151f03629360e37fcb94d4f76e5095b87df0d4 20250808/cpython-3.10.18+20250808-x86_64-apple-darwin-install_only.tar.gz +a648f3c9d136985ccfe57a5507e73d9d0839f7fd09eebd7c247857f2feaecb2a 20250808/cpython-3.10.18+20250808-x86_64-pc-windows-msvc-install_only.tar.gz +0b310a73bb9e7a495dbcad5f685e508ca2e7b36ee8f29301a52285730c425789 20250808/cpython-3.10.18+20250808-x86_64-unknown-linux-gnu-install_only.tar.gz +9cecf6ea2effbe183faebcf7e1160425a4ee17a68e49f2eefe5e1c59c51fa7ee 20250808/cpython-3.10.18+20250808-x86_64-unknown-linux-musl-install_only.tar.gz +d089bfd2c7b98a0942750a195e70d3172beda76d7747097b8afd87028b6e59b6 20250808/cpython-3.11.13+20250808-aarch64-apple-darwin-install_only.tar.gz +a632857c966237e7fd38b44c47c350f6e30d8ec54dcad6c832865ad670f0f22f 20250808/cpython-3.11.13+20250808-aarch64-pc-windows-msvc-install_only.tar.gz +bc57105f8a16acd57b71d926143c7f6ecf61729b40c8b4656f1b98bebd47c710 20250808/cpython-3.11.13+20250808-aarch64-unknown-linux-gnu-install_only.tar.gz +16a0165b0744940702b8fff80b8bf973ac914f78cb6fca28d389583f675e84de 20250808/cpython-3.11.13+20250808-ppc64le-unknown-linux-gnu-install_only.tar.gz +d8e62306be8f41c46bcd62ca68f91a1467f47adff632a35ff413dc1043ed56e8 20250808/cpython-3.11.13+20250808-riscv64-unknown-linux-gnu-install_only.tar.gz +4e302a4514a73baefdd9b327062bdafeb4115a799deec91c185f6ab45a857241 20250808/cpython-3.11.13+20250808-s390x-unknown-linux-gnu-install_only.tar.gz +d946d618f8bba8308b67e460a30612a71e2ccc309f85f6628aaae24e2b816981 20250808/cpython-3.11.13+20250808-x86_64-apple-darwin-install_only.tar.gz +ed963aee33d29ad8abfbb5fe63e42f57a2638a4a11a88e11d8bb66e61f20a6e5 20250808/cpython-3.11.13+20250808-x86_64-pc-windows-msvc-install_only.tar.gz +3ad988c702cbb017fef1208d47dea4138a2e85fd0f7f01ec5e1e335e597131b9 20250808/cpython-3.11.13+20250808-x86_64-unknown-linux-gnu-install_only.tar.gz +3a5810f0696f844289aa06d5c3a1efeab66eee999c25196b7d1954192a2c2100 20250808/cpython-3.11.13+20250808-x86_64-unknown-linux-musl-install_only.tar.gz +8792c4a84c364ab975feca0c27d3157a5435b7baab325a346ae56b223893b661 20250808/cpython-3.12.11+20250808-aarch64-apple-darwin-install_only.tar.gz +00bf7d7e8bcf5d1e9c4dfca0247d8e035147777cd57ee9d4c64dedca86b0a464 20250808/cpython-3.12.11+20250808-aarch64-pc-windows-msvc-install_only.tar.gz +4d7ba5314fab02130d6538f074961ffbf61310cade9180e59026074f9a8939cb 20250808/cpython-3.12.11+20250808-aarch64-unknown-linux-gnu-install_only.tar.gz +2c862eb40a81549d9c11e6bf5a7f07c3406310b14e6a4d16dcdf1c4763ef7090 20250808/cpython-3.12.11+20250808-ppc64le-unknown-linux-gnu-install_only.tar.gz +0bb729b95fabd49c7b495f7c44a9086e3970ea57daf66365741574bd36a17e81 20250808/cpython-3.12.11+20250808-riscv64-unknown-linux-gnu-install_only.tar.gz +99e465882d217d24ac90e99fac8f32e6a644d0340ac05ee510fb5cdf53f0cfb8 20250808/cpython-3.12.11+20250808-s390x-unknown-linux-gnu-install_only.tar.gz +e0c932709dafb05f00e528a7560ef8ee559ac82b75faca60dd1245bca1c1553f 20250808/cpython-3.12.11+20250808-x86_64-apple-darwin-install_only.tar.gz +81214ef71964a40ec269a79067ca490d45298c350583bc3af0e5781451a05c3c 20250808/cpython-3.12.11+20250808-x86_64-pc-windows-msvc-install_only.tar.gz +63d78840bf209af8da8f24e335d910f88387b892ca9187be571d481c071751bb 20250808/cpython-3.12.11+20250808-x86_64-unknown-linux-gnu-install_only.tar.gz +d633d070780590aa03ac5575cd9d7b9e17682d80f14b400313c009c387cf706b 20250808/cpython-3.12.11+20250808-x86_64-unknown-linux-musl-install_only.tar.gz +f2143304012e021a603bf1807bf3e4ce163832e43ab9a9829e53cb136497f207 20250808/cpython-3.13.6+20250808-aarch64-apple-darwin-freethreaded+pgo+lto-full.tar.zst +8a1efa6af4e80f08e2c97dda822a3d6c24d6c98e518242f802c6a43ae8401488 20250808/cpython-3.13.6+20250808-aarch64-apple-darwin-install_only.tar.gz +552cfabcc3b103f4b1c4036d2592d5f0373c9554a2c4d2b6631b04ef7e592067 20250808/cpython-3.13.6+20250808-aarch64-pc-windows-msvc-freethreaded+pgo-full.tar.zst +8e1617bd407ec1a874499daab26ae95080d1e0267ae616d34490137a28705827 20250808/cpython-3.13.6+20250808-aarch64-pc-windows-msvc-install_only.tar.gz +d84a7d64c284be387386b9f5da273f6d05486eb6bd8f9e86e2575cb59604cb22 20250808/cpython-3.13.6+20250808-aarch64-unknown-linux-gnu-freethreaded+pgo+lto-full.tar.zst +11fa0591ae2211c08a42ae54944260e36ddf88a1d5604ea0c49e2477be4e5388 20250808/cpython-3.13.6+20250808-aarch64-unknown-linux-gnu-install_only.tar.gz +e76fcaf1bf80a615520dbe7f85ca0bb557fad96d132d836b0ac721e7cc1e2a37 20250808/cpython-3.13.6+20250808-ppc64le-unknown-linux-gnu-freethreaded+lto-full.tar.zst +8dcf34ae1a685fe1893b52917ae04f23328edadc4acae28499d43850c2bdd26c 20250808/cpython-3.13.6+20250808-ppc64le-unknown-linux-gnu-install_only.tar.gz +24e08a39ba4fc77753e61541e52eed39cc871f4a92a80a3c5dd495056bd8eff9 20250808/cpython-3.13.6+20250808-riscv64-unknown-linux-gnu-freethreaded+lto-full.tar.zst +f8ed75aa6cc2011a046be00b629c3c8295267f34280324feaff34c73e7afce39 20250808/cpython-3.13.6+20250808-riscv64-unknown-linux-gnu-install_only.tar.gz +1609b223fd38a4a7a4d20e7173d7d9390fe2258f7dd9a15dc9ef0fa49613735d 20250808/cpython-3.13.6+20250808-s390x-unknown-linux-gnu-freethreaded+lto-full.tar.zst +7707ee5d19a78bc64ef8a66751ec7f97b64ea06714c7b1b52e8b321c2923ead8 20250808/cpython-3.13.6+20250808-s390x-unknown-linux-gnu-install_only.tar.gz +4360a1278dd0a96b526d108c8fd23498a9d2028dd7791e510fd51ff5ea3f462a 20250808/cpython-3.13.6+20250808-x86_64-apple-darwin-freethreaded+pgo+lto-full.tar.zst +27badce7201321a8363219e438a6205165e5b4884012b1046532203df2ec9379 20250808/cpython-3.13.6+20250808-x86_64-apple-darwin-install_only.tar.gz +4e727cdbe4057b16a170f887c0fa4227a825ac59bcda84ae946c77cc932af78c 20250808/cpython-3.13.6+20250808-x86_64-pc-windows-msvc-freethreaded+pgo-full.tar.zst +af5cc733c33b9aa9f1d74c81a59351e9b27215486d8b6cdbc06d97646a58c953 20250808/cpython-3.13.6+20250808-x86_64-pc-windows-msvc-install_only.tar.gz +e48c13c59cc3c01b79f63c8bccec27d2db6e97f64213b8731e2077b6ed8ed52c 20250808/cpython-3.13.6+20250808-x86_64-unknown-linux-gnu-freethreaded+pgo+lto-full.tar.zst +f844e8c8b6847628b472f7e97d8893a4e93acd5382a902b465776063668c4d64 20250808/cpython-3.13.6+20250808-x86_64-unknown-linux-gnu-install_only.tar.gz +70076dea0ff65b3c05aae1a97b4a556bf613cc73db30309e59134f9d318f4f7b 20250808/cpython-3.13.6+20250808-x86_64-unknown-linux-musl-install_only.tar.gz +43bda24c2fc073bc308bf631203b917a72640d59b59fdad4ba14503d84727012 20251031/cpython-3.10.19+20251031-aarch64-apple-darwin-install_only.tar.gz +f77a8a8aa77f3f943126fa9215a25309da4bf20398fc8f4b4eec54b5fc7570ef 20251031/cpython-3.10.19+20251031-aarch64-unknown-linux-gnu-install_only.tar.gz +1c55d160fc4c3b93528cd6aaa2bb4ca6018a99e5a45919d33dc761a43a69f860 20251031/cpython-3.10.19+20251031-ppc64le-unknown-linux-gnu-install_only.tar.gz +21134d35721cdad4c881f35d0957cc19df9a45d194afb38a099faded3c1cfb4d 20251031/cpython-3.10.19+20251031-riscv64-unknown-linux-gnu-install_only.tar.gz +df0db070f1eb73ab4e371eea32213ddb3500737ea5560a6f0ffd65c82af64ddc 20251031/cpython-3.10.19+20251031-s390x-unknown-linux-gnu-install_only.tar.gz +76c12e633c09c2a790f8a958a55df4495527e0718d1875310c836e757c0c7b55 20251031/cpython-3.10.19+20251031-x86_64-apple-darwin-install_only.tar.gz +cfa08a4caf2df1b43551b843c052d6a8814e2ea0c97268b021f0423646c244c3 20251031/cpython-3.10.19+20251031-x86_64-pc-windows-msvc-install_only.tar.gz +fb1caac917d7b6497bb6f5950da5f1e48d05c43a498948dd97f85760c4382d9f 20251031/cpython-3.10.19+20251031-x86_64-unknown-linux-gnu-install_only.tar.gz +ba85013ed5ac7733fc6840168cc33ed19e9959b363dc80227d54f8fd9c92c0f4 20251031/cpython-3.10.19+20251031-x86_64-unknown-linux-musl-install_only.tar.gz +6de5572b33c65af1c9b7caf00ec593fb04cffb7e14fa393a98261bb9bc464713 20251031/cpython-3.11.14+20251031-aarch64-apple-darwin-install_only.tar.gz +38d0d1466561e15965e8d2c20f5e5be649598f55c761ecab553d087fbd217337 20251031/cpython-3.11.14+20251031-aarch64-pc-windows-msvc-install_only.tar.gz +510edb027527413c4249256194cb8ad2590b52dd93f7123b4cb341aff5d05894 20251031/cpython-3.11.14+20251031-aarch64-unknown-linux-gnu-install_only.tar.gz +4e0bc6a818e0c6a9d7d3ebe1a95591fd84440520577aa837facc96a4b7a80e35 20251031/cpython-3.11.14+20251031-ppc64le-unknown-linux-gnu-install_only.tar.gz +16519e69297144f81b2421333bc9e0b6466cf3c84749b216b695cfb4c9deb32f 20251031/cpython-3.11.14+20251031-riscv64-unknown-linux-gnu-install_only.tar.gz +5f9c1b203cdf34c8bff1aef69b63bbf11309bd16ca6e429d8c3651eaa2b3d080 20251031/cpython-3.11.14+20251031-s390x-unknown-linux-gnu-install_only.tar.gz +4891cbf34e8652b7bd1054b9502395e4b7e048e2e517c040fbf6c8297cb954d6 20251031/cpython-3.11.14+20251031-x86_64-apple-darwin-install_only.tar.gz +5223b83ed9e2aa5e9e17d2ebcf767956e998876339b9cde1980a47e9d4655fb6 20251031/cpython-3.11.14+20251031-x86_64-pc-windows-msvc-install_only.tar.gz +60f0bd473d861cc45d3401d9914e47ccb9fa037f88a91879ed517a62042b8477 20251031/cpython-3.11.14+20251031-x86_64-unknown-linux-gnu-install_only.tar.gz +25e82d1e85b90a8ab724ee633a1811b1921797f5c25ee69c6595052371b91a87 20251031/cpython-3.11.14+20251031-x86_64-unknown-linux-musl-install_only.tar.gz +5e110cb821d2eb8246065d3b46faa655180c976c4e17250f7883c634a629bc63 20251031/cpython-3.12.12+20251031-aarch64-apple-darwin-install_only.tar.gz +b190fed7c2b0f6e1010f554a0d1fd191c0754c4c0718e69d9d795ae559613780 20251031/cpython-3.12.12+20251031-aarch64-pc-windows-msvc-install_only.tar.gz +81b644d166e0bfb918615af8a2363f8fcf26eccdcc60a5334b6a62c088470bac 20251031/cpython-3.12.12+20251031-aarch64-unknown-linux-gnu-install_only.tar.gz +024f5e5678c9768d45cc24d37a8e9d265aae86c4a4602352dee3d7deba367052 20251031/cpython-3.12.12+20251031-ppc64le-unknown-linux-gnu-install_only.tar.gz +b13c57fc372c131e667a99b9680f41c0b4da571cf99ed412103c2fe9ad5ed1fb 20251031/cpython-3.12.12+20251031-riscv64-unknown-linux-gnu-install_only.tar.gz +2bf05bdd56cdf5ea4fd9f2faf151ea4211be96a0d1f4230b85f5dcae620d6400 20251031/cpython-3.12.12+20251031-s390x-unknown-linux-gnu-install_only.tar.gz +687052a046d33be49dc95dd671816709067cf6176ed36c93ea61b1fe0b883b0f 20251031/cpython-3.12.12+20251031-x86_64-apple-darwin-install_only.tar.gz +cff398b3f520c442a1b085dd347126c10c1b03f01ccc0decd8c897a687e893f1 20251031/cpython-3.12.12+20251031-x86_64-pc-windows-msvc-install_only.tar.gz +80c3882f14e15cef8260ef5257d198e8f4371ca265887431d939e0d561de3253 20251031/cpython-3.12.12+20251031-x86_64-unknown-linux-gnu-install_only.tar.gz +0a461330b9b89f2ea3088dde10d7a3f96aa65897b7c5ce2404fa3b5c4b8daa14 20251031/cpython-3.12.12+20251031-x86_64-unknown-linux-musl-install_only.tar.gz +eae1272a72ccce601590a10a9ca2a58199b5fcdf022aa603a527e3e2a04de9bc 20251031/cpython-3.13.9+20251031-aarch64-apple-darwin-freethreaded+pgo+lto-full.tar.zst +1f3568d17383426d52350c2ef7c93c1a5a043198b860cb05e5d19b35f9c25cef 20251031/cpython-3.13.9+20251031-aarch64-apple-darwin-install_only.tar.gz +743ff69935ef28834621647dab30f032dfcd80315732917531eea333210941c7 20251031/cpython-3.13.9+20251031-aarch64-pc-windows-msvc-freethreaded+pgo-full.tar.zst +20db43873d3c4c2175d866806545e4ad4ec6bb72ca95e60082a4df6c24567e8c 20251031/cpython-3.13.9+20251031-aarch64-pc-windows-msvc-install_only.tar.gz +a6e72f9de5d9b46cf6968d6a492f2401a919f9b959f8da2d87f43484b80169ee 20251031/cpython-3.13.9+20251031-aarch64-unknown-linux-gnu-freethreaded+pgo+lto-full.tar.zst +0a56d11b0fb1662e67f892b9d5d1717aef06f24dbb8362bc25b8f784e620d44e 20251031/cpython-3.13.9+20251031-aarch64-unknown-linux-gnu-install_only.tar.gz +0ed5c65437f875c58ba1bee2b8d261d18698d3d0347a2e66f8902fce022a2cda 20251031/cpython-3.13.9+20251031-ppc64le-unknown-linux-gnu-freethreaded+lto-full.tar.zst +99492123902bd5e9a6b1a30135061e93a2e6a11d25107a741d5a756e91054448 20251031/cpython-3.13.9+20251031-ppc64le-unknown-linux-gnu-install_only.tar.gz +584e481d9b5225ffaf02f158fb26d2818207e65fc3c6dc21a6d500277f739220 20251031/cpython-3.13.9+20251031-riscv64-unknown-linux-gnu-freethreaded+lto-full.tar.zst +b3dce3e4ef508773521e1ee1be989fff6118f8fd1fbbd0491d7ff7dfbc98ef06 20251031/cpython-3.13.9+20251031-riscv64-unknown-linux-gnu-install_only.tar.gz +7fa7fb912ca989ceac026a332d56a2c7d6d16ab0e94d89e690de5aade26103e2 20251031/cpython-3.13.9+20251031-s390x-unknown-linux-gnu-freethreaded+lto-full.tar.zst +f10e34aaa856c1b8a69c2ea4a9a6723d520443d1a957bf66dc55491334ca0c1e 20251031/cpython-3.13.9+20251031-s390x-unknown-linux-gnu-install_only.tar.gz +e2bf5fa6a3ef443ade362e08b0a19bbc172f7bfe34dabe933ccaad31d53af5da 20251031/cpython-3.13.9+20251031-x86_64-apple-darwin-freethreaded+pgo+lto-full.tar.zst +48c0f3ca5d31e90658ef99138dc21865bb62f388ab97a1ce72cac176da194ab0 20251031/cpython-3.13.9+20251031-x86_64-apple-darwin-install_only.tar.gz +318a9a1e43dd52054327de3bccc0c5b7afde7b7f2a398ccb4d38e03d28b05386 20251031/cpython-3.13.9+20251031-x86_64-pc-windows-msvc-freethreaded+pgo-full.tar.zst +874593f641f31ea101440c70f81768c35d4d7d6df111fde63094db67465ef787 20251031/cpython-3.13.9+20251031-x86_64-pc-windows-msvc-install_only.tar.gz +dcc29b069d0588fbd4ea29c6df840c8d1207d2a3bce8cd5cd57d1b85373b6048 20251031/cpython-3.13.9+20251031-x86_64-unknown-linux-gnu-freethreaded+pgo+lto-full.tar.zst +6f05b91ee8c7e6dd0f9c60b95bb29130e2d623961de6578b643e80ddd83f96b6 20251031/cpython-3.13.9+20251031-x86_64-unknown-linux-gnu-install_only.tar.gz +ad987197034185e628715da504a50613af213dc21ba6d5ccaeab3db2c464aa6c 20251031/cpython-3.13.9+20251031-x86_64-unknown-linux-musl-install_only.tar.gz +d9c7b430b25bd3837dbb03f945dbe6b7bc526c5940ca96f5db7cdc42f6b2b801 20251031/cpython-3.14.0+20251031-aarch64-apple-darwin-freethreaded+pgo+lto-full.tar.zst +b4bcd3c6c24cab32ae99e1b05c89312b783b4d69431d702e5012fe1fdcad4087 20251031/cpython-3.14.0+20251031-aarch64-apple-darwin-install_only.tar.gz +40266e60f655e49cd1d5303295255909a4b593b08b88be6e6a55b2c9fe6ed13d 20251031/cpython-3.14.0+20251031-aarch64-pc-windows-msvc-freethreaded+pgo-full.tar.zst +599a8b7e12439cd95a201dbdfe95cf363146b1ff91f379555dafd86b170caab9 20251031/cpython-3.14.0+20251031-aarch64-pc-windows-msvc-install_only.tar.gz +f383ef50d1da6ca511212e5ae601923b56636b87351fd5fc847e0ea0a19fa9b3 20251031/cpython-3.14.0+20251031-aarch64-unknown-linux-gnu-freethreaded+pgo+lto-full.tar.zst +128a9cbfb9645d5237ec01704d9d1d2ac5f084464cc43c37a4cd96aa9c3b1ad5 20251031/cpython-3.14.0+20251031-aarch64-unknown-linux-gnu-install_only.tar.gz +cb0e4ff781b856a47f0f461ceb41c78c7eeff65effd0957857ec4702ef1e1bd3 20251031/cpython-3.14.0+20251031-ppc64le-unknown-linux-gnu-freethreaded+lto-full.tar.zst +e16ca51f018e99a609faf953bd3a3aea31f45ee84262d1a517fb3abd98f1f4af 20251031/cpython-3.14.0+20251031-ppc64le-unknown-linux-gnu-install_only.tar.gz +929223470d11a55cd75f880ac3bd4969e42407e2cdf08d4e7e38ba721cf4abec 20251031/cpython-3.14.0+20251031-riscv64-unknown-linux-gnu-freethreaded+lto-full.tar.zst +fca340d8fb7a05cd90e216ce601b25d492ed8c1a3b6a6d77703e0f15ab3711a7 20251031/cpython-3.14.0+20251031-riscv64-unknown-linux-gnu-install_only.tar.gz +613fb1f7b249f798b52af957d181305244e936c8e5c94c84688fcdf93fe14253 20251031/cpython-3.14.0+20251031-s390x-unknown-linux-gnu-freethreaded+lto-full.tar.zst +c5803644970eee931bb0581b3b64511d1a8612f67bc98951a7f7ab5581a9ed04 20251031/cpython-3.14.0+20251031-s390x-unknown-linux-gnu-install_only.tar.gz +b3196f6b57bbb3dc2ee07f348f1d51117ffa376979eceafbf50c15f0f7980bf8 20251031/cpython-3.14.0+20251031-x86_64-apple-darwin-freethreaded+pgo+lto-full.tar.zst +4e71a3ce973be377ef18637826648bb936e2f9490f64a9e4f33a49bcc431d344 20251031/cpython-3.14.0+20251031-x86_64-apple-darwin-install_only.tar.gz +b81de5fc9e783ea6dfcf1098c28a278c874999c71afbb0309f6a8b4276c769d0 20251031/cpython-3.14.0+20251031-x86_64-pc-windows-msvc-freethreaded+pgo-full.tar.zst +39acfcb3857d83eab054a3de11756ffc16b3d49c31393b9800dd2704d1f07fdf 20251031/cpython-3.14.0+20251031-x86_64-pc-windows-msvc-install_only.tar.gz +f4acbef0fbfaf7ab31ac63986da1d93dfa1c5cb797de1dcdc1a988aa18670120 20251031/cpython-3.14.0+20251031-x86_64-unknown-linux-gnu-freethreaded+pgo+lto-full.tar.zst +3dec1ab70758a3467ac3313bbcdabf7a9b3016db5c072c4537e3cf0a9e6290f6 20251031/cpython-3.14.0+20251031-x86_64-unknown-linux-gnu-install_only.tar.gz +d0a2a6d3b1bb00dce2105377fda8aa79675d187f8d6d7010a42f651af25018dc 20251031/cpython-3.14.0+20251031-x86_64-unknown-linux-musl-install_only.tar.gz +12f1b16be4017181ad67904caf9e59e525b9b5d62f49105017d837e27b832959 20251031/cpython-3.15.0a1+20251031-aarch64-apple-darwin-freethreaded+pgo+lto-full.tar.zst +3acf7aa3559b746498b18929456c5cacb84bae4e09249834cbc818970d71de87 20251031/cpython-3.15.0a1+20251031-aarch64-apple-darwin-install_only.tar.gz +54ca78dae455ece6fefbd7f5f287cc55d5ce197caf51921f6d871d15069d9489 20251031/cpython-3.15.0a1+20251031-aarch64-pc-windows-msvc-freethreaded+pgo-full.tar.zst +1508bcd7195008479ed156aad3afbb3a3793097ed530690f0304a8107f0e53e8 20251031/cpython-3.15.0a1+20251031-aarch64-pc-windows-msvc-install_only.tar.gz +981fe8dfc6e7e1d0ffefa945a18d5c4c759bbe21722acf3a5cc7e62f16aa5f3c 20251031/cpython-3.15.0a1+20251031-aarch64-unknown-linux-gnu-freethreaded+pgo+lto-full.tar.zst +d55c2aeece827e6bec83fd18515ee281d9ea0efaa3e2d20130db8f1c7cbb71c6 20251031/cpython-3.15.0a1+20251031-aarch64-unknown-linux-gnu-install_only.tar.gz +088400dec25139f38eeecb48f090ff2ce06a96a1dd79fa8f1dfec1cd1786f5ef 20251031/cpython-3.15.0a1+20251031-ppc64le-unknown-linux-gnu-freethreaded+lto-full.tar.zst +c28beda791c499b16f06256339522f0002a3e9acba003e6b8374755d7be1def2 20251031/cpython-3.15.0a1+20251031-ppc64le-unknown-linux-gnu-install_only.tar.gz +938061a0a31a06672526885de36037ddefd8c4acdb09424691b7000a8c8f8d01 20251031/cpython-3.15.0a1+20251031-riscv64-unknown-linux-gnu-freethreaded+lto-full.tar.zst +36619f576b8154e4b56643c5c4a85c352f152df2989c4e602cbbe9c2b7ded870 20251031/cpython-3.15.0a1+20251031-riscv64-unknown-linux-gnu-install_only.tar.gz +2003e7e40bb44b3db7bca81087bfb738fe6af40e5db61cda8e23b59bf55d409e 20251031/cpython-3.15.0a1+20251031-s390x-unknown-linux-gnu-freethreaded+lto-full.tar.zst +5ea47be2a3a563ddd87ff510dae26b7aa7f3855ca00c5f1056ff8114c067c4e4 20251031/cpython-3.15.0a1+20251031-s390x-unknown-linux-gnu-install_only.tar.gz +64fc29e6c7a2f02a18645d968f1b3fc1d00d12a5ef3fcbb0d077fa8c62c08904 20251031/cpython-3.15.0a1+20251031-x86_64-apple-darwin-freethreaded+pgo+lto-full.tar.zst +0ab19d3ac25f99da438b088751e5ec2421f9f6aa4292fd2dc0f8e49eb3e16bdf 20251031/cpython-3.15.0a1+20251031-x86_64-apple-darwin-install_only.tar.gz +34abc5603e1b4131f753d29b7deac865b9277912b851cbed5a149cf3e6745d3d 20251031/cpython-3.15.0a1+20251031-x86_64-pc-windows-msvc-freethreaded+pgo-full.tar.zst +5f5d6bec2b381cfc771c49972d2a6f7b7e7ab6a1651d8fb6ef3983f3571722b3 20251031/cpython-3.15.0a1+20251031-x86_64-pc-windows-msvc-install_only.tar.gz +0e0272186d9f5169394dbc4d4d72a3f4a5762a04c2e5ac2ab1e23aa41fc8538a 20251031/cpython-3.15.0a1+20251031-x86_64-unknown-linux-gnu-freethreaded+pgo+lto-full.tar.zst +1f356288c2b2713619cb7a4e453d33bf8882f812af2987e21e01e7ae382fefba 20251031/cpython-3.15.0a1+20251031-x86_64-unknown-linux-gnu-install_only.tar.gz +caf5311f333eef082dd69a669ca65aceba09a08fc1e78aad602ad649106f294c 20251031/cpython-3.15.0a1+20251031-x86_64-unknown-linux-musl-install_only.tar.gz +87275619c2706affa4d1090d2ca3dad354b6d69f8b85dbfafe38785870751b9a 20251031/cpython-3.9.25+20251031-aarch64-apple-darwin-install_only.tar.gz +6112d46355857680b81849764a6cf9f38cc4cd0d1cf29d432bc12fe5aeedf9d0 20251031/cpython-3.9.25+20251031-aarch64-unknown-linux-gnu-install_only.tar.gz +828364b6f54fa45ac2dc91f8e45d5b74306372af374a9ef16eeb2ea81253ed3f 20251031/cpython-3.9.25+20251031-ppc64le-unknown-linux-gnu-install_only.tar.gz +17467e0158e5ad04453c447d6773c23b044172276441e22e23058fd3ea053e27 20251031/cpython-3.9.25+20251031-riscv64-unknown-linux-gnu-install_only.tar.gz +3e9539f83e67faa813fd06171199b2d33c89821dfa9a33bf6e27ad67f1b6932d 20251031/cpython-3.9.25+20251031-s390x-unknown-linux-gnu-install_only.tar.gz +ace63cfe27a9487c4d72e1cb518be01c1d985271da0b2158e813801f7d3e5503 20251031/cpython-3.9.25+20251031-x86_64-apple-darwin-install_only.tar.gz +4fb1b416482ce94d73cfa140317a670c596c830671d137b07c26afe8c461768a 20251031/cpython-3.9.25+20251031-x86_64-pc-windows-msvc-install_only.tar.gz +42834f61eb6df43432c3dd6ab9ca3fdf8c06d10a404ebdb53d6902e6b9570b08 20251031/cpython-3.9.25+20251031-x86_64-unknown-linux-gnu-install_only.tar.gz +76593e8c889e81e82db5fe117fe15b69466f85100ab2ec0e4035aa86242b4e93 20251031/cpython-3.9.25+20251031-x86_64-unknown-linux-musl-install_only.tar.gz +3c9fdd76447c1549a0d3bc2a70c63f1daec997ab034206ac0260a03237166dbb 20251202/cpython-3.13.10+20251202-aarch64-apple-darwin-freethreaded+pgo+lto-full.tar.zst +37afe4e77ab62ac50f197b1cb1f3bc02c82735c6be893da0996afcde5dc41048 20251202/cpython-3.13.10+20251202-aarch64-apple-darwin-install_only.tar.gz +cdb7141327bdc244715b25752593e2c9eeb3cc2764f37dfe81cfbc92db9d6d57 20251202/cpython-3.13.10+20251202-aarch64-pc-windows-msvc-freethreaded+pgo-full.tar.zst +9060d644bd32ac0e0af970d0b21e207e6ff416b7c4dc26ffc4f9b043fb45b463 20251202/cpython-3.13.10+20251202-aarch64-pc-windows-msvc-install_only.tar.gz +6d277221fa4b172e00b29c7158ca9661917bc8db9a0084b1a0ff5c3a0ba8b648 20251202/cpython-3.13.10+20251202-aarch64-unknown-linux-gnu-freethreaded+pgo+lto-full.tar.zst +c68280591cda1c9515a04809fa6926020177e8e5892300206e0496ea1d10290e 20251202/cpython-3.13.10+20251202-aarch64-unknown-linux-gnu-install_only.tar.gz +d265d8d1c51e25ed70279540223589f79cf99ad00b50d28b6150c2658c973885 20251202/cpython-3.13.10+20251202-ppc64le-unknown-linux-gnu-freethreaded+lto-full.tar.zst +1507e5528bd88131dc742a2941176aceea1838bc09860c21f179285b7865133b 20251202/cpython-3.13.10+20251202-ppc64le-unknown-linux-gnu-install_only.tar.gz +ec411b4a2d167c3be0a9aeb3905e045d62c8e3c3db0caeade5d47d5f60b98dd0 20251202/cpython-3.13.10+20251202-riscv64-unknown-linux-gnu-freethreaded+lto-full.tar.zst +70169e916860b2e5b34c37c302d699eb2b8f24f28090968881942a37aeb7ed08 20251202/cpython-3.13.10+20251202-riscv64-unknown-linux-gnu-install_only.tar.gz +4fc6443948bf5b729481ea02cc5c68e80cd0da42631f6936587a2b8fd45bc62c 20251202/cpython-3.13.10+20251202-s390x-unknown-linux-gnu-freethreaded+lto-full.tar.zst +c5448863b64aacae62f3a213a6e6cf94ec63f96ee4d518491cd62fd3c81d952f 20251202/cpython-3.13.10+20251202-s390x-unknown-linux-gnu-install_only.tar.gz +6ce608684df0f90350c7a1742e9685a7782d9b26ec99d1bd9d55c8cf9a405040 20251202/cpython-3.13.10+20251202-x86_64-apple-darwin-freethreaded+pgo+lto-full.tar.zst +a02761a4f189f71c0512e88df7ca2843696d61da659e47f8a5c8a9bd2c0d16f4 20251202/cpython-3.13.10+20251202-x86_64-apple-darwin-install_only.tar.gz +6a8b0372ded655e0d55318089fbce3122a446e69bcd120c79aaadfe9b017299c 20251202/cpython-3.13.10+20251202-x86_64-pc-windows-msvc-freethreaded+pgo-full.tar.zst +8b00014c7c35f9ad4cb1c565f067500bacc4125c8bc30e4389ee0be9fd6ffa3d 20251202/cpython-3.13.10+20251202-x86_64-pc-windows-msvc-install_only.tar.gz +e39127fbe8d2ae7d86099f18b4da0918f9b60ce73ed491774d6dcfaa42b5c9ae 20251202/cpython-3.13.10+20251202-x86_64-unknown-linux-gnu-freethreaded+pgo+lto-full.tar.zst +0cac1495fff920219904b1d573aaec0df54d549c226cb45f5c60cb6d2c72727a 20251202/cpython-3.13.10+20251202-x86_64-unknown-linux-gnu-install_only.tar.gz +04108190972ac98e13098abd972ec3f4f8b0880f83c0bb68249ce1a6164fa041 20251202/cpython-3.13.10+20251202-x86_64-unknown-linux-musl-install_only.tar.gz +61f38e947449cf00f32f0838e813358f6bf61025d0797531e5b8b8b175c617f0 20251202/cpython-3.14.1+20251202-aarch64-apple-darwin-freethreaded+pgo+lto-full.tar.zst +cdf1ba0789f529fa34bb5b5619c5da9757ac1067d6b8dd0ee8b78e50078fc561 20251202/cpython-3.14.1+20251202-aarch64-apple-darwin-install_only.tar.gz +ddb10b645de2b1f6f2832a80b115a9cd34a4a760249983027efe46618a8efc48 20251202/cpython-3.14.1+20251202-aarch64-pc-windows-msvc-freethreaded+pgo-full.tar.zst +19129cf8b4d68c4e64c25bae43bca139d871267b59cf7f02b9dcf25f0bf59497 20251202/cpython-3.14.1+20251202-aarch64-pc-windows-msvc-install_only.tar.gz +1a88a1fe21eb443d280999464b1a397605a7ca950d8ab73813ca6868835439a2 20251202/cpython-3.14.1+20251202-aarch64-unknown-linux-gnu-freethreaded+pgo+lto-full.tar.zst +5dde7dba0b8ef34c0d5cb8a721254b1e11028bfc09ff06664879c245fe8df73f 20251202/cpython-3.14.1+20251202-aarch64-unknown-linux-gnu-install_only.tar.gz +7207b736ed2569f307649ffd4b615a5346631bc244730b8702babee377cef528 20251202/cpython-3.14.1+20251202-ppc64le-unknown-linux-gnu-freethreaded+lto-full.tar.zst +d2774701d53e2ac06f8c8c8e52dfa4ff346890de9b417c9a7664195443a4c766 20251202/cpython-3.14.1+20251202-ppc64le-unknown-linux-gnu-install_only.tar.gz +d1356ccd279920edc31bf0350674d966beb9522f9503846ed7855dbb109ccc14 20251202/cpython-3.14.1+20251202-riscv64-unknown-linux-gnu-freethreaded+lto-full.tar.zst +af840506efbcd5026d9140c0a0230e45e46bb1f339a65c10a22875930b2c0159 20251202/cpython-3.14.1+20251202-riscv64-unknown-linux-gnu-install_only.tar.gz +477758eabc06dbc7e5e5d16e97c4672478acd409f420dd2e1b84d3452c0668d1 20251202/cpython-3.14.1+20251202-s390x-unknown-linux-gnu-freethreaded+lto-full.tar.zst +43f8f79bf4c66689d2019f193671d1df3e5e5dbb293382036285e8ce55fc55bb 20251202/cpython-3.14.1+20251202-s390x-unknown-linux-gnu-install_only.tar.gz +c2cb2a9b44285fbc13c3c9b7eea813db6ed8d94909406b059db7afd39b32e786 20251202/cpython-3.14.1+20251202-x86_64-apple-darwin-freethreaded+pgo+lto-full.tar.zst +f25ce050e1d370f9c05c9623b769ffa4b269a6ae17e611b435fd2b8b09972a88 20251202/cpython-3.14.1+20251202-x86_64-apple-darwin-install_only.tar.gz +8ef7048315cac6d26bdbef18512a87b1a24fffa21cec86e32f9a9425f2af9bf6 20251202/cpython-3.14.1+20251202-x86_64-pc-windows-msvc-freethreaded+pgo-full.tar.zst +cb478a5a37eb93ce4d3c27ae64d211d6a5a42475ae53f666a8d1570e71fcf409 20251202/cpython-3.14.1+20251202-x86_64-pc-windows-msvc-install_only.tar.gz +c5d5b89aab7de683e465e36de2477a131435076badda775ef6e9ea21109c1c32 20251202/cpython-3.14.1+20251202-x86_64-unknown-linux-gnu-freethreaded+pgo+lto-full.tar.zst +a72f313bad49846e5e9671af2be7476033a877c80831cf47f431400ccb520090 20251202/cpython-3.14.1+20251202-x86_64-unknown-linux-gnu-install_only.tar.gz +15d50b15713097c38c67b1a06a0498ad102377f9b3999e98e4eefd6bf91bd82d 20251202/cpython-3.14.1+20251202-x86_64-unknown-linux-musl-install_only.tar.gz +4213058b7fcd875596c12b58cd46a399358b0a87ecde4b349cbdd00cf87ed79a 20251209/cpython-3.13.11+20251209-aarch64-apple-darwin-freethreaded+pgo+lto-full.tar.zst +295a9f7bc899ea1cc08baf60bbf511bdd1e4a29b2dd7e5f59b48f18bfa6bf585 20251209/cpython-3.13.11+20251209-aarch64-apple-darwin-install_only.tar.gz +6daf6d092c7294cfe68c4c7bf2698ac134235489c874b3bf796c7972b9dbba30 20251209/cpython-3.13.11+20251209-aarch64-pc-windows-msvc-freethreaded+pgo-full.tar.zst +ba646d0c3b7dd7bdfb770d9b2ebd6cd2df02a37fda90c9c79a7cf59c7df6f165 20251209/cpython-3.13.11+20251209-aarch64-pc-windows-msvc-install_only.tar.gz +290ca3bd0007db9e551f90b08dfcb6c1b2d62c33b2fc3e9a43e77d385d94f569 20251209/cpython-3.13.11+20251209-aarch64-unknown-linux-gnu-freethreaded+pgo+lto-full.tar.zst +ea1e678e6e82301bb32bf3917732125949b6e46d541504465972024a3f165343 20251209/cpython-3.13.11+20251209-aarch64-unknown-linux-gnu-install_only.tar.gz +09d4b50f8abb443f7e3af858c920aa61c2430b0954df465e861caa7078e55e69 20251209/cpython-3.13.11+20251209-ppc64le-unknown-linux-gnu-freethreaded+lto-full.tar.zst +7660e53aad9d35ee256913c6d98427f81f078699962035c5fa8b5c3138695109 20251209/cpython-3.13.11+20251209-ppc64le-unknown-linux-gnu-install_only.tar.gz +5406f2a7cacafbd2aac3ce2de066a0929aab55423824276c36e04cb83babc36c 20251209/cpython-3.13.11+20251209-riscv64-unknown-linux-gnu-freethreaded+lto-full.tar.zst +763fa1548e6a432e9402916e690c74ea30f26dcd2e131893dd506f72b87c27c9 20251209/cpython-3.13.11+20251209-riscv64-unknown-linux-gnu-install_only.tar.gz +3984b67c4292892eaccdd1c094c7ec788884c4c9b3534ab6995f6be96d5ed51d 20251209/cpython-3.13.11+20251209-s390x-unknown-linux-gnu-freethreaded+lto-full.tar.zst +ffb6af51fbfabfc6fbc4e7379bdec70c2f51e972b1d2f45c053493b9da3a1bbe 20251209/cpython-3.13.11+20251209-s390x-unknown-linux-gnu-install_only.tar.gz +d6f489464045d6895ae68b0a04a9e16477e74fe3185a75f3a9a0af8ccd25eade 20251209/cpython-3.13.11+20251209-x86_64-apple-darwin-freethreaded+pgo+lto-full.tar.zst +dac4a0a0a9b71f6b02a8b0886547fa22814474239bffb948e3e77185406ea136 20251209/cpython-3.13.11+20251209-x86_64-apple-darwin-install_only.tar.gz +bb9a29a7ba8f179273b79971da6aaa7be592d78c606a63f99eff3e4c12fb0fae 20251209/cpython-3.13.11+20251209-x86_64-pc-windows-msvc-freethreaded+pgo-full.tar.zst +87822417007045a28a7eccc47fe67b8c61265b99b10dbbfa24d231a3622b1c27 20251209/cpython-3.13.11+20251209-x86_64-pc-windows-msvc-install_only.tar.gz +33f89c957d986d525529b8a980103735776f4d20cf52f55960a057c760188ac3 20251209/cpython-3.13.11+20251209-x86_64-unknown-linux-gnu-freethreaded+pgo+lto-full.tar.zst +1ffa06d714a44aea14c0c54c30656413e5955a6c92074b4b3cb4351dcc28b63b 20251209/cpython-3.13.11+20251209-x86_64-unknown-linux-gnu-install_only.tar.gz +969fe24017380b987c4e3ce15e9edf82a4618c1e61672b2cc9b021a1c98eae78 20251209/cpython-3.13.11+20251209-x86_64-unknown-linux-musl-install_only.tar.gz +d6d17b8ef28326552cdeb2a7541c8a0cb711b378df9b93ebdb461dca065edfea 20251209/cpython-3.14.2+20251209-aarch64-apple-darwin-freethreaded+pgo+lto-full.tar.zst +2f74bd26bd16487aca357c879d11f7b16c0521328e5148a1930ab6357bcb89fe 20251209/cpython-3.14.2+20251209-aarch64-apple-darwin-install_only.tar.gz +43aac5bb4cdba71fc6775d26f47348d573a0b1210911438be71d7d96f4b18b51 20251209/cpython-3.14.2+20251209-aarch64-pc-windows-msvc-freethreaded+pgo-full.tar.zst +0be0d2557d73efa7f6f3f99679f05252d57fe2aad2d81cac3cad410a9b1eacbd 20251209/cpython-3.14.2+20251209-aarch64-pc-windows-msvc-install_only.tar.gz +adfcb90f3a7e1b3fbc6a99f9c8c8dce1f2e26ea72b724bbe4e9fa39e81e2b0db 20251209/cpython-3.14.2+20251209-aarch64-unknown-linux-gnu-freethreaded+pgo+lto-full.tar.zst +869af31b2963194e8a2ecfadc36027c4c1c86a10f4960baec36dadb41b2acf02 20251209/cpython-3.14.2+20251209-aarch64-unknown-linux-gnu-install_only.tar.gz +2b1ce0c5a5f5e5add7e4f934f5bd35ac41660895a30b3098db7f7303d6952a4f 20251209/cpython-3.14.2+20251209-ppc64le-unknown-linux-gnu-freethreaded+lto-full.tar.zst +86129976403fb5d64cf576329f94148f28cf6f82834e94df81ff31e9d5f404e0 20251209/cpython-3.14.2+20251209-ppc64le-unknown-linux-gnu-install_only.tar.gz +4efb610fa07a6ee2639d14d78fc3b6ecb47431c14e1e4bda03c7f7dd60a5c1e5 20251209/cpython-3.14.2+20251209-riscv64-unknown-linux-gnu-freethreaded+lto-full.tar.zst +318dceecf119ea903aef1fb03a552cc592ecd61c08da891b68f5755e21e13511 20251209/cpython-3.14.2+20251209-riscv64-unknown-linux-gnu-install_only.tar.gz +e62f3bb3e66dac6c459690f9e9cd8cc2f6fe1dcf8bfed452af4c3df24cd7874f 20251209/cpython-3.14.2+20251209-s390x-unknown-linux-gnu-freethreaded+lto-full.tar.zst +53875c849a14194344ead1d9cd1e128cadd42a4b83c35eeb212417909ef05a6a 20251209/cpython-3.14.2+20251209-s390x-unknown-linux-gnu-install_only.tar.gz +1fd76c79f7fc1753e8d2ed2f71406c0b65776c75f3e95ed99ffde8c95af2adc1 20251209/cpython-3.14.2+20251209-x86_64-apple-darwin-freethreaded+pgo+lto-full.tar.zst +58fa3e17d13ab956fd11055fb774c98ecfddcdf3b588e5f2369bdbc14ef9d76a 20251209/cpython-3.14.2+20251209-x86_64-apple-darwin-install_only.tar.gz +9927951e3997c186d2813ca1a0f4a8f5a2f771463f7f8ad0752fd3d2be2b74e4 20251209/cpython-3.14.2+20251209-x86_64-pc-windows-msvc-freethreaded+pgo-full.tar.zst +0d660bba9f58cb552e7e99e1f96a9c67b41618c9b8d29f9f3515fe2b5ad1966e 20251209/cpython-3.14.2+20251209-x86_64-pc-windows-msvc-install_only.tar.gz +3728872ffd74989a7b4bbf3f0c629ae8fe821cda2bd6544012c1b92b9f5d5a5b 20251209/cpython-3.14.2+20251209-x86_64-unknown-linux-gnu-freethreaded+pgo+lto-full.tar.zst +121c3249bef497adf601df76a4d89aed6053fc5ec2f8c0ec656b86f0142e8ddd 20251209/cpython-3.14.2+20251209-x86_64-unknown-linux-gnu-install_only.tar.gz +71639cc5d1fb79840467531c5b53ca77170a58edd3f7e2d29330dd736e477469 20251209/cpython-3.14.2+20251209-x86_64-unknown-linux-musl-install_only.tar.gz +5b34488580df13df051a2e84e43cfca2ab28fdd7a61052f35988eb8b481b894a 20251209/cpython-3.15.0a2+20251209-aarch64-apple-darwin-freethreaded+pgo+lto-full.tar.zst +5851f3744fbd39e3e323844cf4f68d7763fb25546aa5ffbb71b1b5ab69c56616 20251209/cpython-3.15.0a2+20251209-aarch64-apple-darwin-install_only.tar.gz +3d99152b4e29b947fb1cfc8d035d1d511e50aeed72886ff4a5fd0a3694bd0b51 20251209/cpython-3.15.0a2+20251209-aarch64-pc-windows-msvc-freethreaded+pgo-full.tar.zst +39bc2fcac13aeba7d650f76badf63350a81c86167a62174cb092eab7a749f4a5 20251209/cpython-3.15.0a2+20251209-aarch64-pc-windows-msvc-install_only.tar.gz +0c2c83236f6e28c103e2660a82be94b2459ee8cfdd90f5dd82f0d503ca2aec09 20251209/cpython-3.15.0a2+20251209-aarch64-unknown-linux-gnu-freethreaded+pgo+lto-full.tar.zst +17ba65d669be3052524e03b4d1426c072ef38df2a9065ff4525d1f4d1bc9f82c 20251209/cpython-3.15.0a2+20251209-aarch64-unknown-linux-gnu-install_only.tar.gz +216842df2377fd032f279ded7fd23d7bdbd92d4c1fa7619523bc0dbdef5bd212 20251209/cpython-3.15.0a2+20251209-ppc64le-unknown-linux-gnu-freethreaded+lto-full.tar.zst +5585bd7c5eefe28b9bf544d902cad9a2f81f33c618f2a1d3c006cbfcdec77abc 20251209/cpython-3.15.0a2+20251209-ppc64le-unknown-linux-gnu-install_only.tar.gz +2a8b56f318d2e21b01b54909554c53d81871b9bb05d23ea7808dde9acec4dc7e 20251209/cpython-3.15.0a2+20251209-riscv64-unknown-linux-gnu-freethreaded+lto-full.tar.zst +bb7252edaffd422bd1c044a4764dfcf83a5d7159942f445abbef524e54ea79a0 20251209/cpython-3.15.0a2+20251209-riscv64-unknown-linux-gnu-install_only.tar.gz +06c4ca3983aad20723f68786e3663ab49fee1bf09326f341649205ed79d34fc6 20251209/cpython-3.15.0a2+20251209-s390x-unknown-linux-gnu-freethreaded+lto-full.tar.zst +03a90ffa9f92d4cf4caeefb9d15f0b39c05c1e60ade6688f32165f957db4f8f3 20251209/cpython-3.15.0a2+20251209-s390x-unknown-linux-gnu-install_only.tar.gz +4d8102b70ea9fe726ee3ae9ad9e9bc4cbe0b6ed18f7989c81aef81de578f0163 20251209/cpython-3.15.0a2+20251209-x86_64-apple-darwin-freethreaded+pgo+lto-full.tar.zst +cee576de4919cd422dbc31eb85d3c145ee82acec84f651daaf32dc669b5149c9 20251209/cpython-3.15.0a2+20251209-x86_64-apple-darwin-install_only.tar.gz +6ff71bac78d650ce621fe6db49f06290e48bcceb61f69cccc7728584f70b6346 20251209/cpython-3.15.0a2+20251209-x86_64-pc-windows-msvc-freethreaded+pgo-full.tar.zst +e538475ee249eacf63bfdae0e70af73e9c47360e6dd3d6825e7a35107e177de5 20251209/cpython-3.15.0a2+20251209-x86_64-pc-windows-msvc-install_only.tar.gz +70f552e213734c0e260a57603bee504dd7ed0e78a10558b591e724ea8730fef5 20251209/cpython-3.15.0a2+20251209-x86_64-unknown-linux-gnu-freethreaded+pgo+lto-full.tar.zst +58addaabfab2de422180d32543fb3878ffc984c8a2e4005ff658a5cd83b31fc7 20251209/cpython-3.15.0a2+20251209-x86_64-unknown-linux-gnu-install_only.tar.gz +dcf844400dc2e7f5f3604e994532e4d49db45f4deefe9afdf6809ca1bc6532ee 20251209/cpython-3.15.0a2+20251209-x86_64-unknown-linux-musl-install_only.tar.gz +e7cf7bc717082bb38f5ca75988ecd8e5dbc1b0535192129371e30235d29d67b5 20260325/cpython-3.13.12+20260325-aarch64-apple-darwin-freethreaded-install_only.tar.gz +688da81bcaa6ed91792397c7d5433b13a4f02f021f940637c3972639bc516dca 20260325/cpython-3.13.12+20260325-aarch64-apple-darwin-install_only.tar.gz +54187be504ea5be2f8ed455e9377112bb04f34c9259eae263779e56b403e3e3f 20260325/cpython-3.13.12+20260325-aarch64-pc-windows-msvc-freethreaded-install_only.tar.gz +d2c8b00044cd2e4c5fc7e697e63d5e481ed44b87c2def0beb42991d59f65d930 20260325/cpython-3.13.12+20260325-aarch64-pc-windows-msvc-install_only.tar.gz +9794866e9a464f349055d791ea8f14dfa7f339ecac5aa9b1084bb2ce388fc598 20260325/cpython-3.13.12+20260325-aarch64-unknown-linux-gnu-freethreaded-install_only.tar.gz +31c6e61eed48ca4e156d0e473025a792338641109e8277a63518ded438390c96 20260325/cpython-3.13.12+20260325-aarch64-unknown-linux-gnu-install_only.tar.gz +1a8a4a97f33740a1cb9fa480321818cdc610c79c9137e511e76dc53635615494 20260325/cpython-3.13.12+20260325-ppc64le-unknown-linux-gnu-freethreaded-install_only.tar.gz +654939bc40d5f76f08eb17335bb19e9efa11eb48a0818eda2293a3f7c3570ae7 20260325/cpython-3.13.12+20260325-ppc64le-unknown-linux-gnu-install_only.tar.gz +178d20e568c25abcca9b1dbedf77e904cc3f10a79d22e31f87ddabd2d28f87dc 20260325/cpython-3.13.12+20260325-riscv64-unknown-linux-gnu-freethreaded-install_only.tar.gz +fc7e1fb553c47b831ed7fa529575145207f000f967513f7b9ea809cce006ed79 20260325/cpython-3.13.12+20260325-riscv64-unknown-linux-gnu-install_only.tar.gz +d23c93ea7502420c71e4acf02999c72ab80797d51843b1b6a315ca7bac3cb780 20260325/cpython-3.13.12+20260325-s390x-unknown-linux-gnu-freethreaded-install_only.tar.gz +7d7919358e88fcc672b061be8c2316c3a604c7074200515d7104166ed611f7f9 20260325/cpython-3.13.12+20260325-s390x-unknown-linux-gnu-install_only.tar.gz +6aff211689e30889cfe90b0b2a76b6f5a7b9e6e0bb28d6a66fd5ba35d36dc78a 20260325/cpython-3.13.12+20260325-x86_64-apple-darwin-freethreaded-install_only.tar.gz +7411e47939783708381017a90944a69641ac84d43f74fb6e2d52576c599a2717 20260325/cpython-3.13.12+20260325-x86_64-apple-darwin-install_only.tar.gz +088754e90ff22962a4ab6f7cb6bdabe5d9e7618266595df2cf7b211766e15132 20260325/cpython-3.13.12+20260325-x86_64-pc-windows-msvc-freethreaded-install_only.tar.gz +5b4093f92d9bffcb0d92aea050f3d77d5a4fc8e918b31cea000ee4b3ca751f1d 20260325/cpython-3.13.12+20260325-x86_64-pc-windows-msvc-install_only.tar.gz +6070796c894ef0a25b5a944c8c0327e155df534302e1612a5ddd57d177ddadf7 20260325/cpython-3.13.12+20260325-x86_64-unknown-linux-gnu-freethreaded-install_only.tar.gz +ebb1051ca2822b9803f46a5f10b6d51d153189ef1b1f1e142f733c0cbeaf86eb 20260325/cpython-3.13.12+20260325-x86_64-unknown-linux-gnu-install_only.tar.gz +b2e9400731c7f18069ec2804ba87a404385fe440f93b7dcb59004b9f56651202 20260325/cpython-3.13.12+20260325-x86_64-unknown-linux-musl-install_only.tar.gz +21f297bc1e0503fa077364417e2213c60951d94fd65d837ae6d9d9201ae27483 20260325/cpython-3.14.3+20260325-aarch64-apple-darwin-freethreaded-install_only.tar.gz +80c996c23aab828134821f078a8a77a6f33f3f2c14000f071718c540e20c64d4 20260325/cpython-3.14.3+20260325-aarch64-apple-darwin-install_only.tar.gz +d0e355df7362d12542108f78b3f8085b21e6824420769117c262ac86569bb2a7 20260325/cpython-3.14.3+20260325-aarch64-pc-windows-msvc-freethreaded-install_only.tar.gz +b35fe7c2fe169574f382cef125e95cbd904ddcb98fc337167356371b6d2e8c60 20260325/cpython-3.14.3+20260325-aarch64-pc-windows-msvc-install_only.tar.gz +112cf42bdf4d04f69ff4f9bf18c8ce45f494bac1645310bfdeff6f2ffb30dd9a 20260325/cpython-3.14.3+20260325-aarch64-unknown-linux-gnu-freethreaded-install_only.tar.gz +6faf5478f910741c477830f5fd842011208af0f9678faf77106c9421b325bfc1 20260325/cpython-3.14.3+20260325-aarch64-unknown-linux-gnu-install_only.tar.gz +9d7e5ba8020fd942a89a57179d9015eb0237c2d95cdbf8378639723663f11706 20260325/cpython-3.14.3+20260325-ppc64le-unknown-linux-gnu-freethreaded-install_only.tar.gz +5eafe32e12f33f98c40de920482b013170dcf97d8c7f5dc780271ccf4cded76a 20260325/cpython-3.14.3+20260325-ppc64le-unknown-linux-gnu-install_only.tar.gz +32955ad52ec7931e76f4509134a2ba5a6ba6ea0cd55e05217c1ccca3967c4a5c 20260325/cpython-3.14.3+20260325-riscv64-unknown-linux-gnu-freethreaded-install_only.tar.gz +481d3faef258964e57b7102c63de12b2bb388c7ed07cfe456f33e63b4e061202 20260325/cpython-3.14.3+20260325-riscv64-unknown-linux-gnu-install_only.tar.gz +7a1d36a1567cd747411c9c2bc7e2b5c1ac277ea7c734f74b158b94101fd5ea43 20260325/cpython-3.14.3+20260325-s390x-unknown-linux-gnu-freethreaded-install_only.tar.gz +d706eae2f4d963187b7c866603aed75d7eb3ea59590b06fb34f5fd7d0fe8e432 20260325/cpython-3.14.3+20260325-s390x-unknown-linux-gnu-install_only.tar.gz +3788781d0f9704f91ab5f7ad2a040d26b0f9b6aba0a2535db21755aebb69e620 20260325/cpython-3.14.3+20260325-x86_64-apple-darwin-freethreaded-install_only.tar.gz +847a49fea36c066f8df7a57cd8c4c02d17667e25d30b7930e8f8ba15e72d7efc 20260325/cpython-3.14.3+20260325-x86_64-apple-darwin-install_only.tar.gz +99dd7e425b3dac23e03f37787d77ee0af531e96b1c748275185342bc6642eb6b 20260325/cpython-3.14.3+20260325-x86_64-pc-windows-msvc-freethreaded-install_only.tar.gz +8b4e1329c4901ce2c0f1c20ac5d2ffa62fc13f12e26b5d1e5a1000f910f980d4 20260325/cpython-3.14.3+20260325-x86_64-pc-windows-msvc-install_only.tar.gz +20d3bcd7f175e09fa08f4cb3039e5f90fe7e4ce2476534e83f5aa21eb0d7cee9 20260325/cpython-3.14.3+20260325-x86_64-unknown-linux-gnu-freethreaded-install_only.tar.gz +18270c5a7b1a572599df5e68b497ba5254811dac43ba6f542245807d821fcb44 20260325/cpython-3.14.3+20260325-x86_64-unknown-linux-gnu-install_only.tar.gz +726a28734d2878a637b0d16ce07ce24c7d6ca1043d8e6f4a23b1b0a3478eedb9 20260325/cpython-3.14.3+20260325-x86_64-unknown-linux-musl-install_only.tar.gz +f76cc83c7db16cfc8794bf6e44d834152b57d8bab4e04e823cbc59ed23ec22f8 20260414/cpython-3.10.20+20260414-aarch64-apple-darwin-install_only.tar.gz +64932c8e8bbdf9d6b66ee85934f6f8ad1d18218b51a87ea06cefd3b84554a3e4 20260414/cpython-3.10.20+20260414-aarch64-unknown-linux-gnu-install_only.tar.gz +76b48eb26ef274045772186e63431419294c41baf6d5a372b722d4c9e711082e 20260414/cpython-3.10.20+20260414-ppc64le-unknown-linux-gnu-install_only.tar.gz +76e1ec72717d17493976fc176ec661f02412666d4f19e50908d8e4303c0511d5 20260414/cpython-3.10.20+20260414-riscv64-unknown-linux-gnu-install_only.tar.gz +2edf241199d11a3ef79a312737c1bcdb86908352c585ca14b667539080630e85 20260414/cpython-3.10.20+20260414-s390x-unknown-linux-gnu-install_only.tar.gz +95a2d794b8981723095190fa94b574ceb4272bb49d83b9e418bb90341e304d09 20260414/cpython-3.10.20+20260414-x86_64-apple-darwin-install_only.tar.gz +0d828683d30185ab9f1110ad2194ef384cef0533b8e0da7e03ce837548841788 20260414/cpython-3.10.20+20260414-x86_64-pc-windows-msvc-install_only.tar.gz +303047011b2c9f58504a930fc974d84547477cf69a3f2962f25552e2395c13af 20260414/cpython-3.10.20+20260414-x86_64-unknown-linux-gnu-install_only.tar.gz +84eb198d318f8b1b8bf59eef5d30d742e13afd97c213fa229578f8fdab0c406f 20260414/cpython-3.10.20+20260414-x86_64-unknown-linux-musl-install_only.tar.gz +a57ffd435652092d16b30e783f9826c55e9c64b0f0a72cbae0a9f39e663137fb 20260414/cpython-3.11.15+20260414-aarch64-apple-darwin-install_only.tar.gz +a882abe4876985c9dc3d433420548506fb0cc9bb9d9fe336a2d3aaf28922aa45 20260414/cpython-3.11.15+20260414-aarch64-pc-windows-msvc-install_only.tar.gz +77836944ae15b74e0b25bdc68a4703a340f2ccb684effc0f45fbd7910e1a1f39 20260414/cpython-3.11.15+20260414-aarch64-unknown-linux-gnu-install_only.tar.gz +30a2107f000dbe304820627cbe2cc257027c20f3241d96e6c7df796b69ac2062 20260414/cpython-3.11.15+20260414-ppc64le-unknown-linux-gnu-install_only.tar.gz +373b98fbf2d04099139a2f6be57593714382ed790be7e7419e358830c23ddd0f 20260414/cpython-3.11.15+20260414-riscv64-unknown-linux-gnu-install_only.tar.gz +7838efa839158c80568de35ac78d438f564f4c32272a2fe7d9e14a9b351d1a62 20260414/cpython-3.11.15+20260414-s390x-unknown-linux-gnu-install_only.tar.gz +317055d80e553764feeaef432d833dd8385c14b83465a8b3fa7c2b7819cba681 20260414/cpython-3.11.15+20260414-x86_64-apple-darwin-install_only.tar.gz +8e69ecf1d9fc194e029aafa608d483bf24ccaa8f56d456d7009f20462d62ad23 20260414/cpython-3.11.15+20260414-x86_64-pc-windows-msvc-install_only.tar.gz +8b14030dd3af9ea7f7c51b4c90feb04afd8a8f45435727e67b875270bd08f3bc 20260414/cpython-3.11.15+20260414-x86_64-unknown-linux-gnu-install_only.tar.gz +ca92d3a68a39fa330498b09714733f347bead7313ba9d9b7fbed837aa4ba7796 20260414/cpython-3.11.15+20260414-x86_64-unknown-linux-musl-install_only.tar.gz +8966b2bcd9fa03ba22c080ad15a86bc12e41a00122b16f4b3740e302261124d9 20260414/cpython-3.12.13+20260414-aarch64-apple-darwin-install_only.tar.gz +f55326c894fde76fc0faffe95d2bce60be533c88a8c44c1b88bbbc17bf6a5cd5 20260414/cpython-3.12.13+20260414-aarch64-pc-windows-msvc-install_only.tar.gz +355d981eafb9b2870af79ddc106ced7266b6f6d2101d8fbcb05620fa386642b9 20260414/cpython-3.12.13+20260414-aarch64-unknown-linux-gnu-install_only.tar.gz +4aef4cffe73c4a65ea486f14d684a9ad3f831a354174d163bb531b5baa70fc49 20260414/cpython-3.12.13+20260414-ppc64le-unknown-linux-gnu-install_only.tar.gz +c2629d69324155132343913f064be93509bd162531e08a292e50c3973ec8b5db 20260414/cpython-3.12.13+20260414-riscv64-unknown-linux-gnu-install_only.tar.gz +e5baafd64180f45165d2751b25d1bcc89254eefc7926f3ab341fc61b541d7606 20260414/cpython-3.12.13+20260414-s390x-unknown-linux-gnu-install_only.tar.gz +801b03fbe004181d55a02ebd8b4e04d74973e70d716062aebe3b3cf32e9be297 20260414/cpython-3.12.13+20260414-x86_64-apple-darwin-install_only.tar.gz +c5a9e011e284c49c48106ca177342f3e3f64e95b4c6652d4a382cc7c9bb1cc46 20260414/cpython-3.12.13+20260414-x86_64-pc-windows-msvc-install_only.tar.gz +cdcf8724d46e4857f8db5ee9f4252dc2f5da34f7940294ec6b312389dd3f41e0 20260414/cpython-3.12.13+20260414-x86_64-unknown-linux-gnu-install_only.tar.gz +d10e971238c130fdf25e577c6538a3effa5589d5fcf53665e3c711edd6a6ff2f 20260414/cpython-3.12.13+20260414-x86_64-unknown-linux-musl-install_only.tar.gz +2662b1c3f6d5ed4d02d877c07f9384acc0d18b9046d54cd2853dad3ca172784f 20260414/cpython-3.13.13+20260414-aarch64-apple-darwin-freethreaded-install_only.tar.gz +c652dad552122cd2e76968ec41c803f8222038169b11310dba0c85928265f5c1 20260414/cpython-3.13.13+20260414-aarch64-apple-darwin-install_only.tar.gz +c6c1aae3809ef585271f6f1bb3643a2c6e0c82b811b93284c6218b31f0b931d7 20260414/cpython-3.13.13+20260414-aarch64-pc-windows-msvc-freethreaded-install_only.tar.gz +586ba71c75f341e1d111399b7f719ae784dc11e8672e93e017388f28684226d0 20260414/cpython-3.13.13+20260414-aarch64-pc-windows-msvc-install_only.tar.gz +46ac7e9476b938ef19f71029a77d28ed1e201335dd0aa0237fcfed2e5ce0ee61 20260414/cpython-3.13.13+20260414-aarch64-unknown-linux-gnu-freethreaded-install_only.tar.gz +6a65f68043d7fadcd580415493d2929d1fd686013f9ae44ddbd3a81307ab256d 20260414/cpython-3.13.13+20260414-aarch64-unknown-linux-gnu-install_only.tar.gz +abe26a6cab523a5d00d75f1353cbad9c5dc04262dcb0dc4a2b47d02384e2a7d7 20260414/cpython-3.13.13+20260414-ppc64le-unknown-linux-gnu-freethreaded-install_only.tar.gz +aef73894107300264222b19e357baf5bad616b1c4bf5daa5c3b97cfee8f5ed7b 20260414/cpython-3.13.13+20260414-ppc64le-unknown-linux-gnu-install_only.tar.gz +eea71fc3625fcc2408171b17fb97e0c6286ed60ed225ca7fd6e2fc5d9cc21dce 20260414/cpython-3.13.13+20260414-riscv64-unknown-linux-gnu-freethreaded-install_only.tar.gz +f47c09f8e7f2fb0bc4afe52422705af4016c8d3ec1cf004b67bb56a86caa62cb 20260414/cpython-3.13.13+20260414-riscv64-unknown-linux-gnu-install_only.tar.gz +dd8b6161c4af3c2f5f29b3535decdcf146ce90d7a062687c9e5229b4151198b0 20260414/cpython-3.13.13+20260414-s390x-unknown-linux-gnu-freethreaded-install_only.tar.gz +4d205af9654e1f33cefd23ff798af470e565f3ac0eba18d2f98f18a2abd07166 20260414/cpython-3.13.13+20260414-s390x-unknown-linux-gnu-install_only.tar.gz +27edbaad8f0c1a8814647d24df3f87eb13c89bbc2cb90e2fc23d8fa48dd64b15 20260414/cpython-3.13.13+20260414-x86_64-apple-darwin-freethreaded-install_only.tar.gz +540337412d2c4220e99280f741dbf45c1e3da3a39edaaab20c6ba1d53e1692ef 20260414/cpython-3.13.13+20260414-x86_64-apple-darwin-install_only.tar.gz +002c07103bfbe1b889f41eb1b9fade81651a21aed35a3512e2a916c5d7903cfe 20260414/cpython-3.13.13+20260414-x86_64-pc-windows-msvc-freethreaded-install_only.tar.gz +ee0cb26453d6e025d36502d765c1639c34830355e46ab3ad31c0360bc4cd9b79 20260414/cpython-3.13.13+20260414-x86_64-pc-windows-msvc-install_only.tar.gz +a183ec7a10c38ab8c3f19968614f1e69ec697199e94525583662dfbc22b70d9a 20260414/cpython-3.13.13+20260414-x86_64-unknown-linux-gnu-freethreaded-install_only.tar.gz +e5ec3b2c5693215d153c434ac018e75511b2c4f96d2bce30468a477cb3a89d5e 20260414/cpython-3.13.13+20260414-x86_64-unknown-linux-gnu-install_only.tar.gz +24ac6bf80dd2991c8be348f777c96c6eb69b71e78d8fa28c09beb3ddca015a47 20260414/cpython-3.13.13+20260414-x86_64-unknown-linux-musl-install_only.tar.gz +a4bfd77675740a0362c137b094f3cd9995775e8e6c0a7874a095dd055fd1ea99 20260414/cpython-3.14.4+20260414-aarch64-apple-darwin-freethreaded-install_only.tar.gz +8b7865e511b17093e090449bf71eb52933c17d45ad5257ddeacaffbb2c7239df 20260414/cpython-3.14.4+20260414-aarch64-apple-darwin-install_only.tar.gz +0458cb9885c30df690cdf304a16ec335cbc7344792ef0e8a904614b24a61316d 20260414/cpython-3.14.4+20260414-aarch64-pc-windows-msvc-freethreaded-install_only.tar.gz +82613380d582d806e562d7701496c34c87753ab13c37aa0afe2039003651f389 20260414/cpython-3.14.4+20260414-aarch64-pc-windows-msvc-install_only.tar.gz +6d84fb153ccb5cb650652aadc490d99881a8d9b68cf273d44cb553e8cd087734 20260414/cpython-3.14.4+20260414-aarch64-unknown-linux-gnu-freethreaded-install_only.tar.gz +5c8db1c21023316adad827a46d917bbbd6a85ae4e39bc3a58febda712c2f963d 20260414/cpython-3.14.4+20260414-aarch64-unknown-linux-gnu-install_only.tar.gz +b5e025e340d0faa1772ef234e320401b0aa5cf6c9d16ed63a8c44be7c531bc58 20260414/cpython-3.14.4+20260414-ppc64le-unknown-linux-gnu-freethreaded-install_only.tar.gz +055977a09de092744bbb22db64144e6afef8592eaac5e2bce4cca33f2592281a 20260414/cpython-3.14.4+20260414-ppc64le-unknown-linux-gnu-install_only.tar.gz +4373553133eb4712bc10f720da29e091a23153f587fdb2c38f1fb105e70db53a 20260414/cpython-3.14.4+20260414-riscv64-unknown-linux-gnu-freethreaded-install_only.tar.gz +e959df167c502fb0bbcacc31a997e25c6b0ff6b5e496321b691955aa702d0c09 20260414/cpython-3.14.4+20260414-riscv64-unknown-linux-gnu-install_only.tar.gz +a6797ad05c7d7f74a2cea28bf012f9199f4d6c1ed6d09f7adfeb9b3c538c6258 20260414/cpython-3.14.4+20260414-s390x-unknown-linux-gnu-freethreaded-install_only.tar.gz +35f70ad05b2c4045889ee0c3d93f61b012654c1d91e10e671f0e5b4d4a6c6637 20260414/cpython-3.14.4+20260414-s390x-unknown-linux-gnu-install_only.tar.gz +1c366767d203b722efbd5b3796d16a08436e8a328afd31e551289efba9bf56d1 20260414/cpython-3.14.4+20260414-x86_64-apple-darwin-freethreaded-install_only.tar.gz +9ecb2b942e6698c04af10a63a3d73c0b2e8d8e11ce44933fbffe8651bef4577d 20260414/cpython-3.14.4+20260414-x86_64-apple-darwin-install_only.tar.gz +5ccaecdb899431f393209647182def14b36d7398bd59be4fa73dd79b48b3f290 20260414/cpython-3.14.4+20260414-x86_64-pc-windows-msvc-freethreaded-install_only.tar.gz +9647bb46d3c236e34c1c11bbb7113444d9711811f0d11c39956168807a955b1a 20260414/cpython-3.14.4+20260414-x86_64-pc-windows-msvc-install_only.tar.gz +c1a845a79da56265dc49628bc3b9e20d34f04674fd2d637ee40cbe259d2b1b95 20260414/cpython-3.14.4+20260414-x86_64-unknown-linux-gnu-freethreaded-install_only.tar.gz +e17275eaf95ceb5877aa6816e209b7733f41fee401d39c3921b88fb73fc4a4ba 20260414/cpython-3.14.4+20260414-x86_64-unknown-linux-gnu-install_only.tar.gz +12687a989a2384665577e1ef9864f33d4c074a1e69b38a8bac8d656531aefa3e 20260414/cpython-3.14.4+20260414-x86_64-unknown-linux-musl-install_only.tar.gz +5791a69a73b76b908f5bdf96da1928de8db696ab198f4ced04b77b22fe712ce0 20260414/cpython-3.15.0a8+20260414-aarch64-apple-darwin-freethreaded-install_only.tar.gz +780d46b3da0e58e15c620d9e7dfd29b54c8359c195f625858f85df9c2c7ecc32 20260414/cpython-3.15.0a8+20260414-aarch64-apple-darwin-install_only.tar.gz +95ddfe7dd52185f7e5d55524eafb48e54d1eab0b0cf013966f144a411f3ddd0f 20260414/cpython-3.15.0a8+20260414-aarch64-pc-windows-msvc-freethreaded-install_only.tar.gz +10fb470e900e65df4e37f8deaf1726397c914861ffc37b43ae3743a7eee88377 20260414/cpython-3.15.0a8+20260414-aarch64-pc-windows-msvc-install_only.tar.gz +b72908bce86036a0a1ba98ca9917ea0b99dc1e6c5d715d3d463c4f330880c09b 20260414/cpython-3.15.0a8+20260414-aarch64-unknown-linux-gnu-freethreaded-install_only.tar.gz +8f6dda4d8ff44976f1aa6a94674a09a503dc50b015297e1b62c8cdc591c90f4f 20260414/cpython-3.15.0a8+20260414-aarch64-unknown-linux-gnu-install_only.tar.gz +b3c8210674140a4c5beefa2d4afd752979222638a0fb68de672c60300b4a6642 20260414/cpython-3.15.0a8+20260414-ppc64le-unknown-linux-gnu-freethreaded-install_only.tar.gz +09f076c63fadbf675143674aa3b23229482b9a44840b8b1808a216def2a9af15 20260414/cpython-3.15.0a8+20260414-ppc64le-unknown-linux-gnu-install_only.tar.gz +1a4984207974563c6aea7dc934579d058dbac7436642081113e86011114b9fdf 20260414/cpython-3.15.0a8+20260414-riscv64-unknown-linux-gnu-freethreaded-install_only.tar.gz +9d41ce752e8b731872f0f5c9c48199e63c789d24ce3ae9e91d6c8008f36e7c51 20260414/cpython-3.15.0a8+20260414-riscv64-unknown-linux-gnu-install_only.tar.gz +f525a6244d73450e0c0a7ba125b5934894ab25ee171f7099c239d4eb7ce2f5f2 20260414/cpython-3.15.0a8+20260414-s390x-unknown-linux-gnu-freethreaded-install_only.tar.gz +1de2593c40cce2d8ea883f8c8580223bfa1478cbd9d0191ba3640aed083c2202 20260414/cpython-3.15.0a8+20260414-s390x-unknown-linux-gnu-install_only.tar.gz +3dcee23c21e4a3518947e988e115c1d824f07540f4326d93d4ea2028918e0193 20260414/cpython-3.15.0a8+20260414-x86_64-apple-darwin-freethreaded-install_only.tar.gz +a7744d34148969a2ec010da6f0a46ddeceda7c02e5cdfa2b4e1811487381491a 20260414/cpython-3.15.0a8+20260414-x86_64-apple-darwin-install_only.tar.gz +6e69670347e3a6ac1d0cd89b9506d825bd2f2690cc51ead5dec61aec6857d08d 20260414/cpython-3.15.0a8+20260414-x86_64-pc-windows-msvc-freethreaded-install_only.tar.gz +3ded476f676fdf260d56a5e49aa083d5ffd218fc3390e4480ed42bee1acfb3fb 20260414/cpython-3.15.0a8+20260414-x86_64-pc-windows-msvc-install_only.tar.gz +2d06d97e230b7f74de0fe4f661918a0ee827b08127b9372e0890e167de52a8c6 20260414/cpython-3.15.0a8+20260414-x86_64-unknown-linux-gnu-freethreaded-install_only.tar.gz +c93f4b15287ac48d7e3a475b245cb59cc51079382747e3e6213d6406c158969d 20260414/cpython-3.15.0a8+20260414-x86_64-unknown-linux-gnu-install_only.tar.gz +0568e953f837f09689eb4dd1af0043ba5e2ebae0c6395b8b9f8344a53b1f1da5 20260414/cpython-3.15.0a8+20260414-x86_64-unknown-linux-musl-freethreaded-install_only.tar.gz +9fbd6f243a424d4ae973e72aa0075122a7cfe05ac8f6cfde986e7b00d0dbc0bf 20260414/cpython-3.15.0a8+20260414-x86_64-unknown-linux-musl-install_only.tar.gz diff --git a/python/private/runtimes_manifest_workspace.bzl b/python/private/runtimes_manifest_workspace.bzl new file mode 100644 index 0000000000..c2da00ac46 --- /dev/null +++ b/python/private/runtimes_manifest_workspace.bzl @@ -0,0 +1,633 @@ +"""Manifest of runtimes for workspace mode builds. + +This is the workspace equivalent of runtimes_manifest.txt. It's a bzl file +to simplify loading of the data under workspace mode, which doesn't +support parsing a runtimes_manifest.txt file. + +NOTE: This file is automatically generated by sync_runtimes_manifest_workspace.py. +Do not edit directly! +""" + +MANIFEST_TEXT = """ +# Manifest of runtimes to make available +# Originally generated circa 2026-06 from the runtimes in TOOL_VERSIONS from the original python/versions.bzl +# To sort this file, execute: ./python/private/tools/sort_manifest.py python/private/runtimes_manifest.txt + +1409acd9a506e2d1d3b65c1488db4e40d8f19d09a7df099667c87a506f71c0ef 20220227/cpython-3.10.2+20220227-aarch64-apple-darwin-install_only.tar.gz +8f351a8cc348bb45c0f95b8634c8345ec6e749e483384188ad865b7428342703 20220227/cpython-3.10.2+20220227-aarch64-unknown-linux-gnu-install_only.tar.gz +8146ad4390710ec69b316a5649912df0247d35f4a42e2aa9615bffd87b3e235a 20220227/cpython-3.10.2+20220227-x86_64-apple-darwin-install_only.tar.gz +a1d9a594cd3103baa24937ad9150c1a389544b4350e859200b3e5c036ac352bd 20220227/cpython-3.10.2+20220227-x86_64-pc-windows-msvc-shared-install_only.tar.gz +9b64eca2a94f7aff9409ad70bdaa7fbbf8148692662e764401883957943620dd 20220227/cpython-3.10.2+20220227-x86_64-unknown-linux-gnu-install_only.tar.gz +2c99983d1e83e4b6e7411ed9334019f193fba626344a50c36fba6c25d4de78a2 20220502/cpython-3.10.4+20220502-aarch64-apple-darwin-install_only.tar.gz +d8098c0c54546637e7516f93b13403b11f9db285def8d7abd825c31407a13d7e 20220502/cpython-3.10.4+20220502-aarch64-unknown-linux-gnu-install_only.tar.gz +f2711eaffff3477826a401d09a013c6802f11c04c63ab3686aa72664f1216a05 20220502/cpython-3.10.4+20220502-x86_64-apple-darwin-install_only.tar.gz +bee24a3a5c83325215521d261d73a5207ab7060ef3481f76f69b4366744eb81d 20220502/cpython-3.10.4+20220502-x86_64-pc-windows-msvc-shared-install_only.tar.gz +f6f871e53a7b1469c13f9bd7920ad98c4589e549acad8e5a1e14760fff3dd5c9 20220502/cpython-3.10.4+20220502-x86_64-unknown-linux-gnu-install_only.tar.gz +efaf66acdb9a4eb33d57702607d2e667b1a319d58c167a43c96896b97419b8b7 20220802/cpython-3.10.6+20220802-aarch64-apple-darwin-install_only.tar.gz +81625f5c97f61e2e3d7e9f62c484b1aa5311f21bd6545451714b949a29da5435 20220802/cpython-3.10.6+20220802-aarch64-unknown-linux-gnu-install_only.tar.gz +7718411adf3ea1480f3f018a643eb0550282aefe39e5ecb3f363a4a566a9398c 20220802/cpython-3.10.6+20220802-x86_64-apple-darwin-install_only.tar.gz +91889a7dbdceea585ff4d3b7856a6bb8f8a4eca83a0ff52a73542c2e67220eaa 20220802/cpython-3.10.6+20220802-x86_64-pc-windows-msvc-shared-install_only.tar.gz +55aa2190d28dcfdf414d96dc5dcea9fe048fadcd583dc3981fec020869826111 20220802/cpython-3.10.6+20220802-x86_64-unknown-linux-gnu-install_only.tar.gz +d52b03817bd245d28e0a8b2f715716cd0fcd112820ccff745636932c76afa20a 20221106/cpython-3.10.8+20221106-aarch64-apple-darwin-install_only.tar.gz +33170bef18c811906b738be530f934640491b065bf16c4d276c6515321918132 20221106/cpython-3.10.8+20221106-aarch64-unknown-linux-gnu-install_only.tar.gz +525b79c7ce5de90ab66bd07b0ac1008bafa147ddc8a41bef15ffb7c9c1e9e7c5 20221106/cpython-3.10.8+20221106-x86_64-apple-darwin-install_only.tar.gz +f2b6d2f77118f06dd2ca04dae1175e44aaa5077a5ed8ddc63333c15347182bfe 20221106/cpython-3.10.8+20221106-x86_64-pc-windows-msvc-shared-install_only.tar.gz +6c8db44ae0e18e320320bbaaafd2d69cde8bfea171ae2d651b7993d1396260b7 20221106/cpython-3.10.8+20221106-x86_64-unknown-linux-gnu-install_only.tar.gz +018d05a779b2de7a476f3b3ff2d10f503d69d14efcedd0774e6dab8c22ef84ff 20230116/cpython-3.10.9+20230116-aarch64-apple-darwin-install_only.tar.gz +2003750f40cd09d4bf7a850342613992f8d9454f03b3c067989911fb37e7a4d1 20230116/cpython-3.10.9+20230116-aarch64-unknown-linux-gnu-install_only.tar.gz +0e685f98dce0e5bc8da93c7081f4e6c10219792e223e4b5886730fd73a7ba4c6 20230116/cpython-3.10.9+20230116-x86_64-apple-darwin-install_only.tar.gz +59c6970cecb357dc1d8554bd0540eb81ee7f6d16a07acf3d14ed294ece02c035 20230116/cpython-3.10.9+20230116-x86_64-pc-windows-msvc-shared-install_only.tar.gz +d196347aeb701a53fe2bb2b095abec38d27d0fa0443f8a1c2023a1bed6e18cdf 20230116/cpython-3.10.9+20230116-x86_64-unknown-linux-gnu-install_only.tar.gz +4918cdf1cab742a90f85318f88b8122aeaa2d04705803c7b6e78e81a3dd40f80 20230116/cpython-3.11.1+20230116-aarch64-apple-darwin-install_only.tar.gz +debf15783bdcb5530504f533d33fda75a7b905cec5361ae8f33da5ba6599f8b4 20230116/cpython-3.11.1+20230116-aarch64-unknown-linux-gnu-install_only.tar.gz +20a4203d069dc9b710f70b09e7da2ce6f473d6b1110f9535fb6f4c469ed54733 20230116/cpython-3.11.1+20230116-x86_64-apple-darwin-install_only.tar.gz +edc08979cb0666a597466176511529c049a6f0bba8adf70df441708f766de5bf 20230116/cpython-3.11.1+20230116-x86_64-pc-windows-msvc-shared-install_only.tar.gz +02a551fefab3750effd0e156c25446547c238688a32fabde2995c941c03a6423 20230116/cpython-3.11.1+20230116-x86_64-unknown-linux-gnu-install_only.tar.gz +8348bc3c2311f94ec63751fb71bd0108174be1c4def002773cf519ee1506f96f 20230507/cpython-3.10.11+20230507-aarch64-apple-darwin-install_only.tar.gz +c7573fdb00239f86b22ea0e8e926ca881d24fde5e5890851339911d76110bc35 20230507/cpython-3.10.11+20230507-aarch64-unknown-linux-gnu-install_only.tar.gz +73a9d4c89ed51be39dd2de4e235078281087283e9fdedef65bec02f503e906ee 20230507/cpython-3.10.11+20230507-ppc64le-unknown-linux-gnu-install_only.tar.gz +bd3fc6e4da6f4033ebf19d66704e73b0804c22641ddae10bbe347c48f82374ad 20230507/cpython-3.10.11+20230507-x86_64-apple-darwin-install_only.tar.gz +9c2d3604a06fcd422289df73015cd00e7271d90de28d2c910f0e2309a7f73a68 20230507/cpython-3.10.11+20230507-x86_64-pc-windows-msvc-shared-install_only.tar.gz +c5bcaac91bc80bfc29cf510669ecad12d506035ecb3ad85ef213416d54aecd79 20230507/cpython-3.10.11+20230507-x86_64-unknown-linux-gnu-install_only.tar.gz +09e412506a8d63edbb6901742b54da9aa7faf120b8dbdce56c57b303fc892c86 20230507/cpython-3.11.3+20230507-aarch64-apple-darwin-install_only.tar.gz +8190accbbbbcf7620f1ff6d668e4dd090c639665d11188ce864b62554d40e5ab 20230507/cpython-3.11.3+20230507-aarch64-unknown-linux-gnu-install_only.tar.gz +767d24f3570b35fedb945f5ac66224c8983f2d556ab83c5cfaa5f3666e9c212c 20230507/cpython-3.11.3+20230507-ppc64le-unknown-linux-gnu-install_only.tar.gz +f710b8d60621308149c100d5175fec39274ed0b9c99645484fd93d1716ef4310 20230507/cpython-3.11.3+20230507-x86_64-apple-darwin-install_only.tar.gz +24741066da6f35a7ff67bee65ce82eae870d84e1181843e64a7076d1571e95af 20230507/cpython-3.11.3+20230507-x86_64-pc-windows-msvc-shared-install_only.tar.gz +da50b87d1ec42b3cb577dfd22a3655e43a53150f4f98a4bfb40757c9d7839ab5 20230507/cpython-3.11.3+20230507-x86_64-unknown-linux-gnu-install_only.tar.gz +bc66c706ea8c5fc891635fda8f9da971a1a901d41342f6798c20ad0b2a25d1d6 20230726/cpython-3.10.12+20230726-aarch64-apple-darwin-install_only.tar.gz +fee80e221663eca5174bd794cb5047e40d3910dbeadcdf1f09d405a4c1c15fe4 20230726/cpython-3.10.12+20230726-aarch64-unknown-linux-gnu-install_only.tar.gz +bb5e8cb0d2e44241725fa9b342238245503e7849917660006b0246a9c97b1d6c 20230726/cpython-3.10.12+20230726-ppc64le-unknown-linux-gnu-install_only.tar.gz +8d33d435ae6fb93ded7fc26798cc0a1a4f546a4e527012a1e2909cc314b332df 20230726/cpython-3.10.12+20230726-s390x-unknown-linux-gnu-install_only.tar.gz +8a6e3ed973a671de468d9c691ed9cb2c3a4858c5defffcf0b08969fba9c1dd04 20230726/cpython-3.10.12+20230726-x86_64-apple-darwin-install_only.tar.gz +c1a31c353ca44de7d1b1a3b6c55a823e9c1eed0423d4f9f66e617bdb1b608685 20230726/cpython-3.10.12+20230726-x86_64-pc-windows-msvc-shared-install_only.tar.gz +a476dbca9184df9fc69fe6309cda5ebaf031d27ca9e529852437c94ec1bc43d3 20230726/cpython-3.10.12+20230726-x86_64-unknown-linux-gnu-install_only.tar.gz +cb6d2948384a857321f2aa40fa67744cd9676a330f08b6dad7070bda0b6120a4 20230726/cpython-3.11.4+20230726-aarch64-apple-darwin-install_only.tar.gz +2e84fc53f4e90e11963281c5c871f593abcb24fc796a50337fa516be99af02fb 20230726/cpython-3.11.4+20230726-aarch64-unknown-linux-gnu-install_only.tar.gz +df7b92ed9cec96b3bb658fb586be947722ecd8e420fb23cee13d2e90abcfcf25 20230726/cpython-3.11.4+20230726-ppc64le-unknown-linux-gnu-install_only.tar.gz +e477f0749161f9aa7887964f089d9460a539f6b4a8fdab5166f898210e1a87a4 20230726/cpython-3.11.4+20230726-s390x-unknown-linux-gnu-install_only.tar.gz +47e1557d93a42585972772e82661047ca5f608293158acb2778dccf120eabb00 20230726/cpython-3.11.4+20230726-x86_64-apple-darwin-install_only.tar.gz +878614c03ea38538ae2f758e36c85d2c0eb1eaaca86cd400ff8c76693ee0b3e1 20230726/cpython-3.11.4+20230726-x86_64-pc-windows-msvc-shared-install_only.tar.gz +e26247302bc8e9083a43ce9e8dd94905b40d464745b1603041f7bc9a93c65d05 20230726/cpython-3.11.4+20230726-x86_64-unknown-linux-gnu-install_only.tar.gz +dab64b3580118ad2073babd7c29fd2053b616479df5c107d31fe2af1f45e948b 20230826/cpython-3.11.5+20230826-aarch64-apple-darwin-install_only.tar.gz +bb5c5d1ea0f199fe2d3f0996fff4b48ca6ddc415a3dbd98f50bff7fce48aac80 20230826/cpython-3.11.5+20230826-aarch64-unknown-linux-gnu-install_only.tar.gz +14121b53e9c8c6d0741f911ae00102a35adbcf5c3cdf732687ef7617b7d7304d 20230826/cpython-3.11.5+20230826-ppc64le-unknown-linux-gnu-install_only.tar.gz +fe459da39874443579d6fe88c68777c6d3e331038e1fb92a0451879fb6beb16d 20230826/cpython-3.11.5+20230826-s390x-unknown-linux-gnu-install_only.tar.gz +4a4efa7378c72f1dd8ebcce1afb99b24c01b07023aa6b8fea50eaedb50bf2bfc 20230826/cpython-3.11.5+20230826-x86_64-apple-darwin-install_only.tar.gz +00f002263efc8aea896bcfaaf906b1f4dab3e5cd3db53e2b69ab9a10ba220b97 20230826/cpython-3.11.5+20230826-x86_64-pc-windows-msvc-shared-install_only.tar.gz +fbed6f7694b2faae5d7c401a856219c945397f772eea5ca50c6eb825cbc9d1e1 20230826/cpython-3.11.5+20230826-x86_64-unknown-linux-gnu-install_only.tar.gz +916c35125b5d8323a21526d7a9154ca626453f63d0878e95b9f613a95006c990 20231002/cpython-3.11.6+20231002-aarch64-apple-darwin-install_only.tar.gz +3e26a672df17708c4dc928475a5974c3fb3a34a9b45c65fb4bd1e50504cc84ec 20231002/cpython-3.11.6+20231002-aarch64-unknown-linux-gnu-install_only.tar.gz +7937035f690a624dba4d014ffd20c342e843dd46f89b0b0a1e5726b85deb8eaf 20231002/cpython-3.11.6+20231002-ppc64le-unknown-linux-gnu-install_only.tar.gz +f9f19823dba3209cedc4647b00f46ed0177242917db20fb7fb539970e384531c 20231002/cpython-3.11.6+20231002-s390x-unknown-linux-gnu-install_only.tar.gz +178cb1716c2abc25cb56ae915096c1a083e60abeba57af001996e8bc6ce1a371 20231002/cpython-3.11.6+20231002-x86_64-apple-darwin-install_only.tar.gz +3933545e6d41462dd6a47e44133ea40995bc6efeed8c2e4cbdf1a699303e95ea 20231002/cpython-3.11.6+20231002-x86_64-pc-windows-msvc-shared-install_only.tar.gz +ee37a7eae6e80148c7e3abc56e48a397c1664f044920463ad0df0fc706eacea8 20231002/cpython-3.11.6+20231002-x86_64-unknown-linux-gnu-install_only.tar.gz +4734a2be2becb813830112c780c9879ac3aff111a0b0cd590e65ec7465774d02 20231002/cpython-3.12.0+20231002-aarch64-apple-darwin-install_only.tar.gz +bccfe67cf5465a3dfb0336f053966e2613a9bc85a6588c2fcf1366ef930c4f88 20231002/cpython-3.12.0+20231002-aarch64-unknown-linux-gnu-install_only.tar.gz +b5dae075467ace32c594c7877fe6ebe0837681f814601d5d90ba4c0dfd87a1f2 20231002/cpython-3.12.0+20231002-ppc64le-unknown-linux-gnu-install_only.tar.gz +5681621349dd85d9726d1b67c84a9686ce78f72e73a6f9e4cc4119911655759e 20231002/cpython-3.12.0+20231002-s390x-unknown-linux-gnu-install_only.tar.gz +5a9e88c8aa52b609d556777b52ebde464ae4b4f77e4aac4eb693af57395c9abf 20231002/cpython-3.12.0+20231002-x86_64-apple-darwin-install_only.tar.gz +facfaa1fbc8653f95057f3c4a0f8aa833dab0e0b316e24ee8686bc761d4b4f8d 20231002/cpython-3.12.0+20231002-x86_64-pc-windows-msvc-shared-install_only.tar.gz +e51a5293f214053ddb4645b2c9f84542e2ef86870b8655704367bd4b29d39fe9 20231002/cpython-3.12.0+20231002-x86_64-unknown-linux-gnu-install_only.tar.gz +b042c966920cf8465385ca3522986b12d745151a72c060991088977ca36d3883 20240107/cpython-3.11.7+20240107-aarch64-apple-darwin-install_only.tar.gz +b102eaf865eb715aa98a8a2ef19037b6cc3ae7dfd4a632802650f29de635aa13 20240107/cpython-3.11.7+20240107-aarch64-unknown-linux-gnu-install_only.tar.gz +b44e1b74afe75c7b19143413632c4386708ae229117f8f950c2094e9681d34c7 20240107/cpython-3.11.7+20240107-ppc64le-unknown-linux-gnu-install_only.tar.gz +49520e3ff494708020f306e30b0964f079170be83e956be4504f850557378a22 20240107/cpython-3.11.7+20240107-s390x-unknown-linux-gnu-install_only.tar.gz +a0e615eef1fafdc742da0008425a9030b7ea68a4ae4e73ac557ef27b112836d4 20240107/cpython-3.11.7+20240107-x86_64-apple-darwin-install_only.tar.gz +67077e6fa918e4f4fd60ba169820b00be7c390c497bf9bc9cab2c255ea8e6f3e 20240107/cpython-3.11.7+20240107-x86_64-pc-windows-msvc-shared-install_only.tar.gz +4a51ce60007a6facf64e5495f4cf322e311ba9f39a8cd3f3e4c026eae488e140 20240107/cpython-3.11.7+20240107-x86_64-unknown-linux-gnu-install_only.tar.gz +f93f8375ca6ac0a35d58ff007043cbd3a88d9609113f1cb59cf7c8d215f064af 20240107/cpython-3.12.1+20240107-aarch64-apple-darwin-install_only.tar.gz +236533ef20e665007a111c2f36efb59c87ae195ad7dca223b6dc03fb07064f0b 20240107/cpython-3.12.1+20240107-aarch64-unknown-linux-gnu-install_only.tar.gz +78051f0d1411ee62bc2af5edfccf6e8400ac4ef82887a2affc19a7ace6a05267 20240107/cpython-3.12.1+20240107-ppc64le-unknown-linux-gnu-install_only.tar.gz +60631211c701f8d2c56e5dd7b154e68868128a019b9db1d53a264f56c0d4aee2 20240107/cpython-3.12.1+20240107-s390x-unknown-linux-gnu-install_only.tar.gz +eca96158c1568dedd9a0b3425375637a83764d1fa74446438293089a8bfac1f8 20240107/cpython-3.12.1+20240107-x86_64-apple-darwin-install_only.tar.gz +fd5a9e0f41959d0341246d3643f2b8794f638adc0cec8dd5e1b6465198eae08a 20240107/cpython-3.12.1+20240107-x86_64-pc-windows-msvc-shared-install_only.tar.gz +74e330b8212ca22fd4d9a2003b9eec14892155566738febc8e5e572f267b9472 20240107/cpython-3.12.1+20240107-x86_64-unknown-linux-gnu-install_only.tar.gz +5fdc0f6a5b5a90fd3c528e8b1da8e3aac931ea8690126c2fdb4254c84a3ff04a 20240224/cpython-3.10.13+20240224-aarch64-apple-darwin-install_only.tar.gz +a898a88705611b372297bb8fe4d23cc16b8603ce5f24494c3a8cfa65d83787f9 20240224/cpython-3.10.13+20240224-aarch64-unknown-linux-gnu-install_only.tar.gz +c23706e138a0351fc1e9def2974af7b8206bac7ecbbb98a78f5aa9e7535fee42 20240224/cpython-3.10.13+20240224-ppc64le-unknown-linux-gnu-install_only.tar.gz +09be8fb2cdfbb4a93d555f268f244dbe4d8ff1854b2658e8043aa4ec08aede3e 20240224/cpython-3.10.13+20240224-s390x-unknown-linux-gnu-install_only.tar.gz +6378dfd22f58bb553ddb02be28304d739cd730c1f95c15c74955c923a1bc3d6a 20240224/cpython-3.10.13+20240224-x86_64-apple-darwin-install_only.tar.gz +086f7fe9156b897bb401273db8359017104168ac36f60f3af4e31ac7acd6634e 20240224/cpython-3.10.13+20240224-x86_64-pc-windows-msvc-shared-install_only.tar.gz +d995d032ca702afd2fc3a689c1f84a6c64972ecd82bba76a61d525f08eb0e195 20240224/cpython-3.10.13+20240224-x86_64-unknown-linux-gnu-install_only.tar.gz +389a51139f5abe071a0d70091ca5df3e7a3dfcfcbe3e0ba6ad85fb4c5638421e 20240224/cpython-3.11.8+20240224-aarch64-apple-darwin-install_only.tar.gz +389b9005fb78dd5a6f68df5ea45ab7b30d9a4b3222af96999e94fd20d4ad0c6a 20240224/cpython-3.11.8+20240224-aarch64-unknown-linux-gnu-install_only.tar.gz +eb2b31f8e50309aae493c6a359c32b723a676f07c641f5e8fe4b6aa4dbb50946 20240224/cpython-3.11.8+20240224-ppc64le-unknown-linux-gnu-install_only.tar.gz +844f64f4c16e24965778281da61d1e0e6cd1358a581df1662da814b1eed096b9 20240224/cpython-3.11.8+20240224-s390x-unknown-linux-gnu-install_only.tar.gz +097f467b0c36706bfec13f199a2eaf924e668f70c6e2bd1f1366806962f7e86e 20240224/cpython-3.11.8+20240224-x86_64-apple-darwin-install_only.tar.gz +b618f1f047349770ee1ef11d1b05899840abd53884b820fd25c7dfe2ec1664d4 20240224/cpython-3.11.8+20240224-x86_64-pc-windows-msvc-shared-install_only.tar.gz +94e13d0e5ad417035b80580f3e893a72e094b0900d5d64e7e34ab08e95439987 20240224/cpython-3.11.8+20240224-x86_64-unknown-linux-gnu-install_only.tar.gz +01c064c00013b0175c7858b159989819ead53f4746d40580b5b0b35b6e80fba6 20240224/cpython-3.12.2+20240224-aarch64-apple-darwin-install_only.tar.gz +e52550379e7c4ac27a87de832d172658bc04150e4e27d4e858e6d8cbb96fd709 20240224/cpython-3.12.2+20240224-aarch64-unknown-linux-gnu-install_only.tar.gz +74bc02c4bbbd26245c37b29b9e12d0a9c1b7ab93477fed8b651c988b6a9a6251 20240224/cpython-3.12.2+20240224-ppc64le-unknown-linux-gnu-install_only.tar.gz +ecd6b0285e5eef94deb784b588b4b425a15a43ae671bf206556659dc141a9825 20240224/cpython-3.12.2+20240224-s390x-unknown-linux-gnu-install_only.tar.gz +a53a6670a202c96fec0b8c55ccc780ea3af5307eb89268d5b41a9775b109c094 20240224/cpython-3.12.2+20240224-x86_64-apple-darwin-install_only.tar.gz +1e5655a6ccb1a64a78460e4e3ee21036c70246800f176a6c91043a3fe3654a3b 20240224/cpython-3.12.2+20240224-x86_64-pc-windows-msvc-shared-install_only.tar.gz +57a37b57f8243caa4cdac016176189573ad7620f0b6da5941c5e40660f9468ab 20240224/cpython-3.12.2+20240224-x86_64-unknown-linux-gnu-install_only.tar.gz +ccc40e5af329ef2af81350db2a88bbd6c17b56676e82d62048c15d548401519e 20240415/cpython-3.12.3+20240415-aarch64-apple-darwin-install_only.tar.gz +ec8126de97945e629cca9aedc80a29c4ae2992c9d69f2655e27ae73906ba187d 20240415/cpython-3.12.3+20240415-aarch64-unknown-linux-gnu-install_only.tar.gz +c5dcf08b8077e617d949bda23027c49712f583120b3ed744f9b143da1d580572 20240415/cpython-3.12.3+20240415-ppc64le-unknown-linux-gnu-install_only.tar.gz +872fc321363b8cdd826fd2cb1adfd1ceb813bc1281f9d410c1c2c4e177e8df86 20240415/cpython-3.12.3+20240415-s390x-unknown-linux-gnu-install_only.tar.gz +c37a22fca8f57d4471e3708de6d13097668c5f160067f264bb2b18f524c890c8 20240415/cpython-3.12.3+20240415-x86_64-apple-darwin-install_only.tar.gz +f7cfa4ad072feb4578c8afca5ba9a54ad591d665a441dd0d63aa366edbe19279 20240415/cpython-3.12.3+20240415-x86_64-pc-windows-msvc-shared-install_only.tar.gz +a73ba777b5d55ca89edef709e6b8521e3f3d4289581f174c8699adfb608d09d6 20240415/cpython-3.12.3+20240415-x86_64-unknown-linux-gnu-install_only.tar.gz +164d89f0df2feb689981864ecc1dffb19e6aa3696c8880166de555494fe92607 20240726/cpython-3.10.14+20240726-aarch64-apple-darwin-install_only.tar.gz +39bcd46b4d70e40da177c55259be16d5c2be7a3f7f93f1e3bde47e71b4833f29 20240726/cpython-3.10.14+20240726-aarch64-unknown-linux-gnu-install_only.tar.gz +549d38b9ef59cba9ab2990025255231bfa1cb32b4bc5eac321667640fdee19d1 20240726/cpython-3.10.14+20240726-ppc64le-unknown-linux-gnu-install_only.tar.gz +de4bc878a8666c734f983db971610980870148f333bda8b0c34abfaeae88d7ec 20240726/cpython-3.10.14+20240726-s390x-unknown-linux-gnu-install_only.tar.gz +1a1455838cd1e8ed0da14a152a2d559a2fd3a6047ba7013e841db4a35a228c1d 20240726/cpython-3.10.14+20240726-x86_64-apple-darwin-install_only.tar.gz +7f68821a8b5445267eca480660364ebd06ec84632b336770c6e39de07ac0f6c3 20240726/cpython-3.10.14+20240726-x86_64-pc-windows-msvc-shared-install_only.tar.gz +32b34cd13d9d745b3db3f3b8398ab2c07de74544829915dbebd8dce39bdc405e 20240726/cpython-3.10.14+20240726-x86_64-unknown-linux-gnu-install_only.tar.gz +cbdac9462bab9671c8e84650e425d3f43b775752a930a2ef954a0d457d5c00c3 20240726/cpython-3.11.9+20240726-aarch64-apple-darwin-install_only.tar.gz +4d17cf988abe24449d649aad3ef974091ab76807904d41839907061925b4c9e3 20240726/cpython-3.11.9+20240726-aarch64-unknown-linux-gnu-install_only.tar.gz +fc4f3c9ef9bfac2ed0282126ff376e544697ad04a5408d6429d46899d7d3bf21 20240726/cpython-3.11.9+20240726-ppc64le-unknown-linux-gnu-install_only.tar.gz +e69b66e53e926460df044f44846eef3fea642f630e829719e1a4112fc370dc56 20240726/cpython-3.11.9+20240726-s390x-unknown-linux-gnu-install_only.tar.gz +dc3174666a30f4c38d04e79a80c3159b4b3aa69597c4676701c8386696811611 20240726/cpython-3.11.9+20240726-x86_64-apple-darwin-install_only.tar.gz +f694be48bdfec1dace6d69a19906b6083f4dd7c7c61f1138ba520e433e5598f8 20240726/cpython-3.11.9+20240726-x86_64-pc-windows-msvc-shared-install_only.tar.gz +f6e955dc9ddfcad74e77abe6f439dac48ebca14b101ed7c85a5bf3206ed2c53d 20240726/cpython-3.11.9+20240726-x86_64-unknown-linux-gnu-install_only.tar.gz +1801025e825c04b3907e4ef6220a13607bc0397628c9485897073110ef7fde15 20240726/cpython-3.12.4+20240726-aarch64-apple-darwin-install_only.tar.gz +a098b18b7e9fea0c66867b76c0124fce9465765017572b2e7b522154c87c78d7 20240726/cpython-3.12.4+20240726-aarch64-unknown-linux-gnu-install_only.tar.gz +04011c4c5b7fe34b0b895edf4ad8748e410686c1d69aaee11d6688d481023bcb 20240726/cpython-3.12.4+20240726-ppc64le-unknown-linux-gnu-install_only.tar.gz +8f8f3e29cf0c2facdbcfee70660939fda7667ac24fee8656d3388fc72f3acc7c 20240726/cpython-3.12.4+20240726-s390x-unknown-linux-gnu-install_only.tar.gz +4c325838c1b0ed13698506fcd515be25c73dcbe195f8522cf98f9148a97601ed 20240726/cpython-3.12.4+20240726-x86_64-apple-darwin-install_only.tar.gz +74309b0f322716409883d38c621743ea7fa0376eb00927b8ee1e1671d3aff450 20240726/cpython-3.12.4+20240726-x86_64-pc-windows-msvc-shared-install_only.tar.gz +e133dd6fc6a2d0033e2658637cc22e9c95f9d7073b80115037ee1f16417a54ac 20240726/cpython-3.12.4+20240726-x86_64-unknown-linux-gnu-install_only.tar.gz +f64776f455a44c24d50f947c813738cfb7b9ac43732c44891bc831fa7940a33c 20241016/cpython-3.10.15+20241016-aarch64-apple-darwin-install_only.tar.gz +eb58581f85fde83d1f3e8e1f8c6f5a15c7ae4fdbe3b1d1083931f9167fdd8dbc 20241016/cpython-3.10.15+20241016-aarch64-unknown-linux-gnu-install_only.tar.gz +0c45af4e7525e2db59901606db32b2896ac1e9830c6f95551402207f537c2ce4 20241016/cpython-3.10.15+20241016-ppc64le-unknown-linux-gnu-install_only.tar.gz +de205896b070e6f5259ac0f2b3379eead875ea84e6a6ef533b89886fcbb46a4c 20241016/cpython-3.10.15+20241016-s390x-unknown-linux-gnu-install_only.tar.gz +90b46dfb1abd98d45663c7a2a8c45d3047a59391d8586d71b459cec7b75f662b 20241016/cpython-3.10.15+20241016-x86_64-apple-darwin-install_only.tar.gz +e48952619796c66ec9719867b87be97edca791c2ef7fbf87d42c417c3331609e 20241016/cpython-3.10.15+20241016-x86_64-pc-windows-msvc-shared-install_only.tar.gz +3db2171e03c1a7acdc599fba583c1b92306d3788b375c9323077367af1e9d9de 20241016/cpython-3.10.15+20241016-x86_64-unknown-linux-gnu-install_only.tar.gz +ed519c47d9620eb916a6f95ec2875396e7b1a9ab993ee40b2f31b837733f318c 20241016/cpython-3.10.15+20241016-x86_64-unknown-linux-musl-install_only.tar.gz +5a69382da99c4620690643517ca1f1f53772331b347e75f536088c42a4cf6620 20241016/cpython-3.11.10+20241016-aarch64-apple-darwin-install_only.tar.gz +803e49259280af0f5466d32829cd9d65a302b0226e424b3f0b261f9daf6aee8f 20241016/cpython-3.11.10+20241016-aarch64-unknown-linux-gnu-install_only.tar.gz +92b666d103902001322f42badbd68da92adc5cebb826af9c1c906c33166e2f34 20241016/cpython-3.11.10+20241016-ppc64le-unknown-linux-gnu-install_only.tar.gz +6d584317651c1ad4a857cb32d1999707e8bb3046fcb2f156d80381814fa19fde 20241016/cpython-3.11.10+20241016-s390x-unknown-linux-gnu-install_only.tar.gz +1e23ffe5bc473e1323ab8f51464da62d77399afb423babf67f8e13c82b69c674 20241016/cpython-3.11.10+20241016-x86_64-apple-darwin-install_only.tar.gz +647b66ff4552e70aec3bf634dd470891b4a2b291e8e8715b3bdb162f577d4c55 20241016/cpython-3.11.10+20241016-x86_64-pc-windows-msvc-shared-install_only.tar.gz +8b50a442b04724a24c1eebb65a36a0c0e833d35374dbdf9c9470d8a97b164cd9 20241016/cpython-3.11.10+20241016-x86_64-unknown-linux-gnu-install_only.tar.gz +d36fc77a8dd76155a7530f6235999a693b9e7c48aa11afeb5610a091cae5aa6f 20241016/cpython-3.11.10+20241016-x86_64-unknown-linux-musl-install_only.tar.gz +4c18852bf9c1a11b56f21bcf0df1946f7e98ee43e9e4c0c5374b2b3765cf9508 20241016/cpython-3.12.7+20241016-aarch64-apple-darwin-install_only.tar.gz +bba3c6be6153f715f2941da34f3a6a69c2d0035c9c5396bc5bb68c6d2bd1065a 20241016/cpython-3.12.7+20241016-aarch64-unknown-linux-gnu-install_only.tar.gz +0a1d1d92e33a969bd2f40a80af53c97b6c0cc1060d384ceff50ff801593bf9d6 20241016/cpython-3.12.7+20241016-ppc64le-unknown-linux-gnu-install_only.tar.gz +935676a0c960b552f95e9ac2e1e385de5de4b34038ff65ffdc688838f1189c17 20241016/cpython-3.12.7+20241016-s390x-unknown-linux-gnu-install_only.tar.gz +60c5271e7edc3c2ab47440b7abf4ed50fbc693880b474f74f05768f5b657045a 20241016/cpython-3.12.7+20241016-x86_64-apple-darwin-install_only.tar.gz +f05531bff16fa77b53be0776587b97b466070e768e6d5920894de988bdcd547a 20241016/cpython-3.12.7+20241016-x86_64-pc-windows-msvc-shared-install_only.tar.gz +43576f7db1033dd57b900307f09c2e86f371152ac8a2607133afa51cbfc36064 20241016/cpython-3.12.7+20241016-x86_64-unknown-linux-gnu-install_only.tar.gz +5ed4a4078db3cbac563af66403aaa156cd6e48831d90382a1820db2b120627b5 20241016/cpython-3.12.7+20241016-x86_64-unknown-linux-musl-install_only.tar.gz +efc2e71c0e05bc5bedb7a846e05f28dd26491b1744ded35ed82f8b49ccfa684b 20241016/cpython-3.13.0+20241016-aarch64-apple-darwin-freethreaded+pgo+lto-full.tar.zst +31397953849d275aa2506580f3fa1cb5a85b6a3d392e495f8030e8b6412f5556 20241016/cpython-3.13.0+20241016-aarch64-apple-darwin-install_only.tar.gz +59b50df9826475d24bb7eff781fa3949112b5e9c92adb29e96a09cdf1216d5bd 20241016/cpython-3.13.0+20241016-aarch64-unknown-linux-gnu-freethreaded+lto-full.tar.zst +e8378c0162b2e0e4cc1f62b29443a3305d116d09583304dbb0149fecaff6347b 20241016/cpython-3.13.0+20241016-aarch64-unknown-linux-gnu-install_only.tar.gz +1217efa5f4ce67fcc9f7eb64165b1bd0912b2a21bc25c1a7e2cb174a21a5df7e 20241016/cpython-3.13.0+20241016-ppc64le-unknown-linux-gnu-freethreaded+lto-full.tar.zst +fc4b7f27c4e84c78f3c8e6c7f8e4023e4638d11f1b36b6b5ce457b1926cebb53 20241016/cpython-3.13.0+20241016-ppc64le-unknown-linux-gnu-install_only.tar.gz +6c3e1e4f19d2b018b65a7e3ef4cd4225c5b9adfbc490218628466e636d5c4b8c 20241016/cpython-3.13.0+20241016-s390x-unknown-linux-gnu-freethreaded+lto-full.tar.zst +66b19e6a07717f6cfcd3a8ca953f0a2eaa232291142f3d26a8d17c979ec0f467 20241016/cpython-3.13.0+20241016-s390x-unknown-linux-gnu-install_only.tar.gz +2e07dfea62fe2215738551a179c87dbed1cc79d1b3654f4d7559889a6d5ce4eb 20241016/cpython-3.13.0+20241016-x86_64-apple-darwin-freethreaded+pgo+lto-full.tar.zst +cff1b7e7cd26f2d47acac1ad6590e27d29829776f77e8afa067e9419f2f6ce77 20241016/cpython-3.13.0+20241016-x86_64-apple-darwin-install_only.tar.gz +bfd89f9acf866463bc4baf01733da5e767d13f5d0112175a4f57ba91f1541310 20241016/cpython-3.13.0+20241016-x86_64-pc-windows-msvc-shared-freethreaded+pgo-full.tar.zst +b25926e8ce4164cf103bacc4f4d154894ea53e07dd3fdd5ebb16fb1a82a7b1a0 20241016/cpython-3.13.0+20241016-x86_64-pc-windows-msvc-shared-install_only.tar.gz +a73adeda301ad843cce05f31a2d3e76222b656984535a7b87696a24a098b216c 20241016/cpython-3.13.0+20241016-x86_64-unknown-linux-gnu-freethreaded+pgo+lto-full.tar.zst +2c8cb15c6a2caadaa98af51df6fe78a8155b8471cb3dd7b9836038e0d3657fb4 20241016/cpython-3.13.0+20241016-x86_64-unknown-linux-gnu-install_only.tar.gz +2f61ee3b628a56aceea63b46c7afe2df3e22a61da706606b0c8efda57f953cf4 20241016/cpython-3.13.0+20241016-x86_64-unknown-linux-musl-install_only.tar.gz +08f05618bdcf8064a7960b25d9ba92155447c9b08e0cf2f46a981e4c6a1bb5a5 20241205/cpython-3.13.1+20241205-aarch64-apple-darwin-freethreaded+pgo+lto-full.tar.zst +88b88b609129c12f4b3841845aca13230f61e97ba97bd0fb28ee64b0e442a34f 20241205/cpython-3.13.1+20241205-aarch64-apple-darwin-install_only.tar.gz +9f2fcb809f9ba6c7c014a8803073a88786701a98971135bce684355062e4bb35 20241205/cpython-3.13.1+20241205-aarch64-unknown-linux-gnu-freethreaded+lto-full.tar.zst +fdfa86c2746d2ae700042c461846e6c37f70c249925b58de8cd02eb8d1423d4e 20241205/cpython-3.13.1+20241205-aarch64-unknown-linux-gnu-install_only.tar.gz +15ceea78dff78ca8ccaac8d9c54b808af30daaa126f1f561e920a6896e098634 20241205/cpython-3.13.1+20241205-ppc64le-unknown-linux-gnu-freethreaded+lto-full.tar.zst +27b20b3237c55430ca1304e687d021f88373f906249f9cd272c5ff2803d5e5c3 20241205/cpython-3.13.1+20241205-ppc64le-unknown-linux-gnu-install_only.tar.gz +ed3c6118d1d12603309c930e93421ac7a30a69045ffd43006f63ecf71d72c317 20241205/cpython-3.13.1+20241205-s390x-unknown-linux-gnu-freethreaded+lto-full.tar.zst +7d0187e20cb5e36c689eec27e4d3de56d8b7f1c50dc5523550fc47377801521f 20241205/cpython-3.13.1+20241205-s390x-unknown-linux-gnu-install_only.tar.gz +dc780fecd215d2cc9e573abf1e13a175fcfa8f6efd100ef888494a248a16cda8 20241205/cpython-3.13.1+20241205-x86_64-apple-darwin-freethreaded+pgo+lto-full.tar.zst +47eef6efb8664e2d1d23a7cdaf56262d784f8ace48f3bfca1b183e95a49888d6 20241205/cpython-3.13.1+20241205-x86_64-apple-darwin-install_only.tar.gz +7537b2ab361c0eabc0eabfca9ffd9862d7f5f6576eda13b97e98aceb5eea4fd3 20241205/cpython-3.13.1+20241205-x86_64-pc-windows-msvc-shared-freethreaded+pgo-full.tar.zst +f51f0493a5f979ff0b8d8c598a8d74f2a4d86a190c2729c85e0af65c36a9cbbe 20241205/cpython-3.13.1+20241205-x86_64-pc-windows-msvc-shared-install_only.tar.gz +9ec1b81213f849d91f5ebe6a16196e85cd6ff7c05ca823ce0ab7ba5b0e9fee84 20241205/cpython-3.13.1+20241205-x86_64-unknown-linux-gnu-freethreaded+pgo+lto-full.tar.zst +242b2727df6c1e00de6a9f0f0dcb4562e168d27f428c785b0eb41a6aeb34d69a 20241205/cpython-3.13.1+20241205-x86_64-unknown-linux-gnu-install_only.tar.gz +76b30c6373b9c0aa2ba610e07da02f384aa210ac79643da38c66d3e6171c6ef5 20241205/cpython-3.13.1+20241205-x86_64-unknown-linux-musl-install_only.tar.gz +e3c4aa607717b23903ca2650d5c3ee24f89b97543e2db2b0f463bddc7a9e92f3 20241206/cpython-3.12.8+20241206-aarch64-apple-darwin-install_only.tar.gz +ce674b55442b732973afb2932c281bb1ded4ad7e22bcf9b07071165770758c7e 20241206/cpython-3.12.8+20241206-aarch64-unknown-linux-gnu-install_only.tar.gz +b7214790b273de9ed0532420054b72ba1393d62d2fc844ec55ade193771bd90c 20241206/cpython-3.12.8+20241206-ppc64le-unknown-linux-gnu-install_only.tar.gz +73102f5dbd7d1e7e9c2f2c80aedf2893d99a7fa407f6674ec8b2f57ba07daee5 20241206/cpython-3.12.8+20241206-s390x-unknown-linux-gnu-install_only.tar.gz +3ba35c706577d755e8e52a4c161a042464577c0e695e2a605362fa469e26de10 20241206/cpython-3.12.8+20241206-x86_64-apple-darwin-install_only.tar.gz +767b4be3ddf6b99e5ade519789c1615c191d8cf99d5aff4685cc18b48931f1e6 20241206/cpython-3.12.8+20241206-x86_64-pc-windows-msvc-shared-install_only.tar.gz +b9d6ee5ddac1198e72d53112698773fc8bb597de095592eb849ca794306699ba 20241206/cpython-3.12.8+20241206-x86_64-unknown-linux-gnu-install_only.tar.gz +6f305888703691dd04cfff85284d23ea0b0146ed7c4415e472f1fb72b3f32cdf 20241206/cpython-3.12.8+20241206-x86_64-unknown-linux-musl-install_only.tar.gz +e99f8457d9c79592c036489c5cfa78df76e4762d170665e499833e045d82608f 20250317/cpython-3.10.16+20250317-aarch64-apple-darwin-install_only.tar.gz +76d0f04d2444e77200fdc70d1c57480e29cca78cb7420d713bc1c523709c198d 20250317/cpython-3.10.16+20250317-aarch64-unknown-linux-gnu-install_only.tar.gz +39c9b3486de984fe1d72d90278229c70d6b08bcf69cd55796881b2d75077b603 20250317/cpython-3.10.16+20250317-ppc64le-unknown-linux-gnu-install_only.tar.gz +ebe949ada9293581c17d9bcdaa8f645f67d95f73eac65def760a71ef9dd6600d 20250317/cpython-3.10.16+20250317-riscv64-unknown-linux-gnu-install_only.tar.gz +9b2fc0b7f1c75b48e799b6fa14f7e24f5c61f2db82e3c65d13ed25e08f7f0857 20250317/cpython-3.10.16+20250317-s390x-unknown-linux-gnu-install_only.tar.gz +e03e62dbe95afa2f56b7344ff3bd061b180a0b690ff77f9a1d7e6601935e05ca 20250317/cpython-3.10.16+20250317-x86_64-apple-darwin-install_only.tar.gz +c7e0eb0ff5b36758b7a8cacd42eb223c056b9c4d36eded9bf5b9fe0c0b9aeb08 20250317/cpython-3.10.16+20250317-x86_64-pc-windows-msvc-install_only.tar.gz +b350c7e63956ca8edb856b91316328e0fd003a840cbd63d08253af43b2c63643 20250317/cpython-3.10.16+20250317-x86_64-unknown-linux-gnu-install_only.tar.gz +6ed64923ee4fbea4c5780f1a5a66651d239191ac10bd23420db4f5e4e0bf79c4 20250317/cpython-3.10.16+20250317-x86_64-unknown-linux-musl-install_only.tar.gz +7c7fd9809da0382a601a79287b5d62d61ce0b15f5a5ee836233727a516e85381 20250317/cpython-3.12.9+20250317-aarch64-apple-darwin-install_only.tar.gz +00c6bf9acef21ac741fea24dc449d0149834d30e9113429e50a95cce4b00bb80 20250317/cpython-3.12.9+20250317-aarch64-unknown-linux-gnu-install_only.tar.gz +25d77599dfd5849f17391d92da0da99079e4e94f19a881f763f5cc62530ef7e1 20250317/cpython-3.12.9+20250317-ppc64le-unknown-linux-gnu-install_only.tar.gz +e97ab0fdf443b302c56a52b4fd08f513bf3be66aa47263f0f9df3c6e60e05f2e 20250317/cpython-3.12.9+20250317-riscv64-unknown-linux-gnu-install_only.tar.gz +7492d079ffa8425c8f6c58e43b237c37e3fb7b31e2e14635927bb4d3397ba21e 20250317/cpython-3.12.9+20250317-s390x-unknown-linux-gnu-install_only.tar.gz +1ee1b1bb9fbce5c145c4bec9a3c98d7a4fa22543e09a7c1d932bc8599283c2dc 20250317/cpython-3.12.9+20250317-x86_64-apple-darwin-install_only.tar.gz +d15361fd202dd74ae9c3eece1abdab7655f1eba90bf6255cad1d7c53d463ed4d 20250317/cpython-3.12.9+20250317-x86_64-pc-windows-msvc-install_only.tar.gz +ef382fb88cbb41a3b0801690bd716b8a1aec07a6c6471010bcc6bd14cd575226 20250317/cpython-3.12.9+20250317-x86_64-unknown-linux-gnu-install_only.tar.gz +94e3837da1adf9964aab2d6047b33f70167de3096d1f9a2d1fa9340b1bbf537d 20250317/cpython-3.12.9+20250317-x86_64-unknown-linux-musl-install_only.tar.gz +c98c9c977e6fa05c3813bd49f3553904d89d60fed27e2e36468da7afa1d6d5e2 20250317/cpython-3.13.2+20250317-aarch64-apple-darwin-freethreaded+pgo+lto-full.tar.zst +faa44274a331eb39786362818b21b3a4e74514e8805000b20b0e55c590cecb94 20250317/cpython-3.13.2+20250317-aarch64-apple-darwin-install_only.tar.gz +b8635e59e3143fd17f19a3dfe8ccc246ee6587c87da359bd1bcab35eefbb5f19 20250317/cpython-3.13.2+20250317-aarch64-unknown-linux-gnu-freethreaded+lto-full.tar.zst +9c67260446fee6ea706dad577a0b32936c63f449c25d66e4383d5846b2ab2e36 20250317/cpython-3.13.2+20250317-aarch64-unknown-linux-gnu-install_only.tar.gz +6ae8fa44cb2edf4ab49cff1820b53c40c10349c0f39e11b8cd76ce7f3e7e1def 20250317/cpython-3.13.2+20250317-ppc64le-unknown-linux-gnu-freethreaded+lto-full.tar.zst +345b53d2f86c9dbd7f1320657cb227ff9a42ef63ff21f129abbbc8c82a375147 20250317/cpython-3.13.2+20250317-ppc64le-unknown-linux-gnu-install_only.tar.gz +2af1b8850c52801fb6189e7a17a51e0c93d9e46ddefcca72247b76329c97d02a 20250317/cpython-3.13.2+20250317-riscv64-unknown-linux-gnu-freethreaded+lto-full.tar.zst +172d22b2330737f3a028ea538ffe497c39a066a8d3200b22dd4d177a3332ad85 20250317/cpython-3.13.2+20250317-riscv64-unknown-linux-gnu-install_only.tar.gz +c074144cc80c2af32c420b79a9df26e8db405212619990c1fbdd308bd75afe3f 20250317/cpython-3.13.2+20250317-s390x-unknown-linux-gnu-freethreaded+lto-full.tar.zst +ec3b16ea8a97e3138acec72bc5ff35949950c62c8994a8ec8e213fd93f0e806b 20250317/cpython-3.13.2+20250317-s390x-unknown-linux-gnu-install_only.tar.gz +0d73e4348d8d4b5159058609d2303705190405b485dd09ad05d870d7e0f36e0f 20250317/cpython-3.13.2+20250317-x86_64-apple-darwin-freethreaded+pgo+lto-full.tar.zst +ee4526e84b5ce5b11141c50060b385320f2773616249a741f90c96d460ce8e8f 20250317/cpython-3.13.2+20250317-x86_64-apple-darwin-install_only.tar.gz +c51b4845fda5421e044067c111192f645234081d704313f74ee77fa013a186ea 20250317/cpython-3.13.2+20250317-x86_64-pc-windows-msvc-freethreaded+pgo-full.tar.zst +84d7b52f3558c8e35c670a4fa14080c75e3ec584adfae49fec8b51008b75b21e 20250317/cpython-3.13.2+20250317-x86_64-pc-windows-msvc-install_only.tar.gz +1aea5062614c036904b55c1cc2fb4b500b7f6f7a4cacc263f4888889d355eef8 20250317/cpython-3.13.2+20250317-x86_64-unknown-linux-gnu-freethreaded+pgo+lto-full.tar.zst +db011f0cd29cab2291584958f4e2eb001b0e6051848d89b38a2dc23c5c54e512 20250317/cpython-3.13.2+20250317-x86_64-unknown-linux-gnu-install_only.tar.gz +00bb2d629f7eacbb5c6b44dc04af26d1f1da64cee3425b0d8eb5135a93830296 20250317/cpython-3.13.2+20250317-x86_64-unknown-linux-musl-install_only.tar.gz +278dccade56b4bbeecb9a613b77012cf5c1433a5e9b8ef99230d5e61f31d9e02 20250610/cpython-3.13.4+20250610-aarch64-apple-darwin-freethreaded+pgo+lto-full.tar.zst +c2ce6601b2668c7bd1f799986af5ddfbff36e88795741864aba6e578cb02ed7f 20250610/cpython-3.13.4+20250610-aarch64-apple-darwin-install_only.tar.gz +b1c1bd6ab9ef95b464d92a6a911cef1a8d9f0b0f6a192f694ef18ed15d882edf 20250610/cpython-3.13.4+20250610-aarch64-unknown-linux-gnu-freethreaded+lto-full.tar.zst +3c2596ece08ffe17e11bc1f27aeb4ce1195d2490a83d695d36ef4933d5c5ca53 20250610/cpython-3.13.4+20250610-aarch64-unknown-linux-gnu-install_only.tar.gz +ed66ae213a62b286b9b7338b816ccd2815f5248b7a28a185dc8159fe004149ae 20250610/cpython-3.13.4+20250610-ppc64le-unknown-linux-gnu-freethreaded+lto-full.tar.zst +b3cc13ee177b8db1d3e9b2eac413484e3c6a356f97d91dc59de8d3fd8cf79d6b 20250610/cpython-3.13.4+20250610-ppc64le-unknown-linux-gnu-install_only.tar.gz +913264545215236660e4178bc3e5b57a20a444a8deb5c11680c95afc960b4016 20250610/cpython-3.13.4+20250610-riscv64-unknown-linux-gnu-freethreaded+lto-full.tar.zst +d1b989e57a9ce29f6c945eeffe0e9750c222fdd09e99d2f8d6b0d8532a523053 20250610/cpython-3.13.4+20250610-riscv64-unknown-linux-gnu-install_only.tar.gz +7556a38ab5e507c1ec22bc38f9859982bc956cab7f4de05a2faac114feb306db 20250610/cpython-3.13.4+20250610-s390x-unknown-linux-gnu-freethreaded+lto-full.tar.zst +d1d19fb01961ac6476712fdd6c5031f74c83666f6f11aa066207e9a158f7e3d8 20250610/cpython-3.13.4+20250610-s390x-unknown-linux-gnu-install_only.tar.gz +64ab7ac8c88002d9ba20a92f72945bfa350268e944a7922500af75d20330574d 20250610/cpython-3.13.4+20250610-x86_64-apple-darwin-freethreaded+pgo+lto-full.tar.zst +79feb6ca68f3921d07af52d9db06cf134e6f36916941ea850ab0bc20f5ff638b 20250610/cpython-3.13.4+20250610-x86_64-apple-darwin-install_only.tar.gz +9457504547edb2e0156bf76b53c7e4941c7f61c0eff9fd5f4d816d3df51c58e3 20250610/cpython-3.13.4+20250610-x86_64-pc-windows-msvc-freethreaded+pgo-full.tar.zst +29ac3585cc2dcfd79e3fe380c272d00e9d34351fc456e149403c86d3fea34057 20250610/cpython-3.13.4+20250610-x86_64-pc-windows-msvc-install_only.tar.gz +864df6e6819e8f8e855ce30f34410fdc5867d0616e904daeb9a40e5806e970d7 20250610/cpython-3.13.4+20250610-x86_64-unknown-linux-gnu-freethreaded+pgo+lto-full.tar.zst +44e5477333ebca298a7a0a316985c6c3533b8645f92a83f7f73c44033832bf32 20250610/cpython-3.13.4+20250610-x86_64-unknown-linux-gnu-install_only.tar.gz +a3afbfa94b9ff4d9fc426b47eb3c8446cada535075b8d51b7bdc9d9ab9911fc2 20250610/cpython-3.13.4+20250610-x86_64-unknown-linux-musl-install_only.tar.gz +a94c02b2d597cd6b075a713fe4e9a909cc97ca6a3b2b2ce86eda21be2062d48e 20250808/cpython-3.10.18+20250808-aarch64-apple-darwin-install_only.tar.gz +ef7de3b715d519e246d98ff7856247f7f7b357068705f09c6f300b7e7b76c701 20250808/cpython-3.10.18+20250808-aarch64-unknown-linux-gnu-install_only.tar.gz +f580efed11cc54e1a221c052e8bc88bfbc12844d3ca8949da828351a1232386e 20250808/cpython-3.10.18+20250808-ppc64le-unknown-linux-gnu-install_only.tar.gz +0d7e460e30203a9225b6f417ae972f66415a1cc0e32b37ebc48d195816282669 20250808/cpython-3.10.18+20250808-riscv64-unknown-linux-gnu-install_only.tar.gz +d4ada974daadb08a0184c19232ee3b03b3137aa70609760e1a94aaf7b12989ef 20250808/cpython-3.10.18+20250808-s390x-unknown-linux-gnu-install_only.tar.gz +da96fe2ba841640215788ddb9f151f03629360e37fcb94d4f76e5095b87df0d4 20250808/cpython-3.10.18+20250808-x86_64-apple-darwin-install_only.tar.gz +a648f3c9d136985ccfe57a5507e73d9d0839f7fd09eebd7c247857f2feaecb2a 20250808/cpython-3.10.18+20250808-x86_64-pc-windows-msvc-install_only.tar.gz +0b310a73bb9e7a495dbcad5f685e508ca2e7b36ee8f29301a52285730c425789 20250808/cpython-3.10.18+20250808-x86_64-unknown-linux-gnu-install_only.tar.gz +9cecf6ea2effbe183faebcf7e1160425a4ee17a68e49f2eefe5e1c59c51fa7ee 20250808/cpython-3.10.18+20250808-x86_64-unknown-linux-musl-install_only.tar.gz +d089bfd2c7b98a0942750a195e70d3172beda76d7747097b8afd87028b6e59b6 20250808/cpython-3.11.13+20250808-aarch64-apple-darwin-install_only.tar.gz +a632857c966237e7fd38b44c47c350f6e30d8ec54dcad6c832865ad670f0f22f 20250808/cpython-3.11.13+20250808-aarch64-pc-windows-msvc-install_only.tar.gz +bc57105f8a16acd57b71d926143c7f6ecf61729b40c8b4656f1b98bebd47c710 20250808/cpython-3.11.13+20250808-aarch64-unknown-linux-gnu-install_only.tar.gz +16a0165b0744940702b8fff80b8bf973ac914f78cb6fca28d389583f675e84de 20250808/cpython-3.11.13+20250808-ppc64le-unknown-linux-gnu-install_only.tar.gz +d8e62306be8f41c46bcd62ca68f91a1467f47adff632a35ff413dc1043ed56e8 20250808/cpython-3.11.13+20250808-riscv64-unknown-linux-gnu-install_only.tar.gz +4e302a4514a73baefdd9b327062bdafeb4115a799deec91c185f6ab45a857241 20250808/cpython-3.11.13+20250808-s390x-unknown-linux-gnu-install_only.tar.gz +d946d618f8bba8308b67e460a30612a71e2ccc309f85f6628aaae24e2b816981 20250808/cpython-3.11.13+20250808-x86_64-apple-darwin-install_only.tar.gz +ed963aee33d29ad8abfbb5fe63e42f57a2638a4a11a88e11d8bb66e61f20a6e5 20250808/cpython-3.11.13+20250808-x86_64-pc-windows-msvc-install_only.tar.gz +3ad988c702cbb017fef1208d47dea4138a2e85fd0f7f01ec5e1e335e597131b9 20250808/cpython-3.11.13+20250808-x86_64-unknown-linux-gnu-install_only.tar.gz +3a5810f0696f844289aa06d5c3a1efeab66eee999c25196b7d1954192a2c2100 20250808/cpython-3.11.13+20250808-x86_64-unknown-linux-musl-install_only.tar.gz +8792c4a84c364ab975feca0c27d3157a5435b7baab325a346ae56b223893b661 20250808/cpython-3.12.11+20250808-aarch64-apple-darwin-install_only.tar.gz +00bf7d7e8bcf5d1e9c4dfca0247d8e035147777cd57ee9d4c64dedca86b0a464 20250808/cpython-3.12.11+20250808-aarch64-pc-windows-msvc-install_only.tar.gz +4d7ba5314fab02130d6538f074961ffbf61310cade9180e59026074f9a8939cb 20250808/cpython-3.12.11+20250808-aarch64-unknown-linux-gnu-install_only.tar.gz +2c862eb40a81549d9c11e6bf5a7f07c3406310b14e6a4d16dcdf1c4763ef7090 20250808/cpython-3.12.11+20250808-ppc64le-unknown-linux-gnu-install_only.tar.gz +0bb729b95fabd49c7b495f7c44a9086e3970ea57daf66365741574bd36a17e81 20250808/cpython-3.12.11+20250808-riscv64-unknown-linux-gnu-install_only.tar.gz +99e465882d217d24ac90e99fac8f32e6a644d0340ac05ee510fb5cdf53f0cfb8 20250808/cpython-3.12.11+20250808-s390x-unknown-linux-gnu-install_only.tar.gz +e0c932709dafb05f00e528a7560ef8ee559ac82b75faca60dd1245bca1c1553f 20250808/cpython-3.12.11+20250808-x86_64-apple-darwin-install_only.tar.gz +81214ef71964a40ec269a79067ca490d45298c350583bc3af0e5781451a05c3c 20250808/cpython-3.12.11+20250808-x86_64-pc-windows-msvc-install_only.tar.gz +63d78840bf209af8da8f24e335d910f88387b892ca9187be571d481c071751bb 20250808/cpython-3.12.11+20250808-x86_64-unknown-linux-gnu-install_only.tar.gz +d633d070780590aa03ac5575cd9d7b9e17682d80f14b400313c009c387cf706b 20250808/cpython-3.12.11+20250808-x86_64-unknown-linux-musl-install_only.tar.gz +f2143304012e021a603bf1807bf3e4ce163832e43ab9a9829e53cb136497f207 20250808/cpython-3.13.6+20250808-aarch64-apple-darwin-freethreaded+pgo+lto-full.tar.zst +8a1efa6af4e80f08e2c97dda822a3d6c24d6c98e518242f802c6a43ae8401488 20250808/cpython-3.13.6+20250808-aarch64-apple-darwin-install_only.tar.gz +552cfabcc3b103f4b1c4036d2592d5f0373c9554a2c4d2b6631b04ef7e592067 20250808/cpython-3.13.6+20250808-aarch64-pc-windows-msvc-freethreaded+pgo-full.tar.zst +8e1617bd407ec1a874499daab26ae95080d1e0267ae616d34490137a28705827 20250808/cpython-3.13.6+20250808-aarch64-pc-windows-msvc-install_only.tar.gz +d84a7d64c284be387386b9f5da273f6d05486eb6bd8f9e86e2575cb59604cb22 20250808/cpython-3.13.6+20250808-aarch64-unknown-linux-gnu-freethreaded+pgo+lto-full.tar.zst +11fa0591ae2211c08a42ae54944260e36ddf88a1d5604ea0c49e2477be4e5388 20250808/cpython-3.13.6+20250808-aarch64-unknown-linux-gnu-install_only.tar.gz +e76fcaf1bf80a615520dbe7f85ca0bb557fad96d132d836b0ac721e7cc1e2a37 20250808/cpython-3.13.6+20250808-ppc64le-unknown-linux-gnu-freethreaded+lto-full.tar.zst +8dcf34ae1a685fe1893b52917ae04f23328edadc4acae28499d43850c2bdd26c 20250808/cpython-3.13.6+20250808-ppc64le-unknown-linux-gnu-install_only.tar.gz +24e08a39ba4fc77753e61541e52eed39cc871f4a92a80a3c5dd495056bd8eff9 20250808/cpython-3.13.6+20250808-riscv64-unknown-linux-gnu-freethreaded+lto-full.tar.zst +f8ed75aa6cc2011a046be00b629c3c8295267f34280324feaff34c73e7afce39 20250808/cpython-3.13.6+20250808-riscv64-unknown-linux-gnu-install_only.tar.gz +1609b223fd38a4a7a4d20e7173d7d9390fe2258f7dd9a15dc9ef0fa49613735d 20250808/cpython-3.13.6+20250808-s390x-unknown-linux-gnu-freethreaded+lto-full.tar.zst +7707ee5d19a78bc64ef8a66751ec7f97b64ea06714c7b1b52e8b321c2923ead8 20250808/cpython-3.13.6+20250808-s390x-unknown-linux-gnu-install_only.tar.gz +4360a1278dd0a96b526d108c8fd23498a9d2028dd7791e510fd51ff5ea3f462a 20250808/cpython-3.13.6+20250808-x86_64-apple-darwin-freethreaded+pgo+lto-full.tar.zst +27badce7201321a8363219e438a6205165e5b4884012b1046532203df2ec9379 20250808/cpython-3.13.6+20250808-x86_64-apple-darwin-install_only.tar.gz +4e727cdbe4057b16a170f887c0fa4227a825ac59bcda84ae946c77cc932af78c 20250808/cpython-3.13.6+20250808-x86_64-pc-windows-msvc-freethreaded+pgo-full.tar.zst +af5cc733c33b9aa9f1d74c81a59351e9b27215486d8b6cdbc06d97646a58c953 20250808/cpython-3.13.6+20250808-x86_64-pc-windows-msvc-install_only.tar.gz +e48c13c59cc3c01b79f63c8bccec27d2db6e97f64213b8731e2077b6ed8ed52c 20250808/cpython-3.13.6+20250808-x86_64-unknown-linux-gnu-freethreaded+pgo+lto-full.tar.zst +f844e8c8b6847628b472f7e97d8893a4e93acd5382a902b465776063668c4d64 20250808/cpython-3.13.6+20250808-x86_64-unknown-linux-gnu-install_only.tar.gz +70076dea0ff65b3c05aae1a97b4a556bf613cc73db30309e59134f9d318f4f7b 20250808/cpython-3.13.6+20250808-x86_64-unknown-linux-musl-install_only.tar.gz +43bda24c2fc073bc308bf631203b917a72640d59b59fdad4ba14503d84727012 20251031/cpython-3.10.19+20251031-aarch64-apple-darwin-install_only.tar.gz +f77a8a8aa77f3f943126fa9215a25309da4bf20398fc8f4b4eec54b5fc7570ef 20251031/cpython-3.10.19+20251031-aarch64-unknown-linux-gnu-install_only.tar.gz +1c55d160fc4c3b93528cd6aaa2bb4ca6018a99e5a45919d33dc761a43a69f860 20251031/cpython-3.10.19+20251031-ppc64le-unknown-linux-gnu-install_only.tar.gz +21134d35721cdad4c881f35d0957cc19df9a45d194afb38a099faded3c1cfb4d 20251031/cpython-3.10.19+20251031-riscv64-unknown-linux-gnu-install_only.tar.gz +df0db070f1eb73ab4e371eea32213ddb3500737ea5560a6f0ffd65c82af64ddc 20251031/cpython-3.10.19+20251031-s390x-unknown-linux-gnu-install_only.tar.gz +76c12e633c09c2a790f8a958a55df4495527e0718d1875310c836e757c0c7b55 20251031/cpython-3.10.19+20251031-x86_64-apple-darwin-install_only.tar.gz +cfa08a4caf2df1b43551b843c052d6a8814e2ea0c97268b021f0423646c244c3 20251031/cpython-3.10.19+20251031-x86_64-pc-windows-msvc-install_only.tar.gz +fb1caac917d7b6497bb6f5950da5f1e48d05c43a498948dd97f85760c4382d9f 20251031/cpython-3.10.19+20251031-x86_64-unknown-linux-gnu-install_only.tar.gz +ba85013ed5ac7733fc6840168cc33ed19e9959b363dc80227d54f8fd9c92c0f4 20251031/cpython-3.10.19+20251031-x86_64-unknown-linux-musl-install_only.tar.gz +6de5572b33c65af1c9b7caf00ec593fb04cffb7e14fa393a98261bb9bc464713 20251031/cpython-3.11.14+20251031-aarch64-apple-darwin-install_only.tar.gz +38d0d1466561e15965e8d2c20f5e5be649598f55c761ecab553d087fbd217337 20251031/cpython-3.11.14+20251031-aarch64-pc-windows-msvc-install_only.tar.gz +510edb027527413c4249256194cb8ad2590b52dd93f7123b4cb341aff5d05894 20251031/cpython-3.11.14+20251031-aarch64-unknown-linux-gnu-install_only.tar.gz +4e0bc6a818e0c6a9d7d3ebe1a95591fd84440520577aa837facc96a4b7a80e35 20251031/cpython-3.11.14+20251031-ppc64le-unknown-linux-gnu-install_only.tar.gz +16519e69297144f81b2421333bc9e0b6466cf3c84749b216b695cfb4c9deb32f 20251031/cpython-3.11.14+20251031-riscv64-unknown-linux-gnu-install_only.tar.gz +5f9c1b203cdf34c8bff1aef69b63bbf11309bd16ca6e429d8c3651eaa2b3d080 20251031/cpython-3.11.14+20251031-s390x-unknown-linux-gnu-install_only.tar.gz +4891cbf34e8652b7bd1054b9502395e4b7e048e2e517c040fbf6c8297cb954d6 20251031/cpython-3.11.14+20251031-x86_64-apple-darwin-install_only.tar.gz +5223b83ed9e2aa5e9e17d2ebcf767956e998876339b9cde1980a47e9d4655fb6 20251031/cpython-3.11.14+20251031-x86_64-pc-windows-msvc-install_only.tar.gz +60f0bd473d861cc45d3401d9914e47ccb9fa037f88a91879ed517a62042b8477 20251031/cpython-3.11.14+20251031-x86_64-unknown-linux-gnu-install_only.tar.gz +25e82d1e85b90a8ab724ee633a1811b1921797f5c25ee69c6595052371b91a87 20251031/cpython-3.11.14+20251031-x86_64-unknown-linux-musl-install_only.tar.gz +5e110cb821d2eb8246065d3b46faa655180c976c4e17250f7883c634a629bc63 20251031/cpython-3.12.12+20251031-aarch64-apple-darwin-install_only.tar.gz +b190fed7c2b0f6e1010f554a0d1fd191c0754c4c0718e69d9d795ae559613780 20251031/cpython-3.12.12+20251031-aarch64-pc-windows-msvc-install_only.tar.gz +81b644d166e0bfb918615af8a2363f8fcf26eccdcc60a5334b6a62c088470bac 20251031/cpython-3.12.12+20251031-aarch64-unknown-linux-gnu-install_only.tar.gz +024f5e5678c9768d45cc24d37a8e9d265aae86c4a4602352dee3d7deba367052 20251031/cpython-3.12.12+20251031-ppc64le-unknown-linux-gnu-install_only.tar.gz +b13c57fc372c131e667a99b9680f41c0b4da571cf99ed412103c2fe9ad5ed1fb 20251031/cpython-3.12.12+20251031-riscv64-unknown-linux-gnu-install_only.tar.gz +2bf05bdd56cdf5ea4fd9f2faf151ea4211be96a0d1f4230b85f5dcae620d6400 20251031/cpython-3.12.12+20251031-s390x-unknown-linux-gnu-install_only.tar.gz +687052a046d33be49dc95dd671816709067cf6176ed36c93ea61b1fe0b883b0f 20251031/cpython-3.12.12+20251031-x86_64-apple-darwin-install_only.tar.gz +cff398b3f520c442a1b085dd347126c10c1b03f01ccc0decd8c897a687e893f1 20251031/cpython-3.12.12+20251031-x86_64-pc-windows-msvc-install_only.tar.gz +80c3882f14e15cef8260ef5257d198e8f4371ca265887431d939e0d561de3253 20251031/cpython-3.12.12+20251031-x86_64-unknown-linux-gnu-install_only.tar.gz +0a461330b9b89f2ea3088dde10d7a3f96aa65897b7c5ce2404fa3b5c4b8daa14 20251031/cpython-3.12.12+20251031-x86_64-unknown-linux-musl-install_only.tar.gz +eae1272a72ccce601590a10a9ca2a58199b5fcdf022aa603a527e3e2a04de9bc 20251031/cpython-3.13.9+20251031-aarch64-apple-darwin-freethreaded+pgo+lto-full.tar.zst +1f3568d17383426d52350c2ef7c93c1a5a043198b860cb05e5d19b35f9c25cef 20251031/cpython-3.13.9+20251031-aarch64-apple-darwin-install_only.tar.gz +743ff69935ef28834621647dab30f032dfcd80315732917531eea333210941c7 20251031/cpython-3.13.9+20251031-aarch64-pc-windows-msvc-freethreaded+pgo-full.tar.zst +20db43873d3c4c2175d866806545e4ad4ec6bb72ca95e60082a4df6c24567e8c 20251031/cpython-3.13.9+20251031-aarch64-pc-windows-msvc-install_only.tar.gz +a6e72f9de5d9b46cf6968d6a492f2401a919f9b959f8da2d87f43484b80169ee 20251031/cpython-3.13.9+20251031-aarch64-unknown-linux-gnu-freethreaded+pgo+lto-full.tar.zst +0a56d11b0fb1662e67f892b9d5d1717aef06f24dbb8362bc25b8f784e620d44e 20251031/cpython-3.13.9+20251031-aarch64-unknown-linux-gnu-install_only.tar.gz +0ed5c65437f875c58ba1bee2b8d261d18698d3d0347a2e66f8902fce022a2cda 20251031/cpython-3.13.9+20251031-ppc64le-unknown-linux-gnu-freethreaded+lto-full.tar.zst +99492123902bd5e9a6b1a30135061e93a2e6a11d25107a741d5a756e91054448 20251031/cpython-3.13.9+20251031-ppc64le-unknown-linux-gnu-install_only.tar.gz +584e481d9b5225ffaf02f158fb26d2818207e65fc3c6dc21a6d500277f739220 20251031/cpython-3.13.9+20251031-riscv64-unknown-linux-gnu-freethreaded+lto-full.tar.zst +b3dce3e4ef508773521e1ee1be989fff6118f8fd1fbbd0491d7ff7dfbc98ef06 20251031/cpython-3.13.9+20251031-riscv64-unknown-linux-gnu-install_only.tar.gz +7fa7fb912ca989ceac026a332d56a2c7d6d16ab0e94d89e690de5aade26103e2 20251031/cpython-3.13.9+20251031-s390x-unknown-linux-gnu-freethreaded+lto-full.tar.zst +f10e34aaa856c1b8a69c2ea4a9a6723d520443d1a957bf66dc55491334ca0c1e 20251031/cpython-3.13.9+20251031-s390x-unknown-linux-gnu-install_only.tar.gz +e2bf5fa6a3ef443ade362e08b0a19bbc172f7bfe34dabe933ccaad31d53af5da 20251031/cpython-3.13.9+20251031-x86_64-apple-darwin-freethreaded+pgo+lto-full.tar.zst +48c0f3ca5d31e90658ef99138dc21865bb62f388ab97a1ce72cac176da194ab0 20251031/cpython-3.13.9+20251031-x86_64-apple-darwin-install_only.tar.gz +318a9a1e43dd52054327de3bccc0c5b7afde7b7f2a398ccb4d38e03d28b05386 20251031/cpython-3.13.9+20251031-x86_64-pc-windows-msvc-freethreaded+pgo-full.tar.zst +874593f641f31ea101440c70f81768c35d4d7d6df111fde63094db67465ef787 20251031/cpython-3.13.9+20251031-x86_64-pc-windows-msvc-install_only.tar.gz +dcc29b069d0588fbd4ea29c6df840c8d1207d2a3bce8cd5cd57d1b85373b6048 20251031/cpython-3.13.9+20251031-x86_64-unknown-linux-gnu-freethreaded+pgo+lto-full.tar.zst +6f05b91ee8c7e6dd0f9c60b95bb29130e2d623961de6578b643e80ddd83f96b6 20251031/cpython-3.13.9+20251031-x86_64-unknown-linux-gnu-install_only.tar.gz +ad987197034185e628715da504a50613af213dc21ba6d5ccaeab3db2c464aa6c 20251031/cpython-3.13.9+20251031-x86_64-unknown-linux-musl-install_only.tar.gz +d9c7b430b25bd3837dbb03f945dbe6b7bc526c5940ca96f5db7cdc42f6b2b801 20251031/cpython-3.14.0+20251031-aarch64-apple-darwin-freethreaded+pgo+lto-full.tar.zst +b4bcd3c6c24cab32ae99e1b05c89312b783b4d69431d702e5012fe1fdcad4087 20251031/cpython-3.14.0+20251031-aarch64-apple-darwin-install_only.tar.gz +40266e60f655e49cd1d5303295255909a4b593b08b88be6e6a55b2c9fe6ed13d 20251031/cpython-3.14.0+20251031-aarch64-pc-windows-msvc-freethreaded+pgo-full.tar.zst +599a8b7e12439cd95a201dbdfe95cf363146b1ff91f379555dafd86b170caab9 20251031/cpython-3.14.0+20251031-aarch64-pc-windows-msvc-install_only.tar.gz +f383ef50d1da6ca511212e5ae601923b56636b87351fd5fc847e0ea0a19fa9b3 20251031/cpython-3.14.0+20251031-aarch64-unknown-linux-gnu-freethreaded+pgo+lto-full.tar.zst +128a9cbfb9645d5237ec01704d9d1d2ac5f084464cc43c37a4cd96aa9c3b1ad5 20251031/cpython-3.14.0+20251031-aarch64-unknown-linux-gnu-install_only.tar.gz +cb0e4ff781b856a47f0f461ceb41c78c7eeff65effd0957857ec4702ef1e1bd3 20251031/cpython-3.14.0+20251031-ppc64le-unknown-linux-gnu-freethreaded+lto-full.tar.zst +e16ca51f018e99a609faf953bd3a3aea31f45ee84262d1a517fb3abd98f1f4af 20251031/cpython-3.14.0+20251031-ppc64le-unknown-linux-gnu-install_only.tar.gz +929223470d11a55cd75f880ac3bd4969e42407e2cdf08d4e7e38ba721cf4abec 20251031/cpython-3.14.0+20251031-riscv64-unknown-linux-gnu-freethreaded+lto-full.tar.zst +fca340d8fb7a05cd90e216ce601b25d492ed8c1a3b6a6d77703e0f15ab3711a7 20251031/cpython-3.14.0+20251031-riscv64-unknown-linux-gnu-install_only.tar.gz +613fb1f7b249f798b52af957d181305244e936c8e5c94c84688fcdf93fe14253 20251031/cpython-3.14.0+20251031-s390x-unknown-linux-gnu-freethreaded+lto-full.tar.zst +c5803644970eee931bb0581b3b64511d1a8612f67bc98951a7f7ab5581a9ed04 20251031/cpython-3.14.0+20251031-s390x-unknown-linux-gnu-install_only.tar.gz +b3196f6b57bbb3dc2ee07f348f1d51117ffa376979eceafbf50c15f0f7980bf8 20251031/cpython-3.14.0+20251031-x86_64-apple-darwin-freethreaded+pgo+lto-full.tar.zst +4e71a3ce973be377ef18637826648bb936e2f9490f64a9e4f33a49bcc431d344 20251031/cpython-3.14.0+20251031-x86_64-apple-darwin-install_only.tar.gz +b81de5fc9e783ea6dfcf1098c28a278c874999c71afbb0309f6a8b4276c769d0 20251031/cpython-3.14.0+20251031-x86_64-pc-windows-msvc-freethreaded+pgo-full.tar.zst +39acfcb3857d83eab054a3de11756ffc16b3d49c31393b9800dd2704d1f07fdf 20251031/cpython-3.14.0+20251031-x86_64-pc-windows-msvc-install_only.tar.gz +f4acbef0fbfaf7ab31ac63986da1d93dfa1c5cb797de1dcdc1a988aa18670120 20251031/cpython-3.14.0+20251031-x86_64-unknown-linux-gnu-freethreaded+pgo+lto-full.tar.zst +3dec1ab70758a3467ac3313bbcdabf7a9b3016db5c072c4537e3cf0a9e6290f6 20251031/cpython-3.14.0+20251031-x86_64-unknown-linux-gnu-install_only.tar.gz +d0a2a6d3b1bb00dce2105377fda8aa79675d187f8d6d7010a42f651af25018dc 20251031/cpython-3.14.0+20251031-x86_64-unknown-linux-musl-install_only.tar.gz +12f1b16be4017181ad67904caf9e59e525b9b5d62f49105017d837e27b832959 20251031/cpython-3.15.0a1+20251031-aarch64-apple-darwin-freethreaded+pgo+lto-full.tar.zst +3acf7aa3559b746498b18929456c5cacb84bae4e09249834cbc818970d71de87 20251031/cpython-3.15.0a1+20251031-aarch64-apple-darwin-install_only.tar.gz +54ca78dae455ece6fefbd7f5f287cc55d5ce197caf51921f6d871d15069d9489 20251031/cpython-3.15.0a1+20251031-aarch64-pc-windows-msvc-freethreaded+pgo-full.tar.zst +1508bcd7195008479ed156aad3afbb3a3793097ed530690f0304a8107f0e53e8 20251031/cpython-3.15.0a1+20251031-aarch64-pc-windows-msvc-install_only.tar.gz +981fe8dfc6e7e1d0ffefa945a18d5c4c759bbe21722acf3a5cc7e62f16aa5f3c 20251031/cpython-3.15.0a1+20251031-aarch64-unknown-linux-gnu-freethreaded+pgo+lto-full.tar.zst +d55c2aeece827e6bec83fd18515ee281d9ea0efaa3e2d20130db8f1c7cbb71c6 20251031/cpython-3.15.0a1+20251031-aarch64-unknown-linux-gnu-install_only.tar.gz +088400dec25139f38eeecb48f090ff2ce06a96a1dd79fa8f1dfec1cd1786f5ef 20251031/cpython-3.15.0a1+20251031-ppc64le-unknown-linux-gnu-freethreaded+lto-full.tar.zst +c28beda791c499b16f06256339522f0002a3e9acba003e6b8374755d7be1def2 20251031/cpython-3.15.0a1+20251031-ppc64le-unknown-linux-gnu-install_only.tar.gz +938061a0a31a06672526885de36037ddefd8c4acdb09424691b7000a8c8f8d01 20251031/cpython-3.15.0a1+20251031-riscv64-unknown-linux-gnu-freethreaded+lto-full.tar.zst +36619f576b8154e4b56643c5c4a85c352f152df2989c4e602cbbe9c2b7ded870 20251031/cpython-3.15.0a1+20251031-riscv64-unknown-linux-gnu-install_only.tar.gz +2003e7e40bb44b3db7bca81087bfb738fe6af40e5db61cda8e23b59bf55d409e 20251031/cpython-3.15.0a1+20251031-s390x-unknown-linux-gnu-freethreaded+lto-full.tar.zst +5ea47be2a3a563ddd87ff510dae26b7aa7f3855ca00c5f1056ff8114c067c4e4 20251031/cpython-3.15.0a1+20251031-s390x-unknown-linux-gnu-install_only.tar.gz +64fc29e6c7a2f02a18645d968f1b3fc1d00d12a5ef3fcbb0d077fa8c62c08904 20251031/cpython-3.15.0a1+20251031-x86_64-apple-darwin-freethreaded+pgo+lto-full.tar.zst +0ab19d3ac25f99da438b088751e5ec2421f9f6aa4292fd2dc0f8e49eb3e16bdf 20251031/cpython-3.15.0a1+20251031-x86_64-apple-darwin-install_only.tar.gz +34abc5603e1b4131f753d29b7deac865b9277912b851cbed5a149cf3e6745d3d 20251031/cpython-3.15.0a1+20251031-x86_64-pc-windows-msvc-freethreaded+pgo-full.tar.zst +5f5d6bec2b381cfc771c49972d2a6f7b7e7ab6a1651d8fb6ef3983f3571722b3 20251031/cpython-3.15.0a1+20251031-x86_64-pc-windows-msvc-install_only.tar.gz +0e0272186d9f5169394dbc4d4d72a3f4a5762a04c2e5ac2ab1e23aa41fc8538a 20251031/cpython-3.15.0a1+20251031-x86_64-unknown-linux-gnu-freethreaded+pgo+lto-full.tar.zst +1f356288c2b2713619cb7a4e453d33bf8882f812af2987e21e01e7ae382fefba 20251031/cpython-3.15.0a1+20251031-x86_64-unknown-linux-gnu-install_only.tar.gz +caf5311f333eef082dd69a669ca65aceba09a08fc1e78aad602ad649106f294c 20251031/cpython-3.15.0a1+20251031-x86_64-unknown-linux-musl-install_only.tar.gz +87275619c2706affa4d1090d2ca3dad354b6d69f8b85dbfafe38785870751b9a 20251031/cpython-3.9.25+20251031-aarch64-apple-darwin-install_only.tar.gz +6112d46355857680b81849764a6cf9f38cc4cd0d1cf29d432bc12fe5aeedf9d0 20251031/cpython-3.9.25+20251031-aarch64-unknown-linux-gnu-install_only.tar.gz +828364b6f54fa45ac2dc91f8e45d5b74306372af374a9ef16eeb2ea81253ed3f 20251031/cpython-3.9.25+20251031-ppc64le-unknown-linux-gnu-install_only.tar.gz +17467e0158e5ad04453c447d6773c23b044172276441e22e23058fd3ea053e27 20251031/cpython-3.9.25+20251031-riscv64-unknown-linux-gnu-install_only.tar.gz +3e9539f83e67faa813fd06171199b2d33c89821dfa9a33bf6e27ad67f1b6932d 20251031/cpython-3.9.25+20251031-s390x-unknown-linux-gnu-install_only.tar.gz +ace63cfe27a9487c4d72e1cb518be01c1d985271da0b2158e813801f7d3e5503 20251031/cpython-3.9.25+20251031-x86_64-apple-darwin-install_only.tar.gz +4fb1b416482ce94d73cfa140317a670c596c830671d137b07c26afe8c461768a 20251031/cpython-3.9.25+20251031-x86_64-pc-windows-msvc-install_only.tar.gz +42834f61eb6df43432c3dd6ab9ca3fdf8c06d10a404ebdb53d6902e6b9570b08 20251031/cpython-3.9.25+20251031-x86_64-unknown-linux-gnu-install_only.tar.gz +76593e8c889e81e82db5fe117fe15b69466f85100ab2ec0e4035aa86242b4e93 20251031/cpython-3.9.25+20251031-x86_64-unknown-linux-musl-install_only.tar.gz +3c9fdd76447c1549a0d3bc2a70c63f1daec997ab034206ac0260a03237166dbb 20251202/cpython-3.13.10+20251202-aarch64-apple-darwin-freethreaded+pgo+lto-full.tar.zst +37afe4e77ab62ac50f197b1cb1f3bc02c82735c6be893da0996afcde5dc41048 20251202/cpython-3.13.10+20251202-aarch64-apple-darwin-install_only.tar.gz +cdb7141327bdc244715b25752593e2c9eeb3cc2764f37dfe81cfbc92db9d6d57 20251202/cpython-3.13.10+20251202-aarch64-pc-windows-msvc-freethreaded+pgo-full.tar.zst +9060d644bd32ac0e0af970d0b21e207e6ff416b7c4dc26ffc4f9b043fb45b463 20251202/cpython-3.13.10+20251202-aarch64-pc-windows-msvc-install_only.tar.gz +6d277221fa4b172e00b29c7158ca9661917bc8db9a0084b1a0ff5c3a0ba8b648 20251202/cpython-3.13.10+20251202-aarch64-unknown-linux-gnu-freethreaded+pgo+lto-full.tar.zst +c68280591cda1c9515a04809fa6926020177e8e5892300206e0496ea1d10290e 20251202/cpython-3.13.10+20251202-aarch64-unknown-linux-gnu-install_only.tar.gz +d265d8d1c51e25ed70279540223589f79cf99ad00b50d28b6150c2658c973885 20251202/cpython-3.13.10+20251202-ppc64le-unknown-linux-gnu-freethreaded+lto-full.tar.zst +1507e5528bd88131dc742a2941176aceea1838bc09860c21f179285b7865133b 20251202/cpython-3.13.10+20251202-ppc64le-unknown-linux-gnu-install_only.tar.gz +ec411b4a2d167c3be0a9aeb3905e045d62c8e3c3db0caeade5d47d5f60b98dd0 20251202/cpython-3.13.10+20251202-riscv64-unknown-linux-gnu-freethreaded+lto-full.tar.zst +70169e916860b2e5b34c37c302d699eb2b8f24f28090968881942a37aeb7ed08 20251202/cpython-3.13.10+20251202-riscv64-unknown-linux-gnu-install_only.tar.gz +4fc6443948bf5b729481ea02cc5c68e80cd0da42631f6936587a2b8fd45bc62c 20251202/cpython-3.13.10+20251202-s390x-unknown-linux-gnu-freethreaded+lto-full.tar.zst +c5448863b64aacae62f3a213a6e6cf94ec63f96ee4d518491cd62fd3c81d952f 20251202/cpython-3.13.10+20251202-s390x-unknown-linux-gnu-install_only.tar.gz +6ce608684df0f90350c7a1742e9685a7782d9b26ec99d1bd9d55c8cf9a405040 20251202/cpython-3.13.10+20251202-x86_64-apple-darwin-freethreaded+pgo+lto-full.tar.zst +a02761a4f189f71c0512e88df7ca2843696d61da659e47f8a5c8a9bd2c0d16f4 20251202/cpython-3.13.10+20251202-x86_64-apple-darwin-install_only.tar.gz +6a8b0372ded655e0d55318089fbce3122a446e69bcd120c79aaadfe9b017299c 20251202/cpython-3.13.10+20251202-x86_64-pc-windows-msvc-freethreaded+pgo-full.tar.zst +8b00014c7c35f9ad4cb1c565f067500bacc4125c8bc30e4389ee0be9fd6ffa3d 20251202/cpython-3.13.10+20251202-x86_64-pc-windows-msvc-install_only.tar.gz +e39127fbe8d2ae7d86099f18b4da0918f9b60ce73ed491774d6dcfaa42b5c9ae 20251202/cpython-3.13.10+20251202-x86_64-unknown-linux-gnu-freethreaded+pgo+lto-full.tar.zst +0cac1495fff920219904b1d573aaec0df54d549c226cb45f5c60cb6d2c72727a 20251202/cpython-3.13.10+20251202-x86_64-unknown-linux-gnu-install_only.tar.gz +04108190972ac98e13098abd972ec3f4f8b0880f83c0bb68249ce1a6164fa041 20251202/cpython-3.13.10+20251202-x86_64-unknown-linux-musl-install_only.tar.gz +61f38e947449cf00f32f0838e813358f6bf61025d0797531e5b8b8b175c617f0 20251202/cpython-3.14.1+20251202-aarch64-apple-darwin-freethreaded+pgo+lto-full.tar.zst +cdf1ba0789f529fa34bb5b5619c5da9757ac1067d6b8dd0ee8b78e50078fc561 20251202/cpython-3.14.1+20251202-aarch64-apple-darwin-install_only.tar.gz +ddb10b645de2b1f6f2832a80b115a9cd34a4a760249983027efe46618a8efc48 20251202/cpython-3.14.1+20251202-aarch64-pc-windows-msvc-freethreaded+pgo-full.tar.zst +19129cf8b4d68c4e64c25bae43bca139d871267b59cf7f02b9dcf25f0bf59497 20251202/cpython-3.14.1+20251202-aarch64-pc-windows-msvc-install_only.tar.gz +1a88a1fe21eb443d280999464b1a397605a7ca950d8ab73813ca6868835439a2 20251202/cpython-3.14.1+20251202-aarch64-unknown-linux-gnu-freethreaded+pgo+lto-full.tar.zst +5dde7dba0b8ef34c0d5cb8a721254b1e11028bfc09ff06664879c245fe8df73f 20251202/cpython-3.14.1+20251202-aarch64-unknown-linux-gnu-install_only.tar.gz +7207b736ed2569f307649ffd4b615a5346631bc244730b8702babee377cef528 20251202/cpython-3.14.1+20251202-ppc64le-unknown-linux-gnu-freethreaded+lto-full.tar.zst +d2774701d53e2ac06f8c8c8e52dfa4ff346890de9b417c9a7664195443a4c766 20251202/cpython-3.14.1+20251202-ppc64le-unknown-linux-gnu-install_only.tar.gz +d1356ccd279920edc31bf0350674d966beb9522f9503846ed7855dbb109ccc14 20251202/cpython-3.14.1+20251202-riscv64-unknown-linux-gnu-freethreaded+lto-full.tar.zst +af840506efbcd5026d9140c0a0230e45e46bb1f339a65c10a22875930b2c0159 20251202/cpython-3.14.1+20251202-riscv64-unknown-linux-gnu-install_only.tar.gz +477758eabc06dbc7e5e5d16e97c4672478acd409f420dd2e1b84d3452c0668d1 20251202/cpython-3.14.1+20251202-s390x-unknown-linux-gnu-freethreaded+lto-full.tar.zst +43f8f79bf4c66689d2019f193671d1df3e5e5dbb293382036285e8ce55fc55bb 20251202/cpython-3.14.1+20251202-s390x-unknown-linux-gnu-install_only.tar.gz +c2cb2a9b44285fbc13c3c9b7eea813db6ed8d94909406b059db7afd39b32e786 20251202/cpython-3.14.1+20251202-x86_64-apple-darwin-freethreaded+pgo+lto-full.tar.zst +f25ce050e1d370f9c05c9623b769ffa4b269a6ae17e611b435fd2b8b09972a88 20251202/cpython-3.14.1+20251202-x86_64-apple-darwin-install_only.tar.gz +8ef7048315cac6d26bdbef18512a87b1a24fffa21cec86e32f9a9425f2af9bf6 20251202/cpython-3.14.1+20251202-x86_64-pc-windows-msvc-freethreaded+pgo-full.tar.zst +cb478a5a37eb93ce4d3c27ae64d211d6a5a42475ae53f666a8d1570e71fcf409 20251202/cpython-3.14.1+20251202-x86_64-pc-windows-msvc-install_only.tar.gz +c5d5b89aab7de683e465e36de2477a131435076badda775ef6e9ea21109c1c32 20251202/cpython-3.14.1+20251202-x86_64-unknown-linux-gnu-freethreaded+pgo+lto-full.tar.zst +a72f313bad49846e5e9671af2be7476033a877c80831cf47f431400ccb520090 20251202/cpython-3.14.1+20251202-x86_64-unknown-linux-gnu-install_only.tar.gz +15d50b15713097c38c67b1a06a0498ad102377f9b3999e98e4eefd6bf91bd82d 20251202/cpython-3.14.1+20251202-x86_64-unknown-linux-musl-install_only.tar.gz +4213058b7fcd875596c12b58cd46a399358b0a87ecde4b349cbdd00cf87ed79a 20251209/cpython-3.13.11+20251209-aarch64-apple-darwin-freethreaded+pgo+lto-full.tar.zst +295a9f7bc899ea1cc08baf60bbf511bdd1e4a29b2dd7e5f59b48f18bfa6bf585 20251209/cpython-3.13.11+20251209-aarch64-apple-darwin-install_only.tar.gz +6daf6d092c7294cfe68c4c7bf2698ac134235489c874b3bf796c7972b9dbba30 20251209/cpython-3.13.11+20251209-aarch64-pc-windows-msvc-freethreaded+pgo-full.tar.zst +ba646d0c3b7dd7bdfb770d9b2ebd6cd2df02a37fda90c9c79a7cf59c7df6f165 20251209/cpython-3.13.11+20251209-aarch64-pc-windows-msvc-install_only.tar.gz +290ca3bd0007db9e551f90b08dfcb6c1b2d62c33b2fc3e9a43e77d385d94f569 20251209/cpython-3.13.11+20251209-aarch64-unknown-linux-gnu-freethreaded+pgo+lto-full.tar.zst +ea1e678e6e82301bb32bf3917732125949b6e46d541504465972024a3f165343 20251209/cpython-3.13.11+20251209-aarch64-unknown-linux-gnu-install_only.tar.gz +09d4b50f8abb443f7e3af858c920aa61c2430b0954df465e861caa7078e55e69 20251209/cpython-3.13.11+20251209-ppc64le-unknown-linux-gnu-freethreaded+lto-full.tar.zst +7660e53aad9d35ee256913c6d98427f81f078699962035c5fa8b5c3138695109 20251209/cpython-3.13.11+20251209-ppc64le-unknown-linux-gnu-install_only.tar.gz +5406f2a7cacafbd2aac3ce2de066a0929aab55423824276c36e04cb83babc36c 20251209/cpython-3.13.11+20251209-riscv64-unknown-linux-gnu-freethreaded+lto-full.tar.zst +763fa1548e6a432e9402916e690c74ea30f26dcd2e131893dd506f72b87c27c9 20251209/cpython-3.13.11+20251209-riscv64-unknown-linux-gnu-install_only.tar.gz +3984b67c4292892eaccdd1c094c7ec788884c4c9b3534ab6995f6be96d5ed51d 20251209/cpython-3.13.11+20251209-s390x-unknown-linux-gnu-freethreaded+lto-full.tar.zst +ffb6af51fbfabfc6fbc4e7379bdec70c2f51e972b1d2f45c053493b9da3a1bbe 20251209/cpython-3.13.11+20251209-s390x-unknown-linux-gnu-install_only.tar.gz +d6f489464045d6895ae68b0a04a9e16477e74fe3185a75f3a9a0af8ccd25eade 20251209/cpython-3.13.11+20251209-x86_64-apple-darwin-freethreaded+pgo+lto-full.tar.zst +dac4a0a0a9b71f6b02a8b0886547fa22814474239bffb948e3e77185406ea136 20251209/cpython-3.13.11+20251209-x86_64-apple-darwin-install_only.tar.gz +bb9a29a7ba8f179273b79971da6aaa7be592d78c606a63f99eff3e4c12fb0fae 20251209/cpython-3.13.11+20251209-x86_64-pc-windows-msvc-freethreaded+pgo-full.tar.zst +87822417007045a28a7eccc47fe67b8c61265b99b10dbbfa24d231a3622b1c27 20251209/cpython-3.13.11+20251209-x86_64-pc-windows-msvc-install_only.tar.gz +33f89c957d986d525529b8a980103735776f4d20cf52f55960a057c760188ac3 20251209/cpython-3.13.11+20251209-x86_64-unknown-linux-gnu-freethreaded+pgo+lto-full.tar.zst +1ffa06d714a44aea14c0c54c30656413e5955a6c92074b4b3cb4351dcc28b63b 20251209/cpython-3.13.11+20251209-x86_64-unknown-linux-gnu-install_only.tar.gz +969fe24017380b987c4e3ce15e9edf82a4618c1e61672b2cc9b021a1c98eae78 20251209/cpython-3.13.11+20251209-x86_64-unknown-linux-musl-install_only.tar.gz +d6d17b8ef28326552cdeb2a7541c8a0cb711b378df9b93ebdb461dca065edfea 20251209/cpython-3.14.2+20251209-aarch64-apple-darwin-freethreaded+pgo+lto-full.tar.zst +2f74bd26bd16487aca357c879d11f7b16c0521328e5148a1930ab6357bcb89fe 20251209/cpython-3.14.2+20251209-aarch64-apple-darwin-install_only.tar.gz +43aac5bb4cdba71fc6775d26f47348d573a0b1210911438be71d7d96f4b18b51 20251209/cpython-3.14.2+20251209-aarch64-pc-windows-msvc-freethreaded+pgo-full.tar.zst +0be0d2557d73efa7f6f3f99679f05252d57fe2aad2d81cac3cad410a9b1eacbd 20251209/cpython-3.14.2+20251209-aarch64-pc-windows-msvc-install_only.tar.gz +adfcb90f3a7e1b3fbc6a99f9c8c8dce1f2e26ea72b724bbe4e9fa39e81e2b0db 20251209/cpython-3.14.2+20251209-aarch64-unknown-linux-gnu-freethreaded+pgo+lto-full.tar.zst +869af31b2963194e8a2ecfadc36027c4c1c86a10f4960baec36dadb41b2acf02 20251209/cpython-3.14.2+20251209-aarch64-unknown-linux-gnu-install_only.tar.gz +2b1ce0c5a5f5e5add7e4f934f5bd35ac41660895a30b3098db7f7303d6952a4f 20251209/cpython-3.14.2+20251209-ppc64le-unknown-linux-gnu-freethreaded+lto-full.tar.zst +86129976403fb5d64cf576329f94148f28cf6f82834e94df81ff31e9d5f404e0 20251209/cpython-3.14.2+20251209-ppc64le-unknown-linux-gnu-install_only.tar.gz +4efb610fa07a6ee2639d14d78fc3b6ecb47431c14e1e4bda03c7f7dd60a5c1e5 20251209/cpython-3.14.2+20251209-riscv64-unknown-linux-gnu-freethreaded+lto-full.tar.zst +318dceecf119ea903aef1fb03a552cc592ecd61c08da891b68f5755e21e13511 20251209/cpython-3.14.2+20251209-riscv64-unknown-linux-gnu-install_only.tar.gz +e62f3bb3e66dac6c459690f9e9cd8cc2f6fe1dcf8bfed452af4c3df24cd7874f 20251209/cpython-3.14.2+20251209-s390x-unknown-linux-gnu-freethreaded+lto-full.tar.zst +53875c849a14194344ead1d9cd1e128cadd42a4b83c35eeb212417909ef05a6a 20251209/cpython-3.14.2+20251209-s390x-unknown-linux-gnu-install_only.tar.gz +1fd76c79f7fc1753e8d2ed2f71406c0b65776c75f3e95ed99ffde8c95af2adc1 20251209/cpython-3.14.2+20251209-x86_64-apple-darwin-freethreaded+pgo+lto-full.tar.zst +58fa3e17d13ab956fd11055fb774c98ecfddcdf3b588e5f2369bdbc14ef9d76a 20251209/cpython-3.14.2+20251209-x86_64-apple-darwin-install_only.tar.gz +9927951e3997c186d2813ca1a0f4a8f5a2f771463f7f8ad0752fd3d2be2b74e4 20251209/cpython-3.14.2+20251209-x86_64-pc-windows-msvc-freethreaded+pgo-full.tar.zst +0d660bba9f58cb552e7e99e1f96a9c67b41618c9b8d29f9f3515fe2b5ad1966e 20251209/cpython-3.14.2+20251209-x86_64-pc-windows-msvc-install_only.tar.gz +3728872ffd74989a7b4bbf3f0c629ae8fe821cda2bd6544012c1b92b9f5d5a5b 20251209/cpython-3.14.2+20251209-x86_64-unknown-linux-gnu-freethreaded+pgo+lto-full.tar.zst +121c3249bef497adf601df76a4d89aed6053fc5ec2f8c0ec656b86f0142e8ddd 20251209/cpython-3.14.2+20251209-x86_64-unknown-linux-gnu-install_only.tar.gz +71639cc5d1fb79840467531c5b53ca77170a58edd3f7e2d29330dd736e477469 20251209/cpython-3.14.2+20251209-x86_64-unknown-linux-musl-install_only.tar.gz +5b34488580df13df051a2e84e43cfca2ab28fdd7a61052f35988eb8b481b894a 20251209/cpython-3.15.0a2+20251209-aarch64-apple-darwin-freethreaded+pgo+lto-full.tar.zst +5851f3744fbd39e3e323844cf4f68d7763fb25546aa5ffbb71b1b5ab69c56616 20251209/cpython-3.15.0a2+20251209-aarch64-apple-darwin-install_only.tar.gz +3d99152b4e29b947fb1cfc8d035d1d511e50aeed72886ff4a5fd0a3694bd0b51 20251209/cpython-3.15.0a2+20251209-aarch64-pc-windows-msvc-freethreaded+pgo-full.tar.zst +39bc2fcac13aeba7d650f76badf63350a81c86167a62174cb092eab7a749f4a5 20251209/cpython-3.15.0a2+20251209-aarch64-pc-windows-msvc-install_only.tar.gz +0c2c83236f6e28c103e2660a82be94b2459ee8cfdd90f5dd82f0d503ca2aec09 20251209/cpython-3.15.0a2+20251209-aarch64-unknown-linux-gnu-freethreaded+pgo+lto-full.tar.zst +17ba65d669be3052524e03b4d1426c072ef38df2a9065ff4525d1f4d1bc9f82c 20251209/cpython-3.15.0a2+20251209-aarch64-unknown-linux-gnu-install_only.tar.gz +216842df2377fd032f279ded7fd23d7bdbd92d4c1fa7619523bc0dbdef5bd212 20251209/cpython-3.15.0a2+20251209-ppc64le-unknown-linux-gnu-freethreaded+lto-full.tar.zst +5585bd7c5eefe28b9bf544d902cad9a2f81f33c618f2a1d3c006cbfcdec77abc 20251209/cpython-3.15.0a2+20251209-ppc64le-unknown-linux-gnu-install_only.tar.gz +2a8b56f318d2e21b01b54909554c53d81871b9bb05d23ea7808dde9acec4dc7e 20251209/cpython-3.15.0a2+20251209-riscv64-unknown-linux-gnu-freethreaded+lto-full.tar.zst +bb7252edaffd422bd1c044a4764dfcf83a5d7159942f445abbef524e54ea79a0 20251209/cpython-3.15.0a2+20251209-riscv64-unknown-linux-gnu-install_only.tar.gz +06c4ca3983aad20723f68786e3663ab49fee1bf09326f341649205ed79d34fc6 20251209/cpython-3.15.0a2+20251209-s390x-unknown-linux-gnu-freethreaded+lto-full.tar.zst +03a90ffa9f92d4cf4caeefb9d15f0b39c05c1e60ade6688f32165f957db4f8f3 20251209/cpython-3.15.0a2+20251209-s390x-unknown-linux-gnu-install_only.tar.gz +4d8102b70ea9fe726ee3ae9ad9e9bc4cbe0b6ed18f7989c81aef81de578f0163 20251209/cpython-3.15.0a2+20251209-x86_64-apple-darwin-freethreaded+pgo+lto-full.tar.zst +cee576de4919cd422dbc31eb85d3c145ee82acec84f651daaf32dc669b5149c9 20251209/cpython-3.15.0a2+20251209-x86_64-apple-darwin-install_only.tar.gz +6ff71bac78d650ce621fe6db49f06290e48bcceb61f69cccc7728584f70b6346 20251209/cpython-3.15.0a2+20251209-x86_64-pc-windows-msvc-freethreaded+pgo-full.tar.zst +e538475ee249eacf63bfdae0e70af73e9c47360e6dd3d6825e7a35107e177de5 20251209/cpython-3.15.0a2+20251209-x86_64-pc-windows-msvc-install_only.tar.gz +70f552e213734c0e260a57603bee504dd7ed0e78a10558b591e724ea8730fef5 20251209/cpython-3.15.0a2+20251209-x86_64-unknown-linux-gnu-freethreaded+pgo+lto-full.tar.zst +58addaabfab2de422180d32543fb3878ffc984c8a2e4005ff658a5cd83b31fc7 20251209/cpython-3.15.0a2+20251209-x86_64-unknown-linux-gnu-install_only.tar.gz +dcf844400dc2e7f5f3604e994532e4d49db45f4deefe9afdf6809ca1bc6532ee 20251209/cpython-3.15.0a2+20251209-x86_64-unknown-linux-musl-install_only.tar.gz +e7cf7bc717082bb38f5ca75988ecd8e5dbc1b0535192129371e30235d29d67b5 20260325/cpython-3.13.12+20260325-aarch64-apple-darwin-freethreaded-install_only.tar.gz +688da81bcaa6ed91792397c7d5433b13a4f02f021f940637c3972639bc516dca 20260325/cpython-3.13.12+20260325-aarch64-apple-darwin-install_only.tar.gz +54187be504ea5be2f8ed455e9377112bb04f34c9259eae263779e56b403e3e3f 20260325/cpython-3.13.12+20260325-aarch64-pc-windows-msvc-freethreaded-install_only.tar.gz +d2c8b00044cd2e4c5fc7e697e63d5e481ed44b87c2def0beb42991d59f65d930 20260325/cpython-3.13.12+20260325-aarch64-pc-windows-msvc-install_only.tar.gz +9794866e9a464f349055d791ea8f14dfa7f339ecac5aa9b1084bb2ce388fc598 20260325/cpython-3.13.12+20260325-aarch64-unknown-linux-gnu-freethreaded-install_only.tar.gz +31c6e61eed48ca4e156d0e473025a792338641109e8277a63518ded438390c96 20260325/cpython-3.13.12+20260325-aarch64-unknown-linux-gnu-install_only.tar.gz +1a8a4a97f33740a1cb9fa480321818cdc610c79c9137e511e76dc53635615494 20260325/cpython-3.13.12+20260325-ppc64le-unknown-linux-gnu-freethreaded-install_only.tar.gz +654939bc40d5f76f08eb17335bb19e9efa11eb48a0818eda2293a3f7c3570ae7 20260325/cpython-3.13.12+20260325-ppc64le-unknown-linux-gnu-install_only.tar.gz +178d20e568c25abcca9b1dbedf77e904cc3f10a79d22e31f87ddabd2d28f87dc 20260325/cpython-3.13.12+20260325-riscv64-unknown-linux-gnu-freethreaded-install_only.tar.gz +fc7e1fb553c47b831ed7fa529575145207f000f967513f7b9ea809cce006ed79 20260325/cpython-3.13.12+20260325-riscv64-unknown-linux-gnu-install_only.tar.gz +d23c93ea7502420c71e4acf02999c72ab80797d51843b1b6a315ca7bac3cb780 20260325/cpython-3.13.12+20260325-s390x-unknown-linux-gnu-freethreaded-install_only.tar.gz +7d7919358e88fcc672b061be8c2316c3a604c7074200515d7104166ed611f7f9 20260325/cpython-3.13.12+20260325-s390x-unknown-linux-gnu-install_only.tar.gz +6aff211689e30889cfe90b0b2a76b6f5a7b9e6e0bb28d6a66fd5ba35d36dc78a 20260325/cpython-3.13.12+20260325-x86_64-apple-darwin-freethreaded-install_only.tar.gz +7411e47939783708381017a90944a69641ac84d43f74fb6e2d52576c599a2717 20260325/cpython-3.13.12+20260325-x86_64-apple-darwin-install_only.tar.gz +088754e90ff22962a4ab6f7cb6bdabe5d9e7618266595df2cf7b211766e15132 20260325/cpython-3.13.12+20260325-x86_64-pc-windows-msvc-freethreaded-install_only.tar.gz +5b4093f92d9bffcb0d92aea050f3d77d5a4fc8e918b31cea000ee4b3ca751f1d 20260325/cpython-3.13.12+20260325-x86_64-pc-windows-msvc-install_only.tar.gz +6070796c894ef0a25b5a944c8c0327e155df534302e1612a5ddd57d177ddadf7 20260325/cpython-3.13.12+20260325-x86_64-unknown-linux-gnu-freethreaded-install_only.tar.gz +ebb1051ca2822b9803f46a5f10b6d51d153189ef1b1f1e142f733c0cbeaf86eb 20260325/cpython-3.13.12+20260325-x86_64-unknown-linux-gnu-install_only.tar.gz +b2e9400731c7f18069ec2804ba87a404385fe440f93b7dcb59004b9f56651202 20260325/cpython-3.13.12+20260325-x86_64-unknown-linux-musl-install_only.tar.gz +21f297bc1e0503fa077364417e2213c60951d94fd65d837ae6d9d9201ae27483 20260325/cpython-3.14.3+20260325-aarch64-apple-darwin-freethreaded-install_only.tar.gz +80c996c23aab828134821f078a8a77a6f33f3f2c14000f071718c540e20c64d4 20260325/cpython-3.14.3+20260325-aarch64-apple-darwin-install_only.tar.gz +d0e355df7362d12542108f78b3f8085b21e6824420769117c262ac86569bb2a7 20260325/cpython-3.14.3+20260325-aarch64-pc-windows-msvc-freethreaded-install_only.tar.gz +b35fe7c2fe169574f382cef125e95cbd904ddcb98fc337167356371b6d2e8c60 20260325/cpython-3.14.3+20260325-aarch64-pc-windows-msvc-install_only.tar.gz +112cf42bdf4d04f69ff4f9bf18c8ce45f494bac1645310bfdeff6f2ffb30dd9a 20260325/cpython-3.14.3+20260325-aarch64-unknown-linux-gnu-freethreaded-install_only.tar.gz +6faf5478f910741c477830f5fd842011208af0f9678faf77106c9421b325bfc1 20260325/cpython-3.14.3+20260325-aarch64-unknown-linux-gnu-install_only.tar.gz +9d7e5ba8020fd942a89a57179d9015eb0237c2d95cdbf8378639723663f11706 20260325/cpython-3.14.3+20260325-ppc64le-unknown-linux-gnu-freethreaded-install_only.tar.gz +5eafe32e12f33f98c40de920482b013170dcf97d8c7f5dc780271ccf4cded76a 20260325/cpython-3.14.3+20260325-ppc64le-unknown-linux-gnu-install_only.tar.gz +32955ad52ec7931e76f4509134a2ba5a6ba6ea0cd55e05217c1ccca3967c4a5c 20260325/cpython-3.14.3+20260325-riscv64-unknown-linux-gnu-freethreaded-install_only.tar.gz +481d3faef258964e57b7102c63de12b2bb388c7ed07cfe456f33e63b4e061202 20260325/cpython-3.14.3+20260325-riscv64-unknown-linux-gnu-install_only.tar.gz +7a1d36a1567cd747411c9c2bc7e2b5c1ac277ea7c734f74b158b94101fd5ea43 20260325/cpython-3.14.3+20260325-s390x-unknown-linux-gnu-freethreaded-install_only.tar.gz +d706eae2f4d963187b7c866603aed75d7eb3ea59590b06fb34f5fd7d0fe8e432 20260325/cpython-3.14.3+20260325-s390x-unknown-linux-gnu-install_only.tar.gz +3788781d0f9704f91ab5f7ad2a040d26b0f9b6aba0a2535db21755aebb69e620 20260325/cpython-3.14.3+20260325-x86_64-apple-darwin-freethreaded-install_only.tar.gz +847a49fea36c066f8df7a57cd8c4c02d17667e25d30b7930e8f8ba15e72d7efc 20260325/cpython-3.14.3+20260325-x86_64-apple-darwin-install_only.tar.gz +99dd7e425b3dac23e03f37787d77ee0af531e96b1c748275185342bc6642eb6b 20260325/cpython-3.14.3+20260325-x86_64-pc-windows-msvc-freethreaded-install_only.tar.gz +8b4e1329c4901ce2c0f1c20ac5d2ffa62fc13f12e26b5d1e5a1000f910f980d4 20260325/cpython-3.14.3+20260325-x86_64-pc-windows-msvc-install_only.tar.gz +20d3bcd7f175e09fa08f4cb3039e5f90fe7e4ce2476534e83f5aa21eb0d7cee9 20260325/cpython-3.14.3+20260325-x86_64-unknown-linux-gnu-freethreaded-install_only.tar.gz +18270c5a7b1a572599df5e68b497ba5254811dac43ba6f542245807d821fcb44 20260325/cpython-3.14.3+20260325-x86_64-unknown-linux-gnu-install_only.tar.gz +726a28734d2878a637b0d16ce07ce24c7d6ca1043d8e6f4a23b1b0a3478eedb9 20260325/cpython-3.14.3+20260325-x86_64-unknown-linux-musl-install_only.tar.gz +f76cc83c7db16cfc8794bf6e44d834152b57d8bab4e04e823cbc59ed23ec22f8 20260414/cpython-3.10.20+20260414-aarch64-apple-darwin-install_only.tar.gz +64932c8e8bbdf9d6b66ee85934f6f8ad1d18218b51a87ea06cefd3b84554a3e4 20260414/cpython-3.10.20+20260414-aarch64-unknown-linux-gnu-install_only.tar.gz +76b48eb26ef274045772186e63431419294c41baf6d5a372b722d4c9e711082e 20260414/cpython-3.10.20+20260414-ppc64le-unknown-linux-gnu-install_only.tar.gz +76e1ec72717d17493976fc176ec661f02412666d4f19e50908d8e4303c0511d5 20260414/cpython-3.10.20+20260414-riscv64-unknown-linux-gnu-install_only.tar.gz +2edf241199d11a3ef79a312737c1bcdb86908352c585ca14b667539080630e85 20260414/cpython-3.10.20+20260414-s390x-unknown-linux-gnu-install_only.tar.gz +95a2d794b8981723095190fa94b574ceb4272bb49d83b9e418bb90341e304d09 20260414/cpython-3.10.20+20260414-x86_64-apple-darwin-install_only.tar.gz +0d828683d30185ab9f1110ad2194ef384cef0533b8e0da7e03ce837548841788 20260414/cpython-3.10.20+20260414-x86_64-pc-windows-msvc-install_only.tar.gz +303047011b2c9f58504a930fc974d84547477cf69a3f2962f25552e2395c13af 20260414/cpython-3.10.20+20260414-x86_64-unknown-linux-gnu-install_only.tar.gz +84eb198d318f8b1b8bf59eef5d30d742e13afd97c213fa229578f8fdab0c406f 20260414/cpython-3.10.20+20260414-x86_64-unknown-linux-musl-install_only.tar.gz +a57ffd435652092d16b30e783f9826c55e9c64b0f0a72cbae0a9f39e663137fb 20260414/cpython-3.11.15+20260414-aarch64-apple-darwin-install_only.tar.gz +a882abe4876985c9dc3d433420548506fb0cc9bb9d9fe336a2d3aaf28922aa45 20260414/cpython-3.11.15+20260414-aarch64-pc-windows-msvc-install_only.tar.gz +77836944ae15b74e0b25bdc68a4703a340f2ccb684effc0f45fbd7910e1a1f39 20260414/cpython-3.11.15+20260414-aarch64-unknown-linux-gnu-install_only.tar.gz +30a2107f000dbe304820627cbe2cc257027c20f3241d96e6c7df796b69ac2062 20260414/cpython-3.11.15+20260414-ppc64le-unknown-linux-gnu-install_only.tar.gz +373b98fbf2d04099139a2f6be57593714382ed790be7e7419e358830c23ddd0f 20260414/cpython-3.11.15+20260414-riscv64-unknown-linux-gnu-install_only.tar.gz +7838efa839158c80568de35ac78d438f564f4c32272a2fe7d9e14a9b351d1a62 20260414/cpython-3.11.15+20260414-s390x-unknown-linux-gnu-install_only.tar.gz +317055d80e553764feeaef432d833dd8385c14b83465a8b3fa7c2b7819cba681 20260414/cpython-3.11.15+20260414-x86_64-apple-darwin-install_only.tar.gz +8e69ecf1d9fc194e029aafa608d483bf24ccaa8f56d456d7009f20462d62ad23 20260414/cpython-3.11.15+20260414-x86_64-pc-windows-msvc-install_only.tar.gz +8b14030dd3af9ea7f7c51b4c90feb04afd8a8f45435727e67b875270bd08f3bc 20260414/cpython-3.11.15+20260414-x86_64-unknown-linux-gnu-install_only.tar.gz +ca92d3a68a39fa330498b09714733f347bead7313ba9d9b7fbed837aa4ba7796 20260414/cpython-3.11.15+20260414-x86_64-unknown-linux-musl-install_only.tar.gz +8966b2bcd9fa03ba22c080ad15a86bc12e41a00122b16f4b3740e302261124d9 20260414/cpython-3.12.13+20260414-aarch64-apple-darwin-install_only.tar.gz +f55326c894fde76fc0faffe95d2bce60be533c88a8c44c1b88bbbc17bf6a5cd5 20260414/cpython-3.12.13+20260414-aarch64-pc-windows-msvc-install_only.tar.gz +355d981eafb9b2870af79ddc106ced7266b6f6d2101d8fbcb05620fa386642b9 20260414/cpython-3.12.13+20260414-aarch64-unknown-linux-gnu-install_only.tar.gz +4aef4cffe73c4a65ea486f14d684a9ad3f831a354174d163bb531b5baa70fc49 20260414/cpython-3.12.13+20260414-ppc64le-unknown-linux-gnu-install_only.tar.gz +c2629d69324155132343913f064be93509bd162531e08a292e50c3973ec8b5db 20260414/cpython-3.12.13+20260414-riscv64-unknown-linux-gnu-install_only.tar.gz +e5baafd64180f45165d2751b25d1bcc89254eefc7926f3ab341fc61b541d7606 20260414/cpython-3.12.13+20260414-s390x-unknown-linux-gnu-install_only.tar.gz +801b03fbe004181d55a02ebd8b4e04d74973e70d716062aebe3b3cf32e9be297 20260414/cpython-3.12.13+20260414-x86_64-apple-darwin-install_only.tar.gz +c5a9e011e284c49c48106ca177342f3e3f64e95b4c6652d4a382cc7c9bb1cc46 20260414/cpython-3.12.13+20260414-x86_64-pc-windows-msvc-install_only.tar.gz +cdcf8724d46e4857f8db5ee9f4252dc2f5da34f7940294ec6b312389dd3f41e0 20260414/cpython-3.12.13+20260414-x86_64-unknown-linux-gnu-install_only.tar.gz +d10e971238c130fdf25e577c6538a3effa5589d5fcf53665e3c711edd6a6ff2f 20260414/cpython-3.12.13+20260414-x86_64-unknown-linux-musl-install_only.tar.gz +2662b1c3f6d5ed4d02d877c07f9384acc0d18b9046d54cd2853dad3ca172784f 20260414/cpython-3.13.13+20260414-aarch64-apple-darwin-freethreaded-install_only.tar.gz +c652dad552122cd2e76968ec41c803f8222038169b11310dba0c85928265f5c1 20260414/cpython-3.13.13+20260414-aarch64-apple-darwin-install_only.tar.gz +c6c1aae3809ef585271f6f1bb3643a2c6e0c82b811b93284c6218b31f0b931d7 20260414/cpython-3.13.13+20260414-aarch64-pc-windows-msvc-freethreaded-install_only.tar.gz +586ba71c75f341e1d111399b7f719ae784dc11e8672e93e017388f28684226d0 20260414/cpython-3.13.13+20260414-aarch64-pc-windows-msvc-install_only.tar.gz +46ac7e9476b938ef19f71029a77d28ed1e201335dd0aa0237fcfed2e5ce0ee61 20260414/cpython-3.13.13+20260414-aarch64-unknown-linux-gnu-freethreaded-install_only.tar.gz +6a65f68043d7fadcd580415493d2929d1fd686013f9ae44ddbd3a81307ab256d 20260414/cpython-3.13.13+20260414-aarch64-unknown-linux-gnu-install_only.tar.gz +abe26a6cab523a5d00d75f1353cbad9c5dc04262dcb0dc4a2b47d02384e2a7d7 20260414/cpython-3.13.13+20260414-ppc64le-unknown-linux-gnu-freethreaded-install_only.tar.gz +aef73894107300264222b19e357baf5bad616b1c4bf5daa5c3b97cfee8f5ed7b 20260414/cpython-3.13.13+20260414-ppc64le-unknown-linux-gnu-install_only.tar.gz +eea71fc3625fcc2408171b17fb97e0c6286ed60ed225ca7fd6e2fc5d9cc21dce 20260414/cpython-3.13.13+20260414-riscv64-unknown-linux-gnu-freethreaded-install_only.tar.gz +f47c09f8e7f2fb0bc4afe52422705af4016c8d3ec1cf004b67bb56a86caa62cb 20260414/cpython-3.13.13+20260414-riscv64-unknown-linux-gnu-install_only.tar.gz +dd8b6161c4af3c2f5f29b3535decdcf146ce90d7a062687c9e5229b4151198b0 20260414/cpython-3.13.13+20260414-s390x-unknown-linux-gnu-freethreaded-install_only.tar.gz +4d205af9654e1f33cefd23ff798af470e565f3ac0eba18d2f98f18a2abd07166 20260414/cpython-3.13.13+20260414-s390x-unknown-linux-gnu-install_only.tar.gz +27edbaad8f0c1a8814647d24df3f87eb13c89bbc2cb90e2fc23d8fa48dd64b15 20260414/cpython-3.13.13+20260414-x86_64-apple-darwin-freethreaded-install_only.tar.gz +540337412d2c4220e99280f741dbf45c1e3da3a39edaaab20c6ba1d53e1692ef 20260414/cpython-3.13.13+20260414-x86_64-apple-darwin-install_only.tar.gz +002c07103bfbe1b889f41eb1b9fade81651a21aed35a3512e2a916c5d7903cfe 20260414/cpython-3.13.13+20260414-x86_64-pc-windows-msvc-freethreaded-install_only.tar.gz +ee0cb26453d6e025d36502d765c1639c34830355e46ab3ad31c0360bc4cd9b79 20260414/cpython-3.13.13+20260414-x86_64-pc-windows-msvc-install_only.tar.gz +a183ec7a10c38ab8c3f19968614f1e69ec697199e94525583662dfbc22b70d9a 20260414/cpython-3.13.13+20260414-x86_64-unknown-linux-gnu-freethreaded-install_only.tar.gz +e5ec3b2c5693215d153c434ac018e75511b2c4f96d2bce30468a477cb3a89d5e 20260414/cpython-3.13.13+20260414-x86_64-unknown-linux-gnu-install_only.tar.gz +24ac6bf80dd2991c8be348f777c96c6eb69b71e78d8fa28c09beb3ddca015a47 20260414/cpython-3.13.13+20260414-x86_64-unknown-linux-musl-install_only.tar.gz +a4bfd77675740a0362c137b094f3cd9995775e8e6c0a7874a095dd055fd1ea99 20260414/cpython-3.14.4+20260414-aarch64-apple-darwin-freethreaded-install_only.tar.gz +8b7865e511b17093e090449bf71eb52933c17d45ad5257ddeacaffbb2c7239df 20260414/cpython-3.14.4+20260414-aarch64-apple-darwin-install_only.tar.gz +0458cb9885c30df690cdf304a16ec335cbc7344792ef0e8a904614b24a61316d 20260414/cpython-3.14.4+20260414-aarch64-pc-windows-msvc-freethreaded-install_only.tar.gz +82613380d582d806e562d7701496c34c87753ab13c37aa0afe2039003651f389 20260414/cpython-3.14.4+20260414-aarch64-pc-windows-msvc-install_only.tar.gz +6d84fb153ccb5cb650652aadc490d99881a8d9b68cf273d44cb553e8cd087734 20260414/cpython-3.14.4+20260414-aarch64-unknown-linux-gnu-freethreaded-install_only.tar.gz +5c8db1c21023316adad827a46d917bbbd6a85ae4e39bc3a58febda712c2f963d 20260414/cpython-3.14.4+20260414-aarch64-unknown-linux-gnu-install_only.tar.gz +b5e025e340d0faa1772ef234e320401b0aa5cf6c9d16ed63a8c44be7c531bc58 20260414/cpython-3.14.4+20260414-ppc64le-unknown-linux-gnu-freethreaded-install_only.tar.gz +055977a09de092744bbb22db64144e6afef8592eaac5e2bce4cca33f2592281a 20260414/cpython-3.14.4+20260414-ppc64le-unknown-linux-gnu-install_only.tar.gz +4373553133eb4712bc10f720da29e091a23153f587fdb2c38f1fb105e70db53a 20260414/cpython-3.14.4+20260414-riscv64-unknown-linux-gnu-freethreaded-install_only.tar.gz +e959df167c502fb0bbcacc31a997e25c6b0ff6b5e496321b691955aa702d0c09 20260414/cpython-3.14.4+20260414-riscv64-unknown-linux-gnu-install_only.tar.gz +a6797ad05c7d7f74a2cea28bf012f9199f4d6c1ed6d09f7adfeb9b3c538c6258 20260414/cpython-3.14.4+20260414-s390x-unknown-linux-gnu-freethreaded-install_only.tar.gz +35f70ad05b2c4045889ee0c3d93f61b012654c1d91e10e671f0e5b4d4a6c6637 20260414/cpython-3.14.4+20260414-s390x-unknown-linux-gnu-install_only.tar.gz +1c366767d203b722efbd5b3796d16a08436e8a328afd31e551289efba9bf56d1 20260414/cpython-3.14.4+20260414-x86_64-apple-darwin-freethreaded-install_only.tar.gz +9ecb2b942e6698c04af10a63a3d73c0b2e8d8e11ce44933fbffe8651bef4577d 20260414/cpython-3.14.4+20260414-x86_64-apple-darwin-install_only.tar.gz +5ccaecdb899431f393209647182def14b36d7398bd59be4fa73dd79b48b3f290 20260414/cpython-3.14.4+20260414-x86_64-pc-windows-msvc-freethreaded-install_only.tar.gz +9647bb46d3c236e34c1c11bbb7113444d9711811f0d11c39956168807a955b1a 20260414/cpython-3.14.4+20260414-x86_64-pc-windows-msvc-install_only.tar.gz +c1a845a79da56265dc49628bc3b9e20d34f04674fd2d637ee40cbe259d2b1b95 20260414/cpython-3.14.4+20260414-x86_64-unknown-linux-gnu-freethreaded-install_only.tar.gz +e17275eaf95ceb5877aa6816e209b7733f41fee401d39c3921b88fb73fc4a4ba 20260414/cpython-3.14.4+20260414-x86_64-unknown-linux-gnu-install_only.tar.gz +12687a989a2384665577e1ef9864f33d4c074a1e69b38a8bac8d656531aefa3e 20260414/cpython-3.14.4+20260414-x86_64-unknown-linux-musl-install_only.tar.gz +5791a69a73b76b908f5bdf96da1928de8db696ab198f4ced04b77b22fe712ce0 20260414/cpython-3.15.0a8+20260414-aarch64-apple-darwin-freethreaded-install_only.tar.gz +780d46b3da0e58e15c620d9e7dfd29b54c8359c195f625858f85df9c2c7ecc32 20260414/cpython-3.15.0a8+20260414-aarch64-apple-darwin-install_only.tar.gz +95ddfe7dd52185f7e5d55524eafb48e54d1eab0b0cf013966f144a411f3ddd0f 20260414/cpython-3.15.0a8+20260414-aarch64-pc-windows-msvc-freethreaded-install_only.tar.gz +10fb470e900e65df4e37f8deaf1726397c914861ffc37b43ae3743a7eee88377 20260414/cpython-3.15.0a8+20260414-aarch64-pc-windows-msvc-install_only.tar.gz +b72908bce86036a0a1ba98ca9917ea0b99dc1e6c5d715d3d463c4f330880c09b 20260414/cpython-3.15.0a8+20260414-aarch64-unknown-linux-gnu-freethreaded-install_only.tar.gz +8f6dda4d8ff44976f1aa6a94674a09a503dc50b015297e1b62c8cdc591c90f4f 20260414/cpython-3.15.0a8+20260414-aarch64-unknown-linux-gnu-install_only.tar.gz +b3c8210674140a4c5beefa2d4afd752979222638a0fb68de672c60300b4a6642 20260414/cpython-3.15.0a8+20260414-ppc64le-unknown-linux-gnu-freethreaded-install_only.tar.gz +09f076c63fadbf675143674aa3b23229482b9a44840b8b1808a216def2a9af15 20260414/cpython-3.15.0a8+20260414-ppc64le-unknown-linux-gnu-install_only.tar.gz +1a4984207974563c6aea7dc934579d058dbac7436642081113e86011114b9fdf 20260414/cpython-3.15.0a8+20260414-riscv64-unknown-linux-gnu-freethreaded-install_only.tar.gz +9d41ce752e8b731872f0f5c9c48199e63c789d24ce3ae9e91d6c8008f36e7c51 20260414/cpython-3.15.0a8+20260414-riscv64-unknown-linux-gnu-install_only.tar.gz +f525a6244d73450e0c0a7ba125b5934894ab25ee171f7099c239d4eb7ce2f5f2 20260414/cpython-3.15.0a8+20260414-s390x-unknown-linux-gnu-freethreaded-install_only.tar.gz +1de2593c40cce2d8ea883f8c8580223bfa1478cbd9d0191ba3640aed083c2202 20260414/cpython-3.15.0a8+20260414-s390x-unknown-linux-gnu-install_only.tar.gz +3dcee23c21e4a3518947e988e115c1d824f07540f4326d93d4ea2028918e0193 20260414/cpython-3.15.0a8+20260414-x86_64-apple-darwin-freethreaded-install_only.tar.gz +a7744d34148969a2ec010da6f0a46ddeceda7c02e5cdfa2b4e1811487381491a 20260414/cpython-3.15.0a8+20260414-x86_64-apple-darwin-install_only.tar.gz +6e69670347e3a6ac1d0cd89b9506d825bd2f2690cc51ead5dec61aec6857d08d 20260414/cpython-3.15.0a8+20260414-x86_64-pc-windows-msvc-freethreaded-install_only.tar.gz +3ded476f676fdf260d56a5e49aa083d5ffd218fc3390e4480ed42bee1acfb3fb 20260414/cpython-3.15.0a8+20260414-x86_64-pc-windows-msvc-install_only.tar.gz +2d06d97e230b7f74de0fe4f661918a0ee827b08127b9372e0890e167de52a8c6 20260414/cpython-3.15.0a8+20260414-x86_64-unknown-linux-gnu-freethreaded-install_only.tar.gz +c93f4b15287ac48d7e3a475b245cb59cc51079382747e3e6213d6406c158969d 20260414/cpython-3.15.0a8+20260414-x86_64-unknown-linux-gnu-install_only.tar.gz +0568e953f837f09689eb4dd1af0043ba5e2ebae0c6395b8b9f8344a53b1f1da5 20260414/cpython-3.15.0a8+20260414-x86_64-unknown-linux-musl-freethreaded-install_only.tar.gz +9fbd6f243a424d4ae973e72aa0075122a7cfe05ac8f6cfde986e7b00d0dbc0bf 20260414/cpython-3.15.0a8+20260414-x86_64-unknown-linux-musl-install_only.tar.gz +""" diff --git a/python/private/sentinel.bzl b/python/private/sentinel_impl.bzl similarity index 100% rename from python/private/sentinel.bzl rename to python/private/sentinel_impl.bzl diff --git a/python/private/site_init_template.py b/python/private/site_init_template.py index af80b22a24..12be98eb57 100644 --- a/python/private/site_init_template.py +++ b/python/private/site_init_template.py @@ -98,7 +98,15 @@ def _get_windows_path_with_unc_prefix(path): # Related doc: https://docs.microsoft.com/en-us/windows/win32/fileio/maximum-file-path-limitation?tabs=cmd#enable-long-paths-in-windows-10-version-1607-and-later import platform - if platform.win32_ver()[1] >= "10.0.14393": + win32_version = None + # Windows 2022 with Python 3.12.8 gives flakey errors, so try a couple times. + for _ in range(3): + try: + win32_version = platform.win32_ver()[1] + break + except (ValueError, KeyError): + pass + if win32_version and win32_version >= "10.0.14393": return path # import sysconfig only now to maintain python 2.6 compatibility diff --git a/python/private/stage2_bootstrap_template.py b/python/private/stage2_bootstrap_template.py index c4326bc026..f445ad2b6a 100644 --- a/python/private/stage2_bootstrap_template.py +++ b/python/private/stage2_bootstrap_template.py @@ -9,11 +9,16 @@ # and is a special case of #7091. # # Python 3.11 introduced an PYTHONSAFEPATH (-P) option that disables this -# behaviour, which we set in the stage 1 bootstrap. +# behaviour, which we set in the stage 1 bootstrap. Isolated mode (-I) also +# disables it, including on older interpreters without safe_path. # So the prepended entry needs to be removed only if the above option is either # unset or not supported by the interpreter. # NOTE: This can be removed when Python 3.10 and below is no longer supported -if not getattr(sys.flags, "safe_path", False): +if ( + not getattr(sys.flags, "safe_path", False) + and not getattr(sys.flags, "isolated", False) + and sys.path +): del sys.path[0] import contextlib @@ -357,6 +362,7 @@ def _maybe_collect_coverage(enable): print_verbose_coverage("Sources:\n" + "\n".join(unique_dirs)) import coverage + from coverage.exceptions import NoDataError coverage_dir = os.environ["COVERAGE_DIR"] unique_id = uuid.uuid4() @@ -406,17 +412,27 @@ def _maybe_collect_coverage(enable): yield finally: cov.stop() - lcov_path = os.path.join(coverage_dir, "pylcov.dat") + lcov_path = os.path.join(coverage_dir, "pylcov_{}.dat".format(unique_id)) print_verbose_coverage("generating lcov from:", lcov_path) - cov.lcov_report( - outfile=lcov_path, - # Ignore errors because sometimes instrumented files aren't - # readable afterwards. e.g. if they come from /dev/fd or if - # they were transient code-under-test in /tmp - ignore_errors=True, - ) - if os.path.isfile(lcov_path): - unresolve_symlinks(lcov_path) + try: + cov.lcov_report( + outfile=lcov_path, + # Ignore errors because sometimes instrumented files aren't + # readable afterwards. e.g. if they come from /dev/fd or if + # they were transient code-under-test in /tmp + ignore_errors=True, + ) + except NoDataError: + # coverage.py raises NoDataError if no instrumented Python code ran + # (e.g. tests not running Python, or subprocess-only binaries). + # Skip the report to avoid failing otherwise passing tests. + # See https://github.com/bazel-contrib/rules_python/issues/2762. + print_verbose_coverage( + "no coverage data collected; skipping lcov report:", lcov_path + ) + else: + if os.path.isfile(lcov_path): + unresolve_symlinks(lcov_path) finally: try: os.unlink(rcfile_name) @@ -528,8 +544,10 @@ def main(): # means only other generated files are importable (not source files). # # To replicate this behavior, we add main's directory within the runfiles - # when safe path isn't enabled. - if not getattr(sys.flags, "safe_path", False): + # when safe path or isolated mode isn't enabled. + if not getattr(sys.flags, "safe_path", False) and not getattr( + sys.flags, "isolated", False + ): prepend_path_entries = [ os.path.join(runfiles_root, os.path.dirname(main_rel_path)) ] diff --git a/python/private/stamp.bzl b/python/private/stamp_impl.bzl similarity index 96% rename from python/private/stamp.bzl rename to python/private/stamp_impl.bzl index 6bc0cd9d23..77e3da1f72 100644 --- a/python/private/stamp.bzl +++ b/python/private/stamp_impl.bzl @@ -18,6 +18,8 @@ This module can be removed likely after the following PRs ar addressed: - https://github.com/bazelbuild/bazel/issues/11164 """ +load(":visibility.bzl", "NOT_ACTUALLY_PUBLIC") + StampSettingInfo = provider( doc = "Information about the `--stamp` command line flag", fields = { @@ -50,7 +52,7 @@ Stamped binaries are not rebuilt unless their dependencies change. }, ) -def stamp_build_setting(name, visibility = ["//visibility:public"]): +def stamp_build_setting(name, visibility = NOT_ACTUALLY_PUBLIC): native.config_setting( name = "stamp_detect", values = {"stamp": "1"}, diff --git a/python/private/text_util.bzl b/python/private/text_util.bzl index eaccadf970..eedf66009d 100644 --- a/python/private/text_util.bzl +++ b/python/private/text_util.bzl @@ -76,13 +76,14 @@ def _render_select(selects, *, no_match_error = None, key_repr = repr, value_rep return "{}({})".format(name, args) -def _render_list(items, *, hanging_indent = ""): +def _render_list(items, *, hanging_indent = "", value_repr = repr): """Convert a list to formatted text. Args: items: list of items. hanging_indent: str, indent to apply to second and following lines of the formatted text. + value_repr: callable, function to represent each item. Returns: The list pretty formatted as a string. @@ -91,12 +92,12 @@ def _render_list(items, *, hanging_indent = ""): return "[]" if len(items) == 1: - return "[{}]".format(repr(items[0])) + return "[{}]".format(value_repr(items[0])) text = "\n".join([ "[", _indent("\n".join([ - "{},".format(repr(item)) + "{},".format(value_repr(item)) for item in items ])), "]", @@ -106,6 +107,18 @@ def _render_list(items, *, hanging_indent = ""): return text def _render_str(value): + """Render a string value. + + If value is None, it is automatically rendered as the Starlark literal `None`. + + Args: + value: str or None. + + Returns: + The value represented as Starlark source text. + """ + if value == None: + return "None" return repr(value) def _render_string_list_dict(value): @@ -180,6 +193,11 @@ def _render_dict_dict(d): lines.append("}") return "\n".join(lines) +def _render_struct(value): + """Render a struct value.""" + fields = {k: repr(getattr(value, k)) for k in sorted(dir(value)) if k not in ["to_json", "to_proto"]} + return _render_call("struct", **fields) + render = struct( alias = _render_alias, dict = _render_dict, @@ -192,6 +210,7 @@ render = struct( list = _render_list, select = _render_select, str = _render_str, + struct = _render_struct, toolchain_prefix = _toolchain_prefix, tuple = _render_tuple, string_list_dict = _render_string_list_dict, diff --git a/python/private/toolchain_types.bzl b/python/private/toolchain_types.bzl index 5b5cce90ee..a3a52409a6 100644 --- a/python/private/toolchain_types.bzl +++ b/python/private/toolchain_types.bzl @@ -22,3 +22,4 @@ TARGET_TOOLCHAIN_TYPE = Label("//python:toolchain_type") EXEC_TOOLS_TOOLCHAIN_TYPE = Label("//python:exec_tools_toolchain_type") PY_CC_TOOLCHAIN_TYPE = Label("//python/cc:toolchain_type") LAUNCHER_MAKER_TOOLCHAIN_TYPE = Label("@bazel_tools//tools/launcher:launcher_maker_toolchain_type") +CC_TOOLCHAIN_TYPE = Label("@bazel_tools//tools/cpp:toolchain_type") diff --git a/python/private/tools/sort_manifest.py b/python/private/tools/sort_manifest.py new file mode 100755 index 0000000000..41bf4181e5 --- /dev/null +++ b/python/private/tools/sort_manifest.py @@ -0,0 +1,123 @@ +#!/usr/bin/env python3 + +"""Sorts python-build-standalone manifest files by filename.""" + +import argparse +import sys +from pathlib import Path + + +def sort_manifest(manifest_path: Path) -> bool: + """Sorts a manifest file in place by filename. Returns True if modified.""" + # Read using pathlib.Path + lines = manifest_path.read_text(encoding="utf-8").splitlines(keepends=True) + + if not lines: + return False + + first_entry_idx = -1 + last_entry_idx = -1 + for idx, line in enumerate(lines): + stripped = line.strip() + if stripped and not stripped.startswith("#"): + if first_entry_idx == -1: + first_entry_idx = idx + last_entry_idx = idx + + if first_entry_idx == -1: + return False + + # Extract top-level comments (comments at the top followed by a blank newline) + top_level_comments = [] + pre_entry_lines = lines[:first_entry_idx] + + last_blank_idx = -1 + for idx, line in enumerate(pre_entry_lines): + if not line.strip(): + last_blank_idx = idx + + if last_blank_idx != -1: + top_level_comments = pre_entry_lines[: last_blank_idx + 1] + remaining_pre = pre_entry_lines[last_blank_idx + 1 :] + else: + remaining_pre = pre_entry_lines + + # Extract bottom-level comments + bottom_level_comments = lines[last_entry_idx + 1 :] + + # Group middle lines into actual catalog entries with their attached comments/blank lines + middle_lines = remaining_pre + lines[first_entry_idx : last_entry_idx + 1] + + entries = [] + current_attached = [] + + for line in middle_lines: + stripped = line.strip() + if stripped and not stripped.startswith("#"): + parts = [p for p in stripped.split(" ") if p] + if len(parts) == 2: + sha256, filename = parts[0], parts[1] + normalized_line = f"{sha256} {filename}\n" + else: + filename = parts[0] if parts else "" + normalized_line = line + + block = current_attached + [normalized_line] + entries.append((filename, block)) + current_attached = [] + else: + current_attached.append(line) + + if current_attached: + bottom_level_comments = current_attached + bottom_level_comments + + # Sort entries lexicographically by filename + entries.sort(key=lambda e: e[0]) + + new_lines = top_level_comments + for _, block in entries: + new_lines.extend(block) + new_lines.extend(bottom_level_comments) + + if new_lines == lines: + return False + + manifest_path.write_text("".join(new_lines), encoding="utf-8") + return True + + +def main(): + parser = argparse.ArgumentParser(description="Sort manifest files by filename.") + parser.add_argument( + "manifests", + nargs="*", + type=Path, + help="Path to manifest files to sort.", + ) + args = parser.parse_args() + + manifests = args.manifests + if not manifests: + repo_root = Path(__file__).resolve().parent.parent.parent.parent + default_manifest = repo_root / "python" / "private" / "runtimes_manifest.txt" + if default_manifest.exists(): + manifests = [default_manifest] + else: + print("No manifests provided.", file=sys.stderr) + sys.exit(1) + + changed = False + for m in manifests: + if m.exists(): + if sort_manifest(m): + print(f"Sorted {m}") + changed = True + else: + print(f"Warning: Manifest not found: {m}", file=sys.stderr) + + if changed: + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/python/private/tools/sync_runtimes_manifest_workspace.py b/python/private/tools/sync_runtimes_manifest_workspace.py new file mode 100755 index 0000000000..450358542a --- /dev/null +++ b/python/private/tools/sync_runtimes_manifest_workspace.py @@ -0,0 +1,80 @@ +#!/usr/bin/env python3 + +"""Synchronizes runtimes_manifest_workspace.bzl with runtimes_manifest.txt.""" + +import argparse +import sys +from pathlib import Path + + +def sync_workspace_manifest(txt_path: Path, bzl_path: Path) -> bool: + with open(txt_path, "r", encoding="utf-8") as f: + txt_content = f.read() + header = '''"""Manifest of runtimes for workspace mode builds. + +This is the workspace equivalent of runtimes_manifest.txt. It's a bzl file +to simplify loading of the data under workspace mode, which doesn't +support parsing a runtimes_manifest.txt file. + +NOTE: This file is automatically generated by sync_runtimes_manifest_workspace.py. +Do not edit directly! +""" + +MANIFEST_TEXT = """ +''' + + new_content = header + txt_content + '"""\n' + + if bzl_path.exists(): + with open(bzl_path, "r", encoding="utf-8") as f: + old_content = f.read() + else: + old_content = "" + + if new_content != old_content: + with open(bzl_path, "w", encoding="utf-8", newline="\n") as f: + f.write(new_content) + return True + + return False + + +def main(): + parser = argparse.ArgumentParser( + description="Sync runtimes workspace bzl file with runtimes manifest text file." + ) + parser.add_argument( + "txt_path", + nargs="?", + type=Path, + help="Path to runtimes_manifest.txt", + ) + parser.add_argument( + "bzl_path", + nargs="?", + type=Path, + help="Path to runtimes_manifest_workspace.bzl", + ) + args = parser.parse_args() + + txt_path = args.txt_path + bzl_path = args.bzl_path + + if not txt_path or not bzl_path: + repo_root = Path(__file__).resolve().parent.parent.parent.parent + txt_path = repo_root / "python" / "private" / "runtimes_manifest.txt" + bzl_path = repo_root / "python" / "private" / "runtimes_manifest_workspace.bzl" + + if not txt_path.exists(): + print(f"Error: Manifest not found: {txt_path}", file=sys.stderr) + sys.exit(1) + + if sync_workspace_manifest(txt_path, bzl_path): + print(f"Updated {bzl_path}") + if not args.bzl_path: + # Exit 1 for pre-commit mode (in-place modification) + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/python/private/transition_labels.bzl b/python/private/transition_labels.bzl index 5f0aa69056..d337044cb5 100644 --- a/python/private/transition_labels.bzl +++ b/python/private/transition_labels.bzl @@ -12,6 +12,7 @@ _BASE_TRANSITION_LABELS = [ labels.BOOTSTRAP_IMPL, labels.DEBUGGER, labels.EXEC_TOOLS_TOOLCHAIN, + "//command_line_option:extra_toolchains", labels.PIP_ENV_MARKER_CONFIG, labels.PIP_WHL_OSX_VERSION, labels.PRECOMPILE, @@ -20,6 +21,7 @@ _BASE_TRANSITION_LABELS = [ labels.PYTHON_VERSION, labels.PY_FREETHREADED, labels.PY_LINUX_LIBC, + labels.VENV, labels.VENVS_SITE_PACKAGES, labels.VENVS_USE_DECLARE_SYMLINK, ] diff --git a/python/private/uncachable_version_file.bzl b/python/private/uncachable_version_file.bzl index 9b1d65a469..0f58df8043 100644 --- a/python/private/uncachable_version_file.bzl +++ b/python/private/uncachable_version_file.bzl @@ -28,12 +28,13 @@ actions depending on this file will always re-run. implementation = _uncachable_version_file_impl, ) -def define_uncachable_version_file(name): +def define_uncachable_version_file(name, visibility = None): native.alias( name = name, actual = select({ ":stamp_detect": ":uncachable_version_file_impl", "//conditions:default": ":sentinel", }), + visibility = visibility, ) uncachable_version_file(name = "uncachable_version_file_impl") diff --git a/python/private/whl_filegroup/BUILD.bazel b/python/private/whl_filegroup/BUILD.bazel index b4246ca080..4d0a096311 100644 --- a/python/private/whl_filegroup/BUILD.bazel +++ b/python/private/whl_filegroup/BUILD.bazel @@ -7,14 +7,14 @@ filegroup( visibility = ["//python/private:__pkg__"], ) -bzl_library( - name = "whl_filegroup_bzl", - srcs = ["whl_filegroup.bzl"], - visibility = ["//:__subpackages__"], -) - py_binary( name = "extract_wheel_files", srcs = ["extract_wheel_files.py"], visibility = ["//visibility:public"], ) + +bzl_library( + name = "whl_filegroup", + srcs = ["whl_filegroup.bzl"], + visibility = ["//python:__subpackages__"], +) diff --git a/python/private/zipapp/BUILD.bazel b/python/private/zipapp/BUILD.bazel index 543fe0a185..66f0b4f667 100644 --- a/python/private/zipapp/BUILD.bazel +++ b/python/private/zipapp/BUILD.bazel @@ -11,25 +11,6 @@ filegroup( srcs = glob(["**"]), ) -bzl_library( - name = "py_zipapp_rule_bzl", - srcs = ["py_zipapp_rule.bzl"], - deps = [ - "//python/private:attributes_bzl", - "//python/private:builders_bzl", - "//python/private:common_bzl", - "//python/private:common_labels_bzl", - "//python/private:py_executable_info_bzl", - "//python/private:py_info_bzl", - "//python/private:py_internal_bzl", - "//python/private:py_interpreter_program_bzl", - "//python/private:py_runtime_info_bzl", - "//python/private:toolchain_types_bzl", - "//python/private:transition_labels_bzl", - "@bazel_skylib//lib:paths", - ], -) - filegroup( name = "zip_main_template", srcs = ["zip_main_template.py"], @@ -47,3 +28,21 @@ filegroup( srcs = ["zipapp_stage2_bootstrap_template.py"], visibility = ["//visibility:public"], ) + +bzl_library( + name = "py_zipapp_rule", + srcs = ["py_zipapp_rule.bzl"], + deps = [ + "//python/private:attributes", + "//python/private:builders", + "//python/private:common", + "//python/private:common_labels", + "//python/private:py_executable_info", + "//python/private:py_internal", + "//python/private:py_runtime_info", + "//python/private:toolchain_types", + "//python/private:transition_labels", + "@bazel_skylib//lib:paths", + "@rules_python_internal//:rules_python_config", + ], +) diff --git a/python/private/zipapp/py_zipapp_rule.bzl b/python/private/zipapp/py_zipapp_rule.bzl index 4d31c99311..b1a9399e90 100644 --- a/python/private/zipapp/py_zipapp_rule.bzl +++ b/python/private/zipapp/py_zipapp_rule.bzl @@ -344,7 +344,7 @@ These values are transitioned on, so will affect the analysis graph and the associated memory overhead. The more unique configurations in your overall build, the more memory and (often unnecessary) re-analysis and re-building can occur. See -https://bazel.build/extending/config#memory-performance-considerations for + for more information about risks and considerations. ::: """, diff --git a/python/py_info.bzl b/python/py_info.bzl index 350a4dbd9e..8e46926c53 100644 --- a/python/py_info.bzl +++ b/python/py_info.bzl @@ -14,6 +14,17 @@ """Public entry point for PyInfo.""" -load("//python/private:py_info.bzl", _PyInfo = "PyInfo") +load( + "//python/private:py_info.bzl", + _PyInfo = "PyInfo", + _VenvSymlinkEntry = "VenvSymlinkEntry", + _VenvSymlinkKind = "VenvSymlinkKind", +) PyInfo = _PyInfo + +# buildifier: disable=name-conventions +VenvSymlinkEntry = _VenvSymlinkEntry + +# buildifier: disable=name-conventions +VenvSymlinkKind = _VenvSymlinkKind diff --git a/python/runfiles/README.md b/python/runfiles/README.md index b5315a48f5..4ed51a3b12 100644 --- a/python/runfiles/README.md +++ b/python/runfiles/README.md @@ -61,7 +61,17 @@ with open(r.Rlocation("my_workspace/path/to/my/data.txt"), "r") as f: Here `my_workspace` is the name you specified via `module(name = "...")` in your `MODULE.bazel` file (with `--enable_bzlmod`, default as of Bazel 7) or `workspace(name = "...")` in `WORKSPACE` (with `--noenable_bzlmod`). -The code above creates a manifest- or directory-based implementation based on the environment variables in `os.environ`. See `Runfiles.Create()` for more info. +The code above creates a manifest- or directory-based implementation based on +the environment variables in `os.environ`. See `Runfiles.Create()` for more +info. + +Alternatively, `Runfiles.CreateOrRaise()` can be used to raise an error +instead of returning `None` if runfiles cannot be found: + +```python +r = Runfiles.CreateOrRaise() +``` + If you want to explicitly create a manifest- or directory-based implementation, you can do so as follows: diff --git a/python/runfiles/runfiles.py b/python/runfiles/runfiles.py index 7236d4b851..1c6dca6088 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 @@ -159,33 +162,31 @@ class Path(pathlib.Path): using the associated `Runfiles` instance when converted to a string. """ - # Mypy isn't smart enough to realize `self` in the methods + # Static type checkers may not 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 [missing-attribute] for pyrefly. + obj = cast("Path", super()._make_child(args)) # 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( + 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 # 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) # 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 @@ -614,7 +619,7 @@ def CurrentRepository(self, frame: int = 1) -> str: More information about the difference between canonical repository names and the `@repo` part of labels is available at: - https://bazel.build/build/bzlmod#repository-names + NOTE: This function inspects the callstack to determine where in the runfiles the caller is located to determine which repository it came @@ -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: @@ -728,6 +733,46 @@ def Create(env: Optional[Dict[str, str]] = None) -> Optional["Runfiles"]: return None + # TODO: Update return type to Self when 3.11 is the min version + # https://peps.python.org/pep-0673/ + @staticmethod + def CreateOrRaise(env: dict[str, str] | None = None) -> Runfiles: + """Returns a new `Runfiles` instance, or raises an error. + + The returned object is either: + - manifest-based, meaning it looks up runfile paths from a manifest + file, or + - directory-based, meaning it looks up runfile paths under a given + directory path + + If `env` contains "RUNFILES_MANIFEST_FILE" with non-empty value, this + method returns a manifest-based implementation. The object eagerly + reads and caches the whole manifest file upon instantiation; this may + be relevant for performance consideration. + + Otherwise, if `env` contains "RUNFILES_DIR" with non-empty value + (checked in this priority order), this method returns a directory-based + implementation. + + If neither cases apply, this method raises a `RuntimeError`. + + Args: + env: {string: string}; optional; the map of environment variables. If + None, this function uses the environment variable map of this + process. + Raises: + RuntimeError: if runfiles cannot be found. + + :::{versionadded} VERSION_NEXT_FEATURE + ::: + """ + runfiles = Runfiles.Create(env=env) + if runfiles is None: + raise RuntimeError( + "Cannot create Runfiles: $RUNFILES_MANIFEST_FILE and $RUNFILES_DIR are both unset or empty" + ) + return runfiles + # Support legacy imports by defining a private symbol. _Runfiles = Runfiles @@ -741,5 +786,14 @@ 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: dict[str, str] | None = None) -> Runfiles: + """Refer to `Runfiles.CreateOrRaise`. + + :::{versionadded} VERSION_NEXT_FEATURE + ::: + """ + return Runfiles.CreateOrRaise(env) diff --git a/python/uv/BUILD.bazel b/python/uv/BUILD.bazel index 7ce6ce0523..93aefbfbd7 100644 --- a/python/uv/BUILD.bazel +++ b/python/uv/BUILD.bazel @@ -45,33 +45,25 @@ current_toolchain( ) bzl_library( - name = "lock_bzl", + name = "lock", srcs = ["lock.bzl"], - # EXPERIMENTAL: Visibility is restricted to allow for changes. - visibility = ["//:__subpackages__"], - deps = ["//python/uv/private:lock_bzl"], + deps = ["//python/uv/private:lock"], ) bzl_library( - name = "uv_bzl", + name = "uv", srcs = ["uv.bzl"], - # EXPERIMENTAL: Visibility is restricted to allow for changes. - visibility = ["//:__subpackages__"], - deps = ["//python/uv/private:uv_bzl"], + deps = ["//python/uv/private:uv"], ) bzl_library( - name = "uv_toolchain_bzl", + name = "uv_toolchain", srcs = ["uv_toolchain.bzl"], - # EXPERIMENTAL: Visibility is restricted to allow for changes. - visibility = ["//:__subpackages__"], - deps = ["//python/uv/private:uv_toolchain_bzl"], + deps = ["//python/uv/private:uv_toolchain"], ) bzl_library( - name = "uv_toolchain_info_bzl", + name = "uv_toolchain_info", srcs = ["uv_toolchain_info.bzl"], - # EXPERIMENTAL: Visibility is restricted to allow for changes. - visibility = ["//:__subpackages__"], - deps = ["//python/uv/private:uv_toolchain_info_bzl"], + deps = ["//python/uv/private:uv_toolchain_info"], ) diff --git a/python/uv/lock.bzl b/python/uv/lock.bzl index 7bcca780a0..7fd50082ea 100644 --- a/python/uv/lock.bzl +++ b/python/uv/lock.bzl @@ -45,6 +45,28 @@ native_test( ) ``` +### `[tool.uv]` settings support + +When a `pyproject.toml` file is included in {attr}`lock.srcs`, the +`--project` flag is automatically passed to `uv pip compile` using the +directory of the shortest-path `pyproject.toml`. This causes `uv` to +read `[tool.uv]` settings such as `no-build-isolation`, +`exclude-dependencies`, and `[tool.uv.workspace]` from that file. + +If the auto-detection doesn't select the right project (e.g. in complex +workspace layouts), use the `project` parameter to override it: + +```starlark +lock( + name = "requirements", + srcs = [ + "pyproject.toml", + "requirements.in", + ], + project = "subproject", +) +``` + EXPERIMENTAL: This is experimental and may be changed without notice. """ diff --git a/python/uv/private/BUILD.bazel b/python/uv/private/BUILD.bazel index 3e4d6c7baa..ce0ce0ec81 100644 --- a/python/uv/private/BUILD.bazel +++ b/python/uv/private/BUILD.bazel @@ -15,13 +15,8 @@ load("@bazel_skylib//:bzl_library.bzl", "bzl_library") load("//python/private:bzlmod_enabled.bzl", "BZLMOD_ENABLED") # buildifier: disable=bzl-visibility -exports_files( - srcs = [ - "lock_copier.py", - ], - # only because this is used from a macro to template - visibility = ["//visibility:public"], -) +# public only because this is used from a macro to template +_NOT_REALLY_PUBLIC = ["//visibility:public"] filegroup( name = "distribution", @@ -29,80 +24,117 @@ filegroup( visibility = ["//python/uv:__pkg__"], ) +filegroup( + name = "lock_copier_template", + srcs = ["template/lock_copier.py"], + target_compatible_with = [] if BZLMOD_ENABLED else ["@platforms//:incompatible"], + visibility = _NOT_REALLY_PUBLIC, +) + +filegroup( + name = "uv_lock_template", + srcs = select({ + "@platforms//os:windows": ["template/uv_lock.bat"], + "//conditions:default": ["template/uv_lock.sh"], + }), + target_compatible_with = [] if BZLMOD_ENABLED else ["@platforms//:incompatible"], + visibility = _NOT_REALLY_PUBLIC, +) + +filegroup( + name = "uv_pip_compile_template", + srcs = select({ + "@platforms//os:windows": ["template/uv_pip_compile.bat"], + "//conditions:default": ["template/uv_pip_compile.sh"], + }), + target_compatible_with = [] if BZLMOD_ENABLED else ["@platforms//:incompatible"], + visibility = _NOT_REALLY_PUBLIC, +) + bzl_library( - name = "current_toolchain_bzl", + name = "current_toolchain", srcs = ["current_toolchain.bzl"], visibility = ["//python/uv:__subpackages__"], + deps = [":toolchain_types"], ) +# keep bzl_library( - name = "lock_bzl", + name = "lock", srcs = ["lock.bzl"], - visibility = ["//python/uv:__subpackages__"], + visibility = [ + "//python/uv:__subpackages__", + "//tools/private:__subpackages__", + ], deps = [ - ":toolchain_types_bzl", - "//python:py_binary_bzl", - "//python/private:bzlmod_enabled_bzl", - "//python/private:common_labels_bzl", - "//python/private:toolchain_types_bzl", + ":toolchain_types", + "//python:py_binary", + "//python/private:bzlmod_enabled", + "//python/private:common_labels", + "//python/private:toolchain_types", "@bazel_skylib//lib:shell", ], ) bzl_library( - name = "toolchain_types_bzl", - srcs = ["toolchain_types.bzl"], + name = "toolchains_hub", + srcs = ["toolchains_hub.bzl"], visibility = ["//python/uv:__subpackages__"], + deps = [":toolchain_types"], ) bzl_library( - name = "uv_bzl", + name = "uv", srcs = ["uv.bzl"], visibility = ["//python/uv:__subpackages__"], deps = [ - ":toolchain_types_bzl", - ":uv_repository_bzl", - ":uv_toolchains_repo_bzl", - "//python/private:auth_bzl", - "//python/private:common_labels_bzl", + ":toolchain_types", + ":uv_repository", + ":uv_toolchains_repo", + "//python/private:auth", + "//python/private:common_labels", ], ) bzl_library( - name = "uv_repository_bzl", + name = "uv_repository", srcs = ["uv_repository.bzl"], visibility = ["//python/uv:__subpackages__"], - deps = ["//python/private:auth_bzl"], + deps = ["//python/private:auth"], ) bzl_library( - name = "uv_toolchain_bzl", + name = "uv_toolchain", srcs = ["uv_toolchain.bzl"], visibility = ["//python/uv:__subpackages__"], - deps = [":uv_toolchain_info_bzl"], + deps = [":uv_toolchain_info"], ) bzl_library( - name = "uv_toolchain_info_bzl", - srcs = ["uv_toolchain_info.bzl"], + name = "uv_toolchains_repo", + srcs = ["uv_toolchains_repo.bzl"], visibility = ["//python/uv:__subpackages__"], + deps = ["//python/private:text_util"], ) +# keep bzl_library( - name = "uv_toolchains_repo_bzl", - srcs = ["uv_toolchains_repo.bzl"], - visibility = ["//python/uv:__subpackages__"], - deps = [ - "//python/private:text_util_bzl", + name = "uv_lock_to_requirements", + srcs = ["uv_lock_to_requirements.bzl"], + visibility = [ + "//python/private:__subpackages__", + "//python/uv:__subpackages__", ], ) -filegroup( - name = "lock_template", - srcs = select({ - "@platforms//os:windows": ["lock.bat"], - "//conditions:default": ["lock.sh"], - }), - target_compatible_with = [] if BZLMOD_ENABLED else ["@platforms//:incompatible"], - visibility = ["//visibility:public"], +bzl_library( + name = "toolchain_types", + srcs = ["toolchain_types.bzl"], + visibility = ["//python/uv:__subpackages__"], +) + +bzl_library( + name = "uv_toolchain_info", + srcs = ["uv_toolchain_info.bzl"], + visibility = ["//python/uv:__subpackages__"], ) diff --git a/python/uv/private/lock.bat b/python/uv/private/lock.bat deleted file mode 100755 index 3954c10347..0000000000 --- a/python/uv/private/lock.bat +++ /dev/null @@ -1,7 +0,0 @@ -if defined BUILD_WORKSPACE_DIRECTORY ( - set "out=%BUILD_WORKSPACE_DIRECTORY%\{{src_out}}" -) else ( - exit /b 1 -) - -"{{args}}" --output-file "%out%" %* diff --git a/python/uv/private/lock.bzl b/python/uv/private/lock.bzl index b007baf9c1..695591e924 100644 --- a/python/uv/private/lock.bzl +++ b/python/uv/private/lock.bzl @@ -30,6 +30,7 @@ _RunLockInfo = provider( "args": "The args passed to the `uv` by default when running the runnable target.", "env": "The env passed to the execution.", "srcs": "Source files required to run the runnable target.", + "template": "The template file for writing a script.", }, ) @@ -70,9 +71,11 @@ def _args(ctx): add_all = _add_all, ) -def _lock_impl(ctx): - srcs = ctx.files.srcs +def _common_lock(ctx, locker): fname = "{}.out".format(ctx.label.name) + + # TODO @aignas 2026-06-21: do not append python_version for uv.lock as it should work for all + # python versions python_version = ctx.attr.python_version if python_version: fname = "{}.{}.out".format( @@ -85,22 +88,41 @@ def _lock_impl(ctx): uv = toolchain_info.uv_toolchain_info.uv[DefaultInfo].files_to_run.executable args = _args(ctx) + args.add(uv) + + # The output params are: + # * srcs are the srcs for the locking command action inputs tracking + # * output_filename if set is to ensure that we can do special prep for uv.lock versus + # requirements.txt + # * mnemonic is for the action mnemonic + # * progress_message is the same + srcs, output_filename, mnemonic, progress_message = locker(args, output) + args.add_all([ - uv, - "pip", - "compile", "--no-python-downloads", "--no-cache", ]) - pkg = ctx.label.package - update_target = ctx.attr.update_target - args.add("--custom-compile-command", "bazel run //{}:{}".format(pkg, update_target)) - if ctx.attr.generate_hashes: - args.add("--generate-hashes") - if not ctx.attr.strip_extras: - args.add("--no-strip-extras") - args.add_all(ctx.files.build_constraints, before_each = "--build-constraints") - args.add_all(ctx.files.constraints, before_each = "--constraints") + + project = None + if ctx.attr.project: + project = ctx.attr.project + else: + # Autodetect the project based on the `pyproject.toml` location - it will be the first src that + # we see that is named "pyproject.toml" + for src in srcs: + if src.basename == "pyproject.toml": + if project == None: + project = src.dirname + elif len(project) > len(src.dirname): + # select the shortest match + project = src.dirname + + if project == None: + project = ctx.label.package + + if project: + args.add_all([project], before_each = "--project") + args.add_all(ctx.attr.args) exec_tools = ctx.toolchains[EXEC_TOOLS_TOOLCHAIN_TYPE].exec_tools @@ -108,41 +130,100 @@ def _lock_impl(ctx): python = runtime.interpreter or runtime.interpreter_path python_files = runtime.files or depset() args.add("--python", python) - args.add_all(srcs) - - args.run_shell.add("--output-file", output) # These arguments does not change behaviour, but it reduces the output from # the command, which is especially verbose in stderr. - args.run_shell.add("--no-progress") - args.run_shell.add("--quiet") + args.add("--no-progress") + args.add("--quiet") if ctx.files.existing_output: - command = '{python} -c {python_cmd} && "$@"'.format( - python = getattr(python, "path", python), - python_cmd = shell.quote( - "from shutil import copy; copy(\"{src}\", \"{dst}\")".format( - src = ctx.files.existing_output[0].path, - dst = output.path, - ), - ), + src_out = ctx.files.existing_output[0].path + elif output_filename: + # special case - the output filename has to be in the source tree and it has to have a + # special name, we use the project folder to determine this. + + if not project: + fail("Cannot lock this if the project dir is unset or cannot be infered") + + src_out = "{project}/{out_filename}".format( + project = project, + out_filename = output_filename, ) else: - command = '"$@"' + src_out = "" + + is_windows = ctx.attr.is_windows + if is_windows: + path_sep = "\\" + ext = ".bat" + else: + path_sep = "/" + ext = "" - srcs = srcs + ctx.files.build_constraints + ctx.files.constraints + output_path = output.path.replace("/", path_sep) if is_windows else output.path + src_out_path = src_out.replace("/", path_sep) if is_windows else src_out + + # On Windows, all args must be embedded in the .bat script because + # arguments are not passed on the command line. + if is_windows: + args_parts = [] + for i, arg in enumerate(args.run_info): + if hasattr(arg, "path"): + arg = arg.path + + # Only use backslashes for the executable itself (first arg) + # to ensure CMD can run it, but keep forward slashes for arguments + # so that uv writes consistent paths in comments. + if i == 0: + a = arg.replace("/", "\\") + else: + a = arg + a = a.replace('"', '""') + args_parts.append('"' + a + '"') + + # uv pip compile adds --output-file to run_shell (not run_info). + # For the lock case, output_filename is "uv.lock" and uv lock + # writes to the project directory without --output-file. + if not output_filename: + args_parts.append('"--output-file"') + args_parts.append('"' + output_path + '"') + windows_args = " ".join(args_parts) + else: + windows_args = " ".join([]) + + script = ctx.actions.declare_file(ctx.label.name + "_lock" + ext) + ctx.actions.expand_template( + template = ctx.files._template[0], + substitutions = { + '"{{args}}"': windows_args, + "{{out}}": output_path, + "{{src_out}}": src_out_path, + }, + output = script, + is_executable = True, + ) - ctx.actions.run_shell( - command = command, + ctx.actions.run( + executable = script, + mnemonic = mnemonic, inputs = srcs + ctx.files.existing_output, - mnemonic = "PyRequirementsLockUv", outputs = [output], - arguments = [args.run_shell], + # On Windows, the command line is embedded directly in the .bat + # script (with backslash paths). On POSIX, args are forwarded via + # exec "$@" in the .sh script. + arguments = [args.run_shell] if not ctx.attr.is_windows else [], tools = [ uv, python_files, + script, ], - progress_message = "Creating a requirements.txt with uv: %{label}", + + # User reported being unable to add `--action_env` and get it to work. + # Without this flag. + # + # Ref: https://app.slack.com/client/TA4K1KQ87/CA306CEV6 + use_default_shell_env = True, + progress_message = progress_message, env = ctx.attr.env, ) @@ -155,9 +236,36 @@ def _lock_impl(ctx): srcs + [uv], transitive = [python_files], ), + template = ctx.files._template[0], ), ] +def _pip_compile_impl(ctx): + def _setup_args(args, output): + args.add_all(["pip", "compile"]) + pkg = ctx.label.package + update_target = ctx.attr.update_target + args.add("--custom-compile-command", "bazel run //{}:{}".format(pkg, update_target)) + + if ctx.attr.generate_hashes: + args.add("--generate-hashes") + if not ctx.attr.strip_extras: + args.add("--no-strip-extras") + + args.add_all(ctx.files.build_constraints, before_each = "--build-constraints") + args.add_all(ctx.files.constraints, before_each = "--constraints") + + args.run_shell.add("--output-file", output) + mnemonic = "PyRequirementsLockUv" + progress_message = "Creating a requirements.txt with uv: %{label}" + + args.add_all(ctx.files.srcs) + srcs = ctx.files.srcs + ctx.files.build_constraints + ctx.files.constraints + + return srcs, None, mnemonic, progress_message + + return _common_lock(ctx, _setup_args) + def _transition_impl(input_settings, attr): settings = { labels.PYTHON_VERSION: input_settings[labels.PYTHON_VERSION], @@ -172,16 +280,60 @@ _python_version_transition = transition( outputs = [labels.PYTHON_VERSION], ) -_lock = rule( - implementation = _lock_impl, +_common_attrs = { + "args": attr.string_list( + doc = "Public, see the docs in the macro.", + ), + "env": attr.string_dict( + doc = "Public, see the docs in the macro.", + ), + "existing_output": attr.label( + mandatory = False, + allow_single_file = True, + doc = """\ +An already existing output file that is used as a basis for further +modifications and the locking is not done from scratch. +""", + ), + "is_windows": attr.bool(mandatory = True), + "output": attr.string( + doc = "Public, see the docs in the macro.", + mandatory = True, + ), + "project": attr.string( + doc = """\ +Overrides the `--project` directory passed to `uv pip compile`. +If not set, the project directory is auto-detected: when +`pyproject.toml` files are in {obj}`lock.srcs`, the one with the +shortest directory path is selected. This makes `uv` read +`[tool.uv]` settings (e.g. `no-build-isolation`, +`exclude-dependencies`) from that `pyproject.toml`. +""", + ), + "python_version": attr.string( + doc = "Public, see the docs in the macro.", + ), + "srcs": attr.label_list( + mandatory = True, + allow_files = True, + doc = "Public, see the docs in the macro.", + ), + "_allowlist_function_transition": attr.label( + default = "@bazel_tools//tools/allowlists/function_transition_allowlist", + ), +} + +_pip_compile = rule( + implementation = _pip_compile_impl, doc = """\ The lock rule that does the locking in a build action (that makes it possible to use RBE) and also prepares information for a `bazel run` executable rule. + +:::{versionchanged} 2.1.0 +Added the {attr}`project` to configure the project setting if autodetection fails. +::: """, attrs = { - "args": attr.string_list( - doc = "Public, see the docs in the macro.", - ), "build_constraints": attr.label_list( allow_files = True, doc = "Public, see the docs in the macro.", @@ -190,33 +342,10 @@ to use RBE) and also prepares information for a `bazel run` executable rule. allow_files = True, doc = "Public, see the docs in the macro.", ), - "env": attr.string_dict( - doc = "Public, see the docs in the macro.", - ), - "existing_output": attr.label( - mandatory = False, - allow_single_file = True, - doc = """\ -An already existing output file that is used as a basis for further -modifications and the locking is not done from scratch. -""", - ), "generate_hashes": attr.bool( doc = "Public, see the docs in the macro.", default = True, ), - "output": attr.string( - doc = "Public, see the docs in the macro.", - mandatory = True, - ), - "python_version": attr.string( - doc = "Public, see the docs in the macro.", - ), - "srcs": attr.label_list( - mandatory = True, - allow_files = True, - doc = "Public, see the docs in the macro.", - ), "strip_extras": attr.bool( doc = "Public, see the docs in the macro.", default = False, @@ -227,8 +356,46 @@ modifications and the locking is not done from scratch. The string to input for the 'uv pip compile'. """, ), - "_allowlist_function_transition": attr.label( - default = "@bazel_tools//tools/allowlists/function_transition_allowlist", + "_template": attr.label( + default = "//python/uv/private:uv_pip_compile_template", + doc = """\ +The template to be used for 'uv pip compile'. This is either .bat or bash +script depending on what the target platform is executed on. +""", + ), + } | _common_attrs, + toolchains = [ + EXEC_TOOLS_TOOLCHAIN_TYPE, + UV_TOOLCHAIN_TYPE, + ], + cfg = _python_version_transition, +) + +def _lock_impl(ctx): + def _setup_args(args, _output): + args.add("lock") + mnemonic = "PyUvLock" + progress_message = "Creating a uv.lock with uv: %{label}" + + return ctx.files.srcs, "uv.lock", mnemonic, progress_message + + return _common_lock(ctx, _setup_args) + +_lock = rule( + implementation = _lock_impl, + doc = """\ +The lock rule that does the locking in a build action and also prepares information for a `bazel +run` executable rule. + +:::{versionadded} 2.2.0 +::: +""", + attrs = _common_attrs | { + "_template": attr.label( + default = "//python/uv/private:uv_lock_template", + doc = """\ +The template to be used for 'uv lock'. Used when output ends with '.lock'. +""", ), }, toolchains = [ @@ -238,10 +405,10 @@ The string to input for the 'uv pip compile'. cfg = _python_version_transition, ) -def _lock_run_impl(ctx): +def _run_impl(ctx): if ctx.attr.is_windows: path_sep = "\\" - ext = ".exe" + ext = ".bat" else: path_sep = "/" ext = "" @@ -250,12 +417,18 @@ def _lock_run_impl(ctx): if hasattr(arg, "short_path"): arg = arg.short_path - return shell.quote(arg.replace("/", path_sep)) + arg = arg.replace("/", path_sep) + if ctx.attr.is_windows: + # On Windows, CMD uses double quotes for quoting, and internal + # double quotes are escaped by doubling them. + return '"' + arg.replace('"', '""') + '"' + return shell.quote(arg) info = ctx.attr.lock[_RunLockInfo] + executable = ctx.actions.declare_file(ctx.label.name + ext) ctx.actions.expand_template( - template = ctx.files._template[0], + template = info.template, substitutions = { '"{{args}}"': " ".join([_maybe_path(arg) for arg in info.args]), "{{src_out}}": "{}/{}".format(ctx.label.package, ctx.attr.output).replace( @@ -277,8 +450,8 @@ def _lock_run_impl(ctx): ), ] -_lock_run = rule( - implementation = _lock_run_impl, +_run_locker = rule( + implementation = _run_impl, doc = """\ """, attrs = { @@ -291,13 +464,6 @@ _lock_run = rule( "output": attr.string( doc = """\ The output that we would be updated, relative to the package the macro is used in. -""", - ), - "_template": attr.label( - default = "//python/uv/private:lock_template", - doc = """\ -The template to be used for 'uv pip compile'. This is either .ps1 or bash -script depending on what the target platform is executed on. """, ), }, @@ -318,8 +484,10 @@ def _maybe_file(path): path: {type}`str` the file name. """ for p in native.glob([path], allow_empty = True): - if path == p: - return p + if path != p: + continue + + return p return None @@ -332,7 +500,7 @@ def _expand_template_impl(ctx): dst = "{}/{}".format(pkg, ctx.attr.output) if pkg else ctx.attr.output ctx.actions.expand_template( - template = ctx.files._template[0], + template = ctx.files._lock_copier_template[0], substitutions = { "{{dst}}": dst, "{{src}}": "{}".format(ctx.files.src[0].short_path), @@ -348,8 +516,8 @@ _expand_template = rule( "output": attr.string(mandatory = True), "src": attr.label(mandatory = True), "update_target": attr.string(mandatory = True), - "_template": attr.label( - default = "//python/uv/private:lock_copier.py", + "_lock_copier_template": attr.label( + default = "//python/uv/private:lock_copier_template", allow_single_file = True, ), }, @@ -367,6 +535,7 @@ def lock( env = None, generate_hashes = True, python_version = None, + project = None, strip_extras = False, **kwargs): """Pin the requirements based on the src files. @@ -406,13 +575,23 @@ def lock( build_constraints: {type}`list[Label]` The list of build constraints to use. constraints: {type}`list[Label]` The list of constraints files to use. generate_hashes: {type}`bool` Generate hashes for all of the - requirements. This is a must if you want to use - {attr}`pip.parse.experimental_index_url`. Defaults to `True`. + requirements. Only meaningful for `requirements.txt` style output. + Defaults to `True`. strip_extras: {type}`bool` whether to strip extras from the output. Currently `rules_python` requires `--no-strip-extras` to properly function, but sometimes one may want to not have the extras if you are compiling the requirements file for using it as a constraints file. Defaults to `False`. + project: {type}`str | None` overrides the `--project` directory + passed to `uv pip compile`. By default the project directory + is auto-detected: when {obj}`lock.srcs` contains + `pyproject.toml` files, the one with the shortest directory + path is selected. This causes `uv` to read `[tool.uv]` + settings such as `no-build-isolation` and + `exclude-dependencies` from that `pyproject.toml`. If no + `pyproject.toml` is in `srcs` and no `project` is given, the + Bazel package directory is used as fallback. + {versionadded} 2.1.0 python_version: {type}`str | None` the python_version to transition to when locking the requirements. Defaults to the default python version configured by the {obj}`python` module extension. @@ -430,38 +609,52 @@ def lock( if not BZLMOD_ENABLED: kwargs["target_compatible_with"] = ["@platforms//:incompatible"] - _lock( - name = name, - args = args, - build_constraints = build_constraints, - constraints = constraints, - env = env, - existing_output = maybe_out, - generate_hashes = generate_hashes, - python_version = python_version, - srcs = srcs, - strip_extras = strip_extras, - update_target = update_target, - output = out, - tags = [ + uv_kwargs = { + "is_windows": select({ + "@platforms//os:windows": True, + "//conditions:default": False, + }), + "output": out, + } | kwargs + + lock_target_kwargs = { + "args": args, + "env": env, + "existing_output": maybe_out, + "project": project, + "python_version": python_version, + "srcs": srcs, + "tags": [ "no-cache", "requires-network", ] + tags, - **kwargs - ) + } | uv_kwargs + + # NOTE @aignas 2026-06-20: if the user passes these args the command will fail + # with an error message instead of silently ignoring the args + if build_constraints: + lock_target_kwargs["build_constraints"] = build_constraints + if constraints: + lock_target_kwargs["constraints"] = constraints + + if out.endswith(".lock"): + _lock(name = name, **lock_target_kwargs) + else: + _pip_compile( + name = name, + generate_hashes = generate_hashes, + strip_extras = strip_extras, + update_target = update_target, + **lock_target_kwargs + ) # A target for updating the in-tree version directly by skipping the in-action - # uv pip compile. - _lock_run( + # uv pip compile or uv lock, depending what is defined for the locker_target. + _run_locker( name = locker_target, lock = name, - output = out, - is_windows = select({ - "@platforms//os:windows": True, - "//conditions:default": False, - }), tags = tags, - **kwargs + **uv_kwargs ) # FIXME @aignas 2025-03-20: is it possible to extend `py_binary` so that the diff --git a/python/uv/private/lock.sh b/python/uv/private/lock.sh deleted file mode 100755 index ffb19b2bea..0000000000 --- a/python/uv/private/lock.sh +++ /dev/null @@ -1,9 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -if [[ -n "${BUILD_WORKSPACE_DIRECTORY:-}" ]]; then - readonly out="${BUILD_WORKSPACE_DIRECTORY}/{{src_out}}" -else - exit 1 -fi -exec "{{args}}" --output-file "$out" "$@" diff --git a/python/uv/private/lock_copier.py b/python/uv/private/template/lock_copier.py similarity index 95% rename from python/uv/private/lock_copier.py rename to python/uv/private/template/lock_copier.py index bcc64c1661..8756fc4de6 100644 --- a/python/uv/private/lock_copier.py +++ b/python/uv/private/template/lock_copier.py @@ -55,7 +55,7 @@ def main(): "This must be either run as `bazel test` via a `native_test` or similar or via `bazel run`" ) - print(f"cp /{src} /{dst}") + print(f"cp /{src.as_posix()} /{dst}") build_workspace = Path(environ["BUILD_WORKSPACE_DIRECTORY"]) dst_real_path = build_workspace / dst diff --git a/python/uv/private/template/uv_lock.bat b/python/uv/private/template/uv_lock.bat new file mode 100755 index 0000000000..d4f4558c53 --- /dev/null +++ b/python/uv/private/template/uv_lock.bat @@ -0,0 +1,21 @@ +@echo off +if not defined BUILD_WORKSPACE_DIRECTORY goto :not_in_workspace +"{{args}}" %* +exit /b %ERRORLEVEL% + +:not_in_workspace + +if not exist "{{src_out}}" goto :no_src_out +copy /y "{{src_out}}" "{{out}}" +del /f "{{src_out}}" +copy /y "{{out}}" "{{src_out}}" +"{{args}}" %* +set "exit_code=%ERRORLEVEL%" +copy /y "{{src_out}}" "{{out}}" +exit /b %exit_code% + +:no_src_out +"{{args}}" %* +set "exit_code=%ERRORLEVEL%" +copy /y "{{src_out}}" "{{out}}" +exit /b %exit_code% \ No newline at end of file diff --git a/python/uv/private/template/uv_lock.sh b/python/uv/private/template/uv_lock.sh new file mode 100755 index 0000000000..a6dd6d3c39 --- /dev/null +++ b/python/uv/private/template/uv_lock.sh @@ -0,0 +1,32 @@ +#!/usr/bin/env bash +set -euo pipefail + +if [[ -n "${BUILD_WORKSPACE_DIRECTORY:-}" ]]; then + exec "{{args}}" "$@" +fi + +# Build action mode +# +# If the uv.lock exists, remove because the existing uv.lock file is read-only, then symlink so +# that we can reuse the existing contents and not do a full relock all the time. If +# nothing exists, just symlink. +# +# On Windows we do it with file copies: +# 1. If the file exists: +# 1. Copy the current file to out. +# 2. Rm the existing file +# 3. Copy the contents back +# 4. Run uv +# 5. Copy the contents to out. +# 1. If the current uv.lock does not exist yet +# 1. Run uv +# 2. Copy the contents to out. +readonly out="{{out}}" +if [[ -f "{{src_out}}" ]]; then + cp "{{src_out}}" "$out" + rm "{{src_out}}" + ln -s "$(pwd)"/"$out" "{{src_out}}" +else + ln -s "$(pwd)"/"$out" "{{src_out}}" +fi +exec "$@" diff --git a/python/uv/private/template/uv_pip_compile.bat b/python/uv/private/template/uv_pip_compile.bat new file mode 100755 index 0000000000..2ea1b5a44a --- /dev/null +++ b/python/uv/private/template/uv_pip_compile.bat @@ -0,0 +1,8 @@ +@echo off +if not defined BUILD_WORKSPACE_DIRECTORY goto :else +set "out=%BUILD_WORKSPACE_DIRECTORY%\{{src_out}}" +"{{args}}" --output-file "%out%" %* +exit /b %ERRORLEVEL% + +:else +"{{args}}" %* diff --git a/python/uv/private/template/uv_pip_compile.sh b/python/uv/private/template/uv_pip_compile.sh new file mode 100755 index 0000000000..ba379ed0f9 --- /dev/null +++ b/python/uv/private/template/uv_pip_compile.sh @@ -0,0 +1,15 @@ +#!/usr/bin/env bash +set -euo pipefail + +if [[ -n "${BUILD_WORKSPACE_DIRECTORY:-}" ]]; then + readonly out="${BUILD_WORKSPACE_DIRECTORY}/{{src_out}}" + exec "{{args}}" --output-file "$out" "$@" +fi + +# Build action mode: seed the output with the source file, then run +# the full command (which includes --output-file from the action args). +readonly out="{{out}}" +if [[ -f "{{src_out}}" ]]; then + cp "{{src_out}}" "$out" +fi +exec "$@" diff --git a/python/uv/private/uv_lock_to_requirements.bzl b/python/uv/private/uv_lock_to_requirements.bzl new file mode 100644 index 0000000000..73d912bbfd --- /dev/null +++ b/python/uv/private/uv_lock_to_requirements.bzl @@ -0,0 +1,139 @@ +"""Convert a parsed uv.lock to requirements.txt format.""" + +def uv_lock_extras_map(uv_lock): + """Compute extras for each package from uv.lock data. + + Args: + uv_lock: a decoded JSON struct from a uv.lock file. + + Returns: + A dict of {package_name: [extra1, extra2, ...]} for packages with extras. + """ + extras_map = {} + for pkg in uv_lock.get("package", []): + pkg_name = pkg.get("name", "") + + for extra in pkg.get("provides-extras", pkg.get("extras", [])): + _add_extras(extras_map, pkg_name, [extra]) + + opt_deps = pkg.get("optional-dependencies", {}) + if opt_deps: + _add_extras(extras_map, pkg_name, _sorted(opt_deps.keys())) + + deps = pkg.get("dependencies", []) + for dep in deps: + dep_name = dep.get("name", "") + dep_extras_raw = dep.get("extra", []) + dep_extras = [dep_extras_raw] if type(dep_extras_raw) == "string" else dep_extras_raw + if dep_extras and dep_name != pkg_name: + _add_extras(extras_map, dep_name, dep_extras) + + metadata = pkg.get("metadata", {}) + for rd in metadata.get("requires-dist", []): + rd_name = rd.get("name", "") + rd_extras_raw = rd.get("extras", []) + rd_extras = [rd_extras_raw] if type(rd_extras_raw) == "string" else rd_extras_raw + if rd_extras and rd_name != pkg_name: + _add_extras(extras_map, rd_name, rd_extras) + + return extras_map + +def uv_lock_to_requirements(uv_lock): + """Convert a parsed uv.lock JSON struct to a requirements.txt formatted string. + + Args: + uv_lock: a decoded JSON struct from a uv.lock file. + + Returns: + A requirements.txt formatted string. + """ + packages = uv_lock.get("package", []) + extras_map = uv_lock_extras_map(uv_lock) + + dependents = {} + for pkg in packages: + pkg_name = pkg.get("name", "") + deps = pkg.get("dependencies", []) + for dep in deps: + dep_name = dep.get("name", "") + if dep_name != pkg_name: + _add_dependent(dependents, dep_name, pkg_name) + opt_deps = pkg.get("optional-dependencies", {}) + for _extra, deps in opt_deps.items(): + for dep in deps: + dep_name = dep.get("name", "") + if dep_name != pkg_name: + _add_dependent(dependents, dep_name, pkg_name) + + lines = [] + for pkg in packages: + source = pkg.get("source", {}) + if not source.get("registry"): + continue + + pkg_name = pkg.get("name", "") + version = pkg.get("version", "") + + markers = pkg.get("resolution-markers", []) + hashes = _collect_hashes(pkg) + + pkg_extras = extras_map.get(pkg_name, []) + if pkg_extras: + req = "{}[{}]=={}".format(pkg_name, ",".join(pkg_extras), version) + else: + req = "{}=={}".format(pkg_name, version) + if markers: + req += " ; " + " or ".join(markers) + + if hashes: + req += " \\" + lines.append(req) + + _emit_hashes(lines, hashes) + + dep_vias = _sorted(dependents.get(pkg_name, [])) + if dep_vias: + if len(dep_vias) == 1: + lines.append(" # via " + dep_vias[0]) + else: + lines.append(" # via") + for via in dep_vias: + lines.append(" # " + via) + + lines.append("") + + return "\n".join(lines) + +def _add_dependent(dependents, dep_name, dependent_name): + if dep_name not in dependents: + dependents[dep_name] = [] + if dependent_name not in dependents[dep_name]: + dependents[dep_name].append(dependent_name) + +def _add_extras(extras_map, pkg_name, extras): + existing = extras_map.get(pkg_name, []) + for extra in extras: + if extra not in existing: + existing.append(extra) + extras_map[pkg_name] = _sorted(existing) + +def _collect_hashes(pkg): + hashes = [] + for wheel in pkg.get("wheels", []): + whash = wheel.get("hash", "") + if whash.startswith("sha256:"): + hashes.append(whash[len("sha256:"):]) + sdist = pkg.get("sdist") + if sdist: + shash = sdist.get("hash", "") + if shash.startswith("sha256:"): + hashes.append(shash[len("sha256:"):]) + return _sorted(hashes) + +def _sorted(items): + return sorted(items) + +def _emit_hashes(lines, hashes): + for i, h in enumerate(hashes): + suffix = " \\" if i < len(hashes) - 1 else "" + lines.append(" --hash=sha256:{}{}".format(h, suffix)) diff --git a/python/versions.bzl b/python/versions.bzl index ed819e6b34..6e3efb34d6 100644 --- a/python/versions.bzl +++ b/python/versions.bzl @@ -15,8 +15,12 @@ """The Python versions we use for the toolchains. """ +load("//python/private:pbs_manifest.bzl", "parse_runtime_manifest") load("//python/private:platform_info.bzl", "platform_info") +##load("@rules_python_internal//:manifest_tool_versions.bzl", "MANIFEST_ENTRIES") +load("//python/private:runtimes_manifest_workspace.bzl", "MANIFEST_TEXT") + # Values present in the @platforms//os package MACOS_NAME = "osx" LINUX_NAME = "linux" @@ -26,1239 +30,16 @@ FREETHREADED = "-freethreaded" MUSL = "-musl" INSTALL_ONLY = "install_only" -DEFAULT_RELEASE_BASE_URL = "https://github.com/astral-sh/python-build-standalone/releases/download" - _GITHUB_PREFIX = "https://github.com/astral-sh/python-build-standalone/releases/download" _LEGACY_GITHUB_PREFIX = "https://github.com/indygreg/python-build-standalone/releases/download" _ASTRAL_PREFIX = "https://releases.astral.sh/github/python-build-standalone/releases/download" -# When updating the versions and releases, run the following command to get -# the hashes: -# bazel run //python/private:print_toolchains_checksums --//python/config_settings:python_version={major}.{minor}.{patch} -# -# To print hashes for all of the specified versions, run: -# bazel run //python/private:print_toolchains_checksums --//python/config_settings:python_version="" -# -# Note, to users looking at how to specify their tool versions, coverage_tool version for each -# interpreter can be specified by: -# "3.8.10": { -# "url": "20210506/cpython-{python_version}-{platform}-pgo+lto-20210506T0943.tar.zst", -# "sha256": { -# "x86_64-apple-darwin": "8d06bec08db8cdd0f64f4f05ee892cf2fcbc58cfb1dd69da2caab78fac420238", -# "x86_64-unknown-linux-gnu": "aec8c4c53373b90be7e2131093caa26063be6d9d826f599c935c0e1042af3355", -# }, -# "coverage_tool": { -# "x86_64-apple-darwin": """, -# "x86_64-unknown-linux-gnu": """, -# }, -# "strip_prefix": "python", -# }, -# -# It is possible to provide lists in "url". It is also possible to provide patches or patch_strip. -# -# buildifier: disable=unsorted-dict-items -TOOL_VERSIONS = { - "3.9.25": { - "url": "20251031/cpython-{python_version}+20251031-{platform}-{build}.tar.gz", - "sha256": { - "aarch64-apple-darwin": "87275619c2706affa4d1090d2ca3dad354b6d69f8b85dbfafe38785870751b9a", - "aarch64-unknown-linux-gnu": "6112d46355857680b81849764a6cf9f38cc4cd0d1cf29d432bc12fe5aeedf9d0", - "ppc64le-unknown-linux-gnu": "828364b6f54fa45ac2dc91f8e45d5b74306372af374a9ef16eeb2ea81253ed3f", - "riscv64-unknown-linux-gnu": "17467e0158e5ad04453c447d6773c23b044172276441e22e23058fd3ea053e27", - "s390x-unknown-linux-gnu": "3e9539f83e67faa813fd06171199b2d33c89821dfa9a33bf6e27ad67f1b6932d", - "x86_64-apple-darwin": "ace63cfe27a9487c4d72e1cb518be01c1d985271da0b2158e813801f7d3e5503", - "x86_64-pc-windows-msvc": "4fb1b416482ce94d73cfa140317a670c596c830671d137b07c26afe8c461768a", - "x86_64-unknown-linux-gnu": "42834f61eb6df43432c3dd6ab9ca3fdf8c06d10a404ebdb53d6902e6b9570b08", - "x86_64-unknown-linux-musl": "76593e8c889e81e82db5fe117fe15b69466f85100ab2ec0e4035aa86242b4e93", - }, - "strip_prefix": "python", - }, - "3.10.2": { - "url": "20220227/cpython-{python_version}+20220227-{platform}-{build}.tar.gz", - "sha256": { - "aarch64-apple-darwin": "1409acd9a506e2d1d3b65c1488db4e40d8f19d09a7df099667c87a506f71c0ef", - "aarch64-unknown-linux-gnu": "8f351a8cc348bb45c0f95b8634c8345ec6e749e483384188ad865b7428342703", - "x86_64-apple-darwin": "8146ad4390710ec69b316a5649912df0247d35f4a42e2aa9615bffd87b3e235a", - "x86_64-pc-windows-msvc": "a1d9a594cd3103baa24937ad9150c1a389544b4350e859200b3e5c036ac352bd", - "x86_64-unknown-linux-gnu": "9b64eca2a94f7aff9409ad70bdaa7fbbf8148692662e764401883957943620dd", - }, - "strip_prefix": "python", - }, - "3.10.4": { - "url": "20220502/cpython-{python_version}+20220502-{platform}-{build}.tar.gz", - "sha256": { - "aarch64-apple-darwin": "2c99983d1e83e4b6e7411ed9334019f193fba626344a50c36fba6c25d4de78a2", - "aarch64-unknown-linux-gnu": "d8098c0c54546637e7516f93b13403b11f9db285def8d7abd825c31407a13d7e", - "x86_64-apple-darwin": "f2711eaffff3477826a401d09a013c6802f11c04c63ab3686aa72664f1216a05", - "x86_64-pc-windows-msvc": "bee24a3a5c83325215521d261d73a5207ab7060ef3481f76f69b4366744eb81d", - "x86_64-unknown-linux-gnu": "f6f871e53a7b1469c13f9bd7920ad98c4589e549acad8e5a1e14760fff3dd5c9", - }, - "strip_prefix": "python", - }, - "3.10.6": { - "url": "20220802/cpython-{python_version}+20220802-{platform}-{build}.tar.gz", - "sha256": { - "aarch64-apple-darwin": "efaf66acdb9a4eb33d57702607d2e667b1a319d58c167a43c96896b97419b8b7", - "aarch64-unknown-linux-gnu": "81625f5c97f61e2e3d7e9f62c484b1aa5311f21bd6545451714b949a29da5435", - "x86_64-apple-darwin": "7718411adf3ea1480f3f018a643eb0550282aefe39e5ecb3f363a4a566a9398c", - "x86_64-pc-windows-msvc": "91889a7dbdceea585ff4d3b7856a6bb8f8a4eca83a0ff52a73542c2e67220eaa", - "x86_64-unknown-linux-gnu": "55aa2190d28dcfdf414d96dc5dcea9fe048fadcd583dc3981fec020869826111", - }, - "strip_prefix": "python", - }, - "3.10.8": { - "url": "20221106/cpython-{python_version}+20221106-{platform}-{build}.tar.gz", - "sha256": { - "aarch64-apple-darwin": "d52b03817bd245d28e0a8b2f715716cd0fcd112820ccff745636932c76afa20a", - "aarch64-unknown-linux-gnu": "33170bef18c811906b738be530f934640491b065bf16c4d276c6515321918132", - "x86_64-apple-darwin": "525b79c7ce5de90ab66bd07b0ac1008bafa147ddc8a41bef15ffb7c9c1e9e7c5", - "x86_64-pc-windows-msvc": "f2b6d2f77118f06dd2ca04dae1175e44aaa5077a5ed8ddc63333c15347182bfe", - "x86_64-unknown-linux-gnu": "6c8db44ae0e18e320320bbaaafd2d69cde8bfea171ae2d651b7993d1396260b7", - }, - "strip_prefix": "python", - }, - "3.10.9": { - "url": "20230116/cpython-{python_version}+20230116-{platform}-{build}.tar.gz", - "sha256": { - "aarch64-apple-darwin": "018d05a779b2de7a476f3b3ff2d10f503d69d14efcedd0774e6dab8c22ef84ff", - "aarch64-unknown-linux-gnu": "2003750f40cd09d4bf7a850342613992f8d9454f03b3c067989911fb37e7a4d1", - "x86_64-apple-darwin": "0e685f98dce0e5bc8da93c7081f4e6c10219792e223e4b5886730fd73a7ba4c6", - "x86_64-pc-windows-msvc": "59c6970cecb357dc1d8554bd0540eb81ee7f6d16a07acf3d14ed294ece02c035", - "x86_64-unknown-linux-gnu": "d196347aeb701a53fe2bb2b095abec38d27d0fa0443f8a1c2023a1bed6e18cdf", - }, - "strip_prefix": "python", - }, - "3.10.11": { - "url": "20230507/cpython-{python_version}+20230507-{platform}-{build}.tar.gz", - "sha256": { - "aarch64-apple-darwin": "8348bc3c2311f94ec63751fb71bd0108174be1c4def002773cf519ee1506f96f", - "aarch64-unknown-linux-gnu": "c7573fdb00239f86b22ea0e8e926ca881d24fde5e5890851339911d76110bc35", - "ppc64le-unknown-linux-gnu": "73a9d4c89ed51be39dd2de4e235078281087283e9fdedef65bec02f503e906ee", - "x86_64-apple-darwin": "bd3fc6e4da6f4033ebf19d66704e73b0804c22641ddae10bbe347c48f82374ad", - "x86_64-pc-windows-msvc": "9c2d3604a06fcd422289df73015cd00e7271d90de28d2c910f0e2309a7f73a68", - "x86_64-unknown-linux-gnu": "c5bcaac91bc80bfc29cf510669ecad12d506035ecb3ad85ef213416d54aecd79", - }, - "strip_prefix": "python", - }, - "3.10.12": { - "url": "20230726/cpython-{python_version}+20230726-{platform}-{build}.tar.gz", - "sha256": { - "aarch64-apple-darwin": "bc66c706ea8c5fc891635fda8f9da971a1a901d41342f6798c20ad0b2a25d1d6", - "aarch64-unknown-linux-gnu": "fee80e221663eca5174bd794cb5047e40d3910dbeadcdf1f09d405a4c1c15fe4", - "ppc64le-unknown-linux-gnu": "bb5e8cb0d2e44241725fa9b342238245503e7849917660006b0246a9c97b1d6c", - "s390x-unknown-linux-gnu": "8d33d435ae6fb93ded7fc26798cc0a1a4f546a4e527012a1e2909cc314b332df", - "x86_64-apple-darwin": "8a6e3ed973a671de468d9c691ed9cb2c3a4858c5defffcf0b08969fba9c1dd04", - "x86_64-pc-windows-msvc": "c1a31c353ca44de7d1b1a3b6c55a823e9c1eed0423d4f9f66e617bdb1b608685", - "x86_64-unknown-linux-gnu": "a476dbca9184df9fc69fe6309cda5ebaf031d27ca9e529852437c94ec1bc43d3", - }, - "strip_prefix": "python", - }, - "3.10.13": { - "url": "20240224/cpython-{python_version}+20240224-{platform}-{build}.tar.gz", - "sha256": { - "aarch64-apple-darwin": "5fdc0f6a5b5a90fd3c528e8b1da8e3aac931ea8690126c2fdb4254c84a3ff04a", - "aarch64-unknown-linux-gnu": "a898a88705611b372297bb8fe4d23cc16b8603ce5f24494c3a8cfa65d83787f9", - "ppc64le-unknown-linux-gnu": "c23706e138a0351fc1e9def2974af7b8206bac7ecbbb98a78f5aa9e7535fee42", - "s390x-unknown-linux-gnu": "09be8fb2cdfbb4a93d555f268f244dbe4d8ff1854b2658e8043aa4ec08aede3e", - "x86_64-apple-darwin": "6378dfd22f58bb553ddb02be28304d739cd730c1f95c15c74955c923a1bc3d6a", - "x86_64-pc-windows-msvc": "086f7fe9156b897bb401273db8359017104168ac36f60f3af4e31ac7acd6634e", - "x86_64-unknown-linux-gnu": "d995d032ca702afd2fc3a689c1f84a6c64972ecd82bba76a61d525f08eb0e195", - }, - "strip_prefix": "python", - }, - "3.10.14": { - "url": "20240726/cpython-{python_version}+20240726-{platform}-{build}.tar.gz", - "sha256": { - "aarch64-apple-darwin": "164d89f0df2feb689981864ecc1dffb19e6aa3696c8880166de555494fe92607", - "aarch64-unknown-linux-gnu": "39bcd46b4d70e40da177c55259be16d5c2be7a3f7f93f1e3bde47e71b4833f29", - "ppc64le-unknown-linux-gnu": "549d38b9ef59cba9ab2990025255231bfa1cb32b4bc5eac321667640fdee19d1", - "s390x-unknown-linux-gnu": "de4bc878a8666c734f983db971610980870148f333bda8b0c34abfaeae88d7ec", - "x86_64-apple-darwin": "1a1455838cd1e8ed0da14a152a2d559a2fd3a6047ba7013e841db4a35a228c1d", - "x86_64-pc-windows-msvc": "7f68821a8b5445267eca480660364ebd06ec84632b336770c6e39de07ac0f6c3", - "x86_64-unknown-linux-gnu": "32b34cd13d9d745b3db3f3b8398ab2c07de74544829915dbebd8dce39bdc405e", - }, - "strip_prefix": "python", - }, - "3.10.15": { - "url": "20241016/cpython-{python_version}+20241016-{platform}-{build}.tar.gz", - "sha256": { - "aarch64-apple-darwin": "f64776f455a44c24d50f947c813738cfb7b9ac43732c44891bc831fa7940a33c", - "aarch64-unknown-linux-gnu": "eb58581f85fde83d1f3e8e1f8c6f5a15c7ae4fdbe3b1d1083931f9167fdd8dbc", - "ppc64le-unknown-linux-gnu": "0c45af4e7525e2db59901606db32b2896ac1e9830c6f95551402207f537c2ce4", - "s390x-unknown-linux-gnu": "de205896b070e6f5259ac0f2b3379eead875ea84e6a6ef533b89886fcbb46a4c", - "x86_64-apple-darwin": "90b46dfb1abd98d45663c7a2a8c45d3047a59391d8586d71b459cec7b75f662b", - "x86_64-pc-windows-msvc": "e48952619796c66ec9719867b87be97edca791c2ef7fbf87d42c417c3331609e", - "x86_64-unknown-linux-gnu": "3db2171e03c1a7acdc599fba583c1b92306d3788b375c9323077367af1e9d9de", - "x86_64-unknown-linux-musl": "ed519c47d9620eb916a6f95ec2875396e7b1a9ab993ee40b2f31b837733f318c", - }, - "strip_prefix": "python", - }, - "3.10.16": { - "url": "20250317/cpython-{python_version}+20250317-{platform}-{build}.tar.gz", - "sha256": { - "aarch64-apple-darwin": "e99f8457d9c79592c036489c5cfa78df76e4762d170665e499833e045d82608f", - "aarch64-unknown-linux-gnu": "76d0f04d2444e77200fdc70d1c57480e29cca78cb7420d713bc1c523709c198d", - "ppc64le-unknown-linux-gnu": "39c9b3486de984fe1d72d90278229c70d6b08bcf69cd55796881b2d75077b603", - "riscv64-unknown-linux-gnu": "ebe949ada9293581c17d9bcdaa8f645f67d95f73eac65def760a71ef9dd6600d", - "s390x-unknown-linux-gnu": "9b2fc0b7f1c75b48e799b6fa14f7e24f5c61f2db82e3c65d13ed25e08f7f0857", - "x86_64-apple-darwin": "e03e62dbe95afa2f56b7344ff3bd061b180a0b690ff77f9a1d7e6601935e05ca", - "x86_64-pc-windows-msvc": "c7e0eb0ff5b36758b7a8cacd42eb223c056b9c4d36eded9bf5b9fe0c0b9aeb08", - "x86_64-unknown-linux-gnu": "b350c7e63956ca8edb856b91316328e0fd003a840cbd63d08253af43b2c63643", - "x86_64-unknown-linux-musl": "6ed64923ee4fbea4c5780f1a5a66651d239191ac10bd23420db4f5e4e0bf79c4", - }, - "strip_prefix": "python", - }, - "3.10.18": { - "url": "20250808/cpython-{python_version}+20250808-{platform}-{build}.tar.gz", - "sha256": { - "aarch64-apple-darwin": "a94c02b2d597cd6b075a713fe4e9a909cc97ca6a3b2b2ce86eda21be2062d48e", - "aarch64-unknown-linux-gnu": "ef7de3b715d519e246d98ff7856247f7f7b357068705f09c6f300b7e7b76c701", - "ppc64le-unknown-linux-gnu": "f580efed11cc54e1a221c052e8bc88bfbc12844d3ca8949da828351a1232386e", - "riscv64-unknown-linux-gnu": "0d7e460e30203a9225b6f417ae972f66415a1cc0e32b37ebc48d195816282669", - "s390x-unknown-linux-gnu": "d4ada974daadb08a0184c19232ee3b03b3137aa70609760e1a94aaf7b12989ef", - "x86_64-apple-darwin": "da96fe2ba841640215788ddb9f151f03629360e37fcb94d4f76e5095b87df0d4", - "x86_64-pc-windows-msvc": "a648f3c9d136985ccfe57a5507e73d9d0839f7fd09eebd7c247857f2feaecb2a", - "x86_64-unknown-linux-gnu": "0b310a73bb9e7a495dbcad5f685e508ca2e7b36ee8f29301a52285730c425789", - "x86_64-unknown-linux-musl": "9cecf6ea2effbe183faebcf7e1160425a4ee17a68e49f2eefe5e1c59c51fa7ee", - }, - "strip_prefix": "python", - }, - "3.10.19": { - "url": "20251031/cpython-{python_version}+20251031-{platform}-{build}.tar.gz", - "sha256": { - "aarch64-apple-darwin": "43bda24c2fc073bc308bf631203b917a72640d59b59fdad4ba14503d84727012", - "aarch64-unknown-linux-gnu": "f77a8a8aa77f3f943126fa9215a25309da4bf20398fc8f4b4eec54b5fc7570ef", - "ppc64le-unknown-linux-gnu": "1c55d160fc4c3b93528cd6aaa2bb4ca6018a99e5a45919d33dc761a43a69f860", - "riscv64-unknown-linux-gnu": "21134d35721cdad4c881f35d0957cc19df9a45d194afb38a099faded3c1cfb4d", - "s390x-unknown-linux-gnu": "df0db070f1eb73ab4e371eea32213ddb3500737ea5560a6f0ffd65c82af64ddc", - "x86_64-apple-darwin": "76c12e633c09c2a790f8a958a55df4495527e0718d1875310c836e757c0c7b55", - "x86_64-pc-windows-msvc": "cfa08a4caf2df1b43551b843c052d6a8814e2ea0c97268b021f0423646c244c3", - "x86_64-unknown-linux-gnu": "fb1caac917d7b6497bb6f5950da5f1e48d05c43a498948dd97f85760c4382d9f", - "x86_64-unknown-linux-musl": "ba85013ed5ac7733fc6840168cc33ed19e9959b363dc80227d54f8fd9c92c0f4", - }, - "strip_prefix": "python", - }, - "3.10.20": { - "url": "20260414/cpython-{python_version}+20260414-{platform}-{build}.{ext}", - "sha256": { - "aarch64-apple-darwin": "f76cc83c7db16cfc8794bf6e44d834152b57d8bab4e04e823cbc59ed23ec22f8", - "aarch64-unknown-linux-gnu": "64932c8e8bbdf9d6b66ee85934f6f8ad1d18218b51a87ea06cefd3b84554a3e4", - "ppc64le-unknown-linux-gnu": "76b48eb26ef274045772186e63431419294c41baf6d5a372b722d4c9e711082e", - "riscv64-unknown-linux-gnu": "76e1ec72717d17493976fc176ec661f02412666d4f19e50908d8e4303c0511d5", - "s390x-unknown-linux-gnu": "2edf241199d11a3ef79a312737c1bcdb86908352c585ca14b667539080630e85", - "x86_64-apple-darwin": "95a2d794b8981723095190fa94b574ceb4272bb49d83b9e418bb90341e304d09", - "x86_64-pc-windows-msvc": "0d828683d30185ab9f1110ad2194ef384cef0533b8e0da7e03ce837548841788", - "x86_64-unknown-linux-gnu": "303047011b2c9f58504a930fc974d84547477cf69a3f2962f25552e2395c13af", - "x86_64-unknown-linux-musl": "84eb198d318f8b1b8bf59eef5d30d742e13afd97c213fa229578f8fdab0c406f", - }, - "strip_prefix": "python", - }, - "3.11.1": { - "url": "20230116/cpython-{python_version}+20230116-{platform}-{build}.tar.gz", - "sha256": { - "aarch64-apple-darwin": "4918cdf1cab742a90f85318f88b8122aeaa2d04705803c7b6e78e81a3dd40f80", - "aarch64-unknown-linux-gnu": "debf15783bdcb5530504f533d33fda75a7b905cec5361ae8f33da5ba6599f8b4", - "x86_64-apple-darwin": "20a4203d069dc9b710f70b09e7da2ce6f473d6b1110f9535fb6f4c469ed54733", - "x86_64-pc-windows-msvc": "edc08979cb0666a597466176511529c049a6f0bba8adf70df441708f766de5bf", - "x86_64-unknown-linux-gnu": "02a551fefab3750effd0e156c25446547c238688a32fabde2995c941c03a6423", - }, - "strip_prefix": "python", - }, - "3.11.3": { - "url": "20230507/cpython-{python_version}+20230507-{platform}-{build}.tar.gz", - "sha256": { - "aarch64-apple-darwin": "09e412506a8d63edbb6901742b54da9aa7faf120b8dbdce56c57b303fc892c86", - "aarch64-unknown-linux-gnu": "8190accbbbbcf7620f1ff6d668e4dd090c639665d11188ce864b62554d40e5ab", - "ppc64le-unknown-linux-gnu": "767d24f3570b35fedb945f5ac66224c8983f2d556ab83c5cfaa5f3666e9c212c", - "x86_64-apple-darwin": "f710b8d60621308149c100d5175fec39274ed0b9c99645484fd93d1716ef4310", - "x86_64-pc-windows-msvc": "24741066da6f35a7ff67bee65ce82eae870d84e1181843e64a7076d1571e95af", - "x86_64-unknown-linux-gnu": "da50b87d1ec42b3cb577dfd22a3655e43a53150f4f98a4bfb40757c9d7839ab5", - }, - "strip_prefix": "python", - }, - "3.11.4": { - "url": "20230726/cpython-{python_version}+20230726-{platform}-{build}.tar.gz", - "sha256": { - "aarch64-apple-darwin": "cb6d2948384a857321f2aa40fa67744cd9676a330f08b6dad7070bda0b6120a4", - "aarch64-unknown-linux-gnu": "2e84fc53f4e90e11963281c5c871f593abcb24fc796a50337fa516be99af02fb", - "ppc64le-unknown-linux-gnu": "df7b92ed9cec96b3bb658fb586be947722ecd8e420fb23cee13d2e90abcfcf25", - "s390x-unknown-linux-gnu": "e477f0749161f9aa7887964f089d9460a539f6b4a8fdab5166f898210e1a87a4", - "x86_64-apple-darwin": "47e1557d93a42585972772e82661047ca5f608293158acb2778dccf120eabb00", - "x86_64-pc-windows-msvc": "878614c03ea38538ae2f758e36c85d2c0eb1eaaca86cd400ff8c76693ee0b3e1", - "x86_64-unknown-linux-gnu": "e26247302bc8e9083a43ce9e8dd94905b40d464745b1603041f7bc9a93c65d05", - }, - "strip_prefix": "python", - }, - "3.11.5": { - "url": "20230826/cpython-{python_version}+20230826-{platform}-{build}.tar.gz", - "sha256": { - "aarch64-apple-darwin": "dab64b3580118ad2073babd7c29fd2053b616479df5c107d31fe2af1f45e948b", - "aarch64-unknown-linux-gnu": "bb5c5d1ea0f199fe2d3f0996fff4b48ca6ddc415a3dbd98f50bff7fce48aac80", - "ppc64le-unknown-linux-gnu": "14121b53e9c8c6d0741f911ae00102a35adbcf5c3cdf732687ef7617b7d7304d", - "s390x-unknown-linux-gnu": "fe459da39874443579d6fe88c68777c6d3e331038e1fb92a0451879fb6beb16d", - "x86_64-apple-darwin": "4a4efa7378c72f1dd8ebcce1afb99b24c01b07023aa6b8fea50eaedb50bf2bfc", - "x86_64-pc-windows-msvc": "00f002263efc8aea896bcfaaf906b1f4dab3e5cd3db53e2b69ab9a10ba220b97", - "x86_64-unknown-linux-gnu": "fbed6f7694b2faae5d7c401a856219c945397f772eea5ca50c6eb825cbc9d1e1", - }, - "strip_prefix": "python", - }, - "3.11.6": { - "url": "20231002/cpython-{python_version}+20231002-{platform}-{build}.tar.gz", - "sha256": { - "aarch64-apple-darwin": "916c35125b5d8323a21526d7a9154ca626453f63d0878e95b9f613a95006c990", - "aarch64-unknown-linux-gnu": "3e26a672df17708c4dc928475a5974c3fb3a34a9b45c65fb4bd1e50504cc84ec", - "ppc64le-unknown-linux-gnu": "7937035f690a624dba4d014ffd20c342e843dd46f89b0b0a1e5726b85deb8eaf", - "s390x-unknown-linux-gnu": "f9f19823dba3209cedc4647b00f46ed0177242917db20fb7fb539970e384531c", - "x86_64-apple-darwin": "178cb1716c2abc25cb56ae915096c1a083e60abeba57af001996e8bc6ce1a371", - "x86_64-pc-windows-msvc": "3933545e6d41462dd6a47e44133ea40995bc6efeed8c2e4cbdf1a699303e95ea", - "x86_64-unknown-linux-gnu": "ee37a7eae6e80148c7e3abc56e48a397c1664f044920463ad0df0fc706eacea8", - }, - "strip_prefix": "python", - }, - "3.11.7": { - "url": "20240107/cpython-{python_version}+20240107-{platform}-{build}.tar.gz", - "sha256": { - "aarch64-apple-darwin": "b042c966920cf8465385ca3522986b12d745151a72c060991088977ca36d3883", - "aarch64-unknown-linux-gnu": "b102eaf865eb715aa98a8a2ef19037b6cc3ae7dfd4a632802650f29de635aa13", - "ppc64le-unknown-linux-gnu": "b44e1b74afe75c7b19143413632c4386708ae229117f8f950c2094e9681d34c7", - "s390x-unknown-linux-gnu": "49520e3ff494708020f306e30b0964f079170be83e956be4504f850557378a22", - "x86_64-apple-darwin": "a0e615eef1fafdc742da0008425a9030b7ea68a4ae4e73ac557ef27b112836d4", - "x86_64-pc-windows-msvc": "67077e6fa918e4f4fd60ba169820b00be7c390c497bf9bc9cab2c255ea8e6f3e", - "x86_64-unknown-linux-gnu": "4a51ce60007a6facf64e5495f4cf322e311ba9f39a8cd3f3e4c026eae488e140", - }, - "strip_prefix": "python", - }, - "3.11.8": { - "url": "20240224/cpython-{python_version}+20240224-{platform}-{build}.tar.gz", - "sha256": { - "aarch64-apple-darwin": "389a51139f5abe071a0d70091ca5df3e7a3dfcfcbe3e0ba6ad85fb4c5638421e", - "aarch64-unknown-linux-gnu": "389b9005fb78dd5a6f68df5ea45ab7b30d9a4b3222af96999e94fd20d4ad0c6a", - "ppc64le-unknown-linux-gnu": "eb2b31f8e50309aae493c6a359c32b723a676f07c641f5e8fe4b6aa4dbb50946", - "s390x-unknown-linux-gnu": "844f64f4c16e24965778281da61d1e0e6cd1358a581df1662da814b1eed096b9", - "x86_64-apple-darwin": "097f467b0c36706bfec13f199a2eaf924e668f70c6e2bd1f1366806962f7e86e", - "x86_64-pc-windows-msvc": "b618f1f047349770ee1ef11d1b05899840abd53884b820fd25c7dfe2ec1664d4", - "x86_64-unknown-linux-gnu": "94e13d0e5ad417035b80580f3e893a72e094b0900d5d64e7e34ab08e95439987", - }, - "strip_prefix": "python", - }, - "3.11.9": { - "url": "20240726/cpython-{python_version}+20240726-{platform}-{build}.tar.gz", - "sha256": { - "aarch64-apple-darwin": "cbdac9462bab9671c8e84650e425d3f43b775752a930a2ef954a0d457d5c00c3", - "aarch64-unknown-linux-gnu": "4d17cf988abe24449d649aad3ef974091ab76807904d41839907061925b4c9e3", - "ppc64le-unknown-linux-gnu": "fc4f3c9ef9bfac2ed0282126ff376e544697ad04a5408d6429d46899d7d3bf21", - "s390x-unknown-linux-gnu": "e69b66e53e926460df044f44846eef3fea642f630e829719e1a4112fc370dc56", - "x86_64-apple-darwin": "dc3174666a30f4c38d04e79a80c3159b4b3aa69597c4676701c8386696811611", - "x86_64-pc-windows-msvc": "f694be48bdfec1dace6d69a19906b6083f4dd7c7c61f1138ba520e433e5598f8", - "x86_64-unknown-linux-gnu": "f6e955dc9ddfcad74e77abe6f439dac48ebca14b101ed7c85a5bf3206ed2c53d", - }, - "strip_prefix": "python", - }, - "3.11.10": { - "url": "20241016/cpython-{python_version}+20241016-{platform}-{build}.tar.gz", - "sha256": { - "aarch64-apple-darwin": "5a69382da99c4620690643517ca1f1f53772331b347e75f536088c42a4cf6620", - "aarch64-unknown-linux-gnu": "803e49259280af0f5466d32829cd9d65a302b0226e424b3f0b261f9daf6aee8f", - "ppc64le-unknown-linux-gnu": "92b666d103902001322f42badbd68da92adc5cebb826af9c1c906c33166e2f34", - "s390x-unknown-linux-gnu": "6d584317651c1ad4a857cb32d1999707e8bb3046fcb2f156d80381814fa19fde", - "x86_64-apple-darwin": "1e23ffe5bc473e1323ab8f51464da62d77399afb423babf67f8e13c82b69c674", - "x86_64-pc-windows-msvc": "647b66ff4552e70aec3bf634dd470891b4a2b291e8e8715b3bdb162f577d4c55", - "x86_64-unknown-linux-gnu": "8b50a442b04724a24c1eebb65a36a0c0e833d35374dbdf9c9470d8a97b164cd9", - "x86_64-unknown-linux-musl": "d36fc77a8dd76155a7530f6235999a693b9e7c48aa11afeb5610a091cae5aa6f", - }, - "strip_prefix": "python", - }, - "3.11.13": { - "url": "20250808/cpython-{python_version}+20250808-{platform}-{build}.tar.gz", - "sha256": { - "aarch64-apple-darwin": "d089bfd2c7b98a0942750a195e70d3172beda76d7747097b8afd87028b6e59b6", - "aarch64-unknown-linux-gnu": "bc57105f8a16acd57b71d926143c7f6ecf61729b40c8b4656f1b98bebd47c710", - "ppc64le-unknown-linux-gnu": "16a0165b0744940702b8fff80b8bf973ac914f78cb6fca28d389583f675e84de", - "riscv64-unknown-linux-gnu": "d8e62306be8f41c46bcd62ca68f91a1467f47adff632a35ff413dc1043ed56e8", - "s390x-unknown-linux-gnu": "4e302a4514a73baefdd9b327062bdafeb4115a799deec91c185f6ab45a857241", - "x86_64-apple-darwin": "d946d618f8bba8308b67e460a30612a71e2ccc309f85f6628aaae24e2b816981", - "x86_64-pc-windows-msvc": "ed963aee33d29ad8abfbb5fe63e42f57a2638a4a11a88e11d8bb66e61f20a6e5", - "aarch64-pc-windows-msvc": "a632857c966237e7fd38b44c47c350f6e30d8ec54dcad6c832865ad670f0f22f", - "x86_64-unknown-linux-gnu": "3ad988c702cbb017fef1208d47dea4138a2e85fd0f7f01ec5e1e335e597131b9", - "x86_64-unknown-linux-musl": "3a5810f0696f844289aa06d5c3a1efeab66eee999c25196b7d1954192a2c2100", - }, - "strip_prefix": "python", - }, - "3.11.14": { - "url": "20251031/cpython-{python_version}+20251031-{platform}-{build}.tar.gz", - "sha256": { - "aarch64-apple-darwin": "6de5572b33c65af1c9b7caf00ec593fb04cffb7e14fa393a98261bb9bc464713", - "aarch64-unknown-linux-gnu": "510edb027527413c4249256194cb8ad2590b52dd93f7123b4cb341aff5d05894", - "ppc64le-unknown-linux-gnu": "4e0bc6a818e0c6a9d7d3ebe1a95591fd84440520577aa837facc96a4b7a80e35", - "riscv64-unknown-linux-gnu": "16519e69297144f81b2421333bc9e0b6466cf3c84749b216b695cfb4c9deb32f", - "s390x-unknown-linux-gnu": "5f9c1b203cdf34c8bff1aef69b63bbf11309bd16ca6e429d8c3651eaa2b3d080", - "x86_64-apple-darwin": "4891cbf34e8652b7bd1054b9502395e4b7e048e2e517c040fbf6c8297cb954d6", - "x86_64-pc-windows-msvc": "5223b83ed9e2aa5e9e17d2ebcf767956e998876339b9cde1980a47e9d4655fb6", - "aarch64-pc-windows-msvc": "38d0d1466561e15965e8d2c20f5e5be649598f55c761ecab553d087fbd217337", - "x86_64-unknown-linux-gnu": "60f0bd473d861cc45d3401d9914e47ccb9fa037f88a91879ed517a62042b8477", - "x86_64-unknown-linux-musl": "25e82d1e85b90a8ab724ee633a1811b1921797f5c25ee69c6595052371b91a87", - }, - "strip_prefix": "python", - }, - "3.11.15": { - "url": "20260414/cpython-{python_version}+20260414-{platform}-{build}.{ext}", - "sha256": { - "aarch64-apple-darwin": "a57ffd435652092d16b30e783f9826c55e9c64b0f0a72cbae0a9f39e663137fb", - "aarch64-unknown-linux-gnu": "77836944ae15b74e0b25bdc68a4703a340f2ccb684effc0f45fbd7910e1a1f39", - "ppc64le-unknown-linux-gnu": "30a2107f000dbe304820627cbe2cc257027c20f3241d96e6c7df796b69ac2062", - "riscv64-unknown-linux-gnu": "373b98fbf2d04099139a2f6be57593714382ed790be7e7419e358830c23ddd0f", - "s390x-unknown-linux-gnu": "7838efa839158c80568de35ac78d438f564f4c32272a2fe7d9e14a9b351d1a62", - "x86_64-apple-darwin": "317055d80e553764feeaef432d833dd8385c14b83465a8b3fa7c2b7819cba681", - "x86_64-pc-windows-msvc": "8e69ecf1d9fc194e029aafa608d483bf24ccaa8f56d456d7009f20462d62ad23", - "aarch64-pc-windows-msvc": "a882abe4876985c9dc3d433420548506fb0cc9bb9d9fe336a2d3aaf28922aa45", - "x86_64-unknown-linux-gnu": "8b14030dd3af9ea7f7c51b4c90feb04afd8a8f45435727e67b875270bd08f3bc", - "x86_64-unknown-linux-musl": "ca92d3a68a39fa330498b09714733f347bead7313ba9d9b7fbed837aa4ba7796", - }, - "strip_prefix": "python", - }, - "3.12.0": { - "url": "20231002/cpython-{python_version}+20231002-{platform}-{build}.tar.gz", - "sha256": { - "aarch64-apple-darwin": "4734a2be2becb813830112c780c9879ac3aff111a0b0cd590e65ec7465774d02", - "aarch64-unknown-linux-gnu": "bccfe67cf5465a3dfb0336f053966e2613a9bc85a6588c2fcf1366ef930c4f88", - "ppc64le-unknown-linux-gnu": "b5dae075467ace32c594c7877fe6ebe0837681f814601d5d90ba4c0dfd87a1f2", - "s390x-unknown-linux-gnu": "5681621349dd85d9726d1b67c84a9686ce78f72e73a6f9e4cc4119911655759e", - "x86_64-apple-darwin": "5a9e88c8aa52b609d556777b52ebde464ae4b4f77e4aac4eb693af57395c9abf", - "x86_64-pc-windows-msvc": "facfaa1fbc8653f95057f3c4a0f8aa833dab0e0b316e24ee8686bc761d4b4f8d", - "x86_64-unknown-linux-gnu": "e51a5293f214053ddb4645b2c9f84542e2ef86870b8655704367bd4b29d39fe9", - }, - "strip_prefix": "python", - }, - "3.12.1": { - "url": "20240107/cpython-{python_version}+20240107-{platform}-{build}.tar.gz", - "sha256": { - "aarch64-apple-darwin": "f93f8375ca6ac0a35d58ff007043cbd3a88d9609113f1cb59cf7c8d215f064af", - "aarch64-unknown-linux-gnu": "236533ef20e665007a111c2f36efb59c87ae195ad7dca223b6dc03fb07064f0b", - "ppc64le-unknown-linux-gnu": "78051f0d1411ee62bc2af5edfccf6e8400ac4ef82887a2affc19a7ace6a05267", - "s390x-unknown-linux-gnu": "60631211c701f8d2c56e5dd7b154e68868128a019b9db1d53a264f56c0d4aee2", - "x86_64-apple-darwin": "eca96158c1568dedd9a0b3425375637a83764d1fa74446438293089a8bfac1f8", - "x86_64-pc-windows-msvc": "fd5a9e0f41959d0341246d3643f2b8794f638adc0cec8dd5e1b6465198eae08a", - "x86_64-unknown-linux-gnu": "74e330b8212ca22fd4d9a2003b9eec14892155566738febc8e5e572f267b9472", - }, - "strip_prefix": "python", - }, - "3.12.2": { - "url": "20240224/cpython-{python_version}+20240224-{platform}-{build}.tar.gz", - "sha256": { - "aarch64-apple-darwin": "01c064c00013b0175c7858b159989819ead53f4746d40580b5b0b35b6e80fba6", - "aarch64-unknown-linux-gnu": "e52550379e7c4ac27a87de832d172658bc04150e4e27d4e858e6d8cbb96fd709", - "ppc64le-unknown-linux-gnu": "74bc02c4bbbd26245c37b29b9e12d0a9c1b7ab93477fed8b651c988b6a9a6251", - "s390x-unknown-linux-gnu": "ecd6b0285e5eef94deb784b588b4b425a15a43ae671bf206556659dc141a9825", - "x86_64-apple-darwin": "a53a6670a202c96fec0b8c55ccc780ea3af5307eb89268d5b41a9775b109c094", - "x86_64-pc-windows-msvc": "1e5655a6ccb1a64a78460e4e3ee21036c70246800f176a6c91043a3fe3654a3b", - "x86_64-unknown-linux-gnu": "57a37b57f8243caa4cdac016176189573ad7620f0b6da5941c5e40660f9468ab", - }, - "strip_prefix": "python", - }, - "3.12.3": { - "url": "20240415/cpython-{python_version}+20240415-{platform}-{build}.tar.gz", - "sha256": { - "aarch64-apple-darwin": "ccc40e5af329ef2af81350db2a88bbd6c17b56676e82d62048c15d548401519e", - "aarch64-unknown-linux-gnu": "ec8126de97945e629cca9aedc80a29c4ae2992c9d69f2655e27ae73906ba187d", - "ppc64le-unknown-linux-gnu": "c5dcf08b8077e617d949bda23027c49712f583120b3ed744f9b143da1d580572", - "s390x-unknown-linux-gnu": "872fc321363b8cdd826fd2cb1adfd1ceb813bc1281f9d410c1c2c4e177e8df86", - "x86_64-apple-darwin": "c37a22fca8f57d4471e3708de6d13097668c5f160067f264bb2b18f524c890c8", - "x86_64-pc-windows-msvc": "f7cfa4ad072feb4578c8afca5ba9a54ad591d665a441dd0d63aa366edbe19279", - "x86_64-unknown-linux-gnu": "a73ba777b5d55ca89edef709e6b8521e3f3d4289581f174c8699adfb608d09d6", - }, - "strip_prefix": "python", - }, - "3.12.4": { - "url": "20240726/cpython-{python_version}+20240726-{platform}-{build}.tar.gz", - "sha256": { - "aarch64-apple-darwin": "1801025e825c04b3907e4ef6220a13607bc0397628c9485897073110ef7fde15", - "aarch64-unknown-linux-gnu": "a098b18b7e9fea0c66867b76c0124fce9465765017572b2e7b522154c87c78d7", - "ppc64le-unknown-linux-gnu": "04011c4c5b7fe34b0b895edf4ad8748e410686c1d69aaee11d6688d481023bcb", - "s390x-unknown-linux-gnu": "8f8f3e29cf0c2facdbcfee70660939fda7667ac24fee8656d3388fc72f3acc7c", - "x86_64-apple-darwin": "4c325838c1b0ed13698506fcd515be25c73dcbe195f8522cf98f9148a97601ed", - "x86_64-pc-windows-msvc": "74309b0f322716409883d38c621743ea7fa0376eb00927b8ee1e1671d3aff450", - "x86_64-unknown-linux-gnu": "e133dd6fc6a2d0033e2658637cc22e9c95f9d7073b80115037ee1f16417a54ac", - }, - "strip_prefix": "python", - }, - "3.12.7": { - "url": "20241016/cpython-{python_version}+20241016-{platform}-{build}.tar.gz", - "sha256": { - "aarch64-apple-darwin": "4c18852bf9c1a11b56f21bcf0df1946f7e98ee43e9e4c0c5374b2b3765cf9508", - "aarch64-unknown-linux-gnu": "bba3c6be6153f715f2941da34f3a6a69c2d0035c9c5396bc5bb68c6d2bd1065a", - "ppc64le-unknown-linux-gnu": "0a1d1d92e33a969bd2f40a80af53c97b6c0cc1060d384ceff50ff801593bf9d6", - "s390x-unknown-linux-gnu": "935676a0c960b552f95e9ac2e1e385de5de4b34038ff65ffdc688838f1189c17", - "x86_64-apple-darwin": "60c5271e7edc3c2ab47440b7abf4ed50fbc693880b474f74f05768f5b657045a", - "x86_64-pc-windows-msvc": "f05531bff16fa77b53be0776587b97b466070e768e6d5920894de988bdcd547a", - "x86_64-unknown-linux-gnu": "43576f7db1033dd57b900307f09c2e86f371152ac8a2607133afa51cbfc36064", - "x86_64-unknown-linux-musl": "5ed4a4078db3cbac563af66403aaa156cd6e48831d90382a1820db2b120627b5", - }, - "strip_prefix": "python", - }, - "3.12.8": { - "url": "20241206/cpython-{python_version}+20241206-{platform}-{build}.tar.gz", - "sha256": { - "aarch64-apple-darwin": "e3c4aa607717b23903ca2650d5c3ee24f89b97543e2db2b0f463bddc7a9e92f3", - "aarch64-unknown-linux-gnu": "ce674b55442b732973afb2932c281bb1ded4ad7e22bcf9b07071165770758c7e", - "ppc64le-unknown-linux-gnu": "b7214790b273de9ed0532420054b72ba1393d62d2fc844ec55ade193771bd90c", - "s390x-unknown-linux-gnu": "73102f5dbd7d1e7e9c2f2c80aedf2893d99a7fa407f6674ec8b2f57ba07daee5", - "x86_64-apple-darwin": "3ba35c706577d755e8e52a4c161a042464577c0e695e2a605362fa469e26de10", - "x86_64-pc-windows-msvc": "767b4be3ddf6b99e5ade519789c1615c191d8cf99d5aff4685cc18b48931f1e6", - "x86_64-unknown-linux-gnu": "b9d6ee5ddac1198e72d53112698773fc8bb597de095592eb849ca794306699ba", - "x86_64-unknown-linux-musl": "6f305888703691dd04cfff85284d23ea0b0146ed7c4415e472f1fb72b3f32cdf", - }, - "strip_prefix": "python", - }, - "3.12.9": { - "url": "20250317/cpython-{python_version}+20250317-{platform}-{build}.tar.gz", - "sha256": { - "aarch64-apple-darwin": "7c7fd9809da0382a601a79287b5d62d61ce0b15f5a5ee836233727a516e85381", - "aarch64-unknown-linux-gnu": "00c6bf9acef21ac741fea24dc449d0149834d30e9113429e50a95cce4b00bb80", - "ppc64le-unknown-linux-gnu": "25d77599dfd5849f17391d92da0da99079e4e94f19a881f763f5cc62530ef7e1", - "riscv64-unknown-linux-gnu": "e97ab0fdf443b302c56a52b4fd08f513bf3be66aa47263f0f9df3c6e60e05f2e", - "s390x-unknown-linux-gnu": "7492d079ffa8425c8f6c58e43b237c37e3fb7b31e2e14635927bb4d3397ba21e", - "x86_64-apple-darwin": "1ee1b1bb9fbce5c145c4bec9a3c98d7a4fa22543e09a7c1d932bc8599283c2dc", - "x86_64-pc-windows-msvc": "d15361fd202dd74ae9c3eece1abdab7655f1eba90bf6255cad1d7c53d463ed4d", - "x86_64-unknown-linux-gnu": "ef382fb88cbb41a3b0801690bd716b8a1aec07a6c6471010bcc6bd14cd575226", - "x86_64-unknown-linux-musl": "94e3837da1adf9964aab2d6047b33f70167de3096d1f9a2d1fa9340b1bbf537d", - }, - "strip_prefix": "python", - }, - "3.12.11": { - "url": "20250808/cpython-{python_version}+20250808-{platform}-{build}.tar.gz", - "sha256": { - "aarch64-apple-darwin": "8792c4a84c364ab975feca0c27d3157a5435b7baab325a346ae56b223893b661", - "aarch64-unknown-linux-gnu": "4d7ba5314fab02130d6538f074961ffbf61310cade9180e59026074f9a8939cb", - "aarch64-pc-windows-msvc": "00bf7d7e8bcf5d1e9c4dfca0247d8e035147777cd57ee9d4c64dedca86b0a464", - "ppc64le-unknown-linux-gnu": "2c862eb40a81549d9c11e6bf5a7f07c3406310b14e6a4d16dcdf1c4763ef7090", - "riscv64-unknown-linux-gnu": "0bb729b95fabd49c7b495f7c44a9086e3970ea57daf66365741574bd36a17e81", - "s390x-unknown-linux-gnu": "99e465882d217d24ac90e99fac8f32e6a644d0340ac05ee510fb5cdf53f0cfb8", - "x86_64-apple-darwin": "e0c932709dafb05f00e528a7560ef8ee559ac82b75faca60dd1245bca1c1553f", - "x86_64-pc-windows-msvc": "81214ef71964a40ec269a79067ca490d45298c350583bc3af0e5781451a05c3c", - "x86_64-unknown-linux-gnu": "63d78840bf209af8da8f24e335d910f88387b892ca9187be571d481c071751bb", - "x86_64-unknown-linux-musl": "d633d070780590aa03ac5575cd9d7b9e17682d80f14b400313c009c387cf706b", - }, - "strip_prefix": "python", - }, - "3.12.12": { - "url": "20251031/cpython-{python_version}+20251031-{platform}-{build}.tar.gz", - "sha256": { - "aarch64-apple-darwin": "5e110cb821d2eb8246065d3b46faa655180c976c4e17250f7883c634a629bc63", - "aarch64-unknown-linux-gnu": "81b644d166e0bfb918615af8a2363f8fcf26eccdcc60a5334b6a62c088470bac", - "aarch64-pc-windows-msvc": "b190fed7c2b0f6e1010f554a0d1fd191c0754c4c0718e69d9d795ae559613780", - "ppc64le-unknown-linux-gnu": "024f5e5678c9768d45cc24d37a8e9d265aae86c4a4602352dee3d7deba367052", - "riscv64-unknown-linux-gnu": "b13c57fc372c131e667a99b9680f41c0b4da571cf99ed412103c2fe9ad5ed1fb", - "s390x-unknown-linux-gnu": "2bf05bdd56cdf5ea4fd9f2faf151ea4211be96a0d1f4230b85f5dcae620d6400", - "x86_64-apple-darwin": "687052a046d33be49dc95dd671816709067cf6176ed36c93ea61b1fe0b883b0f", - "x86_64-pc-windows-msvc": "cff398b3f520c442a1b085dd347126c10c1b03f01ccc0decd8c897a687e893f1", - "x86_64-unknown-linux-gnu": "80c3882f14e15cef8260ef5257d198e8f4371ca265887431d939e0d561de3253", - "x86_64-unknown-linux-musl": "0a461330b9b89f2ea3088dde10d7a3f96aa65897b7c5ce2404fa3b5c4b8daa14", - }, - "strip_prefix": "python", - }, - "3.12.13": { - "url": "20260414/cpython-{python_version}+20260414-{platform}-{build}.{ext}", - "sha256": { - "aarch64-apple-darwin": "8966b2bcd9fa03ba22c080ad15a86bc12e41a00122b16f4b3740e302261124d9", - "aarch64-unknown-linux-gnu": "355d981eafb9b2870af79ddc106ced7266b6f6d2101d8fbcb05620fa386642b9", - "ppc64le-unknown-linux-gnu": "4aef4cffe73c4a65ea486f14d684a9ad3f831a354174d163bb531b5baa70fc49", - "riscv64-unknown-linux-gnu": "c2629d69324155132343913f064be93509bd162531e08a292e50c3973ec8b5db", - "s390x-unknown-linux-gnu": "e5baafd64180f45165d2751b25d1bcc89254eefc7926f3ab341fc61b541d7606", - "x86_64-apple-darwin": "801b03fbe004181d55a02ebd8b4e04d74973e70d716062aebe3b3cf32e9be297", - "x86_64-pc-windows-msvc": "c5a9e011e284c49c48106ca177342f3e3f64e95b4c6652d4a382cc7c9bb1cc46", - "aarch64-pc-windows-msvc": "f55326c894fde76fc0faffe95d2bce60be533c88a8c44c1b88bbbc17bf6a5cd5", - "x86_64-unknown-linux-gnu": "cdcf8724d46e4857f8db5ee9f4252dc2f5da34f7940294ec6b312389dd3f41e0", - "x86_64-unknown-linux-musl": "d10e971238c130fdf25e577c6538a3effa5589d5fcf53665e3c711edd6a6ff2f", - }, - "strip_prefix": "python", - }, - "3.13.0": { - "url": "20241016/cpython-{python_version}+20241016-{platform}-{build}.{ext}", - "sha256": { - "aarch64-apple-darwin": "31397953849d275aa2506580f3fa1cb5a85b6a3d392e495f8030e8b6412f5556", - "aarch64-unknown-linux-gnu": "e8378c0162b2e0e4cc1f62b29443a3305d116d09583304dbb0149fecaff6347b", - "ppc64le-unknown-linux-gnu": "fc4b7f27c4e84c78f3c8e6c7f8e4023e4638d11f1b36b6b5ce457b1926cebb53", - "s390x-unknown-linux-gnu": "66b19e6a07717f6cfcd3a8ca953f0a2eaa232291142f3d26a8d17c979ec0f467", - "x86_64-apple-darwin": "cff1b7e7cd26f2d47acac1ad6590e27d29829776f77e8afa067e9419f2f6ce77", - "x86_64-pc-windows-msvc": "b25926e8ce4164cf103bacc4f4d154894ea53e07dd3fdd5ebb16fb1a82a7b1a0", - "x86_64-unknown-linux-gnu": "2c8cb15c6a2caadaa98af51df6fe78a8155b8471cb3dd7b9836038e0d3657fb4", - "x86_64-unknown-linux-musl": "2f61ee3b628a56aceea63b46c7afe2df3e22a61da706606b0c8efda57f953cf4", - "aarch64-apple-darwin-freethreaded": "efc2e71c0e05bc5bedb7a846e05f28dd26491b1744ded35ed82f8b49ccfa684b", - "aarch64-unknown-linux-gnu-freethreaded": "59b50df9826475d24bb7eff781fa3949112b5e9c92adb29e96a09cdf1216d5bd", - "ppc64le-unknown-linux-gnu-freethreaded": "1217efa5f4ce67fcc9f7eb64165b1bd0912b2a21bc25c1a7e2cb174a21a5df7e", - "s390x-unknown-linux-gnu-freethreaded": "6c3e1e4f19d2b018b65a7e3ef4cd4225c5b9adfbc490218628466e636d5c4b8c", - "x86_64-apple-darwin-freethreaded": "2e07dfea62fe2215738551a179c87dbed1cc79d1b3654f4d7559889a6d5ce4eb", - "x86_64-pc-windows-msvc-freethreaded": "bfd89f9acf866463bc4baf01733da5e767d13f5d0112175a4f57ba91f1541310", - "x86_64-unknown-linux-gnu-freethreaded": "a73adeda301ad843cce05f31a2d3e76222b656984535a7b87696a24a098b216c", - }, - "strip_prefix": { - "aarch64-apple-darwin": "python", - "aarch64-unknown-linux-gnu": "python", - "ppc64le-unknown-linux-gnu": "python", - "s390x-unknown-linux-gnu": "python", - "x86_64-apple-darwin": "python", - "x86_64-pc-windows-msvc": "python", - "x86_64-unknown-linux-gnu": "python", - "x86_64-unknown-linux-musl": "python", - "aarch64-apple-darwin-freethreaded": "python/install", - "aarch64-unknown-linux-gnu-freethreaded": "python/install", - "ppc64le-unknown-linux-gnu-freethreaded": "python/install", - "s390x-unknown-linux-gnu-freethreaded": "python/install", - "x86_64-apple-darwin-freethreaded": "python/install", - "x86_64-pc-windows-msvc-freethreaded": "python/install", - "x86_64-unknown-linux-gnu-freethreaded": "python/install", - }, - }, - "3.13.1": { - "url": "20241205/cpython-{python_version}+20241205-{platform}-{build}.{ext}", - "sha256": { - "aarch64-apple-darwin": "88b88b609129c12f4b3841845aca13230f61e97ba97bd0fb28ee64b0e442a34f", - "aarch64-unknown-linux-gnu": "fdfa86c2746d2ae700042c461846e6c37f70c249925b58de8cd02eb8d1423d4e", - "ppc64le-unknown-linux-gnu": "27b20b3237c55430ca1304e687d021f88373f906249f9cd272c5ff2803d5e5c3", - "s390x-unknown-linux-gnu": "7d0187e20cb5e36c689eec27e4d3de56d8b7f1c50dc5523550fc47377801521f", - "x86_64-apple-darwin": "47eef6efb8664e2d1d23a7cdaf56262d784f8ace48f3bfca1b183e95a49888d6", - "x86_64-pc-windows-msvc": "f51f0493a5f979ff0b8d8c598a8d74f2a4d86a190c2729c85e0af65c36a9cbbe", - "x86_64-unknown-linux-gnu": "242b2727df6c1e00de6a9f0f0dcb4562e168d27f428c785b0eb41a6aeb34d69a", - "x86_64-unknown-linux-musl": "76b30c6373b9c0aa2ba610e07da02f384aa210ac79643da38c66d3e6171c6ef5", - "aarch64-apple-darwin-freethreaded": "08f05618bdcf8064a7960b25d9ba92155447c9b08e0cf2f46a981e4c6a1bb5a5", - "aarch64-unknown-linux-gnu-freethreaded": "9f2fcb809f9ba6c7c014a8803073a88786701a98971135bce684355062e4bb35", - "ppc64le-unknown-linux-gnu-freethreaded": "15ceea78dff78ca8ccaac8d9c54b808af30daaa126f1f561e920a6896e098634", - "s390x-unknown-linux-gnu-freethreaded": "ed3c6118d1d12603309c930e93421ac7a30a69045ffd43006f63ecf71d72c317", - "x86_64-apple-darwin-freethreaded": "dc780fecd215d2cc9e573abf1e13a175fcfa8f6efd100ef888494a248a16cda8", - "x86_64-pc-windows-msvc-freethreaded": "7537b2ab361c0eabc0eabfca9ffd9862d7f5f6576eda13b97e98aceb5eea4fd3", - "x86_64-unknown-linux-gnu-freethreaded": "9ec1b81213f849d91f5ebe6a16196e85cd6ff7c05ca823ce0ab7ba5b0e9fee84", - }, - "strip_prefix": { - "aarch64-apple-darwin": "python", - "aarch64-unknown-linux-gnu": "python", - "ppc64le-unknown-linux-gnu": "python", - "s390x-unknown-linux-gnu": "python", - "x86_64-apple-darwin": "python", - "x86_64-pc-windows-msvc": "python", - "x86_64-unknown-linux-gnu": "python", - "x86_64-unknown-linux-musl": "python", - "aarch64-apple-darwin-freethreaded": "python/install", - "aarch64-unknown-linux-gnu-freethreaded": "python/install", - "ppc64le-unknown-linux-gnu-freethreaded": "python/install", - "s390x-unknown-linux-gnu-freethreaded": "python/install", - "x86_64-apple-darwin-freethreaded": "python/install", - "x86_64-pc-windows-msvc-freethreaded": "python/install", - "x86_64-unknown-linux-gnu-freethreaded": "python/install", - }, - }, - "3.13.2": { - "url": "20250317/cpython-{python_version}+20250317-{platform}-{build}.{ext}", - "sha256": { - "aarch64-apple-darwin": "faa44274a331eb39786362818b21b3a4e74514e8805000b20b0e55c590cecb94", - "aarch64-unknown-linux-gnu": "9c67260446fee6ea706dad577a0b32936c63f449c25d66e4383d5846b2ab2e36", - "ppc64le-unknown-linux-gnu": "345b53d2f86c9dbd7f1320657cb227ff9a42ef63ff21f129abbbc8c82a375147", - "riscv64-unknown-linux-gnu": "172d22b2330737f3a028ea538ffe497c39a066a8d3200b22dd4d177a3332ad85", - "s390x-unknown-linux-gnu": "ec3b16ea8a97e3138acec72bc5ff35949950c62c8994a8ec8e213fd93f0e806b", - "x86_64-apple-darwin": "ee4526e84b5ce5b11141c50060b385320f2773616249a741f90c96d460ce8e8f", - "x86_64-pc-windows-msvc": "84d7b52f3558c8e35c670a4fa14080c75e3ec584adfae49fec8b51008b75b21e", - "x86_64-unknown-linux-gnu": "db011f0cd29cab2291584958f4e2eb001b0e6051848d89b38a2dc23c5c54e512", - "x86_64-unknown-linux-musl": "00bb2d629f7eacbb5c6b44dc04af26d1f1da64cee3425b0d8eb5135a93830296", - "aarch64-apple-darwin-freethreaded": "c98c9c977e6fa05c3813bd49f3553904d89d60fed27e2e36468da7afa1d6d5e2", - "aarch64-unknown-linux-gnu-freethreaded": "b8635e59e3143fd17f19a3dfe8ccc246ee6587c87da359bd1bcab35eefbb5f19", - "ppc64le-unknown-linux-gnu-freethreaded": "6ae8fa44cb2edf4ab49cff1820b53c40c10349c0f39e11b8cd76ce7f3e7e1def", - "riscv64-unknown-linux-gnu-freethreaded": "2af1b8850c52801fb6189e7a17a51e0c93d9e46ddefcca72247b76329c97d02a", - "s390x-unknown-linux-gnu-freethreaded": "c074144cc80c2af32c420b79a9df26e8db405212619990c1fbdd308bd75afe3f", - "x86_64-apple-darwin-freethreaded": "0d73e4348d8d4b5159058609d2303705190405b485dd09ad05d870d7e0f36e0f", - "x86_64-pc-windows-msvc-freethreaded": "c51b4845fda5421e044067c111192f645234081d704313f74ee77fa013a186ea", - "x86_64-unknown-linux-gnu-freethreaded": "1aea5062614c036904b55c1cc2fb4b500b7f6f7a4cacc263f4888889d355eef8", - }, - "strip_prefix": { - "aarch64-apple-darwin": "python", - "aarch64-unknown-linux-gnu": "python", - "ppc64le-unknown-linux-gnu": "python", - "s390x-unknown-linux-gnu": "python", - "riscv64-unknown-linux-gnu": "python", - "x86_64-apple-darwin": "python", - "x86_64-pc-windows-msvc": "python", - "x86_64-unknown-linux-gnu": "python", - "x86_64-unknown-linux-musl": "python", - "aarch64-apple-darwin-freethreaded": "python/install", - "aarch64-unknown-linux-gnu-freethreaded": "python/install", - "ppc64le-unknown-linux-gnu-freethreaded": "python/install", - "riscv64-unknown-linux-gnu-freethreaded": "python/install", - "s390x-unknown-linux-gnu-freethreaded": "python/install", - "x86_64-apple-darwin-freethreaded": "python/install", - "x86_64-pc-windows-msvc-freethreaded": "python/install", - "x86_64-unknown-linux-gnu-freethreaded": "python/install", - }, - }, - "3.13.4": { - "url": "20250610/cpython-{python_version}+20250610-{platform}-{build}.{ext}", - "sha256": { - "aarch64-apple-darwin": "c2ce6601b2668c7bd1f799986af5ddfbff36e88795741864aba6e578cb02ed7f", - "aarch64-unknown-linux-gnu": "3c2596ece08ffe17e11bc1f27aeb4ce1195d2490a83d695d36ef4933d5c5ca53", - "ppc64le-unknown-linux-gnu": "b3cc13ee177b8db1d3e9b2eac413484e3c6a356f97d91dc59de8d3fd8cf79d6b", - "riscv64-unknown-linux-gnu": "d1b989e57a9ce29f6c945eeffe0e9750c222fdd09e99d2f8d6b0d8532a523053", - "s390x-unknown-linux-gnu": "d1d19fb01961ac6476712fdd6c5031f74c83666f6f11aa066207e9a158f7e3d8", - "x86_64-apple-darwin": "79feb6ca68f3921d07af52d9db06cf134e6f36916941ea850ab0bc20f5ff638b", - "x86_64-pc-windows-msvc": "29ac3585cc2dcfd79e3fe380c272d00e9d34351fc456e149403c86d3fea34057", - "x86_64-unknown-linux-gnu": "44e5477333ebca298a7a0a316985c6c3533b8645f92a83f7f73c44033832bf32", - "x86_64-unknown-linux-musl": "a3afbfa94b9ff4d9fc426b47eb3c8446cada535075b8d51b7bdc9d9ab9911fc2", - "aarch64-apple-darwin-freethreaded": "278dccade56b4bbeecb9a613b77012cf5c1433a5e9b8ef99230d5e61f31d9e02", - "aarch64-unknown-linux-gnu-freethreaded": "b1c1bd6ab9ef95b464d92a6a911cef1a8d9f0b0f6a192f694ef18ed15d882edf", - "ppc64le-unknown-linux-gnu-freethreaded": "ed66ae213a62b286b9b7338b816ccd2815f5248b7a28a185dc8159fe004149ae", - "riscv64-unknown-linux-gnu-freethreaded": "913264545215236660e4178bc3e5b57a20a444a8deb5c11680c95afc960b4016", - "s390x-unknown-linux-gnu-freethreaded": "7556a38ab5e507c1ec22bc38f9859982bc956cab7f4de05a2faac114feb306db", - "x86_64-apple-darwin-freethreaded": "64ab7ac8c88002d9ba20a92f72945bfa350268e944a7922500af75d20330574d", - "x86_64-pc-windows-msvc-freethreaded": "9457504547edb2e0156bf76b53c7e4941c7f61c0eff9fd5f4d816d3df51c58e3", - "x86_64-unknown-linux-gnu-freethreaded": "864df6e6819e8f8e855ce30f34410fdc5867d0616e904daeb9a40e5806e970d7", - }, - "strip_prefix": { - "aarch64-apple-darwin": "python", - "aarch64-unknown-linux-gnu": "python", - "ppc64le-unknown-linux-gnu": "python", - "s390x-unknown-linux-gnu": "python", - "riscv64-unknown-linux-gnu": "python", - "x86_64-apple-darwin": "python", - "x86_64-pc-windows-msvc": "python", - "x86_64-unknown-linux-gnu": "python", - "x86_64-unknown-linux-musl": "python", - "aarch64-apple-darwin-freethreaded": "python/install", - "aarch64-unknown-linux-gnu-freethreaded": "python/install", - "ppc64le-unknown-linux-gnu-freethreaded": "python/install", - "riscv64-unknown-linux-gnu-freethreaded": "python/install", - "s390x-unknown-linux-gnu-freethreaded": "python/install", - "x86_64-apple-darwin-freethreaded": "python/install", - "x86_64-pc-windows-msvc-freethreaded": "python/install", - "x86_64-unknown-linux-gnu-freethreaded": "python/install", - }, - }, - "3.13.6": { - "url": "20250808/cpython-{python_version}+20250808-{platform}-{build}.{ext}", - "sha256": { - "aarch64-apple-darwin": "8a1efa6af4e80f08e2c97dda822a3d6c24d6c98e518242f802c6a43ae8401488", - "aarch64-unknown-linux-gnu": "11fa0591ae2211c08a42ae54944260e36ddf88a1d5604ea0c49e2477be4e5388", - "ppc64le-unknown-linux-gnu": "8dcf34ae1a685fe1893b52917ae04f23328edadc4acae28499d43850c2bdd26c", - "riscv64-unknown-linux-gnu": "f8ed75aa6cc2011a046be00b629c3c8295267f34280324feaff34c73e7afce39", - "s390x-unknown-linux-gnu": "7707ee5d19a78bc64ef8a66751ec7f97b64ea06714c7b1b52e8b321c2923ead8", - "x86_64-apple-darwin": "27badce7201321a8363219e438a6205165e5b4884012b1046532203df2ec9379", - "x86_64-pc-windows-msvc": "af5cc733c33b9aa9f1d74c81a59351e9b27215486d8b6cdbc06d97646a58c953", - "aarch64-pc-windows-msvc": "8e1617bd407ec1a874499daab26ae95080d1e0267ae616d34490137a28705827", - "aarch64-pc-windows-msvc-freethreaded": "552cfabcc3b103f4b1c4036d2592d5f0373c9554a2c4d2b6631b04ef7e592067", - "x86_64-unknown-linux-gnu": "f844e8c8b6847628b472f7e97d8893a4e93acd5382a902b465776063668c4d64", - "x86_64-unknown-linux-musl": "70076dea0ff65b3c05aae1a97b4a556bf613cc73db30309e59134f9d318f4f7b", - "aarch64-apple-darwin-freethreaded": "f2143304012e021a603bf1807bf3e4ce163832e43ab9a9829e53cb136497f207", - "aarch64-unknown-linux-gnu-freethreaded": "d84a7d64c284be387386b9f5da273f6d05486eb6bd8f9e86e2575cb59604cb22", - "ppc64le-unknown-linux-gnu-freethreaded": "e76fcaf1bf80a615520dbe7f85ca0bb557fad96d132d836b0ac721e7cc1e2a37", - "riscv64-unknown-linux-gnu-freethreaded": "24e08a39ba4fc77753e61541e52eed39cc871f4a92a80a3c5dd495056bd8eff9", - "s390x-unknown-linux-gnu-freethreaded": "1609b223fd38a4a7a4d20e7173d7d9390fe2258f7dd9a15dc9ef0fa49613735d", - "x86_64-apple-darwin-freethreaded": "4360a1278dd0a96b526d108c8fd23498a9d2028dd7791e510fd51ff5ea3f462a", - "x86_64-pc-windows-msvc-freethreaded": "4e727cdbe4057b16a170f887c0fa4227a825ac59bcda84ae946c77cc932af78c", - "x86_64-unknown-linux-gnu-freethreaded": "e48c13c59cc3c01b79f63c8bccec27d2db6e97f64213b8731e2077b6ed8ed52c", - }, - "strip_prefix": { - "aarch64-apple-darwin": "python", - "aarch64-unknown-linux-gnu": "python", - "ppc64le-unknown-linux-gnu": "python", - "s390x-unknown-linux-gnu": "python", - "riscv64-unknown-linux-gnu": "python", - "x86_64-apple-darwin": "python", - "x86_64-pc-windows-msvc": "python", - "aarch64-pc-windows-msvc": "python", - "x86_64-unknown-linux-gnu": "python", - "x86_64-unknown-linux-musl": "python", - "aarch64-apple-darwin-freethreaded": "python/install", - "aarch64-unknown-linux-gnu-freethreaded": "python/install", - "ppc64le-unknown-linux-gnu-freethreaded": "python/install", - "riscv64-unknown-linux-gnu-freethreaded": "python/install", - "s390x-unknown-linux-gnu-freethreaded": "python/install", - "x86_64-apple-darwin-freethreaded": "python/install", - "x86_64-pc-windows-msvc-freethreaded": "python/install", - "aarch64-pc-windows-msvc-freethreaded": "python/install", - "x86_64-unknown-linux-gnu-freethreaded": "python/install", - }, - }, - "3.13.9": { - "url": "20251031/cpython-{python_version}+20251031-{platform}-{build}.{ext}", - "sha256": { - "aarch64-apple-darwin": "1f3568d17383426d52350c2ef7c93c1a5a043198b860cb05e5d19b35f9c25cef", - "aarch64-unknown-linux-gnu": "0a56d11b0fb1662e67f892b9d5d1717aef06f24dbb8362bc25b8f784e620d44e", - "ppc64le-unknown-linux-gnu": "99492123902bd5e9a6b1a30135061e93a2e6a11d25107a741d5a756e91054448", - "riscv64-unknown-linux-gnu": "b3dce3e4ef508773521e1ee1be989fff6118f8fd1fbbd0491d7ff7dfbc98ef06", - "s390x-unknown-linux-gnu": "f10e34aaa856c1b8a69c2ea4a9a6723d520443d1a957bf66dc55491334ca0c1e", - "x86_64-apple-darwin": "48c0f3ca5d31e90658ef99138dc21865bb62f388ab97a1ce72cac176da194ab0", - "x86_64-pc-windows-msvc": "874593f641f31ea101440c70f81768c35d4d7d6df111fde63094db67465ef787", - "aarch64-pc-windows-msvc": "20db43873d3c4c2175d866806545e4ad4ec6bb72ca95e60082a4df6c24567e8c", - "aarch64-pc-windows-msvc-freethreaded": "743ff69935ef28834621647dab30f032dfcd80315732917531eea333210941c7", - "x86_64-unknown-linux-gnu": "6f05b91ee8c7e6dd0f9c60b95bb29130e2d623961de6578b643e80ddd83f96b6", - "x86_64-unknown-linux-musl": "ad987197034185e628715da504a50613af213dc21ba6d5ccaeab3db2c464aa6c", - "aarch64-apple-darwin-freethreaded": "eae1272a72ccce601590a10a9ca2a58199b5fcdf022aa603a527e3e2a04de9bc", - "aarch64-unknown-linux-gnu-freethreaded": "a6e72f9de5d9b46cf6968d6a492f2401a919f9b959f8da2d87f43484b80169ee", - "ppc64le-unknown-linux-gnu-freethreaded": "0ed5c65437f875c58ba1bee2b8d261d18698d3d0347a2e66f8902fce022a2cda", - "riscv64-unknown-linux-gnu-freethreaded": "584e481d9b5225ffaf02f158fb26d2818207e65fc3c6dc21a6d500277f739220", - "s390x-unknown-linux-gnu-freethreaded": "7fa7fb912ca989ceac026a332d56a2c7d6d16ab0e94d89e690de5aade26103e2", - "x86_64-apple-darwin-freethreaded": "e2bf5fa6a3ef443ade362e08b0a19bbc172f7bfe34dabe933ccaad31d53af5da", - "x86_64-pc-windows-msvc-freethreaded": "318a9a1e43dd52054327de3bccc0c5b7afde7b7f2a398ccb4d38e03d28b05386", - "x86_64-unknown-linux-gnu-freethreaded": "dcc29b069d0588fbd4ea29c6df840c8d1207d2a3bce8cd5cd57d1b85373b6048", - }, - "strip_prefix": { - "aarch64-apple-darwin": "python", - "aarch64-unknown-linux-gnu": "python", - "ppc64le-unknown-linux-gnu": "python", - "s390x-unknown-linux-gnu": "python", - "riscv64-unknown-linux-gnu": "python", - "x86_64-apple-darwin": "python", - "x86_64-pc-windows-msvc": "python", - "aarch64-pc-windows-msvc": "python", - "x86_64-unknown-linux-gnu": "python", - "x86_64-unknown-linux-musl": "python", - "aarch64-apple-darwin-freethreaded": "python/install", - "aarch64-unknown-linux-gnu-freethreaded": "python/install", - "ppc64le-unknown-linux-gnu-freethreaded": "python/install", - "riscv64-unknown-linux-gnu-freethreaded": "python/install", - "s390x-unknown-linux-gnu-freethreaded": "python/install", - "x86_64-apple-darwin-freethreaded": "python/install", - "x86_64-pc-windows-msvc-freethreaded": "python/install", - "aarch64-pc-windows-msvc-freethreaded": "python/install", - "x86_64-unknown-linux-gnu-freethreaded": "python/install", - }, - }, - "3.13.10": { - "url": "20251202/cpython-{python_version}+20251202-{platform}-{build}.{ext}", - "sha256": { - "aarch64-apple-darwin": "37afe4e77ab62ac50f197b1cb1f3bc02c82735c6be893da0996afcde5dc41048", - "aarch64-unknown-linux-gnu": "c68280591cda1c9515a04809fa6926020177e8e5892300206e0496ea1d10290e", - "ppc64le-unknown-linux-gnu": "1507e5528bd88131dc742a2941176aceea1838bc09860c21f179285b7865133b", - "riscv64-unknown-linux-gnu": "70169e916860b2e5b34c37c302d699eb2b8f24f28090968881942a37aeb7ed08", - "s390x-unknown-linux-gnu": "c5448863b64aacae62f3a213a6e6cf94ec63f96ee4d518491cd62fd3c81d952f", - "x86_64-apple-darwin": "a02761a4f189f71c0512e88df7ca2843696d61da659e47f8a5c8a9bd2c0d16f4", - "x86_64-pc-windows-msvc": "8b00014c7c35f9ad4cb1c565f067500bacc4125c8bc30e4389ee0be9fd6ffa3d", - "aarch64-pc-windows-msvc": "9060d644bd32ac0e0af970d0b21e207e6ff416b7c4dc26ffc4f9b043fb45b463", - "aarch64-pc-windows-msvc-freethreaded": "cdb7141327bdc244715b25752593e2c9eeb3cc2764f37dfe81cfbc92db9d6d57", - "x86_64-unknown-linux-gnu": "0cac1495fff920219904b1d573aaec0df54d549c226cb45f5c60cb6d2c72727a", - "x86_64-unknown-linux-musl": "04108190972ac98e13098abd972ec3f4f8b0880f83c0bb68249ce1a6164fa041", - "aarch64-apple-darwin-freethreaded": "3c9fdd76447c1549a0d3bc2a70c63f1daec997ab034206ac0260a03237166dbb", - "aarch64-unknown-linux-gnu-freethreaded": "6d277221fa4b172e00b29c7158ca9661917bc8db9a0084b1a0ff5c3a0ba8b648", - "ppc64le-unknown-linux-gnu-freethreaded": "d265d8d1c51e25ed70279540223589f79cf99ad00b50d28b6150c2658c973885", - "riscv64-unknown-linux-gnu-freethreaded": "ec411b4a2d167c3be0a9aeb3905e045d62c8e3c3db0caeade5d47d5f60b98dd0", - "s390x-unknown-linux-gnu-freethreaded": "4fc6443948bf5b729481ea02cc5c68e80cd0da42631f6936587a2b8fd45bc62c", - "x86_64-apple-darwin-freethreaded": "6ce608684df0f90350c7a1742e9685a7782d9b26ec99d1bd9d55c8cf9a405040", - "x86_64-pc-windows-msvc-freethreaded": "6a8b0372ded655e0d55318089fbce3122a446e69bcd120c79aaadfe9b017299c", - "x86_64-unknown-linux-gnu-freethreaded": "e39127fbe8d2ae7d86099f18b4da0918f9b60ce73ed491774d6dcfaa42b5c9ae", - }, - "strip_prefix": { - "aarch64-apple-darwin": "python", - "aarch64-unknown-linux-gnu": "python", - "ppc64le-unknown-linux-gnu": "python", - "s390x-unknown-linux-gnu": "python", - "riscv64-unknown-linux-gnu": "python", - "x86_64-apple-darwin": "python", - "x86_64-pc-windows-msvc": "python", - "aarch64-pc-windows-msvc": "python", - "x86_64-unknown-linux-gnu": "python", - "x86_64-unknown-linux-musl": "python", - "aarch64-apple-darwin-freethreaded": "python/install", - "aarch64-unknown-linux-gnu-freethreaded": "python/install", - "ppc64le-unknown-linux-gnu-freethreaded": "python/install", - "riscv64-unknown-linux-gnu-freethreaded": "python/install", - "s390x-unknown-linux-gnu-freethreaded": "python/install", - "x86_64-apple-darwin-freethreaded": "python/install", - "x86_64-pc-windows-msvc-freethreaded": "python/install", - "aarch64-pc-windows-msvc-freethreaded": "python/install", - "x86_64-unknown-linux-gnu-freethreaded": "python/install", - }, - }, - "3.13.11": { - "url": "20251209/cpython-{python_version}+20251209-{platform}-{build}.{ext}", - "sha256": { - "aarch64-apple-darwin": "295a9f7bc899ea1cc08baf60bbf511bdd1e4a29b2dd7e5f59b48f18bfa6bf585", - "aarch64-unknown-linux-gnu": "ea1e678e6e82301bb32bf3917732125949b6e46d541504465972024a3f165343", - "ppc64le-unknown-linux-gnu": "7660e53aad9d35ee256913c6d98427f81f078699962035c5fa8b5c3138695109", - "riscv64-unknown-linux-gnu": "763fa1548e6a432e9402916e690c74ea30f26dcd2e131893dd506f72b87c27c9", - "s390x-unknown-linux-gnu": "ffb6af51fbfabfc6fbc4e7379bdec70c2f51e972b1d2f45c053493b9da3a1bbe", - "x86_64-apple-darwin": "dac4a0a0a9b71f6b02a8b0886547fa22814474239bffb948e3e77185406ea136", - "x86_64-pc-windows-msvc": "87822417007045a28a7eccc47fe67b8c61265b99b10dbbfa24d231a3622b1c27", - "aarch64-pc-windows-msvc": "ba646d0c3b7dd7bdfb770d9b2ebd6cd2df02a37fda90c9c79a7cf59c7df6f165", - "aarch64-pc-windows-msvc-freethreaded": "6daf6d092c7294cfe68c4c7bf2698ac134235489c874b3bf796c7972b9dbba30", - "x86_64-unknown-linux-gnu": "1ffa06d714a44aea14c0c54c30656413e5955a6c92074b4b3cb4351dcc28b63b", - "x86_64-unknown-linux-musl": "969fe24017380b987c4e3ce15e9edf82a4618c1e61672b2cc9b021a1c98eae78", - "aarch64-apple-darwin-freethreaded": "4213058b7fcd875596c12b58cd46a399358b0a87ecde4b349cbdd00cf87ed79a", - "aarch64-unknown-linux-gnu-freethreaded": "290ca3bd0007db9e551f90b08dfcb6c1b2d62c33b2fc3e9a43e77d385d94f569", - "ppc64le-unknown-linux-gnu-freethreaded": "09d4b50f8abb443f7e3af858c920aa61c2430b0954df465e861caa7078e55e69", - "riscv64-unknown-linux-gnu-freethreaded": "5406f2a7cacafbd2aac3ce2de066a0929aab55423824276c36e04cb83babc36c", - "s390x-unknown-linux-gnu-freethreaded": "3984b67c4292892eaccdd1c094c7ec788884c4c9b3534ab6995f6be96d5ed51d", - "x86_64-apple-darwin-freethreaded": "d6f489464045d6895ae68b0a04a9e16477e74fe3185a75f3a9a0af8ccd25eade", - "x86_64-pc-windows-msvc-freethreaded": "bb9a29a7ba8f179273b79971da6aaa7be592d78c606a63f99eff3e4c12fb0fae", - "x86_64-unknown-linux-gnu-freethreaded": "33f89c957d986d525529b8a980103735776f4d20cf52f55960a057c760188ac3", - }, - "strip_prefix": { - "aarch64-apple-darwin": "python", - "aarch64-unknown-linux-gnu": "python", - "ppc64le-unknown-linux-gnu": "python", - "s390x-unknown-linux-gnu": "python", - "riscv64-unknown-linux-gnu": "python", - "x86_64-apple-darwin": "python", - "x86_64-pc-windows-msvc": "python", - "aarch64-pc-windows-msvc": "python", - "x86_64-unknown-linux-gnu": "python", - "x86_64-unknown-linux-musl": "python", - "aarch64-apple-darwin-freethreaded": "python/install", - "aarch64-unknown-linux-gnu-freethreaded": "python/install", - "ppc64le-unknown-linux-gnu-freethreaded": "python/install", - "riscv64-unknown-linux-gnu-freethreaded": "python/install", - "s390x-unknown-linux-gnu-freethreaded": "python/install", - "x86_64-apple-darwin-freethreaded": "python/install", - "x86_64-pc-windows-msvc-freethreaded": "python/install", - "aarch64-pc-windows-msvc-freethreaded": "python/install", - "x86_64-unknown-linux-gnu-freethreaded": "python/install", - }, - }, - "3.13.12": { - "url": "20260325/cpython-{python_version}+20260325-{platform}-{build}.{ext}", - "sha256": { - "aarch64-apple-darwin": "688da81bcaa6ed91792397c7d5433b13a4f02f021f940637c3972639bc516dca", - "aarch64-unknown-linux-gnu": "31c6e61eed48ca4e156d0e473025a792338641109e8277a63518ded438390c96", - "ppc64le-unknown-linux-gnu": "654939bc40d5f76f08eb17335bb19e9efa11eb48a0818eda2293a3f7c3570ae7", - "riscv64-unknown-linux-gnu": "fc7e1fb553c47b831ed7fa529575145207f000f967513f7b9ea809cce006ed79", - "s390x-unknown-linux-gnu": "7d7919358e88fcc672b061be8c2316c3a604c7074200515d7104166ed611f7f9", - "x86_64-apple-darwin": "7411e47939783708381017a90944a69641ac84d43f74fb6e2d52576c599a2717", - "x86_64-pc-windows-msvc": "5b4093f92d9bffcb0d92aea050f3d77d5a4fc8e918b31cea000ee4b3ca751f1d", - "aarch64-pc-windows-msvc": "d2c8b00044cd2e4c5fc7e697e63d5e481ed44b87c2def0beb42991d59f65d930", - "aarch64-pc-windows-msvc-freethreaded": "d2c8b00044cd2e4c5fc7e697e63d5e481ed44b87c2def0beb42991d59f65d930", - "x86_64-unknown-linux-gnu": "ebb1051ca2822b9803f46a5f10b6d51d153189ef1b1f1e142f733c0cbeaf86eb", - "x86_64-unknown-linux-musl": "b2e9400731c7f18069ec2804ba87a404385fe440f93b7dcb59004b9f56651202", - "aarch64-apple-darwin-freethreaded": "688da81bcaa6ed91792397c7d5433b13a4f02f021f940637c3972639bc516dca", - "aarch64-unknown-linux-gnu-freethreaded": "31c6e61eed48ca4e156d0e473025a792338641109e8277a63518ded438390c96", - "ppc64le-unknown-linux-gnu-freethreaded": "654939bc40d5f76f08eb17335bb19e9efa11eb48a0818eda2293a3f7c3570ae7", - "riscv64-unknown-linux-gnu-freethreaded": "fc7e1fb553c47b831ed7fa529575145207f000f967513f7b9ea809cce006ed79", - "s390x-unknown-linux-gnu-freethreaded": "7d7919358e88fcc672b061be8c2316c3a604c7074200515d7104166ed611f7f9", - "x86_64-apple-darwin-freethreaded": "7411e47939783708381017a90944a69641ac84d43f74fb6e2d52576c599a2717", - "x86_64-pc-windows-msvc-freethreaded": "5b4093f92d9bffcb0d92aea050f3d77d5a4fc8e918b31cea000ee4b3ca751f1d", - "x86_64-unknown-linux-gnu-freethreaded": "ebb1051ca2822b9803f46a5f10b6d51d153189ef1b1f1e142f733c0cbeaf86eb", - }, - "strip_prefix": "python", - }, - "3.13.13": { - "url": "20260414/cpython-{python_version}+20260414-{platform}-{build}.{ext}", - "sha256": { - "aarch64-apple-darwin": "c652dad552122cd2e76968ec41c803f8222038169b11310dba0c85928265f5c1", - "aarch64-unknown-linux-gnu": "6a65f68043d7fadcd580415493d2929d1fd686013f9ae44ddbd3a81307ab256d", - "ppc64le-unknown-linux-gnu": "aef73894107300264222b19e357baf5bad616b1c4bf5daa5c3b97cfee8f5ed7b", - "riscv64-unknown-linux-gnu": "f47c09f8e7f2fb0bc4afe52422705af4016c8d3ec1cf004b67bb56a86caa62cb", - "s390x-unknown-linux-gnu": "4d205af9654e1f33cefd23ff798af470e565f3ac0eba18d2f98f18a2abd07166", - "x86_64-apple-darwin": "540337412d2c4220e99280f741dbf45c1e3da3a39edaaab20c6ba1d53e1692ef", - "x86_64-pc-windows-msvc": "ee0cb26453d6e025d36502d765c1639c34830355e46ab3ad31c0360bc4cd9b79", - "aarch64-pc-windows-msvc": "586ba71c75f341e1d111399b7f719ae784dc11e8672e93e017388f28684226d0", - "aarch64-pc-windows-msvc-freethreaded": "586ba71c75f341e1d111399b7f719ae784dc11e8672e93e017388f28684226d0", - "x86_64-unknown-linux-gnu": "e5ec3b2c5693215d153c434ac018e75511b2c4f96d2bce30468a477cb3a89d5e", - "x86_64-unknown-linux-musl": "24ac6bf80dd2991c8be348f777c96c6eb69b71e78d8fa28c09beb3ddca015a47", - "aarch64-apple-darwin-freethreaded": "c652dad552122cd2e76968ec41c803f8222038169b11310dba0c85928265f5c1", - "aarch64-unknown-linux-gnu-freethreaded": "6a65f68043d7fadcd580415493d2929d1fd686013f9ae44ddbd3a81307ab256d", - "ppc64le-unknown-linux-gnu-freethreaded": "aef73894107300264222b19e357baf5bad616b1c4bf5daa5c3b97cfee8f5ed7b", - "riscv64-unknown-linux-gnu-freethreaded": "f47c09f8e7f2fb0bc4afe52422705af4016c8d3ec1cf004b67bb56a86caa62cb", - "s390x-unknown-linux-gnu-freethreaded": "4d205af9654e1f33cefd23ff798af470e565f3ac0eba18d2f98f18a2abd07166", - "x86_64-apple-darwin-freethreaded": "540337412d2c4220e99280f741dbf45c1e3da3a39edaaab20c6ba1d53e1692ef", - "x86_64-pc-windows-msvc-freethreaded": "ee0cb26453d6e025d36502d765c1639c34830355e46ab3ad31c0360bc4cd9b79", - "x86_64-unknown-linux-gnu-freethreaded": "e5ec3b2c5693215d153c434ac018e75511b2c4f96d2bce30468a477cb3a89d5e", - }, - "strip_prefix": "python", - }, - "3.14.0": { - "url": "20251031/cpython-{python_version}+20251031-{platform}-{build}.{ext}", - "sha256": { - "aarch64-apple-darwin": "b4bcd3c6c24cab32ae99e1b05c89312b783b4d69431d702e5012fe1fdcad4087", - "aarch64-unknown-linux-gnu": "128a9cbfb9645d5237ec01704d9d1d2ac5f084464cc43c37a4cd96aa9c3b1ad5", - "ppc64le-unknown-linux-gnu": "e16ca51f018e99a609faf953bd3a3aea31f45ee84262d1a517fb3abd98f1f4af", - "riscv64-unknown-linux-gnu": "fca340d8fb7a05cd90e216ce601b25d492ed8c1a3b6a6d77703e0f15ab3711a7", - "s390x-unknown-linux-gnu": "c5803644970eee931bb0581b3b64511d1a8612f67bc98951a7f7ab5581a9ed04", - "x86_64-apple-darwin": "4e71a3ce973be377ef18637826648bb936e2f9490f64a9e4f33a49bcc431d344", - "x86_64-pc-windows-msvc": "39acfcb3857d83eab054a3de11756ffc16b3d49c31393b9800dd2704d1f07fdf", - "aarch64-pc-windows-msvc": "599a8b7e12439cd95a201dbdfe95cf363146b1ff91f379555dafd86b170caab9", - "x86_64-unknown-linux-gnu": "3dec1ab70758a3467ac3313bbcdabf7a9b3016db5c072c4537e3cf0a9e6290f6", - "x86_64-unknown-linux-musl": "d0a2a6d3b1bb00dce2105377fda8aa79675d187f8d6d7010a42f651af25018dc", - "aarch64-apple-darwin-freethreaded": "d9c7b430b25bd3837dbb03f945dbe6b7bc526c5940ca96f5db7cdc42f6b2b801", - "aarch64-unknown-linux-gnu-freethreaded": "f383ef50d1da6ca511212e5ae601923b56636b87351fd5fc847e0ea0a19fa9b3", - "ppc64le-unknown-linux-gnu-freethreaded": "cb0e4ff781b856a47f0f461ceb41c78c7eeff65effd0957857ec4702ef1e1bd3", - "riscv64-unknown-linux-gnu-freethreaded": "929223470d11a55cd75f880ac3bd4969e42407e2cdf08d4e7e38ba721cf4abec", - "s390x-unknown-linux-gnu-freethreaded": "613fb1f7b249f798b52af957d181305244e936c8e5c94c84688fcdf93fe14253", - "x86_64-apple-darwin-freethreaded": "b3196f6b57bbb3dc2ee07f348f1d51117ffa376979eceafbf50c15f0f7980bf8", - "x86_64-pc-windows-msvc-freethreaded": "b81de5fc9e783ea6dfcf1098c28a278c874999c71afbb0309f6a8b4276c769d0", - "aarch64-pc-windows-msvc-freethreaded": "40266e60f655e49cd1d5303295255909a4b593b08b88be6e6a55b2c9fe6ed13d", - "x86_64-unknown-linux-gnu-freethreaded": "f4acbef0fbfaf7ab31ac63986da1d93dfa1c5cb797de1dcdc1a988aa18670120", - }, - "strip_prefix": { - "aarch64-apple-darwin": "python", - "aarch64-unknown-linux-gnu": "python", - "ppc64le-unknown-linux-gnu": "python", - "s390x-unknown-linux-gnu": "python", - "riscv64-unknown-linux-gnu": "python", - "x86_64-apple-darwin": "python", - "x86_64-pc-windows-msvc": "python", - "aarch64-pc-windows-msvc": "python", - "x86_64-unknown-linux-gnu": "python", - "x86_64-unknown-linux-musl": "python", - "aarch64-apple-darwin-freethreaded": "python/install", - "aarch64-unknown-linux-gnu-freethreaded": "python/install", - "ppc64le-unknown-linux-gnu-freethreaded": "python/install", - "riscv64-unknown-linux-gnu-freethreaded": "python/install", - "s390x-unknown-linux-gnu-freethreaded": "python/install", - "x86_64-apple-darwin-freethreaded": "python/install", - "x86_64-pc-windows-msvc-freethreaded": "python/install", - "aarch64-pc-windows-msvc-freethreaded": "python/install", - "x86_64-unknown-linux-gnu-freethreaded": "python/install", - }, - }, - "3.14.1": { - "url": "20251202/cpython-{python_version}+20251202-{platform}-{build}.{ext}", - "sha256": { - "aarch64-apple-darwin": "cdf1ba0789f529fa34bb5b5619c5da9757ac1067d6b8dd0ee8b78e50078fc561", - "aarch64-unknown-linux-gnu": "5dde7dba0b8ef34c0d5cb8a721254b1e11028bfc09ff06664879c245fe8df73f", - "ppc64le-unknown-linux-gnu": "d2774701d53e2ac06f8c8c8e52dfa4ff346890de9b417c9a7664195443a4c766", - "riscv64-unknown-linux-gnu": "af840506efbcd5026d9140c0a0230e45e46bb1f339a65c10a22875930b2c0159", - "s390x-unknown-linux-gnu": "43f8f79bf4c66689d2019f193671d1df3e5e5dbb293382036285e8ce55fc55bb", - "x86_64-apple-darwin": "f25ce050e1d370f9c05c9623b769ffa4b269a6ae17e611b435fd2b8b09972a88", - "x86_64-pc-windows-msvc": "cb478a5a37eb93ce4d3c27ae64d211d6a5a42475ae53f666a8d1570e71fcf409", - "aarch64-pc-windows-msvc": "19129cf8b4d68c4e64c25bae43bca139d871267b59cf7f02b9dcf25f0bf59497", - "x86_64-unknown-linux-gnu": "a72f313bad49846e5e9671af2be7476033a877c80831cf47f431400ccb520090", - "x86_64-unknown-linux-musl": "15d50b15713097c38c67b1a06a0498ad102377f9b3999e98e4eefd6bf91bd82d", - "aarch64-apple-darwin-freethreaded": "61f38e947449cf00f32f0838e813358f6bf61025d0797531e5b8b8b175c617f0", - "aarch64-unknown-linux-gnu-freethreaded": "1a88a1fe21eb443d280999464b1a397605a7ca950d8ab73813ca6868835439a2", - "ppc64le-unknown-linux-gnu-freethreaded": "7207b736ed2569f307649ffd4b615a5346631bc244730b8702babee377cef528", - "riscv64-unknown-linux-gnu-freethreaded": "d1356ccd279920edc31bf0350674d966beb9522f9503846ed7855dbb109ccc14", - "s390x-unknown-linux-gnu-freethreaded": "477758eabc06dbc7e5e5d16e97c4672478acd409f420dd2e1b84d3452c0668d1", - "x86_64-apple-darwin-freethreaded": "c2cb2a9b44285fbc13c3c9b7eea813db6ed8d94909406b059db7afd39b32e786", - "x86_64-pc-windows-msvc-freethreaded": "8ef7048315cac6d26bdbef18512a87b1a24fffa21cec86e32f9a9425f2af9bf6", - "aarch64-pc-windows-msvc-freethreaded": "ddb10b645de2b1f6f2832a80b115a9cd34a4a760249983027efe46618a8efc48", - "x86_64-unknown-linux-gnu-freethreaded": "c5d5b89aab7de683e465e36de2477a131435076badda775ef6e9ea21109c1c32", - }, - "strip_prefix": { - "aarch64-apple-darwin": "python", - "aarch64-unknown-linux-gnu": "python", - "ppc64le-unknown-linux-gnu": "python", - "s390x-unknown-linux-gnu": "python", - "riscv64-unknown-linux-gnu": "python", - "x86_64-apple-darwin": "python", - "x86_64-pc-windows-msvc": "python", - "aarch64-pc-windows-msvc": "python", - "x86_64-unknown-linux-gnu": "python", - "x86_64-unknown-linux-musl": "python", - "aarch64-apple-darwin-freethreaded": "python/install", - "aarch64-unknown-linux-gnu-freethreaded": "python/install", - "ppc64le-unknown-linux-gnu-freethreaded": "python/install", - "riscv64-unknown-linux-gnu-freethreaded": "python/install", - "s390x-unknown-linux-gnu-freethreaded": "python/install", - "x86_64-apple-darwin-freethreaded": "python/install", - "x86_64-pc-windows-msvc-freethreaded": "python/install", - "aarch64-pc-windows-msvc-freethreaded": "python/install", - "x86_64-unknown-linux-gnu-freethreaded": "python/install", - }, - }, - "3.14.2": { - "url": "20251209/cpython-{python_version}+20251209-{platform}-{build}.{ext}", - "sha256": { - "aarch64-apple-darwin": "2f74bd26bd16487aca357c879d11f7b16c0521328e5148a1930ab6357bcb89fe", - "aarch64-unknown-linux-gnu": "869af31b2963194e8a2ecfadc36027c4c1c86a10f4960baec36dadb41b2acf02", - "ppc64le-unknown-linux-gnu": "86129976403fb5d64cf576329f94148f28cf6f82834e94df81ff31e9d5f404e0", - "riscv64-unknown-linux-gnu": "318dceecf119ea903aef1fb03a552cc592ecd61c08da891b68f5755e21e13511", - "s390x-unknown-linux-gnu": "53875c849a14194344ead1d9cd1e128cadd42a4b83c35eeb212417909ef05a6a", - "x86_64-apple-darwin": "58fa3e17d13ab956fd11055fb774c98ecfddcdf3b588e5f2369bdbc14ef9d76a", - "x86_64-pc-windows-msvc": "0d660bba9f58cb552e7e99e1f96a9c67b41618c9b8d29f9f3515fe2b5ad1966e", - "aarch64-pc-windows-msvc": "0be0d2557d73efa7f6f3f99679f05252d57fe2aad2d81cac3cad410a9b1eacbd", - "x86_64-unknown-linux-gnu": "121c3249bef497adf601df76a4d89aed6053fc5ec2f8c0ec656b86f0142e8ddd", - "x86_64-unknown-linux-musl": "71639cc5d1fb79840467531c5b53ca77170a58edd3f7e2d29330dd736e477469", - "aarch64-apple-darwin-freethreaded": "d6d17b8ef28326552cdeb2a7541c8a0cb711b378df9b93ebdb461dca065edfea", - "aarch64-unknown-linux-gnu-freethreaded": "adfcb90f3a7e1b3fbc6a99f9c8c8dce1f2e26ea72b724bbe4e9fa39e81e2b0db", - "ppc64le-unknown-linux-gnu-freethreaded": "2b1ce0c5a5f5e5add7e4f934f5bd35ac41660895a30b3098db7f7303d6952a4f", - "riscv64-unknown-linux-gnu-freethreaded": "4efb610fa07a6ee2639d14d78fc3b6ecb47431c14e1e4bda03c7f7dd60a5c1e5", - "s390x-unknown-linux-gnu-freethreaded": "e62f3bb3e66dac6c459690f9e9cd8cc2f6fe1dcf8bfed452af4c3df24cd7874f", - "x86_64-apple-darwin-freethreaded": "1fd76c79f7fc1753e8d2ed2f71406c0b65776c75f3e95ed99ffde8c95af2adc1", - "x86_64-pc-windows-msvc-freethreaded": "9927951e3997c186d2813ca1a0f4a8f5a2f771463f7f8ad0752fd3d2be2b74e4", - "aarch64-pc-windows-msvc-freethreaded": "43aac5bb4cdba71fc6775d26f47348d573a0b1210911438be71d7d96f4b18b51", - "x86_64-unknown-linux-gnu-freethreaded": "3728872ffd74989a7b4bbf3f0c629ae8fe821cda2bd6544012c1b92b9f5d5a5b", - }, - "strip_prefix": { - "aarch64-apple-darwin": "python", - "aarch64-unknown-linux-gnu": "python", - "ppc64le-unknown-linux-gnu": "python", - "s390x-unknown-linux-gnu": "python", - "riscv64-unknown-linux-gnu": "python", - "x86_64-apple-darwin": "python", - "x86_64-pc-windows-msvc": "python", - "aarch64-pc-windows-msvc": "python", - "x86_64-unknown-linux-gnu": "python", - "x86_64-unknown-linux-musl": "python", - "aarch64-apple-darwin-freethreaded": "python/install", - "aarch64-unknown-linux-gnu-freethreaded": "python/install", - "ppc64le-unknown-linux-gnu-freethreaded": "python/install", - "riscv64-unknown-linux-gnu-freethreaded": "python/install", - "s390x-unknown-linux-gnu-freethreaded": "python/install", - "x86_64-apple-darwin-freethreaded": "python/install", - "x86_64-pc-windows-msvc-freethreaded": "python/install", - "aarch64-pc-windows-msvc-freethreaded": "python/install", - "x86_64-unknown-linux-gnu-freethreaded": "python/install", - }, - }, - "3.14.3": { - "url": "20260325/cpython-{python_version}+20260325-{platform}-{build}.{ext}", - "sha256": { - "aarch64-apple-darwin": "80c996c23aab828134821f078a8a77a6f33f3f2c14000f071718c540e20c64d4", - "aarch64-unknown-linux-gnu": "6faf5478f910741c477830f5fd842011208af0f9678faf77106c9421b325bfc1", - "ppc64le-unknown-linux-gnu": "5eafe32e12f33f98c40de920482b013170dcf97d8c7f5dc780271ccf4cded76a", - "riscv64-unknown-linux-gnu": "481d3faef258964e57b7102c63de12b2bb388c7ed07cfe456f33e63b4e061202", - "s390x-unknown-linux-gnu": "d706eae2f4d963187b7c866603aed75d7eb3ea59590b06fb34f5fd7d0fe8e432", - "x86_64-apple-darwin": "847a49fea36c066f8df7a57cd8c4c02d17667e25d30b7930e8f8ba15e72d7efc", - "x86_64-pc-windows-msvc": "8b4e1329c4901ce2c0f1c20ac5d2ffa62fc13f12e26b5d1e5a1000f910f980d4", - "aarch64-pc-windows-msvc": "b35fe7c2fe169574f382cef125e95cbd904ddcb98fc337167356371b6d2e8c60", - "x86_64-unknown-linux-gnu": "18270c5a7b1a572599df5e68b497ba5254811dac43ba6f542245807d821fcb44", - "x86_64-unknown-linux-musl": "726a28734d2878a637b0d16ce07ce24c7d6ca1043d8e6f4a23b1b0a3478eedb9", - "aarch64-apple-darwin-freethreaded": "80c996c23aab828134821f078a8a77a6f33f3f2c14000f071718c540e20c64d4", - "aarch64-unknown-linux-gnu-freethreaded": "6faf5478f910741c477830f5fd842011208af0f9678faf77106c9421b325bfc1", - "ppc64le-unknown-linux-gnu-freethreaded": "5eafe32e12f33f98c40de920482b013170dcf97d8c7f5dc780271ccf4cded76a", - "riscv64-unknown-linux-gnu-freethreaded": "481d3faef258964e57b7102c63de12b2bb388c7ed07cfe456f33e63b4e061202", - "s390x-unknown-linux-gnu-freethreaded": "d706eae2f4d963187b7c866603aed75d7eb3ea59590b06fb34f5fd7d0fe8e432", - "x86_64-apple-darwin-freethreaded": "847a49fea36c066f8df7a57cd8c4c02d17667e25d30b7930e8f8ba15e72d7efc", - "x86_64-pc-windows-msvc-freethreaded": "8b4e1329c4901ce2c0f1c20ac5d2ffa62fc13f12e26b5d1e5a1000f910f980d4", - "aarch64-pc-windows-msvc-freethreaded": "b35fe7c2fe169574f382cef125e95cbd904ddcb98fc337167356371b6d2e8c60", - "x86_64-unknown-linux-gnu-freethreaded": "18270c5a7b1a572599df5e68b497ba5254811dac43ba6f542245807d821fcb44", - }, - "strip_prefix": "python", - }, - "3.14.4": { - "url": "20260414/cpython-{python_version}+20260414-{platform}-{build}.{ext}", - "sha256": { - "aarch64-apple-darwin": "8b7865e511b17093e090449bf71eb52933c17d45ad5257ddeacaffbb2c7239df", - "aarch64-unknown-linux-gnu": "5c8db1c21023316adad827a46d917bbbd6a85ae4e39bc3a58febda712c2f963d", - "ppc64le-unknown-linux-gnu": "055977a09de092744bbb22db64144e6afef8592eaac5e2bce4cca33f2592281a", - "riscv64-unknown-linux-gnu": "e959df167c502fb0bbcacc31a997e25c6b0ff6b5e496321b691955aa702d0c09", - "s390x-unknown-linux-gnu": "35f70ad05b2c4045889ee0c3d93f61b012654c1d91e10e671f0e5b4d4a6c6637", - "x86_64-apple-darwin": "9ecb2b942e6698c04af10a63a3d73c0b2e8d8e11ce44933fbffe8651bef4577d", - "x86_64-pc-windows-msvc": "9647bb46d3c236e34c1c11bbb7113444d9711811f0d11c39956168807a955b1a", - "aarch64-pc-windows-msvc": "82613380d582d806e562d7701496c34c87753ab13c37aa0afe2039003651f389", - "x86_64-unknown-linux-gnu": "e17275eaf95ceb5877aa6816e209b7733f41fee401d39c3921b88fb73fc4a4ba", - "x86_64-unknown-linux-musl": "12687a989a2384665577e1ef9864f33d4c074a1e69b38a8bac8d656531aefa3e", - "aarch64-apple-darwin-freethreaded": "8b7865e511b17093e090449bf71eb52933c17d45ad5257ddeacaffbb2c7239df", - "aarch64-unknown-linux-gnu-freethreaded": "5c8db1c21023316adad827a46d917bbbd6a85ae4e39bc3a58febda712c2f963d", - "ppc64le-unknown-linux-gnu-freethreaded": "055977a09de092744bbb22db64144e6afef8592eaac5e2bce4cca33f2592281a", - "riscv64-unknown-linux-gnu-freethreaded": "e959df167c502fb0bbcacc31a997e25c6b0ff6b5e496321b691955aa702d0c09", - "s390x-unknown-linux-gnu-freethreaded": "35f70ad05b2c4045889ee0c3d93f61b012654c1d91e10e671f0e5b4d4a6c6637", - "x86_64-apple-darwin-freethreaded": "9ecb2b942e6698c04af10a63a3d73c0b2e8d8e11ce44933fbffe8651bef4577d", - "x86_64-pc-windows-msvc-freethreaded": "9647bb46d3c236e34c1c11bbb7113444d9711811f0d11c39956168807a955b1a", - "aarch64-pc-windows-msvc-freethreaded": "82613380d582d806e562d7701496c34c87753ab13c37aa0afe2039003651f389", - "x86_64-unknown-linux-gnu-freethreaded": "e17275eaf95ceb5877aa6816e209b7733f41fee401d39c3921b88fb73fc4a4ba", - }, - "strip_prefix": "python", - }, - "3.15.0a1": { - "url": "20251031/cpython-{python_version}+20251031-{platform}-{build}.{ext}", - "sha256": { - "aarch64-apple-darwin": "3acf7aa3559b746498b18929456c5cacb84bae4e09249834cbc818970d71de87", - "aarch64-unknown-linux-gnu": "d55c2aeece827e6bec83fd18515ee281d9ea0efaa3e2d20130db8f1c7cbb71c6", - "ppc64le-unknown-linux-gnu": "c28beda791c499b16f06256339522f0002a3e9acba003e6b8374755d7be1def2", - "riscv64-unknown-linux-gnu": "36619f576b8154e4b56643c5c4a85c352f152df2989c4e602cbbe9c2b7ded870", - "s390x-unknown-linux-gnu": "5ea47be2a3a563ddd87ff510dae26b7aa7f3855ca00c5f1056ff8114c067c4e4", - "x86_64-apple-darwin": "0ab19d3ac25f99da438b088751e5ec2421f9f6aa4292fd2dc0f8e49eb3e16bdf", - "x86_64-pc-windows-msvc": "5f5d6bec2b381cfc771c49972d2a6f7b7e7ab6a1651d8fb6ef3983f3571722b3", - "aarch64-pc-windows-msvc": "1508bcd7195008479ed156aad3afbb3a3793097ed530690f0304a8107f0e53e8", - "x86_64-unknown-linux-gnu": "1f356288c2b2713619cb7a4e453d33bf8882f812af2987e21e01e7ae382fefba", - "x86_64-unknown-linux-musl": "caf5311f333eef082dd69a669ca65aceba09a08fc1e78aad602ad649106f294c", - "aarch64-apple-darwin-freethreaded": "12f1b16be4017181ad67904caf9e59e525b9b5d62f49105017d837e27b832959", - "aarch64-unknown-linux-gnu-freethreaded": "981fe8dfc6e7e1d0ffefa945a18d5c4c759bbe21722acf3a5cc7e62f16aa5f3c", - "ppc64le-unknown-linux-gnu-freethreaded": "088400dec25139f38eeecb48f090ff2ce06a96a1dd79fa8f1dfec1cd1786f5ef", - "riscv64-unknown-linux-gnu-freethreaded": "938061a0a31a06672526885de36037ddefd8c4acdb09424691b7000a8c8f8d01", - "s390x-unknown-linux-gnu-freethreaded": "2003e7e40bb44b3db7bca81087bfb738fe6af40e5db61cda8e23b59bf55d409e", - "x86_64-apple-darwin-freethreaded": "64fc29e6c7a2f02a18645d968f1b3fc1d00d12a5ef3fcbb0d077fa8c62c08904", - "x86_64-pc-windows-msvc-freethreaded": "34abc5603e1b4131f753d29b7deac865b9277912b851cbed5a149cf3e6745d3d", - "aarch64-pc-windows-msvc-freethreaded": "54ca78dae455ece6fefbd7f5f287cc55d5ce197caf51921f6d871d15069d9489", - "x86_64-unknown-linux-gnu-freethreaded": "0e0272186d9f5169394dbc4d4d72a3f4a5762a04c2e5ac2ab1e23aa41fc8538a", - }, - "strip_prefix": { - "aarch64-apple-darwin": "python", - "aarch64-unknown-linux-gnu": "python", - "ppc64le-unknown-linux-gnu": "python", - "s390x-unknown-linux-gnu": "python", - "riscv64-unknown-linux-gnu": "python", - "x86_64-apple-darwin": "python", - "x86_64-pc-windows-msvc": "python", - "aarch64-pc-windows-msvc": "python", - "x86_64-unknown-linux-gnu": "python", - "x86_64-unknown-linux-musl": "python", - "aarch64-apple-darwin-freethreaded": "python/install", - "aarch64-unknown-linux-gnu-freethreaded": "python/install", - "ppc64le-unknown-linux-gnu-freethreaded": "python/install", - "riscv64-unknown-linux-gnu-freethreaded": "python/install", - "s390x-unknown-linux-gnu-freethreaded": "python/install", - "x86_64-apple-darwin-freethreaded": "python/install", - "x86_64-pc-windows-msvc-freethreaded": "python/install", - "aarch64-pc-windows-msvc-freethreaded": "python/install", - "x86_64-unknown-linux-gnu-freethreaded": "python/install", - }, - }, - "3.15.0a2": { - "url": "20251209/cpython-{python_version}+20251209-{platform}-{build}.{ext}", - "sha256": { - "aarch64-apple-darwin": "5851f3744fbd39e3e323844cf4f68d7763fb25546aa5ffbb71b1b5ab69c56616", - "aarch64-unknown-linux-gnu": "17ba65d669be3052524e03b4d1426c072ef38df2a9065ff4525d1f4d1bc9f82c", - "ppc64le-unknown-linux-gnu": "5585bd7c5eefe28b9bf544d902cad9a2f81f33c618f2a1d3c006cbfcdec77abc", - "riscv64-unknown-linux-gnu": "bb7252edaffd422bd1c044a4764dfcf83a5d7159942f445abbef524e54ea79a0", - "s390x-unknown-linux-gnu": "03a90ffa9f92d4cf4caeefb9d15f0b39c05c1e60ade6688f32165f957db4f8f3", - "x86_64-apple-darwin": "cee576de4919cd422dbc31eb85d3c145ee82acec84f651daaf32dc669b5149c9", - "x86_64-pc-windows-msvc": "e538475ee249eacf63bfdae0e70af73e9c47360e6dd3d6825e7a35107e177de5", - "aarch64-pc-windows-msvc": "39bc2fcac13aeba7d650f76badf63350a81c86167a62174cb092eab7a749f4a5", - "x86_64-unknown-linux-gnu": "58addaabfab2de422180d32543fb3878ffc984c8a2e4005ff658a5cd83b31fc7", - "x86_64-unknown-linux-musl": "dcf844400dc2e7f5f3604e994532e4d49db45f4deefe9afdf6809ca1bc6532ee", - "aarch64-apple-darwin-freethreaded": "5b34488580df13df051a2e84e43cfca2ab28fdd7a61052f35988eb8b481b894a", - "aarch64-unknown-linux-gnu-freethreaded": "0c2c83236f6e28c103e2660a82be94b2459ee8cfdd90f5dd82f0d503ca2aec09", - "ppc64le-unknown-linux-gnu-freethreaded": "216842df2377fd032f279ded7fd23d7bdbd92d4c1fa7619523bc0dbdef5bd212", - "riscv64-unknown-linux-gnu-freethreaded": "2a8b56f318d2e21b01b54909554c53d81871b9bb05d23ea7808dde9acec4dc7e", - "s390x-unknown-linux-gnu-freethreaded": "06c4ca3983aad20723f68786e3663ab49fee1bf09326f341649205ed79d34fc6", - "x86_64-apple-darwin-freethreaded": "4d8102b70ea9fe726ee3ae9ad9e9bc4cbe0b6ed18f7989c81aef81de578f0163", - "x86_64-pc-windows-msvc-freethreaded": "6ff71bac78d650ce621fe6db49f06290e48bcceb61f69cccc7728584f70b6346", - "aarch64-pc-windows-msvc-freethreaded": "3d99152b4e29b947fb1cfc8d035d1d511e50aeed72886ff4a5fd0a3694bd0b51", - "x86_64-unknown-linux-gnu-freethreaded": "70f552e213734c0e260a57603bee504dd7ed0e78a10558b591e724ea8730fef5", - }, - "strip_prefix": { - "aarch64-apple-darwin": "python", - "aarch64-unknown-linux-gnu": "python", - "ppc64le-unknown-linux-gnu": "python", - "s390x-unknown-linux-gnu": "python", - "riscv64-unknown-linux-gnu": "python", - "x86_64-apple-darwin": "python", - "x86_64-pc-windows-msvc": "python", - "aarch64-pc-windows-msvc": "python", - "x86_64-unknown-linux-gnu": "python", - "x86_64-unknown-linux-musl": "python", - "aarch64-apple-darwin-freethreaded": "python/install", - "aarch64-unknown-linux-gnu-freethreaded": "python/install", - "ppc64le-unknown-linux-gnu-freethreaded": "python/install", - "riscv64-unknown-linux-gnu-freethreaded": "python/install", - "s390x-unknown-linux-gnu-freethreaded": "python/install", - "x86_64-apple-darwin-freethreaded": "python/install", - "x86_64-pc-windows-msvc-freethreaded": "python/install", - "aarch64-pc-windows-msvc-freethreaded": "python/install", - "x86_64-unknown-linux-gnu-freethreaded": "python/install", - }, - }, - "3.15.0a8": { - "url": "20260414/cpython-{python_version}+20260414-{platform}-{build}.{ext}", - "sha256": { - "aarch64-apple-darwin": "780d46b3da0e58e15c620d9e7dfd29b54c8359c195f625858f85df9c2c7ecc32", - "aarch64-unknown-linux-gnu": "8f6dda4d8ff44976f1aa6a94674a09a503dc50b015297e1b62c8cdc591c90f4f", - "ppc64le-unknown-linux-gnu": "09f076c63fadbf675143674aa3b23229482b9a44840b8b1808a216def2a9af15", - "riscv64-unknown-linux-gnu": "9d41ce752e8b731872f0f5c9c48199e63c789d24ce3ae9e91d6c8008f36e7c51", - "s390x-unknown-linux-gnu": "1de2593c40cce2d8ea883f8c8580223bfa1478cbd9d0191ba3640aed083c2202", - "x86_64-apple-darwin": "a7744d34148969a2ec010da6f0a46ddeceda7c02e5cdfa2b4e1811487381491a", - "x86_64-pc-windows-msvc": "3ded476f676fdf260d56a5e49aa083d5ffd218fc3390e4480ed42bee1acfb3fb", - "aarch64-pc-windows-msvc": "10fb470e900e65df4e37f8deaf1726397c914861ffc37b43ae3743a7eee88377", - "x86_64-unknown-linux-gnu": "c93f4b15287ac48d7e3a475b245cb59cc51079382747e3e6213d6406c158969d", - "x86_64-unknown-linux-musl": "9fbd6f243a424d4ae973e72aa0075122a7cfe05ac8f6cfde986e7b00d0dbc0bf", - "aarch64-apple-darwin-freethreaded": "780d46b3da0e58e15c620d9e7dfd29b54c8359c195f625858f85df9c2c7ecc32", - "aarch64-unknown-linux-gnu-freethreaded": "8f6dda4d8ff44976f1aa6a94674a09a503dc50b015297e1b62c8cdc591c90f4f", - "ppc64le-unknown-linux-gnu-freethreaded": "09f076c63fadbf675143674aa3b23229482b9a44840b8b1808a216def2a9af15", - "riscv64-unknown-linux-gnu-freethreaded": "9d41ce752e8b731872f0f5c9c48199e63c789d24ce3ae9e91d6c8008f36e7c51", - "s390x-unknown-linux-gnu-freethreaded": "1de2593c40cce2d8ea883f8c8580223bfa1478cbd9d0191ba3640aed083c2202", - "x86_64-apple-darwin-freethreaded": "a7744d34148969a2ec010da6f0a46ddeceda7c02e5cdfa2b4e1811487381491a", - "x86_64-pc-windows-msvc-freethreaded": "3ded476f676fdf260d56a5e49aa083d5ffd218fc3390e4480ed42bee1acfb3fb", - "aarch64-pc-windows-msvc-freethreaded": "10fb470e900e65df4e37f8deaf1726397c914861ffc37b43ae3743a7eee88377", - "x86_64-unknown-linux-gnu-freethreaded": "c93f4b15287ac48d7e3a475b245cb59cc51079382747e3e6213d6406c158969d", - }, - "strip_prefix": "python", - }, -} +DEFAULT_RELEASE_BASE_URL = _GITHUB_PREFIX +DEFAULT_RELEASE_BASE_URLS = [ + _GITHUB_PREFIX, + _ASTRAL_PREFIX, + _LEGACY_GITHUB_PREFIX, +] # buildifier: disable=unsorted-dict-items MINOR_MAPPING = { @@ -1426,26 +207,41 @@ def _generate_platforms(): PLATFORMS = _generate_platforms() -def get_release_info(platform, python_version, base_url = DEFAULT_RELEASE_BASE_URL, tool_versions = TOOL_VERSIONS): +def get_release_info(platform, python_version, base_urls = DEFAULT_RELEASE_BASE_URLS, tool_versions = None): """Resolve the release URL for the requested interpreter version Args: platform: The platform string for the interpreter python_version: The version of the interpreter to get - base_url: The URL to prepend to the 'url' attr in the tool_versions dict + base_urls: The list of URLs to prepend to the 'url' attr in the tool_versions dict tool_versions: A dict listing the interpreter versions, their SHAs and URL Returns: A tuple of (filename, url, archive strip prefix, patches, patch_strip) """ + if tool_versions == None: + tool_versions = TOOL_VERSIONS + + if type(base_urls) == type(""): + base_urls = [base_urls] + elif base_urls == None: + base_urls = DEFAULT_RELEASE_BASE_URLS - base_urls = [base_url] - if base_url == DEFAULT_RELEASE_BASE_URL or base_url.startswith(_GITHUB_PREFIX): - suffix = base_url[len(_GITHUB_PREFIX):] - base_urls.append(_ASTRAL_PREFIX + suffix) - elif base_url.startswith(_LEGACY_GITHUB_PREFIX): - suffix = base_url[len(_LEGACY_GITHUB_PREFIX):] - base_urls.append(_ASTRAL_PREFIX + suffix) + expanded_base_urls = [] + for b_url in base_urls: + if b_url not in expanded_base_urls: + expanded_base_urls.append(b_url) + if b_url.startswith(_GITHUB_PREFIX): + suffix = b_url[len(_GITHUB_PREFIX):] + a_url = _ASTRAL_PREFIX + suffix + if a_url not in expanded_base_urls: + expanded_base_urls.append(a_url) + elif b_url.startswith(_LEGACY_GITHUB_PREFIX): + suffix = b_url[len(_LEGACY_GITHUB_PREFIX):] + a_url = _ASTRAL_PREFIX + suffix + if a_url not in expanded_base_urls: + expanded_base_urls.append(a_url) + base_urls = expanded_base_urls url = tool_versions[python_version]["url"] @@ -1530,3 +326,67 @@ def gen_python_config_settings(name = ""): flag_values = PLATFORMS[platform].flag_values, constraint_values = PLATFORMS[platform].compatible_with, ) + +def _manifest_entry_sort_key(entry): + flavor_rank = {"full": 3, "install_only": 1, "install_only_stripped": 2}.get(entry.archive_flavor, 4) + microarch = entry.microarch + if not microarch: + microarch_rank = 0 + elif microarch.startswith("v") and microarch[1:].isdigit(): + microarch_rank = int(microarch[1:]) + else: + microarch_rank = 999 + return (flavor_rank, microarch_rank) + +def _tool_versions_from_manifest_entries(entries, base_url = DEFAULT_RELEASE_BASE_URL): + """Converts parsed manifest entries into the TOOL_VERSIONS dictionary format. + + Args: + entries: {type}`list[struct]` the parsed manifest entries. + base_url: {type}`str` fallback base URL for standalone distributions. + + Returns: + {type}`dict[str, dict]` the tool versions map. + """ + available_versions = {} + entries = sorted( + entries, + key = _manifest_entry_sort_key, + ) + + for entry in entries: + location = entry.location + sha256 = entry.sha256 + py_version = entry.python_version + + matched_platform = "{}-{}-{}".format(entry.arch, entry.vendor, entry.os) + if entry.libc: + matched_platform += "-" + entry.libc + if entry.freethreaded: + matched_platform += "-freethreaded" + + if matched_platform not in PLATFORMS: + continue + + archive_flavor = entry.archive_flavor + if archive_flavor not in ["install_only", "install_only_stripped", "full"]: + continue + + v_dict = available_versions.setdefault(py_version, {}) + if matched_platform in v_dict.get("sha256", {}): + continue + + if "://" in location: + urls = [location] + else: + urls = ["{}/{}".format(base_url, location)] + + strip_prefix = "python/install" if archive_flavor == "full" else "python" + + v_dict.setdefault("sha256", {})[matched_platform] = sha256 + v_dict.setdefault("url", {})[matched_platform] = urls + v_dict.setdefault("strip_prefix", {})[matched_platform] = strip_prefix + + return available_versions + +TOOL_VERSIONS = _tool_versions_from_manifest_entries(parse_runtime_manifest(MANIFEST_TEXT)) diff --git a/python/zipapp/BUILD.bazel b/python/zipapp/BUILD.bazel index 71249ae45c..77a8752701 100644 --- a/python/zipapp/BUILD.bazel +++ b/python/zipapp/BUILD.bazel @@ -11,19 +11,20 @@ filegroup( ) bzl_library( - name = "py_zipapp_binary_bzl", + name = "py_zipapp_binary", srcs = ["py_zipapp_binary.bzl"], deps = [ - "//python/private:util_bzl", - "//python/private/zipapp:py_zipapp_rule_bzl", + "//python/private:util", + "//python/private/zipapp:py_zipapp_rule", ], ) +# keep bzl_library( - name = "py_zipapp_test_bzl", + name = "py_zipapp_test", srcs = ["py_zipapp_test.bzl"], deps = [ - "//python/private:util_bzl", - "//python/private/zipapp:py_zipapp_rule_bzl", + "//python/private:util", + "//python/private/zipapp:py_zipapp_rule", ], ) diff --git a/replicate_ci b/replicate_ci new file mode 100755 index 0000000000..dd9b67e93e --- /dev/null +++ b/replicate_ci @@ -0,0 +1,167 @@ +#!/usr/bin/env python3 + +import argparse +import os +import shlex +import subprocess +import sys + +import yaml + + +def parse_args(): + parser = argparse.ArgumentParser( + description="Replicate and emulate BazelCI job configurations from presubmit.yml." + ) + parser.add_argument( + "job", + help="The key or name of the CI job to emulate (e.g., ubuntu_workspace).", + ) + return parser.parse_args() + + +def run_cmd(cmd, cwd=None, env=None, shell=False): + if shell: + cmd_str = cmd if isinstance(cmd, str) else " ".join(cmd) + else: + cmd_str = shlex.join(cmd) if isinstance(cmd, list) else str(cmd) + + print(f"\n🚀 Executing: {cmd_str}") + if cwd and cwd != os.getcwd(): + print(f"📁 Directory: {cwd}") + if env and "USE_BAZEL_VERSION" in env: + print(f"🔧 Bazel Version: {env['USE_BAZEL_VERSION']}") + + res = subprocess.run(cmd, cwd=cwd, env=env, shell=shell) + if res.returncode != 0: + print( + f"\n❌ Command failed with return code {res.returncode}: {cmd_str}", + file=sys.stderr, + ) + return False + return True + + +def resolve_bazel_version(task_bazel): + if not task_bazel or task_bazel.startswith("${{"): + return None + return task_bazel + + +def execute_ci_job(job_key, task, repo_root): + job_name = task.get("name", job_key) + print(f"\n{'=' * 80}\n🎯 Replicating CI Job: {job_key} ('{job_name}')\n{'=' * 80}") + + # Setup working directory + cwd = repo_root + if "working_directory" in task: + cwd = os.path.join(repo_root, task["working_directory"]) + if not os.path.exists(cwd): + print( + f"❌ Error: working_directory '{task['working_directory']}' does not exist at '{cwd}'", + file=sys.stderr, + ) + return False + + # Setup environment + env = os.environ.copy() + bzl_version = resolve_bazel_version(task.get("bazel")) + if bzl_version: + env["USE_BAZEL_VERSION"] = bzl_version + + # Execute pre-commands + is_windows = sys.platform.startswith("win") + pre_cmds = task.get("batch_commands" if is_windows else "shell_commands", []) + for pre_cmd in pre_cmds: + if not run_cmd(pre_cmd, cwd=cwd, env=env, shell=True): + return False + + # Execute Build Targets + build_targets = [t for t in task.get("build_targets", []) if t != "--"] + if build_targets: + build_flags = task.get("build_flags", []) + cmd = ["bazel", "build"] + build_flags + ["--"] + build_targets + if not run_cmd(cmd, cwd=cwd, env=env): + return False + + # Execute Test Targets + test_targets = [t for t in task.get("test_targets", []) if t != "--"] + if test_targets: + test_flags = task.get("test_flags", []) + if "--build_tests_only" not in test_flags: + test_flags = ["--build_tests_only"] + test_flags + cmd = ["bazel", "test"] + test_flags + ["--"] + test_targets + if not run_cmd(cmd, cwd=cwd, env=env): + return False + + # Execute Coverage Targets + coverage_targets = [t for t in task.get("coverage_targets", []) if t != "--"] + if coverage_targets: + coverage_flags = task.get("test_flags", []) + cmd = ["bazel", "coverage"] + coverage_flags + ["--"] + coverage_targets + if not run_cmd(cmd, cwd=cwd, env=env): + return False + + print(f"\n🎉 Successfully replicated CI Job: {job_key}") + return True + + +def main(): + args = parse_args() + + repo_root = os.path.abspath(os.path.dirname(__file__)) + presubmit_path = os.path.join(repo_root, ".bazelci/presubmit.yml") + if not os.path.exists(presubmit_path): + print( + f"❌ Error: Presubmit file not found at '{presubmit_path}'", + file=sys.stderr, + ) + sys.exit(1) + + with open(presubmit_path) as f: + presubmit = yaml.safe_load(f) + + tasks = presubmit.get("tasks", {}) + if not tasks: + print( + f"❌ Error: No tasks found in '{presubmit_path}'", + file=sys.stderr, + ) + sys.exit(1) + + # If no job specified, print available jobs and exit + if not args.job: + print("❌ Error: No CI job specified. Provide a job key.\n", file=sys.stderr) + print("📋 Available CI Job Keys:", file=sys.stderr) + for key in sorted(tasks.keys()): + name = tasks[key].get("name", key) + print(f" • {key} ({name})", file=sys.stderr) + sys.exit(1) + + # Match by key or by name + job_key = None + if args.job in tasks: + job_key = args.job + else: + for key, config in tasks.items(): + if config.get("name") == args.job: + job_key = key + break + + if not job_key: + print( + f"❌ Error: CI job '{args.job}' not found in '{presubmit_path}'\n", + file=sys.stderr, + ) + print("📋 Available CI Job Keys:", file=sys.stderr) + for key in sorted(tasks.keys()): + name = tasks[key].get("name", key) + print(f" • {key} ({name})", file=sys.stderr) + sys.exit(1) + + success = execute_ci_job(job_key, tasks[job_key], repo_root) + sys.exit(0 if success else 1) + + +if __name__ == "__main__": + main() diff --git a/sphinxdocs/.bazelignore b/sphinxdocs/.bazelignore new file mode 100644 index 0000000000..48237cb876 --- /dev/null +++ b/sphinxdocs/.bazelignore @@ -0,0 +1,12 @@ +bazel-bin +bazel-out +bazel-testlogs +bazel-sphinxdocs +integration_tests/bcr/bazel-bin +integration_tests/bcr/bazel-bcr +integration_tests/bcr/bazel-out +integration_tests/bcr/bazel-testlogs +integration_tests/persistent_worker/workspace/bazel-bin +integration_tests/persistent_worker/workspace/bazel-out +integration_tests/persistent_worker/workspace/bazel-testlogs +integration_tests/persistent_worker/workspace/bazel-workspace diff --git a/sphinxdocs/.bazelrc b/sphinxdocs/.bazelrc index caefd0af53..6989c2656a 100644 --- a/sphinxdocs/.bazelrc +++ b/sphinxdocs/.bazelrc @@ -15,8 +15,23 @@ build --enable_runfiles # Local disk cache greatly speeds up builds if the regular cache is lost common --disk_cache=~/.cache/bazel/bazel-disk-cache +common --experimental_downloader_config=downloader_config.cfg +common --http_timeout_scaling=10.0 +common --experimental_repository_downloader_retries=10 common --incompatible_python_disallow_native_rules common --incompatible_no_implicit_file_export +# Pass host PATH to build and test actions so bazel_from_env (used by rules_bazel_integration_test +# for local 'self' testing) can locate the user's bazel binary without disabling strict action env. +common --action_env=PATH +common --test_env=PATH 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/.bazelrc.deleted_packages b/sphinxdocs/.bazelrc.deleted_packages index 442c80e960..9a90ef21be 100644 --- a/sphinxdocs/.bazelrc.deleted_packages +++ b/sphinxdocs/.bazelrc.deleted_packages @@ -1 +1,2 @@ common --deleted_packages=integration_tests/bcr +common --deleted_packages=integration_tests/persistent_worker/workspace diff --git a/sphinxdocs/BUILD.bazel b/sphinxdocs/BUILD.bazel new file mode 100644 index 0000000000..d1eec30a09 --- /dev/null +++ b/sphinxdocs/BUILD.bazel @@ -0,0 +1,19 @@ +package(default_visibility = ["//visibility:public"]) + +exports_files(["pyproject.toml"]) + +filegroup( + name = "distribution", + srcs = glob( + ["**/*"], + exclude = [ + "bazel-*/**", + "integration_tests/**", + "tests/**", + ], + ) + [ + "//dev:distribution", + "//sphinxdocs:distribution", + ], + visibility = ["//visibility:public"], +) diff --git a/sphinxdocs/MODULE.bazel b/sphinxdocs/MODULE.bazel index 30fb2196dc..c1f06ccb50 100644 --- a/sphinxdocs/MODULE.bazel +++ b/sphinxdocs/MODULE.bazel @@ -8,7 +8,7 @@ bazel_dep(name = "bazel_skylib", version = "1.8.2") bazel_dep(name = "stardoc", version = "0.7.2", repo_name = "io_bazel_stardoc") bazel_dep(name = "platforms", version = "0.0.11") bazel_dep(name = "protobuf", version = "29.0-rc2", repo_name = "com_google_protobuf") -bazel_dep(name = "rules_python", version = "1.8.5") +bazel_dep(name = "rules_python", version = "2.2.0") dev_pip = use_extension( "@rules_python//python/extensions:pip.bzl", @@ -18,6 +18,40 @@ dev_pip = use_extension( dev_pip.parse( hub_name = "dev_pip", python_version = "3.11", - requirements_lock = "@rules_python//docs:requirements.txt", + requirements_lock = "//dev:requirements.txt", + uv_lock = "//dev:uv.lock", +) +use_repo(dev_pip, "dev_pip", "pypi") + +bazel_dep(name = "rules_bazel_integration_test", version = "0.37.1", dev_dependency = True) + +bazel_binaries = use_extension( + "@rules_bazel_integration_test//:extensions.bzl", + "bazel_binaries", + dev_dependency = True, +) +bazel_binaries.local( + name = "self", + path = "integration_tests/bazel_from_env", +) +use_repo( + bazel_binaries, + "bazel_binaries", + "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, ) -use_repo(dev_pip, "dev_pip") diff --git a/sphinxdocs/dev/BUILD.bazel b/sphinxdocs/dev/BUILD.bazel new file mode 100644 index 0000000000..41e88f2c59 --- /dev/null +++ b/sphinxdocs/dev/BUILD.bazel @@ -0,0 +1,30 @@ +load("@rules_python//python/uv:lock.bzl", "lock") + +package(default_visibility = ["//:__subpackages__"]) + +filegroup( + name = "distribution", + srcs = glob(["**/*"]), + visibility = ["//:__pkg__"], +) + +# Run bazel run //dev:uv_lock.update +lock( + name = "uv_lock", + srcs = ["//:pyproject.toml"], + out = "uv.lock", + python_version = "3.11", +) + +# Run bazel run //dev:requirements.update +lock( + name = "requirements", + srcs = ["//:pyproject.toml"], + out = "requirements.txt", + args = [ + "--emit-index-url", + "--universal", + "--upgrade", + ], + python_version = "3.11", +) diff --git a/docs/requirements.txt b/sphinxdocs/dev/requirements.txt similarity index 64% rename from docs/requirements.txt rename to sphinxdocs/dev/requirements.txt index 6397f0a7f1..e24834fe1e 100644 --- a/docs/requirements.txt +++ b/sphinxdocs/dev/requirements.txt @@ -1,15 +1,15 @@ # This file was autogenerated by uv via the following command: -# bazel run //docs:requirements.update +# bazel run //dev:requirements.update --index-url https://pypi.org/simple absl-py==2.3.1 ; python_full_version < '3.10' \ --hash=sha256:a97820526f7fbfd2ec1bce83f3f25e3a14840dac0d8e02a0b71cd75db3f77fc9 \ --hash=sha256:eeecf07f0c2a93ace0772c92e596ace6d3d3996c042b2128459aaae2a76de11d - # via rules-python-docs (docs/pyproject.toml) -absl-py==2.4.0 ; python_full_version >= '3.10' \ - --hash=sha256:88476fd881ca8aab94ffa78b7b6c632a782ab3ba1cd19c9bd423abc4fb4cd28d \ - --hash=sha256:8c6af82722b35cf71e0f4d1d47dcaebfff286e27110a99fc359349b247dfb5d4 - # via rules-python-docs (docs/pyproject.toml) + # via rules-python-sphinxdocs-dev (dev/pyproject.toml) +absl-py==2.5.0 ; python_full_version >= '3.10' \ + --hash=sha256:0c996f25c0490700fadabe6351630f6111534fa0ae252cc6d2014ea3b141135f \ + --hash=sha256:0f17b89f2a4eaaedc4f28c622998aa690564b3012a396a4ffad0821007fe03ba + # via rules-python-sphinxdocs-dev (dev/pyproject.toml) alabaster==0.7.16 ; python_full_version < '3.10' \ --hash=sha256:75a8b99c28a5dad50dd7f8ccdd447a121ddb3892da9e53d1ca5cca3106d58d65 \ --hash=sha256:b46733c07dce03ae4e150330b975c75737fa60f0a7c591b6c8bf4928a28e2c92 @@ -18,10 +18,6 @@ alabaster==1.0.0 ; python_full_version >= '3.10' \ --hash=sha256:c00dca57bca26fa62a6d7d0a9fcce65f3e026e9bfe33e9c538fd3fbb2144fd9e \ --hash=sha256:fc6786402dc3fcb2de3cabd5fe455a2db534b371124f1f21de8731783dec828b # via sphinx -altgraph==0.17.5 \ - --hash=sha256:c87b395dd12fabde9c99573a9749d67da8d29ef9de0125c7f536699b4a9bc9e7 \ - --hash=sha256:f3a22400bce1b0c701683820ac4f3b159cd301acab067c51c653e06961600597 - # via macholib astroid==3.3.11 \ --hash=sha256:1e5a5011af2920c7c67a53f65d536d65bfa7116feeaf2354d8b94f29573bb0ce \ --hash=sha256:54c760ae8322ece1abd213057c4b5bba7c49818853fc901ef09719a60dbf9dec @@ -30,140 +26,104 @@ babel==2.18.0 \ --hash=sha256:b80b99a14bd085fcacfa15c9165f651fbb3406e66cc603abf11c5750937c992d \ --hash=sha256:e2b422b277c2b9a9630c1d7903c2a00d0830c409c59ac8cae9081c92f1aeba35 # via sphinx -certifi==2026.2.25 \ - --hash=sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa \ - --hash=sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7 +certifi==2026.7.22 \ + --hash=sha256:62f22742b58a1a33014a2b6b706588a8d7e2a88ae7bd1a6ebe8c992928483775 \ + --hash=sha256:741e2c3b351ddf169a738da9f2c048608ff7f2c5cc02f1ebc6b118bb090d5d55 # via requests -charset-normalizer==3.4.7 \ - --hash=sha256:007d05ec7321d12a40227aae9e2bc6dca73f3cb21058999a1df9e193555a9dcc \ - --hash=sha256:03853ed82eeebbce3c2abfdbc98c96dc205f32a79627688ac9a27370ea61a49c \ - --hash=sha256:07d9e39b01743c3717745f4c530a6349eadbfa043c7577eef86c502c15df2c67 \ - --hash=sha256:08e721811161356f97b4059a9ba7bafb23ea5ee2255402c42881c214e173c6b4 \ - --hash=sha256:0c96c3b819b5c3e9e165495db84d41914d6894d55181d2d108cc1a69bfc9cce0 \ - --hash=sha256:0ea948db76d31190bf08bd371623927ee1339d5f2a0b4b1b4a4439a65298703c \ - --hash=sha256:0f7eb884681e3938906ed0434f20c63046eacd0111c4ba96f27b76084cd679f5 \ - --hash=sha256:12a6fff75f6bc66711b73a2f0addfc4c8c15a20e805146a02d147a318962c444 \ - --hash=sha256:12d8baf840cc7889b37c7c770f478adea7adce3dcb3944d02ec87508e2dcf153 \ - --hash=sha256:14265bfe1f09498b9d8ec91e9ec9fa52775edf90fcbde092b25f4a33d444fea9 \ - --hash=sha256:16d971e29578a5e97d7117866d15889a4a07befe0e87e703ed63cd90cb348c01 \ - --hash=sha256:177a0ba5f0211d488e295aaf82707237e331c24788d8d76c96c5a41594723217 \ - --hash=sha256:1a87ca9d5df6fe460483d9a5bbf2b18f620cbed41b432e2bddb686228282d10b \ - --hash=sha256:1c2a768fdd44ee4a9339a9b0b130049139b8ce3c01d2ce09f67f5a68048d477c \ - --hash=sha256:1c2aed2e5e41f24ea8ef1590b8e848a79b56f3a5564a65ceec43c9d692dc7d8a \ - --hash=sha256:1dc8b0ea451d6e69735094606991f32867807881400f808a106ee1d963c46a83 \ - --hash=sha256:1efde3cae86c8c273f1eb3b287be7d8499420cf2fe7585c41d370d3e790054a5 \ - --hash=sha256:202389074300232baeb53ae2569a60901f7efadd4245cf3a3bf0617d60b439d7 \ - --hash=sha256:203104ed3e428044fd943bc4bf45fa73c0730391f9621e37fe39ecf477b128cb \ - --hash=sha256:2257141f39fe65a3fdf38aeccae4b953e5f3b3324f4ff0daf9f15b8518666a2c \ - --hash=sha256:298930cec56029e05497a76988377cbd7457ba864beeea92ad7e844fe74cd1f1 \ - --hash=sha256:2cd4a60d0e2fb04537162c62bbbb4182f53541fe0ede35cdf270a1c1e723cc42 \ - --hash=sha256:2d6eb928e13016cea4f1f21d1e10c1cebd5a421bc57ddf5b1142ae3f86824fab \ - --hash=sha256:2fe249cb4651fd12605b7288b24751d8bfd46d35f12a20b1ba33dea122e690df \ - --hash=sha256:30b8d1d8c52a48c2c5690e152c169b673487a2a58de1ec7393196753063fcd5e \ - --hash=sha256:320ade88cfb846b8cd6b4ddf5ee9e80ee0c1f52401f2456b84ae1ae6a1a5f207 \ - --hash=sha256:3534e7dcbdcf757da6b85a0bbf5b6868786d5982dd959b065e65481644817a18 \ - --hash=sha256:36836d6ff945a00b88ba1e4572d721e60b5b8c98c155d465f56ad19d68f23734 \ - --hash=sha256:38c0109396c4cfc574d502df99742a45c72c08eff0a36158b6f04000043dbf38 \ - --hash=sha256:3946fa46a0cf3e4c8cb1cc52f56bb536310d34f25f01ca9b6c16afa767dab110 \ - --hash=sha256:3bec022aec2c514d9cf199522a802bd007cd588ab17ab2525f20f9c34d067c18 \ - --hash=sha256:3c9a494bc5ec77d43cea229c4f6db1e4d8fe7e1bbffa8b6f0f0032430ff8ab44 \ - --hash=sha256:3dce51d0f5e7951f8bb4900c257dad282f49190fdbebecd4ba99bcc41fef404d \ - --hash=sha256:3dedcc22d73ec993f42055eff4fcfed9318d1eeb9a6606c55892a26964964e48 \ - --hash=sha256:4042d5c8f957e15221d423ba781e85d553722fc4113f523f2feb7b188cc34c5e \ - --hash=sha256:481551899c856c704d58119b5025793fa6730adda3571971af568f66d2424bb5 \ - --hash=sha256:4dc1e73c36828f982bfe79fadf5919923f8a6f4df2860804db9a98c48824ce8d \ - --hash=sha256:4e5163c14bffd570ef2affbfdd77bba66383890797df43dc8b4cc7d6f500bf53 \ - --hash=sha256:511ef87c8aec0783e08ac18565a16d435372bc1ac25a91e6ac7f5ef2b0bff790 \ - --hash=sha256:532bc9bf33a68613fd7d65e4b1c71a6a38d7d42604ecf239c77392e9b4e8998c \ - --hash=sha256:54523e136b8948060c0fa0bc7b1b50c32c186f2fceee897a495406bb6e311d2b \ - --hash=sha256:5649fd1c7bade02f320a462fdefd0b4bd3ce036065836d4f42e0de958038e116 \ - --hash=sha256:56be790f86bfb2c98fb742ce566dfb4816e5a83384616ab59c49e0604d49c51d \ - --hash=sha256:5b77459df20e08151cd6f8b9ef8ef1f961ef73d85c21a555c7eed5b79410ec10 \ - --hash=sha256:5ed6ab538499c8644b8a3e18debabcd7ce684f3fa91cf867521a7a0279cab2d6 \ - --hash=sha256:6178f72c5508bfc5fd446a5905e698c6212932f25bcdd4b47a757a50605a90e2 \ - --hash=sha256:6370e8686f662e6a3941ee48ed4742317cafbe5707e36406e9df792cdb535776 \ - --hash=sha256:64f02c6841d7d83f832cd97ccf8eb8a906d06eb95d5276069175c696b024b60a \ - --hash=sha256:65bcd23054beab4d166035cabbc868a09c1a49d1efe458fe8e4361215df40265 \ - --hash=sha256:66671f93accb62ed07da56613636f3641f1a12c13046ce91ffc923721f23c008 \ - --hash=sha256:6696b7688f54f5af4462118f0bfa7c1621eeb87154f77fa04b9295ce7a8f2943 \ - --hash=sha256:6785f414ae0f3c733c437e0f3929197934f526d19dfaa75e18fdb4f94c6fb374 \ - --hash=sha256:67f6279d125ca0046a7fd386d01b311c6363844deac3e5b069b514ba3e63c246 \ - --hash=sha256:6c114670c45346afedc0d947faf3c7f701051d2518b943679c8ff88befe14f8e \ - --hash=sha256:6e0d51f618228538a3e8f46bd246f87a6cd030565e015803691603f55e12afb5 \ - --hash=sha256:6ed74185b2db44f41ef35fd1617c5888e59792da9bbc9190d6c7300617182616 \ - --hash=sha256:708838739abf24b2ceb208d0e22403dd018faeef86ddac04319a62ae884c4f15 \ - --hash=sha256:715479b9a2802ecac752a3b0efa2b0b60285cf962ee38414211abdfccc233b41 \ - --hash=sha256:733784b6d6def852c814bce5f318d25da2ee65dd4839a0718641c696e09a2960 \ - --hash=sha256:750e02e074872a3fad7f233b47734166440af3cdea0add3e95163110816d6752 \ - --hash=sha256:752a45dc4a6934060b3b0dab47e04edc3326575f82be64bc4fc293914566503e \ - --hash=sha256:7579e913a5339fb8fa133f6bbcfd8e6749696206cf05acdbdca71a1b436d8e72 \ - --hash=sha256:7641bb8895e77f921102f72833904dcd9901df5d6d72a2ab8f31d04b7e51e4e7 \ - --hash=sha256:7804338df6fcc08105c7745f1502ba68d900f45fd770d5bdd5288ddccb8a42d8 \ - --hash=sha256:80d04837f55fc81da168b98de4f4b797ef007fc8a79ab71c6ec9bc4dd662b15b \ - --hash=sha256:813c0e0132266c08eb87469a642cb30aaff57c5f426255419572aaeceeaa7bf4 \ - --hash=sha256:82b271f5137d07749f7bf32f70b17ab6eaabedd297e75dce75081a24f76eb545 \ - --hash=sha256:84c018e49c3bf790f9c2771c45e9313a08c2c2a6342b162cd650258b57817706 \ - --hash=sha256:8751d2787c9131302398b11e6c8068053dcb55d5a8964e114b6e196cf16cb366 \ - --hash=sha256:8778f0c7a52e56f75d12dae53ae320fae900a8b9b4164b981b9c5ce059cd1fcb \ - --hash=sha256:87fad7d9ba98c86bcb41b2dc8dbb326619be2562af1f8ff50776a39e55721c5a \ - --hash=sha256:8d828b6667a32a728a1ad1d93957cdf37489c57b97ae6c4de2860fa749b8fc1e \ - --hash=sha256:8e385e4267ab76874ae30db04c627faaaf0b509e1ccc11a95b3fc3e83f855c00 \ - --hash=sha256:92a0a01ead5e668468e952e4238cccd7c537364eb7d851ab144ab6627dbbe12f \ - --hash=sha256:94e1885b270625a9a828c9793b4d52a64445299baa1fea5a173bf1d3dd9a1a5a \ - --hash=sha256:a180c5e59792af262bf263b21a3c49353f25945d8d9f70628e73de370d55e1e1 \ - --hash=sha256:a277ab8928b9f299723bc1a2dabb1265911b1a76341f90a510368ca44ad9ab66 \ - --hash=sha256:a5fe03b42827c13cdccd08e6c0247b6a6d4b5e3cdc53fd1749f5896adcdc2356 \ - --hash=sha256:a6c5863edfbe888d9eff9c8b8087354e27618d9da76425c119293f11712a6319 \ - --hash=sha256:a89c23ef8d2c6b27fd200a42aa4ac72786e7c60d40efdc76e6011260b6e949c4 \ - --hash=sha256:adb2597b428735679446b46c8badf467b4ca5f5056aae4d51a19f9570301b1ad \ - --hash=sha256:ae196f021b5e7c78e918242d217db021ed2a6ace2bc6ae94c0fc596221c7f58d \ - --hash=sha256:ae89db9e5f98a11a4bf50407d4363e7b09b31e55bc117b4f7d80aab97ba009e5 \ - --hash=sha256:aed52fea0513bac0ccde438c188c8a471c4e0f457c2dd20cdbf6ea7a450046c7 \ - --hash=sha256:aef65cd602a6d0e0ff6f9930fcb1c8fec60dd2cfcb6facaf4bdb0e5873042db0 \ - --hash=sha256:af21eb4409a119e365397b2adbaca4c9ccab56543a65d5dbd9f920d6ac29f686 \ - --hash=sha256:b14b2d9dac08e28bb8046a1a0434b1750eb221c8f5b87a68f4fa11a6f97b5e34 \ - --hash=sha256:bb6d88045545b26da47aa879dd4a89a71d1dce0f0e549b1abcb31dfe4a8eac49 \ - --hash=sha256:bb8cc7534f51d9a017b93e3e85b260924f909601c3df002bcdb58ddb4dc41a5c \ - --hash=sha256:bc17a677b21b3502a21f66a8cc64f5bfad4df8a0b8434d661666f8ce90ac3af1 \ - --hash=sha256:bd6c2a1c7573c64738d716488d2cdd3c00e340e4835707d8fdb8dc1a66ef164e \ - --hash=sha256:bd9b23791fe793e4968dba0c447e12f78e425c59fc0e3b97f6450f4781f3ee60 \ - --hash=sha256:c03a41a8784091e67a39648f70c5f97b5b6a37f216896d44d2cdcb82615339a0 \ - --hash=sha256:c0f081d69a6e58272819b70288d3221a6ee64b98df852631c80f293514d3b274 \ - --hash=sha256:c35abb8bfff0185efac5878da64c45dafd2b37fb0383add1be155a763c1f083d \ - --hash=sha256:c36c333c39be2dbca264d7803333c896ab8fa7d4d6f0ab7edb7dfd7aea6e98c0 \ - --hash=sha256:c45e9440fb78f8ddabcf714b68f936737a121355bf59f3907f4e17721b9d1aae \ - --hash=sha256:c593052c465475e64bbfe5dbd81680f64a67fdc752c56d7a0ae205dc8aeefe0f \ - --hash=sha256:cdd68a1fb318e290a2077696b7eb7a21a49163c455979c639bf5a5dcdc46617d \ - --hash=sha256:ce3412fbe1e31eb81ea42f4169ed94861c56e643189e1e75f0041f3fe7020abe \ - --hash=sha256:cf1493cd8607bec4d8a7b9b004e699fcf8f9103a9284cc94962cb73d20f9d4a3 \ - --hash=sha256:cf29836da5119f3c8a8a70667b0ef5fdca3bb12f80fd06487cfa575b3909b393 \ - --hash=sha256:d4a48e5b3c2a489fae013b7589308a40146ee081f6f509e047e0e096084ceca1 \ - --hash=sha256:d560742f3c0d62afaccf9f41fe485ed69bd7661a241f86a3ef0f0fb8b1a397af \ - --hash=sha256:d6038d37043bced98a66e68d3aa2b6a35505dc01328cd65217cefe82f25def44 \ - --hash=sha256:d61f00a0869d77422d9b2aba989e2d24afa6ffd552af442e0e58de4f35ea6d00 \ - --hash=sha256:d635aab80466bc95771bb78d5370e74d36d1fe31467b6b29b8b57b2a3cd7d22c \ - --hash=sha256:dca4bbc466a95ba9c0234ef56d7dd9509f63da22274589ebd4ed7f1f4d4c54e3 \ - --hash=sha256:dd915403e231e6b1809fe9b6d9fc55cf8fb5e02765ac625d9cd623342a7905d7 \ - --hash=sha256:e044c39e41b92c845bc815e5ae4230804e8e7bc29e399b0437d64222d92809dd \ - --hash=sha256:e060d01aec0a910bdccb8be71faf34e7799ce36950f8294c8bf612cba65a2c9e \ - --hash=sha256:e1421b502d83040e6d7fb2fb18dff63957f720da3d77b2fbd3187ceb63755d7b \ - --hash=sha256:e17b8d5d6a8c47c85e68ca8379def1303fd360c3e22093a807cd34a71cd082b8 \ - --hash=sha256:e5f4d355f0a2b1a31bc3edec6795b46324349c9cb25eed068049e4f472fb4259 \ - --hash=sha256:e712b419df8ba5e42b226c510472b37bd57b38e897d3eca5e8cfd410a29fa859 \ - --hash=sha256:e74327fb75de8986940def6e8dee4f127cc9752bee7355bb323cc5b2659b6d46 \ - --hash=sha256:e80c8378d8f3d83cd3164da1ad2df9e37a666cdde7b1cb2298ed0b558064be30 \ - --hash=sha256:e8ac484bf18ce6975760921bb6148041faa8fef0547200386ea0b52b5d27bf7b \ - --hash=sha256:eca9705049ad3c7345d574e3510665cb2cf844c2f2dcfe675332677f081cbd46 \ - --hash=sha256:ed065083d0898c9d5b4bbec7b026fd755ff7454e6e8b73a67f8c744b13986e24 \ - --hash=sha256:edac0f1ab77644605be2cbba52e6b7f630731fc42b34cb0f634be1a6eface56a \ - --hash=sha256:effc3f449787117233702311a1b7d8f59cba9ced946ba727bdc329ec69028e24 \ - --hash=sha256:f22dec1690b584cea26fade98b2435c132c1b5f68e39f5a0b7627cd7ae31f1dc \ - --hash=sha256:f495a1652cf3fbab2eb0639776dad966c2fb874d79d87ca07f9d5f059b8bd215 \ - --hash=sha256:f496c9c3cc02230093d8330875c4c3cdfc3b73612a5fd921c65d39cbcef08063 \ - --hash=sha256:f59099f9b66f0d7145115e6f80dd8b1d847176df89b234a5a6b3f00437aa0832 \ - --hash=sha256:f59ad4c0e8f6bba240a9bb85504faa1ab438237199d4cce5f622761507b8f6a6 \ - --hash=sha256:fbccdc05410c9ee21bbf16a35f4c1d16123dcdeb8a1d38f33654fa21d0234f79 \ - --hash=sha256:fea24543955a6a729c45a73fe90e08c743f0b3334bbf3201e6c4bc1b0c7fa464 +charset-normalizer==3.4.9 \ + --hash=sha256:0327fcd59a935777d83410750c50600ee9571af2846f71ce40f25b13da1ef380 \ + --hash=sha256:03d07803992c6c7bbc976327f34b18b6160327fc81cb82c9d504720ac0be3b62 \ + --hash=sha256:04ce310cb89c15df659582aee80a0603788732a5e017d5bd5c81158106ce249c \ + --hash=sha256:0d861473f743244d349b50f850d10eb87aeb22bbdcc8e64f79273c94af5a8226 \ + --hash=sha256:0e94703ec9684807f20cfb5eed95c70f67f2a8f21ad620146d7b5a13677b93e5 \ + --hash=sha256:0fa1aec2d32bcc03c8fa0f6f1712caad1adc38509f31142112e5c9daf5b9c833 \ + --hash=sha256:16b65ea0f2465b6fb52aa22de5eca612aa964ddfec00a912e26f4656cbef890b \ + --hash=sha256:16d10d789dd9bcca1173c95af82c58433122564b7bc39385124be735a35cbe99 \ + --hash=sha256:19ac87f93086ce37b86e098888555c4b4bc48102279bae3350098c0ed664b501 \ + --hash=sha256:1d22856ffbe153a602df38e4a5464f0b748a54002e0d69ac6d2ad0a197cc99ec \ + --hash=sha256:21e764fd1e70b6a3e205a0e46f3051701f98a8cb3fad66eeb80e48bb502f8698 \ + --hash=sha256:231ddcbb35e2ff8973e1365db41fe0572662893b99a05deb183b68ad4c0c8bd4 \ + --hash=sha256:253a4a220747e8b5faf57ec320c4f5efb0cef05f647420bf267143ec15dba10a \ + --hash=sha256:280081916dc341820640489a66e4696049401ef1cf6dd672f672e70ad915aca3 \ + --hash=sha256:2a441ea71902098ffe78c5abe6c494f44160b4af614ed16c3d9a3b1d17fd8ee2 \ + --hash=sha256:304b13570067b2547562e308af560b3963857b1fa90bd6afd978130130fe2d6a \ + --hash=sha256:32286a2c8d167e897177b673176c1e3e00d4057caf5d2b64eef9a3666b03018e \ + --hash=sha256:33bdcc2a32c0a0e861f60841a512c8acc658c87c2ac59d89e3a46dacf7d866e4 \ + --hash=sha256:375b83ed0aecfce76c16d198fbc21f3b11b337d68662bea0a995046682a11419 \ + --hash=sha256:3c09a49d6cde137258beb3d551994a2927fd35ad5cf96aed573f61bbd67c5f84 \ + --hash=sha256:3d92613ec25e43b05f042302531ec0f00b8445190e43325880cbd6ab7c2581da \ + --hash=sha256:40a126142a56b2dfc0aacbad1de8310cbf60da7656db0e6b16eebd48e3e93519 \ + --hash=sha256:416c229f77e5ea25b3dfd4b582f8d73d7e43c22320302b9ab128a2d3a0b38efe \ + --hash=sha256:432786d3561e69aeeae6c7e8648964ce0ad05736120135601f87ac26b9c83381 \ + --hash=sha256:43b9e366a31fdd1c87d0eb08f579b4a82b723ea54338f040d6b4e518a026ea29 \ + --hash=sha256:440eede837960000d74978f0eba527be106b5b9aee0daf779d395276ed0b0614 \ + --hash=sha256:45b0cc4e3556cd875e09102988d1ab8356c998b596c9fced84547c8138b487a0 \ + --hash=sha256:476743fe6dfe14a2da12e3ac79125dc84a3b2cf8094369a47a1529b0cd8549fe \ + --hash=sha256:4773092f8019072343a7447203308b176e10199920eb02d6195e81bbb3274c29 \ + --hash=sha256:4b3dac63058cc36820b0dd072f89898604e2d39686fe05321729d00d8ac185a0 \ + --hash=sha256:4d1c96a7a18b9690a4d46df09e3e3382406ae3213727cd1019ebade1c4a81917 \ + --hash=sha256:51307f5c71007673a2bf8232ad973483d281e74cb99c8c5a990af1eefa6277d9 \ + --hash=sha256:51447e9aa2684679af07ca5021c3db526e0284347ebf4ffcec1154c3350cfe32 \ + --hash=sha256:58150c9f9b9a552505912d182ccdf26f6396fb6094816ceebcbb20eecabaed94 \ + --hash=sha256:5b10cd92fc5c498b35a8635df6d5a100207f88b63a4dc1de7ef9a548e1e2cd63 \ + --hash=sha256:5e226f6218febc71f6c1fc2fafb91c226f75bdc1d8fb12d66823716e891608fd \ + --hash=sha256:609b3ba8fcc0fb5ab7af00719d0fb6ad0cb518e48e7712d12fd68f1327951198 \ + --hash=sha256:60f44ade2cf573dad7a277e6f8ca9a51a21dda572b13bd7d8539bb3cd5dbedde \ + --hash=sha256:611057cc5d5c0afc743ba8be6bd828c17e0aaa8643f9d0a9b9bb7dea80eb8012 \ + --hash=sha256:6366a16e1a25018694d6a5d784d09b046edc9eac40ea2b54065c3052672516a1 \ + --hash=sha256:65a7ff3f705e57d392f7261b6d0550fe137c3019477431f1c355e0db0a7d3e15 \ + --hash=sha256:673611bbd43f0810bec0b0f028ddeaaa501190339cac411f347ac76917c3ae7b \ + --hash=sha256:67830fc78e67501f47bb950471b2dcb9b35b140084429318e862895a8e89c993 \ + --hash=sha256:68ce9f4d6b26d5ccbf7fd4459bf75f74a0a146677ebba80597df60cbdb20e6f4 \ + --hash=sha256:68e5f26a1ad57ded6d1cfb85331d1c1a195314756471d97758c48498bb4dcdf5 \ + --hash=sha256:69b157c5d3292bcd443faca052f3096f637f1e074b98212a933c074ae23dc3b8 \ + --hash=sha256:75286256590a6320cf106a0d28970d3560aad9ee09aa7b34fb40524792436d35 \ + --hash=sha256:78841cccf1af7b40f6f716338d50c0902dbe88d9f800b3c973b7a9a0a693a642 \ + --hash=sha256:78fa18e436a1a0e58dbd7e02fc4473f3f32cceb12df9dfca542d075961c307d2 \ + --hash=sha256:79580094b00d1789d1f93ea55bc43cb2f611910c72235b7657f3482ddcc1b22d \ + --hash=sha256:7b86a2b16095d250c6f58b3d9b2eee6f4147754344f3dab0922f7c9bf7d226c9 \ + --hash=sha256:83aed2c10721ddd90f68140685391b50811a880af20654c59af6b6c66c40513c \ + --hash=sha256:84fd18bcc17526fc2b3c1af7d2b9217d32c9c04448c16ec693b9b4f1985c3d33 \ + --hash=sha256:871ff67ea1aad4dfd91736464934d56b32dac49f9fbe16cddba36198a7b3a0db \ + --hash=sha256:898f0e9068ca27d37f8e83a5b962821df851532e6c4a7d615c1c033f9da6eedf \ + --hash=sha256:8a79d9f4d8001473a30c163556b3c3bfebec837495a412dde78b51672f6134f9 \ + --hash=sha256:8c041122946b7ba21bb32c45b1aa57b1be35527690aeb3c5c234521085632eee \ + --hash=sha256:90c44bc373b7687f6948b693cceaea1348ae0975d7474746559494468e3c1d84 \ + --hash=sha256:9104ed0bd76a429d46f9ec0dbc9b08ad1d2dcdf2b00a5a0daa1c145329b35b44 \ + --hash=sha256:920079c3f7456fa213e0829ed2073aaa727fd39d889ead5b4f35d0de5460d04f \ + --hash=sha256:93d59d504b230e83c7a843251681959a0b6a9cd76f6e146ce1b8a80eb8739af9 \ + --hash=sha256:9b2aff1c7b3884512b9512c3eaadd9bab39fb45042ffaaa1dd08ff2b9f8109d9 \ + --hash=sha256:9b8e0f3107e2200b76f6054de99016eac3ee6762713587b36baaa7e4bd2ae177 \ + --hash=sha256:9bb41182d93ea91f60b4bc8fbf4c820c69ef8a12ab2d917f3f1834f1acad07e8 \ + --hash=sha256:9cdef90ae47919cae358d8ab15797a800ed41da7aba5d72419fb510729e2ed4b \ + --hash=sha256:a1786910334ed46ab1dd73222f2cd1e05c2c3bb39f6dddb4f8b36fc382058a39 \ + --hash=sha256:a4cfde78a9f2880208d16a93b795726a3017d5977e08d1e162a7a31322479c41 \ + --hash=sha256:a4fbdde9dd4a9ce5fd52c2b3a347bb50cc89483ef783f1cb00d408c13f7a96c0 \ + --hash=sha256:aa99adc8f081b475a12843953db36831eaf83ec33eb46a90629ca6a5de45a616 \ + --hash=sha256:ac351b3b8014eead140e77e9717e2992c6bbe30b63bc3422422eb84865412e3d \ + --hash=sha256:ad41ba96094304aa090f5a30cb6e4fb3b3f1c264c523394b4c39bbacc4dc92ba \ + --hash=sha256:b5314963fce9b0b12743891de876e724997864ee22aa496f903f426c7e2fa5b2 \ + --hash=sha256:bcf74c1df76758a395bf0af608c04c82257523f55c9868b334f06270d0f2112b \ + --hash=sha256:bd47ba7fc3ca94896759ea0109775132d3e7ab921fbf54038e1bab2e46c313c9 \ + --hash=sha256:c0323c9daef75ef2e5083624b4585018a0c9d5e3b40f607eed81a311270b934b \ + --hash=sha256:c1225416b463483160e4af85d5fc3a9690ccb53fd4b1865a6437825f5ede3209 \ + --hash=sha256:c1c948747b03be832dceed96ca815cef7360de9aa19d37c730f8e3f6101aca48 \ + --hash=sha256:c25fe15c70c59eb7c5ce8c06a1f3fa1da0ecc5ea1e7a5922c40fd2fa9b0d5046 \ + --hash=sha256:cc1b0fff8ead343dae06305f954eb8468ba0ec1a97881f42489d198e4ce3c632 \ + --hash=sha256:cd6280cf040f233bd7d3407b743b4b4c74f70e8e1c4199cb112a62c941c0772a \ + --hash=sha256:cd6c3d4b783c556fa00bf540854e42f135e2f256abd29669fcd0da0f2dec79c2 \ + --hash=sha256:d4d6fcde76f94f5cb9e43e9e9a61f16dacefd228cbbf6f1a09bd9b219a92f1a1 \ + --hash=sha256:ddf4af30b417d9fe16481e9b81c27ab2a7cde1ff7ba3e85653b02db7d145dc7b \ + --hash=sha256:df115d4d83168fdf2cae48ef1ff6d1cb4c466364e30861b37121de0f3bf1b990 \ + --hash=sha256:df7276909358e5635ae203673ab7e509ddd224225a8d6b0790bf13eb2bde1cc5 \ + --hash=sha256:e4fd89cc178bced6ad29cb3e6dd4aa63fa5017c3524dbd0b25998fb64a87cc8b \ + --hash=sha256:e9701d0049d92c16703a42771b98d560b95248949f23f8cf7b4eddd201814fb9 \ + --hash=sha256:ee2f2a527e3c1a6e6411eb4209642e138b544a2d72fe5d0d76daf77b24063534 \ + --hash=sha256:f7fb7d750cfa0a070d2c24e831fd3481019a60dd317ea2b39acbcebc08b6ed81 \ + --hash=sha256:f840ed6d8ecba8255df8c42b87fadeda98ddfc6eeec05e2dc66e26d46dd6f58a \ + --hash=sha256:f86c6358749bd4fda175388691e3ba8c46e24c5347d0afd20f9b7edfc9faf07d \ + --hash=sha256:fa36ec09ef71d158186bc79e359ff5fdd6e7996fe8ab638f00d6b93139ba4fcf \ + --hash=sha256:fe2c7201c642b7c308f1675355ad7ff7b66acfe3541625efe5a3ad38f29d6115 # via requests colorama==0.4.6 ; sys_platform == 'win32' \ --hash=sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44 \ @@ -183,9 +143,9 @@ docutils==0.22.4 ; python_full_version >= '3.11' \ # myst-parser # sphinx # sphinx-rtd-theme -idna==3.11 \ - --hash=sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea \ - --hash=sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902 +idna==3.18 \ + --hash=sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2 \ + --hash=sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848 # via requests imagesize==1.5.0 ; python_full_version < '3.10' \ --hash=sha256:32677681b3f434c2cb496f00e89c5a291247b35b1f527589909e008057da5899 \ @@ -206,19 +166,15 @@ jinja2==3.1.6 \ # myst-parser # readthedocs-sphinx-ext # sphinx -macholib==1.16.4 \ - --hash=sha256:da1a3fa8266e30f0ce7e97c6a54eefaae8edd1e5f86f3eb8b95457cae90265ea \ - --hash=sha256:f408c93ab2e995cd2c46e34fe328b130404be143469e41bc366c807448979362 - # via rules-python-docs (docs/pyproject.toml) markdown-it-py==3.0.0 ; python_full_version < '3.11' \ --hash=sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1 \ --hash=sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb # via # mdit-py-plugins # myst-parser -markdown-it-py==4.0.0 ; python_full_version >= '3.11' \ - --hash=sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147 \ - --hash=sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3 +markdown-it-py==4.2.0 ; python_full_version >= '3.11' \ + --hash=sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49 \ + --hash=sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a # via # mdit-py-plugins # myst-parser @@ -313,15 +269,15 @@ markupsafe==3.0.3 \ --hash=sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a \ --hash=sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50 # via - # rules-python-docs (docs/pyproject.toml) + # rules-python-sphinxdocs-dev (dev/pyproject.toml) # jinja2 mdit-py-plugins==0.4.2 ; python_full_version < '3.10' \ --hash=sha256:0c673c3f889399a33b95e88d2f0d111b4447bdfea7f237dab2d488f459835636 \ --hash=sha256:5f2cd1fdb606ddf152d37ec30e46101a60512bc0e5fa1a7002c36647b09e26b5 # via myst-parser -mdit-py-plugins==0.5.0 ; python_full_version >= '3.10' \ - --hash=sha256:07a08422fc1936a5d26d146759e9155ea466e842f5ab2f7d2266dd084c8dab1f \ - --hash=sha256:f4918cb50119f50446560513a8e311d574ff6aaed72606ddae6d35716fe809c6 +mdit-py-plugins==0.6.1 ; python_full_version >= '3.10' \ + --hash=sha256:214c82fb2ac524472ab6a5bcab1de80f73b50443e187f401bfd77efbc7c6481d \ + --hash=sha256:a2bca0f039f39dbd35fb74ae1b5f998608c437463371f0ff7f49a19a17a114d0 # via myst-parser mdurl==0.1.2 \ --hash=sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8 \ @@ -330,29 +286,21 @@ mdurl==0.1.2 \ myst-parser==3.0.1 ; python_full_version < '3.10' \ --hash=sha256:6457aaa33a5d474aca678b8ead9b3dc298e89c68e67012e73146ea6fd54babf1 \ --hash=sha256:88f0cb406cb363b077d176b51c476f62d60604d68a8dcdf4832e080441301a87 - # via rules-python-docs (docs/pyproject.toml) + # via rules-python-sphinxdocs-dev (dev/pyproject.toml) myst-parser==4.0.1 ; python_full_version == '3.10.*' \ --hash=sha256:5cfea715e4f3574138aecbf7d54132296bfd72bb614d31168f48c477a830a7c4 \ --hash=sha256:9134e88959ec3b5780aedf8a99680ea242869d012e8821db3126d427edc9c95d - # via rules-python-docs (docs/pyproject.toml) -myst-parser==5.0.0 ; python_full_version >= '3.11' \ - --hash=sha256:ab31e516024918296e169139072b81592336f2fef55b8986aa31c9f04b5f7211 \ - --hash=sha256:f6f231452c56e8baa662cc352c548158f6a16fcbd6e3800fc594978002b94f3a - # via rules-python-docs (docs/pyproject.toml) -packaging==26.1 \ - --hash=sha256:5d9c0669c6285e491e0ced2eee587eaf67b670d94a19e94e3984a481aba6802f \ - --hash=sha256:f042152b681c4bfac5cae2742a55e103d27ab2ec0f3d88037136b6bfe7c9c5de + # via rules-python-sphinxdocs-dev (dev/pyproject.toml) +myst-parser==5.1.0 ; python_full_version >= '3.11' \ + --hash=sha256:9c91c52b3cdb4d94a6506e4fab4e2f296c7623a0da0dcbe6de1565c3dad67a8a \ + --hash=sha256:ab69322dc6719dcc7f296479dbb70181b66df6ed315064f92dbc85c0e1bf2f02 + # via rules-python-sphinxdocs-dev (dev/pyproject.toml) +packaging==26.2 \ + --hash=sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e \ + --hash=sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661 # via # readthedocs-sphinx-ext # sphinx -pefile==2024.8.26 \ - --hash=sha256:3ff6c5d8b43e8c37bb6e6dd5085658d658a7a0bdcd20b6a07b1fcfc1c4e9d632 \ - --hash=sha256:76f8b485dcd3b1bb8166f1128d395fa3d87af26360c2358fb75b80019b957c6f - # via rules-python-docs (docs/pyproject.toml) -pyelftools==0.32 \ - --hash=sha256:013df952a006db5e138b1edf6d8a68ecc50630adbd0d83a2d41e7f846163d738 \ - --hash=sha256:6de90ee7b8263e740c8715a925382d4099b354f29ac48ea40d840cf7aa14ace5 - # via rules-python-docs (docs/pyproject.toml) pygments==2.20.0 \ --hash=sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f \ --hash=sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176 @@ -435,16 +383,16 @@ pyyaml==6.0.3 \ readthedocs-sphinx-ext==2.2.5 \ --hash=sha256:ee5fd5b99db9f0c180b2396cbce528aa36671951b9526bb0272dbfce5517bd27 \ --hash=sha256:f8c56184ea011c972dd45a90122568587cc85b0127bc9cf064d17c68bc809daa - # via rules-python-docs (docs/pyproject.toml) + # via rules-python-sphinxdocs-dev (dev/pyproject.toml) requests==2.32.5 ; python_full_version < '3.10' \ --hash=sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6 \ --hash=sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf # via # readthedocs-sphinx-ext # sphinx -requests==2.33.1 ; python_full_version >= '3.10' \ - --hash=sha256:18817f8c57c6263968bc123d237e3b8b08ac046f5456bd1e307ee8f4250d3517 \ - --hash=sha256:4e6d1ef462f3626a1f0a0a9c42dd93c63bad33f9f1c1937509b8c5c8718ab56a +requests==2.34.2 ; python_full_version >= '3.10' \ + --hash=sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0 \ + --hash=sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed # via # readthedocs-sphinx-ext # sphinx @@ -452,15 +400,15 @@ roman-numerals==4.1.0 ; python_full_version >= '3.11' \ --hash=sha256:1af8b147eb1405d5839e78aeb93131690495fe9da5c91856cb33ad55a7f1e5b2 \ --hash=sha256:647ba99caddc2cc1e55a51e4360689115551bf4476d90e8162cf8c345fe233c7 # via sphinx -snowballstemmer==3.0.1 \ - --hash=sha256:6cd7b3897da8d6c9ffb968a6781fa6532dce9c3618a4b127d920dab764a19064 \ - --hash=sha256:6d5eeeec8e9f84d4d56b847692bacf79bc2c8e90c7f80ca4444ff8b6f2e52895 +snowballstemmer==3.1.1 \ + --hash=sha256:7e207fa178741da09cdee59d3ecec3827ad5f92b1fc5c9ff3755b639f71f5752 \ + --hash=sha256:e07bbc54a0d798fe6010a12398422e62a8bfbba95c394fd0956ef58cb4d3e260 # via sphinx sphinx==7.4.7 ; python_full_version < '3.10' \ --hash=sha256:242f92a7ea7e6c5b406fdc2615413890ba9f699114a9c09192d7dfead2ee9cfe \ --hash=sha256:c2419e2135d11f1951cd994d6eb18a1835bd8fdd8429f9ca375dc1f3281bd239 # via - # rules-python-docs (docs/pyproject.toml) + # rules-python-sphinxdocs-dev (dev/pyproject.toml) # myst-parser # sphinx-reredirects # sphinx-rtd-theme @@ -469,7 +417,7 @@ sphinx==8.1.3 ; python_full_version == '3.10.*' \ --hash=sha256:09719015511837b76bf6e03e42eb7595ac8c2e41eeb9c29c5b755c6b677992a2 \ --hash=sha256:43c1911eecb0d3e161ad78611bc905d1ad0e523e4ddc202a58a821773dc4c927 # via - # rules-python-docs (docs/pyproject.toml) + # rules-python-sphinxdocs-dev (dev/pyproject.toml) # myst-parser # sphinx-reredirects # sphinx-rtd-theme @@ -478,7 +426,7 @@ sphinx==9.0.4 ; python_full_version == '3.11.*' \ --hash=sha256:594ef59d042972abbc581d8baa577404abe4e6c3b04ef61bd7fc2acbd51f3fa3 \ --hash=sha256:5bebc595a5e943ea248b99c13814c1c5e10b3ece718976824ffa7959ff95fffb # via - # rules-python-docs (docs/pyproject.toml) + # rules-python-sphinxdocs-dev (dev/pyproject.toml) # myst-parser # sphinx-reredirects # sphinx-rtd-theme @@ -487,7 +435,7 @@ sphinx==9.1.0 ; python_full_version >= '3.12' \ --hash=sha256:7741722357dd75f8190766926071fed3bdc211c74dd2d7d4df5404da95930ddb \ --hash=sha256:c84fdd4e782504495fe4f2c0b3413d6c2bf388589bb352d439b2a3bb99991978 # via - # rules-python-docs (docs/pyproject.toml) + # rules-python-sphinxdocs-dev (dev/pyproject.toml) # myst-parser # sphinx-reredirects # sphinx-rtd-theme @@ -495,19 +443,19 @@ sphinx==9.1.0 ; python_full_version >= '3.12' \ sphinx-autodoc2==0.5.0 \ --hash=sha256:7d76044aa81d6af74447080182b6868c7eb066874edc835e8ddf810735b6565a \ --hash=sha256:e867013b1512f9d6d7e6f6799f8b537d6884462acd118ef361f3f619a60b5c9e - # via rules-python-docs (docs/pyproject.toml) + # via rules-python-sphinxdocs-dev (dev/pyproject.toml) sphinx-reredirects==0.1.6 ; python_full_version < '3.11' \ --hash=sha256:c491cba545f67be9697508727818d8626626366245ae64456fe29f37e9bbea64 \ --hash=sha256:efd50c766fbc5bf40cd5148e10c00f2c00d143027de5c5e48beece93cc40eeea - # via rules-python-docs (docs/pyproject.toml) + # via rules-python-sphinxdocs-dev (dev/pyproject.toml) sphinx-reredirects==1.1.0 ; python_full_version >= '3.11' \ --hash=sha256:4b5692273c72cd2d4d917f4c6f87d5919e4d6114a752d4be033f7f5f6310efd9 \ --hash=sha256:fb9b195335ab14b43f8273287d0c7eeb637ba6c56c66581c11b47202f6718b29 - # via rules-python-docs (docs/pyproject.toml) + # via rules-python-sphinxdocs-dev (dev/pyproject.toml) sphinx-rtd-theme==3.1.0 \ --hash=sha256:1785824ae8e6632060490f67cf3a72d404a85d2d9fc26bce3619944de5682b89 \ --hash=sha256:b44276f2c276e909239a4f6c955aa667aaafeb78597923b1c60babc76db78e4c - # via rules-python-docs (docs/pyproject.toml) + # via rules-python-sphinxdocs-dev (dev/pyproject.toml) sphinxcontrib-applehelp==2.0.0 \ --hash=sha256:2f29ef331735ce958efa4734873f084941970894c6090408b079c61b2e1c06d1 \ --hash=sha256:4cd3f0ec4ac5dd9c17ec65e9ab272c9b867ea77425228e68ecf08d6b28ddbdb5 @@ -587,17 +535,21 @@ tomli==2.4.1 ; python_full_version < '3.11' \ # via # sphinx # sphinx-autodoc2 -typing-extensions==4.15.0 \ - --hash=sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466 \ - --hash=sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548 +typing-extensions==4.16.0 \ + --hash=sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8 \ + --hash=sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5 # via - # rules-python-docs (docs/pyproject.toml) + # rules-python-sphinxdocs-dev (dev/pyproject.toml) # astroid # sphinx-autodoc2 -urllib3==2.6.3 \ +urllib3==2.6.3 ; python_full_version < '3.10' \ --hash=sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed \ --hash=sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4 # via requests +urllib3==2.7.0 ; python_full_version >= '3.10' \ + --hash=sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c \ + --hash=sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897 + # via requests zipp==3.23.1 ; python_full_version < '3.10' \ --hash=sha256:0b3596c50a5c700c9cb40ba8d86d9f2cc4807e9bedb06bcdf7fac85633e444dc \ --hash=sha256:32120e378d32cd9714ad503c1d024619063ec28aad2248dc6672ad13edfa5110 diff --git a/sphinxdocs/dev/uv.lock b/sphinxdocs/dev/uv.lock new file mode 100644 index 0000000000..39ce81556d --- /dev/null +++ b/sphinxdocs/dev/uv.lock @@ -0,0 +1,1073 @@ +version = 1 +revision = 3 +requires-python = ">=3.9" +resolution-markers = [ + "python_full_version >= '3.12'", + "python_full_version == '3.11.*'", + "python_full_version == '3.10.*'", + "python_full_version < '3.10'", +] + +[[package]] +name = "absl-py" +version = "2.3.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +sdist = { url = "https://files.pythonhosted.org/packages/10/2a/c93173ffa1b39c1d0395b7e842bbdc62e556ca9d8d3b5572926f3e4ca752/absl_py-2.3.1.tar.gz", hash = "sha256:a97820526f7fbfd2ec1bce83f3f25e3a14840dac0d8e02a0b71cd75db3f77fc9", size = 116588, upload-time = "2025-07-03T09:31:44.05Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8f/aa/ba0014cc4659328dc818a28827be78e6d97312ab0cb98105a770924dc11e/absl_py-2.3.1-py3-none-any.whl", hash = "sha256:eeecf07f0c2a93ace0772c92e596ace6d3d3996c042b2128459aaae2a76de11d", size = 135811, upload-time = "2025-07-03T09:31:42.253Z" }, +] + +[[package]] +name = "absl-py" +version = "2.5.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.12'", + "python_full_version == '3.11.*'", + "python_full_version == '3.10.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/d0/4f/d79676ab82f2e42fc3611618139f13a9c4c31d0cff4b486982047679a802/absl_py-2.5.0.tar.gz", hash = "sha256:0c996f25c0490700fadabe6351630f6111534fa0ae252cc6d2014ea3b141135f", size = 118119, upload-time = "2026-07-03T10:57:48.157Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/58/0a/a10b45aab35b175aded078a462dc8d0c698f5b13946e7cb0869097b78bb6/absl_py-2.5.0-py3-none-any.whl", hash = "sha256:0f17b89f2a4eaaedc4f28c622998aa690564b3012a396a4ffad0821007fe03ba", size = 137410, upload-time = "2026-07-03T10:57:46.735Z" }, +] + +[[package]] +name = "alabaster" +version = "0.7.16" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +sdist = { url = "https://files.pythonhosted.org/packages/c9/3e/13dd8e5ed9094e734ac430b5d0eb4f2bb001708a8b7856cbf8e084e001ba/alabaster-0.7.16.tar.gz", hash = "sha256:75a8b99c28a5dad50dd7f8ccdd447a121ddb3892da9e53d1ca5cca3106d58d65", size = 23776, upload-time = "2024-01-10T00:56:10.189Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/34/d4e1c02d3bee589efb5dfa17f88ea08bdb3e3eac12bc475462aec52ed223/alabaster-0.7.16-py3-none-any.whl", hash = "sha256:b46733c07dce03ae4e150330b975c75737fa60f0a7c591b6c8bf4928a28e2c92", size = 13511, upload-time = "2024-01-10T00:56:08.388Z" }, +] + +[[package]] +name = "alabaster" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.12'", + "python_full_version == '3.11.*'", + "python_full_version == '3.10.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/a6/f8/d9c74d0daf3f742840fd818d69cfae176fa332022fd44e3469487d5a9420/alabaster-1.0.0.tar.gz", hash = "sha256:c00dca57bca26fa62a6d7d0a9fcce65f3e026e9bfe33e9c538fd3fbb2144fd9e", size = 24210, upload-time = "2024-07-26T18:15:03.762Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/b3/6b4067be973ae96ba0d615946e314c5ae35f9f993eca561b356540bb0c2b/alabaster-1.0.0-py3-none-any.whl", hash = "sha256:fc6786402dc3fcb2de3cabd5fe455a2db534b371124f1f21de8731783dec828b", size = 13929, upload-time = "2024-07-26T18:15:02.05Z" }, +] + +[[package]] +name = "astroid" +version = "3.3.11" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/18/74/dfb75f9ccd592bbedb175d4a32fc643cf569d7c218508bfbd6ea7ef9c091/astroid-3.3.11.tar.gz", hash = "sha256:1e5a5011af2920c7c67a53f65d536d65bfa7116feeaf2354d8b94f29573bb0ce", size = 400439, upload-time = "2025-07-13T18:04:23.177Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/af/0f/3b8fdc946b4d9cc8cc1e8af42c4e409468c84441b933d037e101b3d72d86/astroid-3.3.11-py3-none-any.whl", hash = "sha256:54c760ae8322ece1abd213057c4b5bba7c49818853fc901ef09719a60dbf9dec", size = 275612, upload-time = "2025-07-13T18:04:21.07Z" }, +] + +[[package]] +name = "babel" +version = "2.18.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/b2/51899539b6ceeeb420d40ed3cd4b7a40519404f9baf3d4ac99dc413a834b/babel-2.18.0.tar.gz", hash = "sha256:b80b99a14bd085fcacfa15c9165f651fbb3406e66cc603abf11c5750937c992d", size = 9959554, upload-time = "2026-02-01T12:30:56.078Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/77/f5/21d2de20e8b8b0408f0681956ca2c69f1320a3848ac50e6e7f39c6159675/babel-2.18.0-py3-none-any.whl", hash = "sha256:e2b422b277c2b9a9630c1d7903c2a00d0830c409c59ac8cae9081c92f1aeba35", size = 10196845, upload-time = "2026-02-01T12:30:53.445Z" }, +] + +[[package]] +name = "certifi" +version = "2026.7.22" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a3/c2/24167ea9858356b47a87a50d39908bfdb72ceeefe0041586e704e5376b3a/certifi-2026.7.22.tar.gz", hash = "sha256:741e2c3b351ddf169a738da9f2c048608ff7f2c5cc02f1ebc6b118bb090d5d55", size = 138112, upload-time = "2026-07-22T03:35:12.644Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/a7/71ac2cff56fec219ed242bb11b8efb69fcc4bec75db06fb7bfe35de520e6/certifi-2026.7.22-py3-none-any.whl", hash = "sha256:62f22742b58a1a33014a2b6b706588a8d7e2a88ae7bd1a6ebe8c992928483775", size = 136983, upload-time = "2026-07-22T03:35:11.276Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.9" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bd/2a/23f34ec9d04624958e137efdc394888716353190e75f25dd22c7a2c7a8aa/charset_normalizer-3.4.9.tar.gz", hash = "sha256:673611bbd43f0810bec0b0f028ddeaaa501190339cac411f347ac76917c3ae7b", size = 152439, upload-time = "2026-07-07T14:34:58.454Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ad/81/8e983840c6e5b93b33c2ba81aa3d52c2e42f0e9a690ce7607a2e61da4a5c/charset_normalizer-3.4.9-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cd6280cf040f233bd7d3407b743b4b4c74f70e8e1c4199cb112a62c941c0772a", size = 322240, upload-time = "2026-07-07T14:32:36.236Z" }, + { url = "https://files.pythonhosted.org/packages/de/d1/b4319dc3229d8272fba305e206fc0a148e2de8d4087917ce62ae6382f359/charset_normalizer-3.4.9-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aa99adc8f081b475a12843953db36831eaf83ec33eb46a90629ca6a5de45a616", size = 216475, upload-time = "2026-07-07T14:32:38.142Z" }, + { url = "https://files.pythonhosted.org/packages/80/33/6c99c1b3e6b8bf730e1bc809b9a2608f224145069114c479a2e9e1494346/charset_normalizer-3.4.9-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c1225416b463483160e4af85d5fc3a9690ccb53fd4b1865a6437825f5ede3209", size = 238670, upload-time = "2026-07-07T14:32:39.658Z" }, + { url = "https://files.pythonhosted.org/packages/7f/f4/ffbb83546e1f198ecc70ecd372b65cf2b50f9068b380abd67640f17a8e18/charset_normalizer-3.4.9-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:16d10d789dd9bcca1173c95af82c58433122564b7bc39385124be735a35cbe99", size = 233476, upload-time = "2026-07-07T14:32:41.155Z" }, + { url = "https://files.pythonhosted.org/packages/e8/5f/b98b8da398637b551e427e7be922bdec19177dc54d6811dcdaa503f23aac/charset_normalizer-3.4.9-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9bb41182d93ea91f60b4bc8fbf4c820c69ef8a12ab2d917f3f1834f1acad07e8", size = 223817, upload-time = "2026-07-07T14:32:42.592Z" }, + { url = "https://files.pythonhosted.org/packages/36/31/a276bb2e66243072a3fd06fdcab9cbb61a305b02143d70d2bda21d888fa8/charset_normalizer-3.4.9-cp310-cp310-manylinux_2_31_armv7l.whl", hash = "sha256:bcf74c1df76758a395bf0af608c04c82257523f55c9868b334f06270d0f2112b", size = 207974, upload-time = "2026-07-07T14:32:44.258Z" }, + { url = "https://files.pythonhosted.org/packages/5e/be/7ee4453d7e88dfbc4104ccd34900b9f2c7c17dac22881865fe0e82424a25/charset_normalizer-3.4.9-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b5314963fce9b0b12743891de876e724997864ee22aa496f903f426c7e2fa5b2", size = 221655, upload-time = "2026-07-07T14:32:45.64Z" }, + { url = "https://files.pythonhosted.org/packages/1d/85/181c652953eb5276d198f375b1dd641047392050098100a3a02d6534f657/charset_normalizer-3.4.9-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e9701d0049d92c16703a42771b98d560b95248949f23f8cf7b4eddd201814fb9", size = 219229, upload-time = "2026-07-07T14:32:47.376Z" }, + { url = "https://files.pythonhosted.org/packages/0c/e7/aaf6da33fc9f4691cda8f7efbc9f69179d3d39ec8a4799baf273ee1d8db0/charset_normalizer-3.4.9-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:65a7ff3f705e57d392f7261b6d0550fe137c3019477431f1c355e0db0a7d3e15", size = 209704, upload-time = "2026-07-07T14:32:48.855Z" }, + { url = "https://files.pythonhosted.org/packages/63/01/f2fb3bd3a73be48b173ee0c6aa8d2497af97d5663a8c4c4b491de4c62f7a/charset_normalizer-3.4.9-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:79580094b00d1789d1f93ea55bc43cb2f611910c72235b7657f3482ddcc1b22d", size = 226243, upload-time = "2026-07-07T14:32:50.239Z" }, + { url = "https://files.pythonhosted.org/packages/c4/02/c57a22739fe05246b0b5783b3bfb6afaac4eebb46f3ececdfb2f048f780e/charset_normalizer-3.4.9-cp310-cp310-win32.whl", hash = "sha256:432786d3561e69aeeae6c7e8648964ce0ad05736120135601f87ac26b9c83381", size = 150935, upload-time = "2026-07-07T14:32:51.676Z" }, + { url = "https://files.pythonhosted.org/packages/37/8d/ca39a7559a4797505530d084fd3a49a2c959efbbbff146302fb7be4e3b35/charset_normalizer-3.4.9-cp310-cp310-win_amd64.whl", hash = "sha256:8c041122946b7ba21bb32c45b1aa57b1be35527690aeb3c5c234521085632eee", size = 162314, upload-time = "2026-07-07T14:32:53.193Z" }, + { url = "https://files.pythonhosted.org/packages/01/da/a44bd7a13d426e69e4894557106cd58669097bfad4a8681123b618fbfc5d/charset_normalizer-3.4.9-cp310-cp310-win_arm64.whl", hash = "sha256:375b83ed0aecfce76c16d198fbc21f3b11b337d68662bea0a995046682a11419", size = 153075, upload-time = "2026-07-07T14:32:54.554Z" }, + { url = "https://files.pythonhosted.org/packages/0b/e3/85ec501f206fb049259288c1f3506e53876937fb00edb47009348e66756b/charset_normalizer-3.4.9-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0e94703ec9684807f20cfb5eed95c70f67f2a8f21ad620146d7b5a13677b93e5", size = 317075, upload-time = "2026-07-07T14:32:56.021Z" }, + { url = "https://files.pythonhosted.org/packages/c3/69/2a5385192e67175f7d8bd5ce4f57c24bc956439adeae5c13a99aa28a53d1/charset_normalizer-3.4.9-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2a441ea71902098ffe78c5abe6c494f44160b4af614ed16c3d9a3b1d17fd8ee2", size = 213837, upload-time = "2026-07-07T14:32:57.78Z" }, + { url = "https://files.pythonhosted.org/packages/b3/46/03ddc7da576d814fe0a36dd1f0fd3258e95404b4b2e3c026b7923d7e133f/charset_normalizer-3.4.9-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:304b13570067b2547562e308af560b3963857b1fa90bd6afd978130130fe2d6a", size = 235503, upload-time = "2026-07-07T14:32:59.205Z" }, + { url = "https://files.pythonhosted.org/packages/4e/6e/de0229a7ef40f6f9d28a837eebf4ec47bdca5dab4e900c84f22919af636a/charset_normalizer-3.4.9-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4773092f8019072343a7447203308b176e10199920eb02d6195e81bbb3274c29", size = 229944, upload-time = "2026-07-07T14:33:00.803Z" }, + { url = "https://files.pythonhosted.org/packages/a5/34/49b9060e8418b14fb5cba9cf6bfb383111e2538a03a1fb18e66a95aeb3d5/charset_normalizer-3.4.9-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:04ce310cb89c15df659582aee80a0603788732a5e017d5bd5c81158106ce249c", size = 221276, upload-time = "2026-07-07T14:33:02.199Z" }, + { url = "https://files.pythonhosted.org/packages/44/95/80282cce0fae9c3061203d723ee87da996aed79679e65d8935050ee7ca1f/charset_normalizer-3.4.9-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:c0323c9daef75ef2e5083624b4585018a0c9d5e3b40f607eed81a311270b934b", size = 205260, upload-time = "2026-07-07T14:33:03.698Z" }, + { url = "https://files.pythonhosted.org/packages/0c/74/2f62c8821b969ea3bd67cc2e6976834f48ca5d12664d2559ebcd9bcfbed7/charset_normalizer-3.4.9-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:871ff67ea1aad4dfd91736464934d56b32dac49f9fbe16cddba36198a7b3a0db", size = 217786, upload-time = "2026-07-07T14:33:05.12Z" }, + { url = "https://files.pythonhosted.org/packages/d9/8d/feabb82cb49fcad14515b1d7d1ca4787b0da7fc723a212bf89bc9e0fac52/charset_normalizer-3.4.9-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:67830fc78e67501f47bb950471b2dcb9b35b140084429318e862895a8e89c993", size = 216798, upload-time = "2026-07-07T14:33:06.629Z" }, + { url = "https://files.pythonhosted.org/packages/a5/ff/c946d63bc3786d5b84d960b0f7ab7e25b828486a946b5aa997625bcaf6a6/charset_normalizer-3.4.9-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:3d92613ec25e43b05f042302531ec0f00b8445190e43325880cbd6ab7c2581da", size = 206429, upload-time = "2026-07-07T14:33:08.006Z" }, + { url = "https://files.pythonhosted.org/packages/af/ba/5e5007c370702f85d2ef75791fac7943ed41e080364a673b20142e430e3e/charset_normalizer-3.4.9-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:280081916dc341820640489a66e4696049401ef1cf6dd672f672e70ad915aca3", size = 223066, upload-time = "2026-07-07T14:33:09.783Z" }, + { url = "https://files.pythonhosted.org/packages/83/d5/9096aa3cf532dfad237861544eb47a0f20d5adbf1039760fed8eaae935d9/charset_normalizer-3.4.9-cp311-cp311-win32.whl", hash = "sha256:ac351b3b8014eead140e77e9717e2992c6bbe30b63bc3422422eb84865412e3d", size = 150456, upload-time = "2026-07-07T14:33:11.217Z" }, + { url = "https://files.pythonhosted.org/packages/ed/a1/e29995109e455dc8eff8d0fac6ae509be39561318a7cfeac5d33ad029213/charset_normalizer-3.4.9-cp311-cp311-win_amd64.whl", hash = "sha256:6366a16e1a25018694d6a5d784d09b046edc9eac40ea2b54065c3052672516a1", size = 161410, upload-time = "2026-07-07T14:33:12.743Z" }, + { url = "https://files.pythonhosted.org/packages/4f/8d/1569f4d0032d6ba2a4fe4591c35bf87868c600c41a71eb5c2e1ffa8464c2/charset_normalizer-3.4.9-cp311-cp311-win_arm64.whl", hash = "sha256:1d22856ffbe153a602df38e4a5464f0b748a54002e0d69ac6d2ad0a197cc99ec", size = 152649, upload-time = "2026-07-07T14:33:14.173Z" }, + { url = "https://files.pythonhosted.org/packages/70/4a/ecbd131485c07fcdfad54e28946d513e3da22ef3b4bd854dcafae54ec739/charset_normalizer-3.4.9-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:45b0cc4e3556cd875e09102988d1ab8356c998b596c9fced84547c8138b487a0", size = 319300, upload-time = "2026-07-07T14:33:15.666Z" }, + { url = "https://files.pythonhosted.org/packages/ec/96/5d9364e3342d69f3a045e1777bc47c85c383e6e9466d561b33fdb419d1f9/charset_normalizer-3.4.9-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9b2aff1c7b3884512b9512c3eaadd9bab39fb45042ffaaa1dd08ff2b9f8109d9", size = 215802, upload-time = "2026-07-07T14:33:17.031Z" }, + { url = "https://files.pythonhosted.org/packages/4b/4c/5361f9aa7f2cb58d94f2ab831b3d493f69efb1d239654b4744e3c09527cb/charset_normalizer-3.4.9-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9104ed0bd76a429d46f9ec0dbc9b08ad1d2dcdf2b00a5a0daa1c145329b35b44", size = 237171, upload-time = "2026-07-07T14:33:18.576Z" }, + { url = "https://files.pythonhosted.org/packages/50/78/ce342ca4ff30b2eb49fe6d9578df85974f90c67d294113e94efdd9664cbd/charset_normalizer-3.4.9-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7b86a2b16095d250c6f58b3d9b2eee6f4147754344f3dab0922f7c9bf7d226c9", size = 233075, upload-time = "2026-07-07T14:33:20.084Z" }, + { url = "https://files.pythonhosted.org/packages/01/c4/4fa4c8b3097a11f3c5f09a35b72ed6855fb1d332469504962ab7bafcc702/charset_normalizer-3.4.9-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e226f6218febc71f6c1fc2fafb91c226f75bdc1d8fb12d66823716e891608fd", size = 224256, upload-time = "2026-07-07T14:33:21.747Z" }, + { url = "https://files.pythonhosted.org/packages/87/3a/ad914516df7e358a81aae018caa5e0470ba827fa6d763b1d2e87d920a5f6/charset_normalizer-3.4.9-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:90c44bc373b7687f6948b693cceaea1348ae0975d7474746559494468e3c1d84", size = 208784, upload-time = "2026-07-07T14:33:23.313Z" }, + { url = "https://files.pythonhosted.org/packages/d7/74/3c12f9755717dfe5c5c87da63f35d765fa0c00382ec26bf23f7fae34f2ba/charset_normalizer-3.4.9-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9cdef90ae47919cae358d8ab15797a800ed41da7aba5d72419fb510729e2ed4b", size = 219928, upload-time = "2026-07-07T14:33:24.814Z" }, + { url = "https://files.pythonhosted.org/packages/33/9a/895095b83e7907abd6d3d99aad3a38ad0d9686cc186cb0c94c24320fe63e/charset_normalizer-3.4.9-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:60f44ade2cf573dad7a277e6f8ca9a51a21dda572b13bd7d8539bb3cd5dbedde", size = 218489, upload-time = "2026-07-07T14:33:26.42Z" }, + { url = "https://files.pythonhosted.org/packages/a1/34/ef5c05f412f42520d7709b7d3784d19640839eb7366ded1755511585429f/charset_normalizer-3.4.9-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:a1786910334ed46ab1dd73222f2cd1e05c2c3bb39f6dddb4f8b36fc382058a39", size = 210267, upload-time = "2026-07-07T14:33:27.952Z" }, + { url = "https://files.pythonhosted.org/packages/83/dc/9b29fa4412b318bf3bfea985c35d67eb55e04b59a7c3f2237168b0e0be6f/charset_normalizer-3.4.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:03d07803992c6c7bbc976327f34b18b6160327fc81cb82c9d504720ac0be3b62", size = 226030, upload-time = "2026-07-07T14:33:29.397Z" }, + { url = "https://files.pythonhosted.org/packages/0e/42/6dbc00b8cd16011691203e33570fa42ed5746599a2e878112d16eab403a3/charset_normalizer-3.4.9-cp312-cp312-win32.whl", hash = "sha256:78841cccf1af7b40f6f716338d50c0902dbe88d9f800b3c973b7a9a0a693a642", size = 151185, upload-time = "2026-07-07T14:33:30.781Z" }, + { url = "https://files.pythonhosted.org/packages/80/cc/f920afd1a23c58ccd53c1d36085a71893a4737ff5e66e0371efab6809850/charset_normalizer-3.4.9-cp312-cp312-win_amd64.whl", hash = "sha256:4b3dac63058cc36820b0dd072f89898604e2d39686fe05321729d00d8ac185a0", size = 162557, upload-time = "2026-07-07T14:33:32.176Z" }, + { url = "https://files.pythonhosted.org/packages/f0/e6/0386d43a261ff4e4b30c5857af7df877254b46bec7b9d1b74b6bf969a90b/charset_normalizer-3.4.9-cp312-cp312-win_arm64.whl", hash = "sha256:78fa18e436a1a0e58dbd7e02fc4473f3f32cceb12df9dfca542d075961c307d2", size = 152665, upload-time = "2026-07-07T14:33:33.711Z" }, + { url = "https://files.pythonhosted.org/packages/b2/06/97ec2aeae780b31d742b6352218b43841a6871e2564578ca522dce4a45c3/charset_normalizer-3.4.9-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:440eede837960000d74978f0eba527be106b5b9aee0daf779d395276ed0b0614", size = 317688, upload-time = "2026-07-07T14:33:35.408Z" }, + { url = "https://files.pythonhosted.org/packages/d0/39/8ff066c672434225f8d25f8b739f992af250944392173dcc88362681c9bf/charset_normalizer-3.4.9-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:21e764fd1e70b6a3e205a0e46f3051701f98a8cb3fad66eeb80e48bb502f8698", size = 214982, upload-time = "2026-07-07T14:33:36.996Z" }, + { url = "https://files.pythonhosted.org/packages/92/8f/3a47a3667c83c2df9483d91644c6c107de3bf8874aa1793da9d3012eb986/charset_normalizer-3.4.9-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e4fd89cc178bced6ad29cb3e6dd4aa63fa5017c3524dbd0b25998fb64a87cc8b", size = 236460, upload-time = "2026-07-07T14:33:38.536Z" }, + { url = "https://files.pythonhosted.org/packages/f1/60/b22cdbee7e4013dab8b0d7647fc6181120fbbbc8f7025c226d15bd5a47fc/charset_normalizer-3.4.9-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bd47ba7fc3ca94896759ea0109775132d3e7ab921fbf54038e1bab2e46c313c9", size = 232003, upload-time = "2026-07-07T14:33:40.059Z" }, + { url = "https://files.pythonhosted.org/packages/ea/f8/72eb13dcabe7257035cea8aefd922caad2f110d252bf9f67c4c2ca763aee/charset_normalizer-3.4.9-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:84fd18bcc17526fc2b3c1af7d2b9217d32c9c04448c16ec693b9b4f1985c3d33", size = 223149, upload-time = "2026-07-07T14:33:41.631Z" }, + { url = "https://files.pythonhosted.org/packages/b0/3e/faee8f9de92b14ee1198e9163252bb15efee7301b31256a3b6d9ebfdd0dd/charset_normalizer-3.4.9-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:5b10cd92fc5c498b35a8635df6d5a100207f88b63a4dc1de7ef9a548e1e2cd63", size = 207901, upload-time = "2026-07-07T14:33:43.209Z" }, + { url = "https://files.pythonhosted.org/packages/3a/25/45f30093ae27dd7b92a793b61882a38685f993700113ca36e0c9c14965e1/charset_normalizer-3.4.9-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a4fbdde9dd4a9ce5fd52c2b3a347bb50cc89483ef783f1cb00d408c13f7a96c0", size = 219176, upload-time = "2026-07-07T14:33:44.725Z" }, + { url = "https://files.pythonhosted.org/packages/48/18/c8f397329c35e32f6a837e488986f4ae03bd2abebc453b48714991630c2f/charset_normalizer-3.4.9-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:416c229f77e5ea25b3dfd4b582f8d73d7e43c22320302b9ab128a2d3a0b38efe", size = 217356, upload-time = "2026-07-07T14:33:46.192Z" }, + { url = "https://files.pythonhosted.org/packages/86/7e/5ce0bba863470fd1902d5e5843968951bddf38abe4742fc97116ef4598b3/charset_normalizer-3.4.9-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:75286256590a6320cf106a0d28970d3560aad9ee09aa7b34fb40524792436d35", size = 209614, upload-time = "2026-07-07T14:33:47.705Z" }, + { url = "https://files.pythonhosted.org/packages/6c/ef/2473d3c4d869155be4af1191111d59c4d5c4e0173026f7e85b176e23bf65/charset_normalizer-3.4.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:69b157c5d3292bcd443faca052f3096f637f1e074b98212a933c074ae23dc3b8", size = 224991, upload-time = "2026-07-07T14:33:49.238Z" }, + { url = "https://files.pythonhosted.org/packages/d0/a3/53ddae3db108a088156aa8ddfafd411ebbc1340f48c5573f697b27f69a39/charset_normalizer-3.4.9-cp313-cp313-win32.whl", hash = "sha256:51307f5c71007673a2bf8232ad973483d281e74cb99c8c5a990af1eefa6277d9", size = 150622, upload-time = "2026-07-07T14:33:50.711Z" }, + { url = "https://files.pythonhosted.org/packages/e8/ef/6953a77c7cf2c2ff9998e6f575ab3e380119f100223381565a4f94c1f836/charset_normalizer-3.4.9-cp313-cp313-win_amd64.whl", hash = "sha256:fe2c7201c642b7c308f1675355ad7ff7b66acfe3541625efe5a3ad38f29d6115", size = 161947, upload-time = "2026-07-07T14:33:52.197Z" }, + { url = "https://files.pythonhosted.org/packages/6e/fb/d560d1d1555debbfe7849d9cac6145c1b537709d79576bf22557ed803b82/charset_normalizer-3.4.9-cp313-cp313-win_arm64.whl", hash = "sha256:611057cc5d5c0afc743ba8be6bd828c17e0aaa8643f9d0a9b9bb7dea80eb8012", size = 152594, upload-time = "2026-07-07T14:33:53.486Z" }, + { url = "https://files.pythonhosted.org/packages/7e/8d/496817fa0944239ecae662dd57ea765cfeaec6a735f9f025d4b7b72e7143/charset_normalizer-3.4.9-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:0327fcd59a935777d83410750c50600ee9571af2846f71ce40f25b13da1ef380", size = 317253, upload-time = "2026-07-07T14:33:54.994Z" }, + { url = "https://files.pythonhosted.org/packages/2b/f9/ef4a69ea338ad3c0deceea0f5f7d2380ae8b52132b06d652cb0d2cd86706/charset_normalizer-3.4.9-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a79d9f4d8001473a30c163556b3c3bfebec837495a412dde78b51672f6134f9", size = 215898, upload-time = "2026-07-07T14:33:56.334Z" }, + { url = "https://files.pythonhosted.org/packages/8c/e7/5ddfd76fc061eb52de219658a4aa431cbacadf0a0219c8854f00da50d289/charset_normalizer-3.4.9-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:33bdcc2a32c0a0e861f60841a512c8acc658c87c2ac59d89e3a46dacf7d866e4", size = 236718, upload-time = "2026-07-07T14:33:57.9Z" }, + { url = "https://files.pythonhosted.org/packages/49/ba/768fa3f36048d81c477a0ce61f813bc1454d80917ccfe550abd9f44f5e24/charset_normalizer-3.4.9-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f840ed6d8ecba8255df8c42b87fadeda98ddfc6eeec05e2dc66e26d46dd6f58a", size = 232519, upload-time = "2026-07-07T14:33:59.811Z" }, + { url = "https://files.pythonhosted.org/packages/f4/c4/b3e049d2aa3766180c78507110543d9d50894cc97f57de543f1be521dcdc/charset_normalizer-3.4.9-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c25fe15c70c59eb7c5ce8c06a1f3fa1da0ecc5ea1e7a5922c40fd2fa9b0d5046", size = 223143, upload-time = "2026-07-07T14:34:01.517Z" }, + { url = "https://files.pythonhosted.org/packages/19/79/55c32d06d76ae4feafe053f061f3e3ab70bcf19f4007797ce8c3efda7830/charset_normalizer-3.4.9-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:f7fb7d750cfa0a070d2c24e831fd3481019a60dd317ea2b39acbcebc08b6ed81", size = 206742, upload-time = "2026-07-07T14:34:03.04Z" }, + { url = "https://files.pythonhosted.org/packages/10/e0/47c079dd82d217c807479cd59ffd30af56307ea31c108b75758970459ad3/charset_normalizer-3.4.9-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4d1c96a7a18b9690a4d46df09e3e3382406ae3213727cd1019ebade1c4a81917", size = 219191, upload-time = "2026-07-07T14:34:04.657Z" }, + { url = "https://files.pythonhosted.org/packages/42/ab/b9bc2e77d6b44a7e46ef62ec5cac1c9a6ba7b9135a5d560f002696ec9995/charset_normalizer-3.4.9-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a4cfde78a9f2880208d16a93b795726a3017d5977e08d1e162a7a31322479c41", size = 218328, upload-time = "2026-07-07T14:34:06.115Z" }, + { url = "https://files.pythonhosted.org/packages/f1/78/c9c71d599f5aa2d42bcdd35cbbd46d7f535351a57e40ff7d8e5a7e219401/charset_normalizer-3.4.9-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:d4d6fcde76f94f5cb9e43e9e9a61f16dacefd228cbbf6f1a09bd9b219a92f1a1", size = 207406, upload-time = "2026-07-07T14:34:07.554Z" }, + { url = "https://files.pythonhosted.org/packages/f6/39/c914445c321a845097ce4f6ac7de9a18228a77b766272125a1ce00d851eb/charset_normalizer-3.4.9-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:898f0e9068ca27d37f8e83a5b962821df851532e6c4a7d615c1c033f9da6eedf", size = 225157, upload-time = "2026-07-07T14:34:09.061Z" }, + { url = "https://files.pythonhosted.org/packages/9b/f2/c0d4b8508565a36bc5c624e88ed297f5b0b1095011034d7f5b83a69908b5/charset_normalizer-3.4.9-cp314-cp314-win32.whl", hash = "sha256:c1c948747b03be832dceed96ca815cef7360de9aa19d37c730f8e3f6101aca48", size = 151095, upload-time = "2026-07-07T14:34:10.901Z" }, + { url = "https://files.pythonhosted.org/packages/49/fd/a1d26144398c67486422a72bf5812cda22cb4ccfcd95a290fb41ceb4b8e2/charset_normalizer-3.4.9-cp314-cp314-win_amd64.whl", hash = "sha256:16b65ea0f2465b6fb52aa22de5eca612aa964ddfec00a912e26f4656cbef890b", size = 162796, upload-time = "2026-07-07T14:34:12.47Z" }, + { url = "https://files.pythonhosted.org/packages/20/95/d75e82f8ce9fd323ebf059c16c9aadefb22a1ecde13b7840b35835e4886c/charset_normalizer-3.4.9-cp314-cp314-win_arm64.whl", hash = "sha256:40a126142a56b2dfc0aacbad1de8310cbf60da7656db0e6b16eebd48e3e93519", size = 153334, upload-time = "2026-07-07T14:34:14.044Z" }, + { url = "https://files.pythonhosted.org/packages/00/5e/17398df3a139985ba9d11ed072531986f408c8fca952835ef1ab1820c02b/charset_normalizer-3.4.9-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:609b3ba8fcc0fb5ab7af00719d0fb6ad0cb518e48e7712d12fd68f1327951198", size = 338848, upload-time = "2026-07-07T14:34:15.688Z" }, + { url = "https://files.pythonhosted.org/packages/cd/91/7253a32e86b7e1d1239b1b36ba6dd0f021a21107ab33054b53119cc083b9/charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:51447e9aa2684679af07ca5021c3db526e0284347ebf4ffcec1154c3350cfe32", size = 223022, upload-time = "2026-07-07T14:34:17.248Z" }, + { url = "https://files.pythonhosted.org/packages/cb/32/2e64bd2be10e89c61e57ebe6a93fd98ae88eb7ebe414b5121f22c96c69eb/charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cc1b0fff8ead343dae06305f954eb8468ba0ec1a97881f42489d198e4ce3c632", size = 241590, upload-time = "2026-07-07T14:34:18.813Z" }, + { url = "https://files.pythonhosted.org/packages/3d/ef/d96ec496cfea0c21db43b0ad03891308b02388d054cc902cf0e5a1ad6a88/charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fa36ec09ef71d158186bc79e359ff5fdd6e7996fe8ab638f00d6b93139ba4fcf", size = 239584, upload-time = "2026-07-07T14:34:20.52Z" }, + { url = "https://files.pythonhosted.org/packages/d4/ce/9af95f7876194bd7a14e3dfe4a4de2e0bff02666a3910d72beafd06cc297/charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:df115d4d83168fdf2cae48ef1ff6d1cb4c466364e30861b37121de0f3bf1b990", size = 230224, upload-time = "2026-07-07T14:34:22.189Z" }, + { url = "https://files.pythonhosted.org/packages/52/94/af74dde74a3996bd959c350709bfe50e297823d70a8c1cbd54b838880863/charset_normalizer-3.4.9-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:f86c6358749bd4fda175388691e3ba8c46e24c5347d0afd20f9b7edfc9faf07d", size = 212667, upload-time = "2026-07-07T14:34:23.857Z" }, + { url = "https://files.pythonhosted.org/packages/ee/f0/f1c4fe746c395922961b5916ed1d7d6e7d4c84851d19ed43cc89980ec953/charset_normalizer-3.4.9-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:32286a2c8d167e897177b673176c1e3e00d4057caf5d2b64eef9a3666b03018e", size = 227179, upload-time = "2026-07-07T14:34:25.586Z" }, + { url = "https://files.pythonhosted.org/packages/e4/56/6c745619ac397e8871e2bcd3cea1eec86b877488f33888b3aef5c3ed506e/charset_normalizer-3.4.9-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:83aed2c10721ddd90f68140685391b50811a880af20654c59af6b6c66c40513c", size = 225372, upload-time = "2026-07-07T14:34:27.212Z" }, + { url = "https://files.pythonhosted.org/packages/78/ad/98aae8630ac71f16711968e38a5acfecce41b778bf2f0312851020f565a8/charset_normalizer-3.4.9-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:cd6c3d4b783c556fa00bf540854e42f135e2f256abd29669fcd0da0f2dec79c2", size = 215222, upload-time = "2026-07-07T14:34:28.774Z" }, + { url = "https://files.pythonhosted.org/packages/f7/40/9593d54209765207a7f11073c06494c1721e4ca4a0a426c597679bf7f91e/charset_normalizer-3.4.9-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ee2f2a527e3c1a6e6411eb4209642e138b544a2d72fe5d0d76daf77b24063534", size = 231958, upload-time = "2026-07-07T14:34:30.345Z" }, + { url = "https://files.pythonhosted.org/packages/b1/27/693ee5e8a18191eb38647360c51cd505013e2bd3b366aa43fd5344c21e3c/charset_normalizer-3.4.9-cp314-cp314t-win32.whl", hash = "sha256:0d861473f743244d349b50f850d10eb87aeb22bbdcc8e64f79273c94af5a8226", size = 155580, upload-time = "2026-07-07T14:34:31.884Z" }, + { url = "https://files.pythonhosted.org/packages/80/3f/bd97d3d9c613013d07cb7733d299385b41df37f0471310f5a73dc359f0b8/charset_normalizer-3.4.9-cp314-cp314t-win_amd64.whl", hash = "sha256:9b8e0f3107e2200b76f6054de99016eac3ee6762713587b36baaa7e4bd2ae177", size = 167620, upload-time = "2026-07-07T14:34:33.438Z" }, + { url = "https://files.pythonhosted.org/packages/3d/c6/eee9dca4439b1061f76373f06ea855678cc4a64c1c3c90b50e479edbb8eb/charset_normalizer-3.4.9-cp314-cp314t-win_arm64.whl", hash = "sha256:19ac87f93086ce37b86e098888555c4b4bc48102279bae3350098c0ed664b501", size = 158037, upload-time = "2026-07-07T14:34:35.018Z" }, + { url = "https://files.pythonhosted.org/packages/a6/ec/81e22253f4b7091eca6515bb3da5e45d05a663f7f567bb745695dc60f892/charset_normalizer-3.4.9-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:253a4a220747e8b5faf57ec320c4f5efb0cef05f647420bf267143ec15dba10a", size = 306122, upload-time = "2026-07-07T14:34:36.607Z" }, + { url = "https://files.pythonhosted.org/packages/c8/53/a8c042eb9eee4716f4d42a0f5a571eb32a09ec429be9fb0b8b9d765393ba/charset_normalizer-3.4.9-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:68ce9f4d6b26d5ccbf7fd4459bf75f74a0a146677ebba80597df60cbdb20e6f4", size = 206284, upload-time = "2026-07-07T14:34:38.166Z" }, + { url = "https://files.pythonhosted.org/packages/14/cb/1db8b96547ee3186cd2dd7f2e59dd560a9b80748f3604171f3c153d62811/charset_normalizer-3.4.9-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:58150c9f9b9a552505912d182ccdf26f6396fb6094816ceebcbb20eecabaed94", size = 226837, upload-time = "2026-07-07T14:34:39.77Z" }, + { url = "https://files.pythonhosted.org/packages/6a/05/c94d5cd23396289c54c93b02e0273b4dd8921641d9968c4828caf9bbaad9/charset_normalizer-3.4.9-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:df7276909358e5635ae203673ab7e509ddd224225a8d6b0790bf13eb2bde1cc5", size = 222199, upload-time = "2026-07-07T14:34:41.391Z" }, + { url = "https://files.pythonhosted.org/packages/6d/46/79847edd07244a4a2d443c6655a7b6ee94203c21539414b059f32713c357/charset_normalizer-3.4.9-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3c09a49d6cde137258beb3d551994a2927fd35ad5cf96aed573f61bbd67c5f84", size = 214344, upload-time = "2026-07-07T14:34:42.986Z" }, + { url = "https://files.pythonhosted.org/packages/ec/b4/ef5a49b2e77c00deb43bb3256592b115ba9e4346016e82c516b8d215bf68/charset_normalizer-3.4.9-cp39-cp39-manylinux_2_31_armv7l.whl", hash = "sha256:231ddcbb35e2ff8973e1365db41fe0572662893b99a05deb183b68ad4c0c8bd4", size = 199988, upload-time = "2026-07-07T14:34:44.685Z" }, + { url = "https://files.pythonhosted.org/packages/3d/ca/ad1d7c7d3077dab873f539d3e1d083c0845a762cb0bafdfbe3ef93add598/charset_normalizer-3.4.9-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:920079c3f7456fa213e0829ed2073aaa727fd39d889ead5b4f35d0de5460d04f", size = 211908, upload-time = "2026-07-07T14:34:46.227Z" }, + { url = "https://files.pythonhosted.org/packages/ed/61/710738687f90d01c06a04ed52d6ca1e62dd9b1d8cc2567098167c4691034/charset_normalizer-3.4.9-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:0fa1aec2d32bcc03c8fa0f6f1712caad1adc38509f31142112e5c9daf5b9c833", size = 209320, upload-time = "2026-07-07T14:34:47.753Z" }, + { url = "https://files.pythonhosted.org/packages/5f/c0/6eec7bdabe6cbbcc274ec04596f6d93865751a0541d33d60d1ce179bd372/charset_normalizer-3.4.9-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:ad41ba96094304aa090f5a30cb6e4fb3b3f1c264c523394b4c39bbacc4dc92ba", size = 200980, upload-time = "2026-07-07T14:34:49.362Z" }, + { url = "https://files.pythonhosted.org/packages/eb/78/59344ff9a4a7b5f6530bf7bec2c980047cc42c3a616596cdbd8cb5c1a1af/charset_normalizer-3.4.9-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:43b9e366a31fdd1c87d0eb08f579b4a82b723ea54338f040d6b4e518a026ea29", size = 216545, upload-time = "2026-07-07T14:34:50.98Z" }, + { url = "https://files.pythonhosted.org/packages/17/6d/bff78a4bacc4891bc63ec5bdc6776d8c85e47fab93d0d5f6223068fad0a4/charset_normalizer-3.4.9-cp39-cp39-win32.whl", hash = "sha256:93d59d504b230e83c7a843251681959a0b6a9cd76f6e146ce1b8a80eb8739af9", size = 146256, upload-time = "2026-07-07T14:34:52.509Z" }, + { url = "https://files.pythonhosted.org/packages/a2/55/86048bde1c9d0352940bd7b87d825091a52aef67d01cde6c6f7342c5b552/charset_normalizer-3.4.9-cp39-cp39-win_amd64.whl", hash = "sha256:ddf4af30b417d9fe16481e9b81c27ab2a7cde1ff7ba3e85653b02db7d145dc7b", size = 156413, upload-time = "2026-07-07T14:34:54.117Z" }, + { url = "https://files.pythonhosted.org/packages/28/e9/9fb6099b868c82a40698a748ae0fbd4f31ccc13844c176a07158ba2abbfd/charset_normalizer-3.4.9-cp39-cp39-win_arm64.whl", hash = "sha256:476743fe6dfe14a2da12e3ac79125dc84a3b2cf8094369a47a1529b0cd8549fe", size = 147887, upload-time = "2026-07-07T14:34:55.51Z" }, + { url = "https://files.pythonhosted.org/packages/98/2b/f97f1c193fb855c345d678f5077d6926034db0722df74c8f057020e05a25/charset_normalizer-3.4.9-py3-none-any.whl", hash = "sha256:68e5f26a1ad57ded6d1cfb85331d1c1a195314756471d97758c48498bb4dcdf5", size = 64538, upload-time = "2026-07-07T14:34:56.993Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "docutils" +version = "0.21.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.10.*'", + "python_full_version < '3.10'", +] +sdist = { url = "https://files.pythonhosted.org/packages/ae/ed/aefcc8cd0ba62a0560c3c18c33925362d46c6075480bfa4df87b28e169a9/docutils-0.21.2.tar.gz", hash = "sha256:3a6b18732edf182daa3cd12775bbb338cf5691468f91eeeb109deff6ebfa986f", size = 2204444, upload-time = "2024-04-23T18:57:18.24Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8f/d7/9322c609343d929e75e7e5e6255e614fcc67572cfd083959cdef3b7aad79/docutils-0.21.2-py3-none-any.whl", hash = "sha256:dafca5b9e384f0e419294eb4d2ff9fa826435bf15f15b7bd45723e8ad76811b2", size = 587408, upload-time = "2024-04-23T18:57:14.835Z" }, +] + +[[package]] +name = "docutils" +version = "0.22.4" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.12'", + "python_full_version == '3.11.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/ae/b6/03bb70946330e88ffec97aefd3ea75ba575cb2e762061e0e62a213befee8/docutils-0.22.4.tar.gz", hash = "sha256:4db53b1fde9abecbb74d91230d32ab626d94f6badfc575d6db9194a49df29968", size = 2291750, upload-time = "2025-12-18T19:00:26.443Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/02/10/5da547df7a391dcde17f59520a231527b8571e6f46fc8efb02ccb370ab12/docutils-0.22.4-py3-none-any.whl", hash = "sha256:d0013f540772d1420576855455d050a2180186c91c15779301ac2ccb3eeb68de", size = 633196, upload-time = "2025-12-18T19:00:18.077Z" }, +] + +[[package]] +name = "idna" +version = "3.18" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cd/63/9496c57188a2ee585e0f1db071d75089a11e98aa86eb99d9d7618fc1edce/idna-3.18.tar.gz", hash = "sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848", size = 196711, upload-time = "2026-06-02T14:34:07.794Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl", hash = "sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2", size = 65455, upload-time = "2026-06-02T14:34:06.319Z" }, +] + +[[package]] +name = "imagesize" +version = "1.5.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +sdist = { url = "https://files.pythonhosted.org/packages/cf/59/4b0dd64676aa6fb4986a755790cb6fc558559cf0084effad516820208ec3/imagesize-1.5.0.tar.gz", hash = "sha256:8bfc5363a7f2133a89f0098451e0bcb1cd71aba4dc02bbcecb39d99d40e1b94f", size = 1281127, upload-time = "2026-03-03T01:59:54.651Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/b1/a0662b03103c66cf77101a187f396ea91167cd9b7d5d3a2e465ad2c7ee9b/imagesize-1.5.0-py2.py3-none-any.whl", hash = "sha256:32677681b3f434c2cb496f00e89c5a291247b35b1f527589909e008057da5899", size = 5763, upload-time = "2026-03-03T01:59:52.343Z" }, +] + +[[package]] +name = "imagesize" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.12'", + "python_full_version == '3.11.*'", + "python_full_version == '3.10.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/6c/e6/7bf14eeb8f8b7251141944835abd42eb20a658d89084b7e1f3e5fe394090/imagesize-2.0.0.tar.gz", hash = "sha256:8e8358c4a05c304f1fccf7ff96f036e7243a189e9e42e90851993c558cfe9ee3", size = 1773045, upload-time = "2026-03-03T14:18:29.941Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5f/53/fb7122b71361a0d121b669dcf3d31244ef75badbbb724af388948de543e2/imagesize-2.0.0-py2.py3-none-any.whl", hash = "sha256:5667c5bbb57ab3f1fa4bc366f4fbc971db3d5ed011fd2715fd8001f782718d96", size = 9441, upload-time = "2026-03-03T14:18:27.892Z" }, +] + +[[package]] +name = "importlib-metadata" +version = "8.7.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "zipp", marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f3/49/3b30cad09e7771a4982d9975a8cbf64f00d4a1ececb53297f1d9a7be1b10/importlib_metadata-8.7.1.tar.gz", hash = "sha256:49fef1ae6440c182052f407c8d34a68f72efc36db9ca90dc0113398f2fdde8bb", size = 57107, upload-time = "2025-12-21T10:00:19.278Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fa/5e/f8e9a1d23b9c20a551a8a02ea3637b4642e22c2626e3a13a9a29cdea99eb/importlib_metadata-8.7.1-py3-none-any.whl", hash = "sha256:5a1f80bf1daa489495071efbb095d75a634cf28a8bc299581244063b53176151", size = 27865, upload-time = "2025-12-21T10:00:18.329Z" }, +] + +[[package]] +name = "jinja2" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, +] + +[[package]] +name = "markdown-it-py" +version = "3.0.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.10.*'", + "python_full_version < '3.10'", +] +dependencies = [ + { name = "mdurl", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596, upload-time = "2023-06-03T06:41:14.443Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528, upload-time = "2023-06-03T06:41:11.019Z" }, +] + +[[package]] +name = "markdown-it-py" +version = "4.2.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.12'", + "python_full_version == '3.11.*'", +] +dependencies = [ + { name = "mdurl", marker = "python_full_version >= '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/ff/7841249c247aa650a76b9ee4bbaeae59370dc8bfd2f6c01f3630c35eb134/markdown_it_py-4.2.0.tar.gz", hash = "sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49", size = 82454, upload-time = "2026-05-07T12:08:28.36Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl", hash = "sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a", size = 91687, upload-time = "2026-05-07T12:08:27.182Z" }, +] + +[[package]] +name = "markupsafe" +version = "3.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e8/4b/3541d44f3937ba468b75da9eebcae497dcf67adb65caa16760b0a6807ebb/markupsafe-3.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2f981d352f04553a7171b8e44369f2af4055f888dfb147d55e42d29e29e74559", size = 11631, upload-time = "2025-09-27T18:36:05.558Z" }, + { url = "https://files.pythonhosted.org/packages/98/1b/fbd8eed11021cabd9226c37342fa6ca4e8a98d8188a8d9b66740494960e4/markupsafe-3.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e1c1493fb6e50ab01d20a22826e57520f1284df32f2d8601fdd90b6304601419", size = 12057, upload-time = "2025-09-27T18:36:07.165Z" }, + { url = "https://files.pythonhosted.org/packages/40/01/e560d658dc0bb8ab762670ece35281dec7b6c1b33f5fbc09ebb57a185519/markupsafe-3.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ba88449deb3de88bd40044603fafffb7bc2b055d626a330323a9ed736661695", size = 22050, upload-time = "2025-09-27T18:36:08.005Z" }, + { url = "https://files.pythonhosted.org/packages/af/cd/ce6e848bbf2c32314c9b237839119c5a564a59725b53157c856e90937b7a/markupsafe-3.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f42d0984e947b8adf7dd6dde396e720934d12c506ce84eea8476409563607591", size = 20681, upload-time = "2025-09-27T18:36:08.881Z" }, + { url = "https://files.pythonhosted.org/packages/c9/2a/b5c12c809f1c3045c4d580b035a743d12fcde53cf685dbc44660826308da/markupsafe-3.0.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c0c0b3ade1c0b13b936d7970b1d37a57acde9199dc2aecc4c336773e1d86049c", size = 20705, upload-time = "2025-09-27T18:36:10.131Z" }, + { url = "https://files.pythonhosted.org/packages/cf/e3/9427a68c82728d0a88c50f890d0fc072a1484de2f3ac1ad0bfc1a7214fd5/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f", size = 21524, upload-time = "2025-09-27T18:36:11.324Z" }, + { url = "https://files.pythonhosted.org/packages/bc/36/23578f29e9e582a4d0278e009b38081dbe363c5e7165113fad546918a232/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:d2ee202e79d8ed691ceebae8e0486bd9a2cd4794cec4824e1c99b6f5009502f6", size = 20282, upload-time = "2025-09-27T18:36:12.573Z" }, + { url = "https://files.pythonhosted.org/packages/56/21/dca11354e756ebd03e036bd8ad58d6d7168c80ce1fe5e75218e4945cbab7/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:177b5253b2834fe3678cb4a5f0059808258584c559193998be2601324fdeafb1", size = 20745, upload-time = "2025-09-27T18:36:13.504Z" }, + { url = "https://files.pythonhosted.org/packages/87/99/faba9369a7ad6e4d10b6a5fbf71fa2a188fe4a593b15f0963b73859a1bbd/markupsafe-3.0.3-cp310-cp310-win32.whl", hash = "sha256:2a15a08b17dd94c53a1da0438822d70ebcd13f8c3a95abe3a9ef9f11a94830aa", size = 14571, upload-time = "2025-09-27T18:36:14.779Z" }, + { url = "https://files.pythonhosted.org/packages/d6/25/55dc3ab959917602c96985cb1253efaa4ff42f71194bddeb61eb7278b8be/markupsafe-3.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:c4ffb7ebf07cfe8931028e3e4c85f0357459a3f9f9490886198848f4fa002ec8", size = 15056, upload-time = "2025-09-27T18:36:16.125Z" }, + { url = "https://files.pythonhosted.org/packages/d0/9e/0a02226640c255d1da0b8d12e24ac2aa6734da68bff14c05dd53b94a0fc3/markupsafe-3.0.3-cp310-cp310-win_arm64.whl", hash = "sha256:e2103a929dfa2fcaf9bb4e7c091983a49c9ac3b19c9061b6d5427dd7d14d81a1", size = 13932, upload-time = "2025-09-27T18:36:17.311Z" }, + { url = "https://files.pythonhosted.org/packages/08/db/fefacb2136439fc8dd20e797950e749aa1f4997ed584c62cfb8ef7c2be0e/markupsafe-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad", size = 11631, upload-time = "2025-09-27T18:36:18.185Z" }, + { url = "https://files.pythonhosted.org/packages/e1/2e/5898933336b61975ce9dc04decbc0a7f2fee78c30353c5efba7f2d6ff27a/markupsafe-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a", size = 12058, upload-time = "2025-09-27T18:36:19.444Z" }, + { url = "https://files.pythonhosted.org/packages/1d/09/adf2df3699d87d1d8184038df46a9c80d78c0148492323f4693df54e17bb/markupsafe-3.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50", size = 24287, upload-time = "2025-09-27T18:36:20.768Z" }, + { url = "https://files.pythonhosted.org/packages/30/ac/0273f6fcb5f42e314c6d8cd99effae6a5354604d461b8d392b5ec9530a54/markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf", size = 22940, upload-time = "2025-09-27T18:36:22.249Z" }, + { url = "https://files.pythonhosted.org/packages/19/ae/31c1be199ef767124c042c6c3e904da327a2f7f0cd63a0337e1eca2967a8/markupsafe-3.0.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f", size = 21887, upload-time = "2025-09-27T18:36:23.535Z" }, + { url = "https://files.pythonhosted.org/packages/b2/76/7edcab99d5349a4532a459e1fe64f0b0467a3365056ae550d3bcf3f79e1e/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a", size = 23692, upload-time = "2025-09-27T18:36:24.823Z" }, + { url = "https://files.pythonhosted.org/packages/a4/28/6e74cdd26d7514849143d69f0bf2399f929c37dc2b31e6829fd2045b2765/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115", size = 21471, upload-time = "2025-09-27T18:36:25.95Z" }, + { url = "https://files.pythonhosted.org/packages/62/7e/a145f36a5c2945673e590850a6f8014318d5577ed7e5920a4b3448e0865d/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a", size = 22923, upload-time = "2025-09-27T18:36:27.109Z" }, + { url = "https://files.pythonhosted.org/packages/0f/62/d9c46a7f5c9adbeeeda52f5b8d802e1094e9717705a645efc71b0913a0a8/markupsafe-3.0.3-cp311-cp311-win32.whl", hash = "sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19", size = 14572, upload-time = "2025-09-27T18:36:28.045Z" }, + { url = "https://files.pythonhosted.org/packages/83/8a/4414c03d3f891739326e1783338e48fb49781cc915b2e0ee052aa490d586/markupsafe-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01", size = 15077, upload-time = "2025-09-27T18:36:29.025Z" }, + { url = "https://files.pythonhosted.org/packages/35/73/893072b42e6862f319b5207adc9ae06070f095b358655f077f69a35601f0/markupsafe-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c", size = 13876, upload-time = "2025-09-27T18:36:29.954Z" }, + { url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615, upload-time = "2025-09-27T18:36:30.854Z" }, + { url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020, upload-time = "2025-09-27T18:36:31.971Z" }, + { url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332, upload-time = "2025-09-27T18:36:32.813Z" }, + { url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947, upload-time = "2025-09-27T18:36:33.86Z" }, + { url = "https://files.pythonhosted.org/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962, upload-time = "2025-09-27T18:36:35.099Z" }, + { url = "https://files.pythonhosted.org/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760, upload-time = "2025-09-27T18:36:36.001Z" }, + { url = "https://files.pythonhosted.org/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529, upload-time = "2025-09-27T18:36:36.906Z" }, + { url = "https://files.pythonhosted.org/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015, upload-time = "2025-09-27T18:36:37.868Z" }, + { url = "https://files.pythonhosted.org/packages/2f/e1/78ee7a023dac597a5825441ebd17170785a9dab23de95d2c7508ade94e0e/markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", size = 14540, upload-time = "2025-09-27T18:36:38.761Z" }, + { url = "https://files.pythonhosted.org/packages/aa/5b/bec5aa9bbbb2c946ca2733ef9c4ca91c91b6a24580193e891b5f7dbe8e1e/markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", size = 15105, upload-time = "2025-09-27T18:36:39.701Z" }, + { url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906, upload-time = "2025-09-27T18:36:40.689Z" }, + { url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" }, + { url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" }, + { url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" }, + { url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" }, + { url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" }, + { url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" }, + { url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" }, + { url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" }, + { url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" }, + { url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" }, + { url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" }, + { url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" }, + { url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" }, + { url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" }, + { url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" }, + { url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" }, + { url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" }, + { url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" }, + { url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" }, + { url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" }, + { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" }, + { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" }, + { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" }, + { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" }, + { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" }, + { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" }, + { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" }, + { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" }, + { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" }, + { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" }, + { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" }, + { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" }, + { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" }, + { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" }, + { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" }, + { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" }, + { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" }, + { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, + { url = "https://files.pythonhosted.org/packages/56/23/0d8c13a44bde9154821586520840643467aee574d8ce79a17da539ee7fed/markupsafe-3.0.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:15d939a21d546304880945ca1ecb8a039db6b4dc49b2c5a400387cdae6a62e26", size = 11623, upload-time = "2025-09-27T18:37:29.296Z" }, + { url = "https://files.pythonhosted.org/packages/fd/23/07a2cb9a8045d5f3f0890a8c3bc0859d7a47bfd9a560b563899bec7b72ed/markupsafe-3.0.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f71a396b3bf33ecaa1626c255855702aca4d3d9fea5e051b41ac59a9c1c41edc", size = 12049, upload-time = "2025-09-27T18:37:30.234Z" }, + { url = "https://files.pythonhosted.org/packages/bc/e4/6be85eb81503f8e11b61c0b6369b6e077dcf0a74adbd9ebf6b349937b4e9/markupsafe-3.0.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f4b68347f8c5eab4a13419215bdfd7f8c9b19f2b25520968adfad23eb0ce60c", size = 21923, upload-time = "2025-09-27T18:37:31.177Z" }, + { url = "https://files.pythonhosted.org/packages/6f/bc/4dc914ead3fe6ddaef035341fee0fc956949bbd27335b611829292b89ee2/markupsafe-3.0.3-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e8fc20152abba6b83724d7ff268c249fa196d8259ff481f3b1476383f8f24e42", size = 20543, upload-time = "2025-09-27T18:37:32.168Z" }, + { url = "https://files.pythonhosted.org/packages/89/6e/5fe81fbcfba4aef4093d5f856e5c774ec2057946052d18d168219b7bd9f9/markupsafe-3.0.3-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:949b8d66bc381ee8b007cd945914c721d9aba8e27f71959d750a46f7c282b20b", size = 20585, upload-time = "2025-09-27T18:37:33.166Z" }, + { url = "https://files.pythonhosted.org/packages/f6/f6/e0e5a3d3ae9c4020f696cd055f940ef86b64fe88de26f3a0308b9d3d048c/markupsafe-3.0.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:3537e01efc9d4dccdf77221fb1cb3b8e1a38d5428920e0657ce299b20324d758", size = 21387, upload-time = "2025-09-27T18:37:34.185Z" }, + { url = "https://files.pythonhosted.org/packages/c8/25/651753ef4dea08ea790f4fbb65146a9a44a014986996ca40102e237aa49a/markupsafe-3.0.3-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:591ae9f2a647529ca990bc681daebdd52c8791ff06c2bfa05b65163e28102ef2", size = 20133, upload-time = "2025-09-27T18:37:35.138Z" }, + { url = "https://files.pythonhosted.org/packages/dc/0a/c3cf2b4fef5f0426e8a6d7fce3cb966a17817c568ce59d76b92a233fdbec/markupsafe-3.0.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:a320721ab5a1aba0a233739394eb907f8c8da5c98c9181d1161e77a0c8e36f2d", size = 20588, upload-time = "2025-09-27T18:37:36.096Z" }, + { url = "https://files.pythonhosted.org/packages/cd/1b/a7782984844bd519ad4ffdbebbba2671ec5d0ebbeac34736c15fb86399e8/markupsafe-3.0.3-cp39-cp39-win32.whl", hash = "sha256:df2449253ef108a379b8b5d6b43f4b1a8e81a061d6537becd5582fba5f9196d7", size = 14566, upload-time = "2025-09-27T18:37:37.09Z" }, + { url = "https://files.pythonhosted.org/packages/18/1f/8d9c20e1c9440e215a44be5ab64359e207fcb4f675543f1cf9a2a7f648d0/markupsafe-3.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:7c3fb7d25180895632e5d3148dbdc29ea38ccb7fd210aa27acbd1201a1902c6e", size = 15053, upload-time = "2025-09-27T18:37:38.054Z" }, + { url = "https://files.pythonhosted.org/packages/4e/d3/fe08482b5cd995033556d45041a4f4e76e7f0521112a9c9991d40d39825f/markupsafe-3.0.3-cp39-cp39-win_arm64.whl", hash = "sha256:38664109c14ffc9e7437e86b4dceb442b0096dfe3541d7864d9cbe1da4cf36c8", size = 13928, upload-time = "2025-09-27T18:37:39.037Z" }, +] + +[[package]] +name = "mdit-py-plugins" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +dependencies = [ + { name = "markdown-it-py", version = "3.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/19/03/a2ecab526543b152300717cf232bb4bb8605b6edb946c845016fa9c9c9fd/mdit_py_plugins-0.4.2.tar.gz", hash = "sha256:5f2cd1fdb606ddf152d37ec30e46101a60512bc0e5fa1a7002c36647b09e26b5", size = 43542, upload-time = "2024-09-09T20:27:49.564Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/f7/7782a043553ee469c1ff49cfa1cdace2d6bf99a1f333cf38676b3ddf30da/mdit_py_plugins-0.4.2-py3-none-any.whl", hash = "sha256:0c673c3f889399a33b95e88d2f0d111b4447bdfea7f237dab2d488f459835636", size = 55316, upload-time = "2024-09-09T20:27:48.397Z" }, +] + +[[package]] +name = "mdit-py-plugins" +version = "0.6.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.12'", + "python_full_version == '3.11.*'", + "python_full_version == '3.10.*'", +] +dependencies = [ + { name = "markdown-it-py", version = "3.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, + { name = "markdown-it-py", version = "4.2.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/59/fc/f8d0863f8862f25602c0404d75568e89fb6b4109804645e5cdfb1be5cf56/mdit_py_plugins-0.6.1.tar.gz", hash = "sha256:a2bca0f039f39dbd35fb74ae1b5f998608c437463371f0ff7f49a19a17a114d0", size = 56114, upload-time = "2026-05-13T09:03:38.91Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a5/69/6da5581c6a7fede7dc261bf4e67d6adca4196f176b43288b55b3db395b6e/mdit_py_plugins-0.6.1-py3-none-any.whl", hash = "sha256:214c82fb2ac524472ab6a5bcab1de80f73b50443e187f401bfd77efbc7c6481d", size = 66663, upload-time = "2026-05-13T09:03:37.76Z" }, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, +] + +[[package]] +name = "myst-parser" +version = "3.0.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +dependencies = [ + { name = "docutils", version = "0.21.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "jinja2", marker = "python_full_version < '3.10'" }, + { name = "markdown-it-py", version = "3.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "mdit-py-plugins", version = "0.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "pyyaml", marker = "python_full_version < '3.10'" }, + { name = "sphinx", version = "7.4.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/49/64/e2f13dac02f599980798c01156393b781aec983b52a6e4057ee58f07c43a/myst_parser-3.0.1.tar.gz", hash = "sha256:88f0cb406cb363b077d176b51c476f62d60604d68a8dcdf4832e080441301a87", size = 92392, upload-time = "2024-04-28T20:22:42.116Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e2/de/21aa8394f16add8f7427f0a1326ccd2b3a2a8a3245c9252bc5ac034c6155/myst_parser-3.0.1-py3-none-any.whl", hash = "sha256:6457aaa33a5d474aca678b8ead9b3dc298e89c68e67012e73146ea6fd54babf1", size = 83163, upload-time = "2024-04-28T20:22:39.985Z" }, +] + +[[package]] +name = "myst-parser" +version = "4.0.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.10.*'", +] +dependencies = [ + { name = "docutils", version = "0.21.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, + { name = "jinja2", marker = "python_full_version == '3.10.*'" }, + { name = "markdown-it-py", version = "3.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, + { name = "mdit-py-plugins", version = "0.6.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, + { name = "pyyaml", marker = "python_full_version == '3.10.*'" }, + { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/a5/9626ba4f73555b3735ad86247a8077d4603aa8628537687c839ab08bfe44/myst_parser-4.0.1.tar.gz", hash = "sha256:5cfea715e4f3574138aecbf7d54132296bfd72bb614d31168f48c477a830a7c4", size = 93985, upload-time = "2025-02-12T10:53:03.833Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5f/df/76d0321c3797b54b60fef9ec3bd6f4cfd124b9e422182156a1dd418722cf/myst_parser-4.0.1-py3-none-any.whl", hash = "sha256:9134e88959ec3b5780aedf8a99680ea242869d012e8821db3126d427edc9c95d", size = 84579, upload-time = "2025-02-12T10:53:02.078Z" }, +] + +[[package]] +name = "myst-parser" +version = "5.1.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.12'", + "python_full_version == '3.11.*'", +] +dependencies = [ + { name = "docutils", version = "0.22.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "jinja2", marker = "python_full_version >= '3.11'" }, + { name = "markdown-it-py", version = "4.2.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "mdit-py-plugins", version = "0.6.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "pyyaml", marker = "python_full_version >= '3.11'" }, + { name = "sphinx", version = "9.0.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, + { name = "sphinx", version = "9.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/21/dc/603751677fff302f34396e206b610f556a59d7fe58b9a2145f54e96b48e8/myst_parser-5.1.0.tar.gz", hash = "sha256:ab69322dc6719dcc7f296479dbb70181b66df6ed315064f92dbc85c0e1bf2f02", size = 101182, upload-time = "2026-05-13T09:38:19.361Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/09/dc/f3dfb7488b770f3f67e6545085bf2abea5172e88f57b8ad25ef860ca704c/myst_parser-5.1.0-py3-none-any.whl", hash = "sha256:9c91c52b3cdb4d94a6506e4fab4e2f296c7623a0da0dcbe6de1565c3dad67a8a", size = 85817, upload-time = "2026-05-13T09:38:17.904Z" }, +] + +[[package]] +name = "packaging" +version = "26.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/f1/e7a6dd94a8d4a5626c03e4e99c87f241ba9e350cd9e6d75123f992427270/packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661", size = 228134, upload-time = "2026-04-24T20:15:23.917Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" }, +] + +[[package]] +name = "pygments" +version = "2.20.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, +] + +[[package]] +name = "pyyaml" +version = "6.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/a0/39350dd17dd6d6c6507025c0e53aef67a9293a6d37d3511f23ea510d5800/pyyaml-6.0.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b", size = 184227, upload-time = "2025-09-25T21:31:46.04Z" }, + { url = "https://files.pythonhosted.org/packages/05/14/52d505b5c59ce73244f59c7a50ecf47093ce4765f116cdb98286a71eeca2/pyyaml-6.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956", size = 174019, upload-time = "2025-09-25T21:31:47.706Z" }, + { url = "https://files.pythonhosted.org/packages/43/f7/0e6a5ae5599c838c696adb4e6330a59f463265bfa1e116cfd1fbb0abaaae/pyyaml-6.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8", size = 740646, upload-time = "2025-09-25T21:31:49.21Z" }, + { url = "https://files.pythonhosted.org/packages/2f/3a/61b9db1d28f00f8fd0ae760459a5c4bf1b941baf714e207b6eb0657d2578/pyyaml-6.0.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198", size = 840793, upload-time = "2025-09-25T21:31:50.735Z" }, + { url = "https://files.pythonhosted.org/packages/7a/1e/7acc4f0e74c4b3d9531e24739e0ab832a5edf40e64fbae1a9c01941cabd7/pyyaml-6.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b", size = 770293, upload-time = "2025-09-25T21:31:51.828Z" }, + { url = "https://files.pythonhosted.org/packages/8b/ef/abd085f06853af0cd59fa5f913d61a8eab65d7639ff2a658d18a25d6a89d/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0", size = 732872, upload-time = "2025-09-25T21:31:53.282Z" }, + { url = "https://files.pythonhosted.org/packages/1f/15/2bc9c8faf6450a8b3c9fc5448ed869c599c0a74ba2669772b1f3a0040180/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69", size = 758828, upload-time = "2025-09-25T21:31:54.807Z" }, + { url = "https://files.pythonhosted.org/packages/a3/00/531e92e88c00f4333ce359e50c19b8d1de9fe8d581b1534e35ccfbc5f393/pyyaml-6.0.3-cp310-cp310-win32.whl", hash = "sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e", size = 142415, upload-time = "2025-09-25T21:31:55.885Z" }, + { url = "https://files.pythonhosted.org/packages/2a/fa/926c003379b19fca39dd4634818b00dec6c62d87faf628d1394e137354d4/pyyaml-6.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c", size = 158561, upload-time = "2025-09-25T21:31:57.406Z" }, + { url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" }, + { url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" }, + { url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" }, + { url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114, upload-time = "2025-09-25T21:32:03.376Z" }, + { url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638, upload-time = "2025-09-25T21:32:04.553Z" }, + { url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463, upload-time = "2025-09-25T21:32:06.152Z" }, + { url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986, upload-time = "2025-09-25T21:32:07.367Z" }, + { url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543, upload-time = "2025-09-25T21:32:08.95Z" }, + { url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763, upload-time = "2025-09-25T21:32:09.96Z" }, + { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" }, + { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" }, + { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" }, + { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" }, + { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" }, + { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" }, + { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" }, + { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" }, + { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" }, + { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" }, + { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" }, + { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" }, + { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, + { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" }, + { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" }, + { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" }, + { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, + { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, + { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, + { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, + { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, + { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, + { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, + { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, + { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, + { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, + { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, + { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, + { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, + { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, + { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, + { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, + { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, + { url = "https://files.pythonhosted.org/packages/9f/62/67fc8e68a75f738c9200422bf65693fb79a4cd0dc5b23310e5202e978090/pyyaml-6.0.3-cp39-cp39-macosx_10_13_x86_64.whl", hash = "sha256:b865addae83924361678b652338317d1bd7e79b1f4596f96b96c77a5a34b34da", size = 184450, upload-time = "2025-09-25T21:33:00.618Z" }, + { url = "https://files.pythonhosted.org/packages/ae/92/861f152ce87c452b11b9d0977952259aa7df792d71c1053365cc7b09cc08/pyyaml-6.0.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c3355370a2c156cffb25e876646f149d5d68f5e0a3ce86a5084dd0b64a994917", size = 174319, upload-time = "2025-09-25T21:33:02.086Z" }, + { url = "https://files.pythonhosted.org/packages/d0/cd/f0cfc8c74f8a030017a2b9c771b7f47e5dd702c3e28e5b2071374bda2948/pyyaml-6.0.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3c5677e12444c15717b902a5798264fa7909e41153cdf9ef7ad571b704a63dd9", size = 737631, upload-time = "2025-09-25T21:33:03.25Z" }, + { url = "https://files.pythonhosted.org/packages/ef/b2/18f2bd28cd2055a79a46c9b0895c0b3d987ce40ee471cecf58a1a0199805/pyyaml-6.0.3-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5ed875a24292240029e4483f9d4a4b8a1ae08843b9c54f43fcc11e404532a8a5", size = 836795, upload-time = "2025-09-25T21:33:05.014Z" }, + { url = "https://files.pythonhosted.org/packages/73/b9/793686b2d54b531203c160ef12bec60228a0109c79bae6c1277961026770/pyyaml-6.0.3-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0150219816b6a1fa26fb4699fb7daa9caf09eb1999f3b70fb6e786805e80375a", size = 750767, upload-time = "2025-09-25T21:33:06.398Z" }, + { url = "https://files.pythonhosted.org/packages/a9/86/a137b39a611def2ed78b0e66ce2fe13ee701a07c07aebe55c340ed2a050e/pyyaml-6.0.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:fa160448684b4e94d80416c0fa4aac48967a969efe22931448d853ada8baf926", size = 727982, upload-time = "2025-09-25T21:33:08.708Z" }, + { url = "https://files.pythonhosted.org/packages/dd/62/71c27c94f457cf4418ef8ccc71735324c549f7e3ea9d34aba50874563561/pyyaml-6.0.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:27c0abcb4a5dac13684a37f76e701e054692a9b2d3064b70f5e4eb54810553d7", size = 755677, upload-time = "2025-09-25T21:33:09.876Z" }, + { url = "https://files.pythonhosted.org/packages/29/3d/6f5e0d58bd924fb0d06c3a6bad00effbdae2de5adb5cda5648006ffbd8d3/pyyaml-6.0.3-cp39-cp39-win32.whl", hash = "sha256:1ebe39cb5fc479422b83de611d14e2c0d3bb2a18bbcb01f229ab3cfbd8fee7a0", size = 142592, upload-time = "2025-09-25T21:33:10.983Z" }, + { url = "https://files.pythonhosted.org/packages/f0/0c/25113e0b5e103d7f1490c0e947e303fe4a696c10b501dea7a9f49d4e876c/pyyaml-6.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:2e71d11abed7344e42a8849600193d15b6def118602c4c176f748e4583246007", size = 158777, upload-time = "2025-09-25T21:33:15.55Z" }, +] + +[[package]] +name = "readthedocs-sphinx-ext" +version = "2.2.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jinja2" }, + { name = "packaging" }, + { name = "requests", version = "2.32.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "requests", version = "2.34.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e8/ce/38130d8dec600bf5413eb89a3413dd38f204c7c728c4947e12ff8cb793b7/readthedocs-sphinx-ext-2.2.5.tar.gz", hash = "sha256:ee5fd5b99db9f0c180b2396cbce528aa36671951b9526bb0272dbfce5517bd27", size = 12303, upload-time = "2023-12-19T10:00:49.573Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/64/71/c89e7709a0d4f93af1848e9855112299a820b470d84f917b4dd5998bdd07/readthedocs_sphinx_ext-2.2.5-py2.py3-none-any.whl", hash = "sha256:f8c56184ea011c972dd45a90122568587cc85b0127bc9cf064d17c68bc809daa", size = 11332, upload-time = "2023-12-19T10:00:43.972Z" }, +] + +[[package]] +name = "requests" +version = "2.32.5" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +dependencies = [ + { name = "certifi", marker = "python_full_version < '3.10'" }, + { name = "charset-normalizer", marker = "python_full_version < '3.10'" }, + { name = "idna", marker = "python_full_version < '3.10'" }, + { name = "urllib3", version = "2.6.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" }, +] + +[[package]] +name = "requests" +version = "2.34.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.12'", + "python_full_version == '3.11.*'", + "python_full_version == '3.10.*'", +] +dependencies = [ + { name = "certifi", marker = "python_full_version >= '3.10'" }, + { name = "charset-normalizer", marker = "python_full_version >= '3.10'" }, + { name = "idna", marker = "python_full_version >= '3.10'" }, + { name = "urllib3", version = "2.7.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ac/c3/e2a2b89f2d3e2179abd6d00ebd70bff6273f37fb3e0cc209f48b39d00cbf/requests-2.34.2.tar.gz", hash = "sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed", size = 142856, upload-time = "2026-05-14T19:25:27.735Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0", size = 73075, upload-time = "2026-05-14T19:25:26.443Z" }, +] + +[[package]] +name = "roman-numerals" +version = "4.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ae/f9/41dc953bbeb056c17d5f7a519f50fdf010bd0553be2d630bc69d1e022703/roman_numerals-4.1.0.tar.gz", hash = "sha256:1af8b147eb1405d5839e78aeb93131690495fe9da5c91856cb33ad55a7f1e5b2", size = 9077, upload-time = "2025-12-17T18:25:34.381Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/54/6f679c435d28e0a568d8e8a7c0a93a09010818634c3c3907fc98d8983770/roman_numerals-4.1.0-py3-none-any.whl", hash = "sha256:647ba99caddc2cc1e55a51e4360689115551bf4476d90e8162cf8c345fe233c7", size = 7676, upload-time = "2025-12-17T18:25:33.098Z" }, +] + +[[package]] +name = "rules-python-sphinxdocs-dev" +version = "0.0.0" +source = { virtual = "." } +dependencies = [ + { name = "absl-py", version = "2.3.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "absl-py", version = "2.5.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "markupsafe" }, + { name = "myst-parser", version = "3.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "myst-parser", version = "4.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, + { name = "myst-parser", version = "5.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "readthedocs-sphinx-ext" }, + { name = "sphinx", version = "7.4.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, + { name = "sphinx", version = "9.0.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, + { name = "sphinx", version = "9.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "sphinx-autodoc2" }, + { name = "sphinx-reredirects", version = "0.1.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "sphinx-reredirects", version = "1.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "sphinx-rtd-theme" }, + { name = "typing-extensions" }, +] + +[package.metadata] +requires-dist = [ + { name = "absl-py" }, + { name = "markupsafe" }, + { name = "myst-parser" }, + { name = "readthedocs-sphinx-ext" }, + { name = "sphinx" }, + { name = "sphinx-autodoc2" }, + { name = "sphinx-reredirects" }, + { name = "sphinx-rtd-theme", specifier = ">=2.0" }, + { name = "typing-extensions" }, +] + +[[package]] +name = "snowballstemmer" +version = "3.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/f8/0a71edf031f03c40db17503cb8ca78a69a171254e568e7db241b0ab57ea1/snowballstemmer-3.1.1.tar.gz", hash = "sha256:e07bbc54a0d798fe6010a12398422e62a8bfbba95c394fd0956ef58cb4d3e260", size = 123314, upload-time = "2026-06-03T00:56:40.194Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4c/07/2ebca9b11fb9be7340a818d8d6f63feaebb146be2c4afbd6061701d6df6e/snowballstemmer-3.1.1-py3-none-any.whl", hash = "sha256:7e207fa178741da09cdee59d3ecec3827ad5f92b1fc5c9ff3755b639f71f5752", size = 104164, upload-time = "2026-06-03T00:56:38.614Z" }, +] + +[[package]] +name = "sphinx" +version = "7.4.7" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +dependencies = [ + { name = "alabaster", version = "0.7.16", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "babel", marker = "python_full_version < '3.10'" }, + { name = "colorama", marker = "python_full_version < '3.10' and sys_platform == 'win32'" }, + { name = "docutils", version = "0.21.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "imagesize", version = "1.5.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "importlib-metadata", marker = "python_full_version < '3.10'" }, + { name = "jinja2", marker = "python_full_version < '3.10'" }, + { name = "packaging", marker = "python_full_version < '3.10'" }, + { name = "pygments", marker = "python_full_version < '3.10'" }, + { name = "requests", version = "2.32.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "snowballstemmer", marker = "python_full_version < '3.10'" }, + { name = "sphinxcontrib-applehelp", marker = "python_full_version < '3.10'" }, + { name = "sphinxcontrib-devhelp", marker = "python_full_version < '3.10'" }, + { name = "sphinxcontrib-htmlhelp", marker = "python_full_version < '3.10'" }, + { name = "sphinxcontrib-jsmath", marker = "python_full_version < '3.10'" }, + { name = "sphinxcontrib-qthelp", marker = "python_full_version < '3.10'" }, + { name = "sphinxcontrib-serializinghtml", marker = "python_full_version < '3.10'" }, + { name = "tomli", marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5b/be/50e50cb4f2eff47df05673d361095cafd95521d2a22521b920c67a372dcb/sphinx-7.4.7.tar.gz", hash = "sha256:242f92a7ea7e6c5b406fdc2615413890ba9f699114a9c09192d7dfead2ee9cfe", size = 8067911, upload-time = "2024-07-20T14:46:56.059Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0d/ef/153f6803c5d5f8917dbb7f7fcf6d34a871ede3296fa89c2c703f5f8a6c8e/sphinx-7.4.7-py3-none-any.whl", hash = "sha256:c2419e2135d11f1951cd994d6eb18a1835bd8fdd8429f9ca375dc1f3281bd239", size = 3401624, upload-time = "2024-07-20T14:46:52.142Z" }, +] + +[[package]] +name = "sphinx" +version = "8.1.3" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.10.*'", +] +dependencies = [ + { name = "alabaster", version = "1.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, + { name = "babel", marker = "python_full_version == '3.10.*'" }, + { name = "colorama", marker = "python_full_version == '3.10.*' and sys_platform == 'win32'" }, + { name = "docutils", version = "0.21.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, + { name = "imagesize", version = "2.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, + { name = "jinja2", marker = "python_full_version == '3.10.*'" }, + { name = "packaging", marker = "python_full_version == '3.10.*'" }, + { name = "pygments", marker = "python_full_version == '3.10.*'" }, + { name = "requests", version = "2.34.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, + { name = "snowballstemmer", marker = "python_full_version == '3.10.*'" }, + { name = "sphinxcontrib-applehelp", marker = "python_full_version == '3.10.*'" }, + { name = "sphinxcontrib-devhelp", marker = "python_full_version == '3.10.*'" }, + { name = "sphinxcontrib-htmlhelp", marker = "python_full_version == '3.10.*'" }, + { name = "sphinxcontrib-jsmath", marker = "python_full_version == '3.10.*'" }, + { name = "sphinxcontrib-qthelp", marker = "python_full_version == '3.10.*'" }, + { name = "sphinxcontrib-serializinghtml", marker = "python_full_version == '3.10.*'" }, + { name = "tomli", marker = "python_full_version == '3.10.*'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/be0b61178fe2cdcb67e2a92fc9ebb488e3c51c4f74a36a7824c0adf23425/sphinx-8.1.3.tar.gz", hash = "sha256:43c1911eecb0d3e161ad78611bc905d1ad0e523e4ddc202a58a821773dc4c927", size = 8184611, upload-time = "2024-10-13T20:27:13.93Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/26/60/1ddff83a56d33aaf6f10ec8ce84b4c007d9368b21008876fceda7e7381ef/sphinx-8.1.3-py3-none-any.whl", hash = "sha256:09719015511837b76bf6e03e42eb7595ac8c2e41eeb9c29c5b755c6b677992a2", size = 3487125, upload-time = "2024-10-13T20:27:10.448Z" }, +] + +[[package]] +name = "sphinx" +version = "9.0.4" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.11.*'", +] +dependencies = [ + { name = "alabaster", version = "1.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, + { name = "babel", marker = "python_full_version == '3.11.*'" }, + { name = "colorama", marker = "python_full_version == '3.11.*' and sys_platform == 'win32'" }, + { name = "docutils", version = "0.22.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, + { name = "imagesize", version = "2.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, + { name = "jinja2", marker = "python_full_version == '3.11.*'" }, + { name = "packaging", marker = "python_full_version == '3.11.*'" }, + { name = "pygments", marker = "python_full_version == '3.11.*'" }, + { name = "requests", version = "2.34.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, + { name = "roman-numerals", marker = "python_full_version == '3.11.*'" }, + { name = "snowballstemmer", marker = "python_full_version == '3.11.*'" }, + { name = "sphinxcontrib-applehelp", marker = "python_full_version == '3.11.*'" }, + { name = "sphinxcontrib-devhelp", marker = "python_full_version == '3.11.*'" }, + { name = "sphinxcontrib-htmlhelp", marker = "python_full_version == '3.11.*'" }, + { name = "sphinxcontrib-jsmath", marker = "python_full_version == '3.11.*'" }, + { name = "sphinxcontrib-qthelp", marker = "python_full_version == '3.11.*'" }, + { name = "sphinxcontrib-serializinghtml", marker = "python_full_version == '3.11.*'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/42/50/a8c6ccc36d5eacdfd7913ddccd15a9cee03ecafc5ee2bc40e1f168d85022/sphinx-9.0.4.tar.gz", hash = "sha256:594ef59d042972abbc581d8baa577404abe4e6c3b04ef61bd7fc2acbd51f3fa3", size = 8710502, upload-time = "2025-12-04T07:45:27.343Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c6/3f/4bbd76424c393caead2e1eb89777f575dee5c8653e2d4b6afd7a564f5974/sphinx-9.0.4-py3-none-any.whl", hash = "sha256:5bebc595a5e943ea248b99c13814c1c5e10b3ece718976824ffa7959ff95fffb", size = 3917713, upload-time = "2025-12-04T07:45:24.944Z" }, +] + +[[package]] +name = "sphinx" +version = "9.1.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.12'", +] +dependencies = [ + { name = "alabaster", version = "1.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "babel", marker = "python_full_version >= '3.12'" }, + { name = "colorama", marker = "python_full_version >= '3.12' and sys_platform == 'win32'" }, + { name = "docutils", version = "0.22.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "imagesize", version = "2.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "jinja2", marker = "python_full_version >= '3.12'" }, + { name = "packaging", marker = "python_full_version >= '3.12'" }, + { name = "pygments", marker = "python_full_version >= '3.12'" }, + { name = "requests", version = "2.34.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "roman-numerals", marker = "python_full_version >= '3.12'" }, + { name = "snowballstemmer", marker = "python_full_version >= '3.12'" }, + { name = "sphinxcontrib-applehelp", marker = "python_full_version >= '3.12'" }, + { name = "sphinxcontrib-devhelp", marker = "python_full_version >= '3.12'" }, + { name = "sphinxcontrib-htmlhelp", marker = "python_full_version >= '3.12'" }, + { name = "sphinxcontrib-jsmath", marker = "python_full_version >= '3.12'" }, + { name = "sphinxcontrib-qthelp", marker = "python_full_version >= '3.12'" }, + { name = "sphinxcontrib-serializinghtml", marker = "python_full_version >= '3.12'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cd/bd/f08eb0f4eed5c83f1ba2a3bd18f7745a2b1525fad70660a1c00224ec468a/sphinx-9.1.0.tar.gz", hash = "sha256:7741722357dd75f8190766926071fed3bdc211c74dd2d7d4df5404da95930ddb", size = 8718324, upload-time = "2025-12-31T15:09:27.646Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/73/f7/b1884cb3188ab181fc81fa00c266699dab600f927a964df02ec3d5d1916a/sphinx-9.1.0-py3-none-any.whl", hash = "sha256:c84fdd4e782504495fe4f2c0b3413d6c2bf388589bb352d439b2a3bb99991978", size = 3921742, upload-time = "2025-12-31T15:09:25.561Z" }, +] + +[[package]] +name = "sphinx-autodoc2" +version = "0.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "astroid" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/17/5f/5350046d1aa1a56b063ae08b9ad871025335c9d55fe2372896ea48711da9/sphinx_autodoc2-0.5.0.tar.gz", hash = "sha256:7d76044aa81d6af74447080182b6868c7eb066874edc835e8ddf810735b6565a", size = 115077, upload-time = "2023-11-27T07:27:51.407Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/19/e6/48d47961bbdae755ba9c17dfc65d89356312c67668dcb36c87cfadfa1964/sphinx_autodoc2-0.5.0-py3-none-any.whl", hash = "sha256:e867013b1512f9d6d7e6f6799f8b537d6884462acd118ef361f3f619a60b5c9e", size = 43385, upload-time = "2023-11-27T07:27:49.929Z" }, +] + +[[package]] +name = "sphinx-reredirects" +version = "0.1.6" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.10.*'", + "python_full_version < '3.10'", +] +dependencies = [ + { name = "sphinx", version = "7.4.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/16/6b/bcca2785de4071f604a722444d4d7ba8a9d40de3c14ad52fce93e6d92694/sphinx_reredirects-0.1.6.tar.gz", hash = "sha256:c491cba545f67be9697508727818d8626626366245ae64456fe29f37e9bbea64", size = 7080, upload-time = "2025-03-22T10:52:30.271Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ac/6f/0b3625be30a1a50f9e4c2cb2ec147b08f15ed0e9f8444efcf274b751300b/sphinx_reredirects-0.1.6-py3-none-any.whl", hash = "sha256:efd50c766fbc5bf40cd5148e10c00f2c00d143027de5c5e48beece93cc40eeea", size = 5675, upload-time = "2025-03-22T10:52:29.113Z" }, +] + +[[package]] +name = "sphinx-reredirects" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.12'", + "python_full_version == '3.11.*'", +] +dependencies = [ + { name = "sphinx", version = "9.0.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, + { name = "sphinx", version = "9.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1b/8d/0e39fe2740d7d71417edf9a6424aa80ca2c27c17fc21282cdc39f90d5a40/sphinx_reredirects-1.1.0.tar.gz", hash = "sha256:fb9b195335ab14b43f8273287d0c7eeb637ba6c56c66581c11b47202f6718b29", size = 614624, upload-time = "2025-12-22T08:28:02.792Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/81/b5dd07067f3daac6d23687ec737b2d593740671ebcd145830c8f92d381c5/sphinx_reredirects-1.1.0-py3-none-any.whl", hash = "sha256:4b5692273c72cd2d4d917f4c6f87d5919e4d6114a752d4be033f7f5f6310efd9", size = 6351, upload-time = "2025-12-22T08:27:59.724Z" }, +] + +[[package]] +name = "sphinx-rtd-theme" +version = "3.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "docutils", version = "0.21.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "docutils", version = "0.22.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "sphinx", version = "7.4.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, + { name = "sphinx", version = "9.0.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, + { name = "sphinx", version = "9.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "sphinxcontrib-jquery" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/84/68/a1bfbf38c0f7bccc9b10bbf76b94606f64acb1552ae394f0b8285bfaea25/sphinx_rtd_theme-3.1.0.tar.gz", hash = "sha256:b44276f2c276e909239a4f6c955aa667aaafeb78597923b1c60babc76db78e4c", size = 7620915, upload-time = "2026-01-12T16:03:31.17Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/87/c7/b5c8015d823bfda1a346adb2c634a2101d50bb75d421eb6dcb31acd25ebc/sphinx_rtd_theme-3.1.0-py2.py3-none-any.whl", hash = "sha256:1785824ae8e6632060490f67cf3a72d404a85d2d9fc26bce3619944de5682b89", size = 7655617, upload-time = "2026-01-12T16:03:28.101Z" }, +] + +[[package]] +name = "sphinxcontrib-applehelp" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ba/6e/b837e84a1a704953c62ef8776d45c3e8d759876b4a84fe14eba2859106fe/sphinxcontrib_applehelp-2.0.0.tar.gz", hash = "sha256:2f29ef331735ce958efa4734873f084941970894c6090408b079c61b2e1c06d1", size = 20053, upload-time = "2024-07-29T01:09:00.465Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5d/85/9ebeae2f76e9e77b952f4b274c27238156eae7979c5421fba91a28f4970d/sphinxcontrib_applehelp-2.0.0-py3-none-any.whl", hash = "sha256:4cd3f0ec4ac5dd9c17ec65e9ab272c9b867ea77425228e68ecf08d6b28ddbdb5", size = 119300, upload-time = "2024-07-29T01:08:58.99Z" }, +] + +[[package]] +name = "sphinxcontrib-devhelp" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/d2/5beee64d3e4e747f316bae86b55943f51e82bb86ecd325883ef65741e7da/sphinxcontrib_devhelp-2.0.0.tar.gz", hash = "sha256:411f5d96d445d1d73bb5d52133377b4248ec79db5c793ce7dbe59e074b4dd1ad", size = 12967, upload-time = "2024-07-29T01:09:23.417Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/35/7a/987e583882f985fe4d7323774889ec58049171828b58c2217e7f79cdf44e/sphinxcontrib_devhelp-2.0.0-py3-none-any.whl", hash = "sha256:aefb8b83854e4b0998877524d1029fd3e6879210422ee3780459e28a1f03a8a2", size = 82530, upload-time = "2024-07-29T01:09:21.945Z" }, +] + +[[package]] +name = "sphinxcontrib-htmlhelp" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/93/983afd9aa001e5201eab16b5a444ed5b9b0a7a010541e0ddfbbfd0b2470c/sphinxcontrib_htmlhelp-2.1.0.tar.gz", hash = "sha256:c9e2916ace8aad64cc13a0d233ee22317f2b9025b9cf3295249fa985cc7082e9", size = 22617, upload-time = "2024-07-29T01:09:37.889Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0a/7b/18a8c0bcec9182c05a0b3ec2a776bba4ead82750a55ff798e8d406dae604/sphinxcontrib_htmlhelp-2.1.0-py3-none-any.whl", hash = "sha256:166759820b47002d22914d64a075ce08f4c46818e17cfc9470a9786b759b19f8", size = 98705, upload-time = "2024-07-29T01:09:36.407Z" }, +] + +[[package]] +name = "sphinxcontrib-jquery" +version = "4.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "sphinx", version = "7.4.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, + { name = "sphinx", version = "9.0.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, + { name = "sphinx", version = "9.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/de/f3/aa67467e051df70a6330fe7770894b3e4f09436dea6881ae0b4f3d87cad8/sphinxcontrib-jquery-4.1.tar.gz", hash = "sha256:1620739f04e36a2c779f1a131a2dfd49b2fd07351bf1968ced074365933abc7a", size = 122331, upload-time = "2023-03-14T15:01:01.944Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/85/749bd22d1a68db7291c89e2ebca53f4306c3f205853cf31e9de279034c3c/sphinxcontrib_jquery-4.1-py2.py3-none-any.whl", hash = "sha256:f936030d7d0147dd026a4f2b5a57343d233f1fc7b363f68b3d4f1cb0993878ae", size = 121104, upload-time = "2023-03-14T15:01:00.356Z" }, +] + +[[package]] +name = "sphinxcontrib-jsmath" +version = "1.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b2/e8/9ed3830aeed71f17c026a07a5097edcf44b692850ef215b161b8ad875729/sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8", size = 5787, upload-time = "2019-01-21T16:10:16.347Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/42/4c8646762ee83602e3fb3fbe774c2fac12f317deb0b5dbeeedd2d3ba4b77/sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl", hash = "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178", size = 5071, upload-time = "2019-01-21T16:10:14.333Z" }, +] + +[[package]] +name = "sphinxcontrib-qthelp" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/68/bc/9104308fc285eb3e0b31b67688235db556cd5b0ef31d96f30e45f2e51cae/sphinxcontrib_qthelp-2.0.0.tar.gz", hash = "sha256:4fe7d0ac8fc171045be623aba3e2a8f613f8682731f9153bb2e40ece16b9bbab", size = 17165, upload-time = "2024-07-29T01:09:56.435Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/27/83/859ecdd180cacc13b1f7e857abf8582a64552ea7a061057a6c716e790fce/sphinxcontrib_qthelp-2.0.0-py3-none-any.whl", hash = "sha256:b18a828cdba941ccd6ee8445dbe72ffa3ef8cbe7505d8cd1fa0d42d3f2d5f3eb", size = 88743, upload-time = "2024-07-29T01:09:54.885Z" }, +] + +[[package]] +name = "sphinxcontrib-serializinghtml" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3b/44/6716b257b0aa6bfd51a1b31665d1c205fb12cb5ad56de752dfa15657de2f/sphinxcontrib_serializinghtml-2.0.0.tar.gz", hash = "sha256:e9d912827f872c029017a53f0ef2180b327c3f7fd23c87229f7a8e8b70031d4d", size = 16080, upload-time = "2024-07-29T01:10:09.332Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/52/a7/d2782e4e3f77c8450f727ba74a8f12756d5ba823d81b941f1b04da9d033a/sphinxcontrib_serializinghtml-2.0.0-py3-none-any.whl", hash = "sha256:6e2cb0eef194e10c27ec0023bfeb25badbbb5868244cf5bc5bdc04e4464bf331", size = 92072, upload-time = "2024-07-29T01:10:08.203Z" }, +] + +[[package]] +name = "tomli" +version = "2.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/22/de/48c59722572767841493b26183a0d1cc411d54fd759c5607c4590b6563a6/tomli-2.4.1.tar.gz", hash = "sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f", size = 17543, upload-time = "2026-03-25T20:22:03.828Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/11/db3d5885d8528263d8adc260bb2d28ebf1270b96e98f0e0268d32b8d9900/tomli-2.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f8f0fc26ec2cc2b965b7a3b87cd19c5c6b8c5e5f436b984e85f486d652285c30", size = 154704, upload-time = "2026-03-25T20:21:10.473Z" }, + { url = "https://files.pythonhosted.org/packages/6d/f7/675db52c7e46064a9aa928885a9b20f4124ecb9bc2e1ce74c9106648d202/tomli-2.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4ab97e64ccda8756376892c53a72bd1f964e519c77236368527f758fbc36a53a", size = 149454, upload-time = "2026-03-25T20:21:12.036Z" }, + { url = "https://files.pythonhosted.org/packages/61/71/81c50943cf953efa35bce7646caab3cf457a7d8c030b27cfb40d7235f9ee/tomli-2.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96481a5786729fd470164b47cdb3e0e58062a496f455ee41b4403be77cb5a076", size = 237561, upload-time = "2026-03-25T20:21:13.098Z" }, + { url = "https://files.pythonhosted.org/packages/48/c1/f41d9cb618acccca7df82aaf682f9b49013c9397212cb9f53219e3abac37/tomli-2.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a881ab208c0baf688221f8cecc5401bd291d67e38a1ac884d6736cbcd8247e9", size = 243824, upload-time = "2026-03-25T20:21:14.569Z" }, + { url = "https://files.pythonhosted.org/packages/22/e4/5a816ecdd1f8ca51fb756ef684b90f2780afc52fc67f987e3c61d800a46d/tomli-2.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47149d5bd38761ac8be13a84864bf0b7b70bc051806bc3669ab1cbc56216b23c", size = 242227, upload-time = "2026-03-25T20:21:15.712Z" }, + { url = "https://files.pythonhosted.org/packages/6b/49/2b2a0ef529aa6eec245d25f0c703e020a73955ad7edf73e7f54ddc608aa5/tomli-2.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ec9bfaf3ad2df51ace80688143a6a4ebc09a248f6ff781a9945e51937008fcbc", size = 247859, upload-time = "2026-03-25T20:21:17.001Z" }, + { url = "https://files.pythonhosted.org/packages/83/bd/6c1a630eaca337e1e78c5903104f831bda934c426f9231429396ce3c3467/tomli-2.4.1-cp311-cp311-win32.whl", hash = "sha256:ff2983983d34813c1aeb0fa89091e76c3a22889ee83ab27c5eeb45100560c049", size = 97204, upload-time = "2026-03-25T20:21:18.079Z" }, + { url = "https://files.pythonhosted.org/packages/42/59/71461df1a885647e10b6bb7802d0b8e66480c61f3f43079e0dcd315b3954/tomli-2.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:5ee18d9ebdb417e384b58fe414e8d6af9f4e7a0ae761519fb50f721de398dd4e", size = 108084, upload-time = "2026-03-25T20:21:18.978Z" }, + { url = "https://files.pythonhosted.org/packages/b8/83/dceca96142499c069475b790e7913b1044c1a4337e700751f48ed723f883/tomli-2.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:c2541745709bad0264b7d4705ad453b76ccd191e64aa6f0fc66b69a293a45ece", size = 95285, upload-time = "2026-03-25T20:21:20.309Z" }, + { url = "https://files.pythonhosted.org/packages/c1/ba/42f134a3fe2b370f555f44b1d72feebb94debcab01676bf918d0cb70e9aa/tomli-2.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c742f741d58a28940ce01d58f0ab2ea3ced8b12402f162f4d534dfe18ba1cd6a", size = 155924, upload-time = "2026-03-25T20:21:21.626Z" }, + { url = "https://files.pythonhosted.org/packages/dc/c7/62d7a17c26487ade21c5422b646110f2162f1fcc95980ef7f63e73c68f14/tomli-2.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7f86fd587c4ed9dd76f318225e7d9b29cfc5a9d43de44e5754db8d1128487085", size = 150018, upload-time = "2026-03-25T20:21:23.002Z" }, + { url = "https://files.pythonhosted.org/packages/5c/05/79d13d7c15f13bdef410bdd49a6485b1c37d28968314eabee452c22a7fda/tomli-2.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff18e6a727ee0ab0388507b89d1bc6a22b138d1e2fa56d1ad494586d61d2eae9", size = 244948, upload-time = "2026-03-25T20:21:24.04Z" }, + { url = "https://files.pythonhosted.org/packages/10/90/d62ce007a1c80d0b2c93e02cab211224756240884751b94ca72df8a875ca/tomli-2.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:136443dbd7e1dee43c68ac2694fde36b2849865fa258d39bf822c10e8068eac5", size = 253341, upload-time = "2026-03-25T20:21:25.177Z" }, + { url = "https://files.pythonhosted.org/packages/1a/7e/caf6496d60152ad4ed09282c1885cca4eea150bfd007da84aea07bcc0a3e/tomli-2.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5e262d41726bc187e69af7825504c933b6794dc3fbd5945e41a79bb14c31f585", size = 248159, upload-time = "2026-03-25T20:21:26.364Z" }, + { url = "https://files.pythonhosted.org/packages/99/e7/c6f69c3120de34bbd882c6fba7975f3d7a746e9218e56ab46a1bc4b42552/tomli-2.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5cb41aa38891e073ee49d55fbc7839cfdb2bc0e600add13874d048c94aadddd1", size = 253290, upload-time = "2026-03-25T20:21:27.46Z" }, + { url = "https://files.pythonhosted.org/packages/d6/2f/4a3c322f22c5c66c4b836ec58211641a4067364f5dcdd7b974b4c5da300c/tomli-2.4.1-cp312-cp312-win32.whl", hash = "sha256:da25dc3563bff5965356133435b757a795a17b17d01dbc0f42fb32447ddfd917", size = 98141, upload-time = "2026-03-25T20:21:28.492Z" }, + { url = "https://files.pythonhosted.org/packages/24/22/4daacd05391b92c55759d55eaee21e1dfaea86ce5c571f10083360adf534/tomli-2.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:52c8ef851d9a240f11a88c003eacb03c31fc1c9c4ec64a99a0f922b93874fda9", size = 108847, upload-time = "2026-03-25T20:21:29.386Z" }, + { url = "https://files.pythonhosted.org/packages/68/fd/70e768887666ddd9e9f5d85129e84910f2db2796f9096aa02b721a53098d/tomli-2.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:f758f1b9299d059cc3f6546ae2af89670cb1c4d48ea29c3cacc4fe7de3058257", size = 95088, upload-time = "2026-03-25T20:21:30.677Z" }, + { url = "https://files.pythonhosted.org/packages/07/06/b823a7e818c756d9a7123ba2cda7d07bc2dd32835648d1a7b7b7a05d848d/tomli-2.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:36d2bd2ad5fb9eaddba5226aa02c8ec3fa4f192631e347b3ed28186d43be6b54", size = 155866, upload-time = "2026-03-25T20:21:31.65Z" }, + { url = "https://files.pythonhosted.org/packages/14/6f/12645cf7f08e1a20c7eb8c297c6f11d31c1b50f316a7e7e1e1de6e2e7b7e/tomli-2.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:eb0dc4e38e6a1fd579e5d50369aa2e10acfc9cace504579b2faabb478e76941a", size = 149887, upload-time = "2026-03-25T20:21:33.028Z" }, + { url = "https://files.pythonhosted.org/packages/5c/e0/90637574e5e7212c09099c67ad349b04ec4d6020324539297b634a0192b0/tomli-2.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7f2c7f2b9ca6bdeef8f0fa897f8e05085923eb091721675170254cbc5b02897", size = 243704, upload-time = "2026-03-25T20:21:34.51Z" }, + { url = "https://files.pythonhosted.org/packages/10/8f/d3ddb16c5a4befdf31a23307f72828686ab2096f068eaf56631e136c1fdd/tomli-2.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f3c6818a1a86dd6dca7ddcaaf76947d5ba31aecc28cb1b67009a5877c9a64f3f", size = 251628, upload-time = "2026-03-25T20:21:36.012Z" }, + { url = "https://files.pythonhosted.org/packages/e3/f1/dbeeb9116715abee2485bf0a12d07a8f31af94d71608c171c45f64c0469d/tomli-2.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d312ef37c91508b0ab2cee7da26ec0b3ed2f03ce12bd87a588d771ae15dcf82d", size = 247180, upload-time = "2026-03-25T20:21:37.136Z" }, + { url = "https://files.pythonhosted.org/packages/d3/74/16336ffd19ed4da28a70959f92f506233bd7cfc2332b20bdb01591e8b1d1/tomli-2.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51529d40e3ca50046d7606fa99ce3956a617f9b36380da3b7f0dd3dd28e68cb5", size = 251674, upload-time = "2026-03-25T20:21:38.298Z" }, + { url = "https://files.pythonhosted.org/packages/16/f9/229fa3434c590ddf6c0aa9af64d3af4b752540686cace29e6281e3458469/tomli-2.4.1-cp313-cp313-win32.whl", hash = "sha256:2190f2e9dd7508d2a90ded5ed369255980a1bcdd58e52f7fe24b8162bf9fedbd", size = 97976, upload-time = "2026-03-25T20:21:39.316Z" }, + { url = "https://files.pythonhosted.org/packages/6a/1e/71dfd96bcc1c775420cb8befe7a9d35f2e5b1309798f009dca17b7708c1e/tomli-2.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:8d65a2fbf9d2f8352685bc1364177ee3923d6baf5e7f43ea4959d7d8bc326a36", size = 108755, upload-time = "2026-03-25T20:21:40.248Z" }, + { url = "https://files.pythonhosted.org/packages/83/7a/d34f422a021d62420b78f5c538e5b102f62bea616d1d75a13f0a88acb04a/tomli-2.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:4b605484e43cdc43f0954ddae319fb75f04cc10dd80d830540060ee7cd0243cd", size = 95265, upload-time = "2026-03-25T20:21:41.219Z" }, + { url = "https://files.pythonhosted.org/packages/3c/fb/9a5c8d27dbab540869f7c1f8eb0abb3244189ce780ba9cd73f3770662072/tomli-2.4.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fd0409a3653af6c147209d267a0e4243f0ae46b011aa978b1080359fddc9b6cf", size = 155726, upload-time = "2026-03-25T20:21:42.23Z" }, + { url = "https://files.pythonhosted.org/packages/62/05/d2f816630cc771ad836af54f5001f47a6f611d2d39535364f148b6a92d6b/tomli-2.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a120733b01c45e9a0c34aeef92bf0cf1d56cfe81ed9d47d562f9ed591a9828ac", size = 149859, upload-time = "2026-03-25T20:21:43.386Z" }, + { url = "https://files.pythonhosted.org/packages/ce/48/66341bdb858ad9bd0ceab5a86f90eddab127cf8b046418009f2125630ecb/tomli-2.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:559db847dc486944896521f68d8190be1c9e719fced785720d2216fe7022b662", size = 244713, upload-time = "2026-03-25T20:21:44.474Z" }, + { url = "https://files.pythonhosted.org/packages/df/6d/c5fad00d82b3c7a3ab6189bd4b10e60466f22cfe8a08a9394185c8a8111c/tomli-2.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853", size = 252084, upload-time = "2026-03-25T20:21:45.62Z" }, + { url = "https://files.pythonhosted.org/packages/00/71/3a69e86f3eafe8c7a59d008d245888051005bd657760e96d5fbfb0b740c2/tomli-2.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7f94b27a62cfad8496c8d2513e1a222dd446f095fca8987fceef261225538a15", size = 247973, upload-time = "2026-03-25T20:21:46.937Z" }, + { url = "https://files.pythonhosted.org/packages/67/50/361e986652847fec4bd5e4a0208752fbe64689c603c7ae5ea7cb16b1c0ca/tomli-2.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ede3e6487c5ef5d28634ba3f31f989030ad6af71edfb0055cbbd14189ff240ba", size = 256223, upload-time = "2026-03-25T20:21:48.467Z" }, + { url = "https://files.pythonhosted.org/packages/8c/9a/b4173689a9203472e5467217e0154b00e260621caa227b6fa01feab16998/tomli-2.4.1-cp314-cp314-win32.whl", hash = "sha256:3d48a93ee1c9b79c04bb38772ee1b64dcf18ff43085896ea460ca8dec96f35f6", size = 98973, upload-time = "2026-03-25T20:21:49.526Z" }, + { url = "https://files.pythonhosted.org/packages/14/58/640ac93bf230cd27d002462c9af0d837779f8773bc03dee06b5835208214/tomli-2.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:88dceee75c2c63af144e456745e10101eb67361050196b0b6af5d717254dddf7", size = 109082, upload-time = "2026-03-25T20:21:50.506Z" }, + { url = "https://files.pythonhosted.org/packages/d5/2f/702d5e05b227401c1068f0d386d79a589bb12bf64c3d2c72ce0631e3bc49/tomli-2.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:b8c198f8c1805dc42708689ed6864951fd2494f924149d3e4bce7710f8eb5232", size = 96490, upload-time = "2026-03-25T20:21:51.474Z" }, + { url = "https://files.pythonhosted.org/packages/45/4b/b877b05c8ba62927d9865dd980e34a755de541eb65fffba52b4cc495d4d2/tomli-2.4.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:d4d8fe59808a54658fcc0160ecfb1b30f9089906c50b23bcb4c69eddc19ec2b4", size = 164263, upload-time = "2026-03-25T20:21:52.543Z" }, + { url = "https://files.pythonhosted.org/packages/24/79/6ab420d37a270b89f7195dec5448f79400d9e9c1826df982f3f8e97b24fd/tomli-2.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7008df2e7655c495dd12d2a4ad038ff878d4ca4b81fccaf82b714e07eae4402c", size = 160736, upload-time = "2026-03-25T20:21:53.674Z" }, + { url = "https://files.pythonhosted.org/packages/02/e0/3630057d8eb170310785723ed5adcdfb7d50cb7e6455f85ba8a3deed642b/tomli-2.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1d8591993e228b0c930c4bb0db464bdad97b3289fb981255d6c9a41aedc84b2d", size = 270717, upload-time = "2026-03-25T20:21:55.129Z" }, + { url = "https://files.pythonhosted.org/packages/7a/b4/1613716072e544d1a7891f548d8f9ec6ce2faf42ca65acae01d76ea06bb0/tomli-2.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:734e20b57ba95624ecf1841e72b53f6e186355e216e5412de414e3c51e5e3c41", size = 278461, upload-time = "2026-03-25T20:21:56.228Z" }, + { url = "https://files.pythonhosted.org/packages/05/38/30f541baf6a3f6df77b3df16b01ba319221389e2da59427e221ef417ac0c/tomli-2.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8a650c2dbafa08d42e51ba0b62740dae4ecb9338eefa093aa5c78ceb546fcd5c", size = 274855, upload-time = "2026-03-25T20:21:57.653Z" }, + { url = "https://files.pythonhosted.org/packages/77/a3/ec9dd4fd2c38e98de34223b995a3b34813e6bdadf86c75314c928350ed14/tomli-2.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:504aa796fe0569bb43171066009ead363de03675276d2d121ac1a4572397870f", size = 283144, upload-time = "2026-03-25T20:21:59.089Z" }, + { url = "https://files.pythonhosted.org/packages/ef/be/605a6261cac79fba2ec0c9827e986e00323a1945700969b8ee0b30d85453/tomli-2.4.1-cp314-cp314t-win32.whl", hash = "sha256:b1d22e6e9387bf4739fbe23bfa80e93f6b0373a7f1b96c6227c32bef95a4d7a8", size = 108683, upload-time = "2026-03-25T20:22:00.214Z" }, + { url = "https://files.pythonhosted.org/packages/12/64/da524626d3b9cc40c168a13da8335fe1c51be12c0a63685cc6db7308daae/tomli-2.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:2c1c351919aca02858f740c6d33adea0c5deea37f9ecca1cc1ef9e884a619d26", size = 121196, upload-time = "2026-03-25T20:22:01.169Z" }, + { url = "https://files.pythonhosted.org/packages/5a/cd/e80b62269fc78fc36c9af5a6b89c835baa8af28ff5ad28c7028d60860320/tomli-2.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:eab21f45c7f66c13f2a9e0e1535309cee140182a9cdae1e041d02e47291e8396", size = 100393, upload-time = "2026-03-25T20:22:02.137Z" }, + { url = "https://files.pythonhosted.org/packages/7b/61/cceae43728b7de99d9b847560c262873a1f6c98202171fd5ed62640b494b/tomli-2.4.1-py3-none-any.whl", hash = "sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe", size = 14583, upload-time = "2026-03-25T20:22:03.012Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/cc/6253133b5bb138fc3306cebfbda2c520f545d36b5be2c7255cc528bb45d6/typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5", size = 113555, upload-time = "2026-07-02T08:40:05.92Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8", size = 45571, upload-time = "2026-07-02T08:40:04.659Z" }, +] + +[[package]] +name = "urllib3" +version = "2.6.3" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload-time = "2026-01-07T16:24:43.925Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" }, +] + +[[package]] +name = "urllib3" +version = "2.7.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.12'", + "python_full_version == '3.11.*'", + "python_full_version == '3.10.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", size = 433602, upload-time = "2026-05-07T16:13:18.596Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" }, +] + +[[package]] +name = "zipp" +version = "3.23.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/30/21/093488dfc7cc8964ded15ab726fad40f25fd3d788fd741cc1c5a17d78ee8/zipp-3.23.1.tar.gz", hash = "sha256:32120e378d32cd9714ad503c1d024619063ec28aad2248dc6672ad13edfa5110", size = 25965, upload-time = "2026-04-13T23:21:46.6Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/08/8a/0861bec20485572fbddf3dfba2910e38fe249796cb73ecdeb74e07eeb8d3/zipp-3.23.1-py3-none-any.whl", hash = "sha256:0b3596c50a5c700c9cb40ba8d86d9f2cc4807e9bedb06bcdf7fac85633e444dc", size = 10378, upload-time = "2026-04-13T23:21:45.386Z" }, +] diff --git a/sphinxdocs/docs/BUILD.bazel b/sphinxdocs/docs/BUILD.bazel index 87771f14f1..58c9e7fc18 100644 --- a/sphinxdocs/docs/BUILD.bazel +++ b/sphinxdocs/docs/BUILD.bazel @@ -48,11 +48,11 @@ sphinx_docs_library( sphinx_stardocs( name = "bzl_docs", srcs = [ - "//sphinxdocs:readthedocs_bzl", - "//sphinxdocs:sphinx_bzl", - "//sphinxdocs:sphinx_docs_library_bzl", - "//sphinxdocs:sphinx_stardoc_bzl", - "//sphinxdocs/private:sphinx_docs_library_bzl", + "//sphinxdocs:readthedocs", + "//sphinxdocs:sphinx", + "//sphinxdocs:sphinx_docs_library", + "//sphinxdocs:sphinx_stardoc", + "//sphinxdocs/private:sphinx_docs_library", ], prefix = "api/sphinxdocs/", target_compatible_with = _TARGET_COMPATIBLE_WITH, diff --git a/sphinxdocs/downloader_config.cfg b/sphinxdocs/downloader_config.cfg new file mode 100644 index 0000000000..3fa6264eda --- /dev/null +++ b/sphinxdocs/downloader_config.cfg @@ -0,0 +1,21 @@ +# Try GitHub first (primary) +rewrite ^github\.com/bazel-contrib/bazel_features/(.*) github.com/bazel-contrib/bazel_features/$1 +rewrite ^github\.com/bazel-contrib/rules_go/(.*) github.com/bazel-contrib/rules_go/$1 +rewrite ^github\.com/bazelbuild/bazel-skylib/(.*) github.com/bazelbuild/bazel-skylib/$1 +rewrite ^github\.com/bazelbuild/platforms/(.*) github.com/bazelbuild/platforms/$1 +rewrite ^github\.com/bazelbuild/rules_kotlin/(.*) github.com/bazelbuild/rules_kotlin/$1 +rewrite ^github\.com/bazelbuild/rules_shell/(.*) github.com/bazelbuild/rules_shell/$1 +rewrite ^github\.com/bazelbuild/rules_java/(.*) github.com/bazelbuild/rules_java/$1 +rewrite ^github\.com/bazelbuild/stardoc/(.*) github.com/bazelbuild/stardoc/$1 + + +# Fall back to mirror (secondary) +# Tracking upstream BCR mirror addition: https://github.com/bazelbuild/platforms/issues/139 +rewrite ^github\.com/bazel-contrib/bazel_features/(.*) mirror.bazel.build/github.com/bazel-contrib/bazel_features/$1 +rewrite ^github\.com/bazel-contrib/rules_go/(.*) mirror.bazel.build/github.com/bazel-contrib/rules_go/$1 +rewrite ^github\.com/bazelbuild/bazel-skylib/(.*) mirror.bazel.build/github.com/bazelbuild/bazel-skylib/$1 +rewrite ^github\.com/bazelbuild/platforms/(.*) mirror.bazel.build/github.com/bazelbuild/platforms/$1 +rewrite ^github\.com/bazelbuild/rules_kotlin/(.*) mirror.bazel.build/github.com/bazelbuild/rules_kotlin/$1 +rewrite ^github\.com/bazelbuild/rules_shell/(.*) mirror.bazel.build/github.com/bazelbuild/rules_shell/$1 +rewrite ^github\.com/bazelbuild/rules_java/(.*) mirror.bazel.build/github.com/bazelbuild/rules_java/$1 +rewrite ^github\.com/bazelbuild/stardoc/(.*) mirror.bazel.build/github.com/bazelbuild/stardoc/$1 diff --git a/sphinxdocs/integration_tests/BUILD.bazel b/sphinxdocs/integration_tests/BUILD.bazel new file mode 100644 index 0000000000..45e894fc2e --- /dev/null +++ b/sphinxdocs/integration_tests/BUILD.bazel @@ -0,0 +1,8 @@ +load("@rules_python//python:py_library.bzl", "py_library") + +package(default_visibility = ["//visibility:public"]) + +py_library( + name = "runner_lib", + srcs = ["runner.py"], +) diff --git a/sphinxdocs/integration_tests/bazel_from_env b/sphinxdocs/integration_tests/bazel_from_env new file mode 100755 index 0000000000..a372736f32 --- /dev/null +++ b/sphinxdocs/integration_tests/bazel_from_env @@ -0,0 +1,6 @@ +#!/usr/bin/env bash +# +# A simple wrapper so rules_bazel_integration_test can use the +# bazel version inherited from the environment. + +bazel "$@" diff --git a/sphinxdocs/integration_tests/bcr/MODULE.bazel b/sphinxdocs/integration_tests/bcr/MODULE.bazel index ae1c752513..451f2e920d 100644 --- a/sphinxdocs/integration_tests/bcr/MODULE.bazel +++ b/sphinxdocs/integration_tests/bcr/MODULE.bazel @@ -19,7 +19,7 @@ dev_pip = use_extension( dev_pip.parse( hub_name = "dev_pip", python_version = "3.11", - requirements_lock = "@rules_python//docs:requirements.txt", + requirements_lock = "@sphinxdocs//dev:requirements.txt", ) use_repo(dev_pip, "dev_pip") diff --git a/sphinxdocs/integration_tests/integration_test.bzl b/sphinxdocs/integration_tests/integration_test.bzl new file mode 100644 index 0000000000..b6786b9c6b --- /dev/null +++ b/sphinxdocs/integration_tests/integration_test.bzl @@ -0,0 +1,68 @@ +"""Helpers for running bazel-in-bazel integration tests for sphinxdocs.""" + +load( + "@rules_bazel_integration_test//bazel_integration_test:defs.bzl", + "bazel_integration_test", + "integration_test_utils", +) +load("@rules_python//python:py_test.bzl", "py_test") + +def _test_runner(*, name, bazel_version, py_main, py_deps): + test_runner = "{}_bazel_{}_py_runner".format(name, bazel_version) + py_test( + name = test_runner, + srcs = [py_main], + main = py_main, + deps = ["//integration_tests:runner_lib"] + py_deps, + tags = ["manual"], + ) + return test_runner + +def sphinxdocs_integration_test( + name, + workspace_path = "workspace", + tags = None, + py_main = None, + py_deps = None, + bazel_versions = None, + **kwargs): + """Runs a bazel-in-bazel integration test for sphinxdocs. + + Args: + name: Name of the test. + workspace_path: The directory name of the sub-workspace. + tags: Test tags. + py_main: Main Python test runner script. + py_deps: Dependencies for py_main. + bazel_versions: List of bazel versions to test. + **kwargs: Passed to bazel_integration_test. + """ + workspace_files = integration_test_utils.glob_workspace_files(workspace_path) + native.filegroup( + name = name + "_workspace_files", + srcs = workspace_files + [ + "//:distribution", + ], + ) + kwargs.setdefault("size", "large") + for bazel_version in bazel_versions or ["self"]: + test_runner = _test_runner( + name = name, + bazel_version = bazel_version, + py_main = py_main, + py_deps = py_deps or [], + ) + bazel_integration_test( + name = "{}_bazel_{}".format(name, bazel_version), + workspace_path = workspace_path, + test_runner = test_runner, + bazel_version = bazel_version, + workspace_files = [name + "_workspace_files"], + tags = (tags or []) + [ + "exclusive", + "no-sandbox", + "no-remote-exec", + "integration-test", + ], + **kwargs + ) diff --git a/sphinxdocs/integration_tests/persistent_worker/BUILD.bazel b/sphinxdocs/integration_tests/persistent_worker/BUILD.bazel new file mode 100644 index 0000000000..60fb4cbc6c --- /dev/null +++ b/sphinxdocs/integration_tests/persistent_worker/BUILD.bazel @@ -0,0 +1,8 @@ +load("//integration_tests:integration_test.bzl", "sphinxdocs_integration_test") + +package(default_visibility = ["//visibility:public"]) + +sphinxdocs_integration_test( + name = "persistent_worker_test", + py_main = "persistent_worker_test.py", +) diff --git a/sphinxdocs/integration_tests/persistent_worker/persistent_worker_test.py b/sphinxdocs/integration_tests/persistent_worker/persistent_worker_test.py new file mode 100644 index 0000000000..30a8b46c00 --- /dev/null +++ b/sphinxdocs/integration_tests/persistent_worker/persistent_worker_test.py @@ -0,0 +1,54 @@ +import unittest + +from integration_tests import runner + + +class PersistentWorkerTest(runner.TestCase): + def _check_index_html(self, text: str, should_exist: bool): + index_html = ( + self.repo_root / "bazel-bin" / "docs" / "_build" / "html" / "index.html" + ) + if not index_html.exists(): + self.fail(f"Could not find index.html at {index_html}") + content = index_html.read_text() + if should_exist: + self.assertIn( + text, + content, + f"Expected '{text}' in index.html after build, but not found.", + ) + else: + self.assertNotIn( + text, + content, + f"Expected '{text}' NOT to be in index.html after build, but found it.", + ) + + def test_incremental_add_and_remove_files(self): + # 1. Initial build + result = self.run_bazel("build", "//:docs") + self.assert_result_matches(result, "bazel-bin") + index_html = ( + self.repo_root / "bazel-bin" / "docs" / "_build" / "html" / "index.html" + ) + self.assertTrue( + index_html.exists(), "index.html should exist after initial build" + ) + + # 2. Add a new markdown file and verify it is included across incremental build + page2_md = self.repo_root / "page2.md" + page2_md.write_text("# Page 2\n\nThis is a newly added page.\n") + result = self.run_bazel("build", "//:docs") + self.assert_result_matches(result, "bazel-bin") + self._check_index_html("page2.html", should_exist=True) + + # 3. Remove the added markdown file and verify the persistent worker cleans up + # stale source files and invalidates toctrees without errors or warnings. + page2_md.unlink() + result = self.run_bazel("build", "//:docs") + self.assert_result_matches(result, "bazel-bin") + self._check_index_html("page2.html", should_exist=False) + + +if __name__ == "__main__": + unittest.main() diff --git a/sphinxdocs/integration_tests/persistent_worker/workspace/.bazelrc b/sphinxdocs/integration_tests/persistent_worker/workspace/.bazelrc new file mode 100644 index 0000000000..f3162b9547 --- /dev/null +++ b/sphinxdocs/integration_tests/persistent_worker/workspace/.bazelrc @@ -0,0 +1,8 @@ +# Disable disk caching and remote action caching so Bazel is forced to dispatch builds +# directly to the running persistent worker instead of restoring pre-built HTML cache results across test steps. +common --disk_cache= +build --noremote_accept_cached +common --http_timeout_scaling=10.0 +common --experimental_repository_downloader_retries=10 +common --experimental_downloader_config=downloader_config.cfg +common --lockfile_mode=off diff --git a/sphinxdocs/integration_tests/persistent_worker/workspace/BUILD.bazel b/sphinxdocs/integration_tests/persistent_worker/workspace/BUILD.bazel new file mode 100644 index 0000000000..60e66004fd --- /dev/null +++ b/sphinxdocs/integration_tests/persistent_worker/workspace/BUILD.bazel @@ -0,0 +1,17 @@ +load("@sphinxdocs//sphinxdocs:sphinx.bzl", "sphinx_build_binary", "sphinx_docs") + +sphinx_docs( + name = "docs", + srcs = glob(["*.md"]), + config = "conf.py", + formats = ["html"], + sphinx = ":sphinx-build", +) + +sphinx_build_binary( + name = "sphinx-build", + deps = [ + "@dev_pip//myst_parser", + "@dev_pip//sphinx", + ], +) diff --git a/sphinxdocs/integration_tests/persistent_worker/workspace/MODULE.bazel b/sphinxdocs/integration_tests/persistent_worker/workspace/MODULE.bazel new file mode 100644 index 0000000000..976718f312 --- /dev/null +++ b/sphinxdocs/integration_tests/persistent_worker/workspace/MODULE.bazel @@ -0,0 +1,23 @@ +module(version = "0.0.0") + +bazel_dep(name = "sphinxdocs", version = "0.0.0") +local_path_override( + module_name = "sphinxdocs", + path = "../../..", +) + +bazel_dep(name = "rules_python", version = "1.8.5") + +dev_pip = use_extension( + "@rules_python//python/extensions:pip.bzl", + "pip", + dev_dependency = True, +) +dev_pip.parse( + hub_name = "dev_pip", + python_version = "3.11", + requirements_lock = "@sphinxdocs//dev:requirements.txt", +) +use_repo(dev_pip, "dev_pip") + +bazel_dep(name = "bazel_skylib", version = "1.8.2") diff --git a/sphinxdocs/integration_tests/persistent_worker/workspace/WORKSPACE b/sphinxdocs/integration_tests/persistent_worker/workspace/WORKSPACE new file mode 100644 index 0000000000..4be37fab94 --- /dev/null +++ b/sphinxdocs/integration_tests/persistent_worker/workspace/WORKSPACE @@ -0,0 +1 @@ +# Bzlmod-enabled workspace; this empty WORKSPACE file satisfies rules_bazel_integration_test checks. diff --git a/sphinxdocs/integration_tests/persistent_worker/workspace/conf.py b/sphinxdocs/integration_tests/persistent_worker/workspace/conf.py new file mode 100644 index 0000000000..de33bde101 --- /dev/null +++ b/sphinxdocs/integration_tests/persistent_worker/workspace/conf.py @@ -0,0 +1 @@ +extensions = ["myst_parser"] diff --git a/sphinxdocs/integration_tests/persistent_worker/workspace/downloader_config.cfg b/sphinxdocs/integration_tests/persistent_worker/workspace/downloader_config.cfg new file mode 100644 index 0000000000..3fa6264eda --- /dev/null +++ b/sphinxdocs/integration_tests/persistent_worker/workspace/downloader_config.cfg @@ -0,0 +1,21 @@ +# Try GitHub first (primary) +rewrite ^github\.com/bazel-contrib/bazel_features/(.*) github.com/bazel-contrib/bazel_features/$1 +rewrite ^github\.com/bazel-contrib/rules_go/(.*) github.com/bazel-contrib/rules_go/$1 +rewrite ^github\.com/bazelbuild/bazel-skylib/(.*) github.com/bazelbuild/bazel-skylib/$1 +rewrite ^github\.com/bazelbuild/platforms/(.*) github.com/bazelbuild/platforms/$1 +rewrite ^github\.com/bazelbuild/rules_kotlin/(.*) github.com/bazelbuild/rules_kotlin/$1 +rewrite ^github\.com/bazelbuild/rules_shell/(.*) github.com/bazelbuild/rules_shell/$1 +rewrite ^github\.com/bazelbuild/rules_java/(.*) github.com/bazelbuild/rules_java/$1 +rewrite ^github\.com/bazelbuild/stardoc/(.*) github.com/bazelbuild/stardoc/$1 + + +# Fall back to mirror (secondary) +# Tracking upstream BCR mirror addition: https://github.com/bazelbuild/platforms/issues/139 +rewrite ^github\.com/bazel-contrib/bazel_features/(.*) mirror.bazel.build/github.com/bazel-contrib/bazel_features/$1 +rewrite ^github\.com/bazel-contrib/rules_go/(.*) mirror.bazel.build/github.com/bazel-contrib/rules_go/$1 +rewrite ^github\.com/bazelbuild/bazel-skylib/(.*) mirror.bazel.build/github.com/bazelbuild/bazel-skylib/$1 +rewrite ^github\.com/bazelbuild/platforms/(.*) mirror.bazel.build/github.com/bazelbuild/platforms/$1 +rewrite ^github\.com/bazelbuild/rules_kotlin/(.*) mirror.bazel.build/github.com/bazelbuild/rules_kotlin/$1 +rewrite ^github\.com/bazelbuild/rules_shell/(.*) mirror.bazel.build/github.com/bazelbuild/rules_shell/$1 +rewrite ^github\.com/bazelbuild/rules_java/(.*) mirror.bazel.build/github.com/bazelbuild/rules_java/$1 +rewrite ^github\.com/bazelbuild/stardoc/(.*) mirror.bazel.build/github.com/bazelbuild/stardoc/$1 diff --git a/sphinxdocs/integration_tests/persistent_worker/workspace/index.md b/sphinxdocs/integration_tests/persistent_worker/workspace/index.md new file mode 100644 index 0000000000..d1dca92350 --- /dev/null +++ b/sphinxdocs/integration_tests/persistent_worker/workspace/index.md @@ -0,0 +1,7 @@ +# Test Documentation + +```{toctree} +:glob: true + +* +``` diff --git a/sphinxdocs/integration_tests/runner.py b/sphinxdocs/integration_tests/runner.py new file mode 100644 index 0000000000..c7cc753e93 --- /dev/null +++ b/sphinxdocs/integration_tests/runner.py @@ -0,0 +1,99 @@ +import logging +import os +import os.path +import pathlib +import re +import shlex +import subprocess +import unittest + +_logger = logging.getLogger(__name__) + + +class ExecuteError(Exception): + def __init__(self, result): + self.result = result + + def __str__(self): + return self.result.describe() + + +class ExecuteResult: + def __init__( + self, + args: list[str], + env: dict[str, str], + cwd: pathlib.Path, + proc_result: subprocess.CompletedProcess, + ): + self.args = args + self.env = env + self.cwd = cwd + self.exit_code = proc_result.returncode + self.stdout = proc_result.stdout + self.stderr = proc_result.stderr + + def describe(self) -> str: + env_lines = [ + " " + shlex.quote(f"{key}={value}") + for key, value in sorted(self.env.items()) + ] + env = " \\\n".join(env_lines) + args = shlex.join(self.args) + maybe_stdout_nl = "" if self.stdout.endswith("\n") else "\n" + maybe_stderr_nl = "" if self.stderr.endswith("\n") else "\n" + return f"""\ +COMMAND: +cd {self.cwd} && \\ +env \\ +{env} \\ + {args} +RESULT: exit_code: {self.exit_code} +===== STDOUT START ===== +{self.stdout}{maybe_stdout_nl}===== STDOUT END ===== +===== STDERR START ===== +{self.stderr}{maybe_stderr_nl}===== STDERR END ===== +""" + + +class TestCase(unittest.TestCase): + def setUp(self): + super().setUp() + self.repo_root = pathlib.Path(os.environ["BIT_WORKSPACE_DIR"]) + self.bazel = pathlib.Path(os.environ["BIT_BAZEL_BINARY"]) + outer_test_tmpdir = pathlib.Path(os.environ["TEST_TMPDIR"]) + self.test_tmp_dir = outer_test_tmpdir / "bit_test_tmp" + self.tmp_dir = outer_test_tmpdir / "bit_tmp" + self.bazel_env = { + "PATH": os.environ["PATH"], + "TEST_TMPDIR": str(self.test_tmp_dir), + "TMP": str(self.tmp_dir), + "RUNFILES_DIR": os.environ["TEST_SRCDIR"], + } + + def run_bazel(self, *args: str, check: bool = True) -> ExecuteResult: + cmd_args = [str(self.bazel), *args] + env = self.bazel_env + _logger.info("executing: %s", shlex.join(cmd_args)) + cwd = self.repo_root + proc_result = subprocess.run( + args=cmd_args, + text=True, + capture_output=True, + cwd=cwd, + env=env, + check=False, + ) + exec_result = ExecuteResult(cmd_args, env, cwd, proc_result) + if check and exec_result.exit_code: + raise ExecuteError(exec_result) + else: + return exec_result + + def assert_result_matches(self, result: ExecuteResult, regex: str) -> None: + if not re.search(regex, result.stdout + result.stderr): + self.fail( + "Bazel output did not match expected pattern\n" + + f"expected pattern: {regex}\n" + + f"invocation details:\n{result.describe()}" + ) diff --git a/sphinxdocs/pyproject.toml b/sphinxdocs/pyproject.toml new file mode 100644 index 0000000000..89330debc6 --- /dev/null +++ b/sphinxdocs/pyproject.toml @@ -0,0 +1,15 @@ +[project] +name = "sphinxdocs" +version = "0.0.0" + +dependencies = [ + "absl-py", + "markupsafe", + "myst-parser", + "readthedocs-sphinx-ext", + "sphinx", + "sphinx-autodoc2", + "sphinx-reredirects", + "sphinx_rtd_theme >=2.0", # uv insists on downgrading for some reason + "typing-extensions", +] diff --git a/sphinxdocs/sphinxdocs/BUILD.bazel b/sphinxdocs/sphinxdocs/BUILD.bazel index 5a498b197e..9a871f5268 100644 --- a/sphinxdocs/sphinxdocs/BUILD.bazel +++ b/sphinxdocs/sphinxdocs/BUILD.bazel @@ -20,6 +20,14 @@ package( default_visibility = ["//:__subpackages__"], ) +filegroup( + name = "distribution", + srcs = glob(["**/*"]) + [ + "//sphinxdocs/private:distribution", + ], + visibility = ["//:__pkg__"], +) + # Additional -D values to add to every Sphinx build. # This is usually used to override the version when building repeated_string_list_flag( @@ -42,29 +50,55 @@ bool_flag( ) bzl_library( - name = "sphinx_bzl", + name = "sphinx", srcs = ["sphinx.bzl"], visibility = ["//visibility:public"], - deps = ["//sphinxdocs/private:sphinx_bzl"], + deps = ["//sphinxdocs/private:sphinx"], ) bzl_library( - name = "sphinx_docs_library_bzl", + name = "sphinx_docs_library", srcs = ["sphinx_docs_library.bzl"], visibility = ["//visibility:public"], - deps = ["//sphinxdocs/private:sphinx_docs_library_macro_bzl"], + deps = ["//sphinxdocs/private:sphinx_docs_library_macro"], ) bzl_library( - name = "sphinx_stardoc_bzl", + name = "sphinx_stardoc", srcs = ["sphinx_stardoc.bzl"], visibility = ["//visibility:public"], - deps = ["//sphinxdocs/private:sphinx_stardoc_bzl"], + deps = ["//sphinxdocs/private:sphinx_stardoc"], ) bzl_library( - name = "readthedocs_bzl", + name = "readthedocs", srcs = ["readthedocs.bzl"], visibility = ["//visibility:public"], - deps = ["//sphinxdocs/private:readthedocs_bzl"], + deps = ["//sphinxdocs/private:readthedocs"], +) + +# ========= Deprecated aliases for backwards compatibility ========= + +alias( + name = "sphinx_bzl", + actual = ":sphinx", + deprecation = "Use //sphinxdocs:sphinx instead", +) + +alias( + name = "sphinx_docs_library_bzl", + actual = ":sphinx_docs_library", + deprecation = "Use //sphinxdocs:sphinx_docs_library instead", +) + +alias( + name = "sphinx_stardoc_bzl", + actual = ":sphinx_stardoc", + deprecation = "Use //sphinxdocs:sphinx_stardoc instead", +) + +alias( + name = "readthedocs_bzl", + actual = ":readthedocs", + deprecation = "Use //sphinxdocs:readthedocs instead", ) diff --git a/sphinxdocs/sphinxdocs/private/BUILD.bazel b/sphinxdocs/sphinxdocs/private/BUILD.bazel index 785d2e074d..823f07fe73 100644 --- a/sphinxdocs/sphinxdocs/private/BUILD.bazel +++ b/sphinxdocs/sphinxdocs/private/BUILD.bazel @@ -16,6 +16,7 @@ load("@bazel_skylib//:bzl_library.bzl", "bzl_library") load("@com_google_protobuf//bazel:py_proto_library.bzl", "py_proto_library") load("@rules_python//python:py_binary.bzl", "py_binary") load("@rules_python//python:py_library.bzl", "py_library") +load(":visibility.bzl", "NOT_ACTUALLY_PUBLIC") package( default_visibility = ["//:__subpackages__"], @@ -34,8 +35,14 @@ exports_files( visibility = ["//visibility:public"], ) +filegroup( + name = "distribution", + srcs = glob(["**/*"]), + visibility = ["//sphinxdocs:__pkg__"], +) + bzl_library( - name = "util_bzl", + name = "util", srcs = ["util.bzl"], deps = [ "@bazel_skylib//lib:types", @@ -43,31 +50,31 @@ bzl_library( ) bzl_library( - name = "sphinx_docs_library_macro_bzl", + name = "sphinx_docs_library_macro", srcs = ["sphinx_docs_library_macro.bzl"], deps = [ - ":sphinx_docs_library_bzl", - "//sphinxdocs/private:util_bzl", + ":sphinx_docs_library", + ":util", ], ) bzl_library( - name = "sphinx_docs_library_bzl", + name = "sphinx_docs_library", srcs = ["sphinx_docs_library.bzl"], - deps = [":sphinx_docs_library_info_bzl"], + deps = [":sphinx_docs_library_info"], ) bzl_library( - name = "sphinx_docs_library_info_bzl", + name = "sphinx_docs_library_info", srcs = ["sphinx_docs_library_info.bzl"], ) bzl_library( - name = "sphinx_bzl", + name = "sphinx", srcs = ["sphinx.bzl"], deps = [ - ":sphinx_docs_library_info_bzl", - ":util_bzl", + ":sphinx_docs_library_info", + ":util", "@bazel_skylib//:bzl_library", "@bazel_skylib//lib:paths", "@bazel_skylib//lib:types", @@ -79,12 +86,12 @@ bzl_library( ) bzl_library( - name = "sphinx_stardoc_bzl", + name = "sphinx_stardoc", srcs = ["sphinx_stardoc.bzl"], deps = [ - ":sphinx_docs_library_macro_bzl", - "//sphinxdocs:sphinx_bzl", - "//sphinxdocs/private:util_bzl", + ":sphinx_docs_library_macro", + ":util", + "//sphinxdocs:sphinx", "@bazel_skylib//:bzl_library", "@bazel_skylib//lib:paths", "@bazel_skylib//lib:types", @@ -94,34 +101,45 @@ bzl_library( ) bzl_library( - name = "readthedocs_bzl", + name = "readthedocs", srcs = ["readthedocs.bzl"], deps = [ - ":util_bzl", + ":util", "@rules_python//python:py_binary_bzl", ], ) +bzl_library( + name = "visibility", + srcs = ["visibility.bzl"], +) + py_binary( name = "inventory_builder", srcs = ["inventory_builder.py"], # Only public because it's an implicit attribute - visibility = ["//visibility:public"], + visibility = NOT_ACTUALLY_PUBLIC, ) py_binary( name = "proto_to_markdown", srcs = ["proto_to_markdown.py"], # Only public because it's an implicit attribute - visibility = ["//visibility:public"], + visibility = NOT_ACTUALLY_PUBLIC, deps = [":proto_to_markdown_lib"], ) +py_library( + name = "sphinx_build_lib", + srcs = ["sphinx_build.py"], + visibility = ["//:__subpackages__"], +) + py_library( name = "proto_to_markdown_lib", srcs = ["proto_to_markdown.py"], # Only public because it's an implicit attribute - visibility = ["//visibility:public"], + visibility = NOT_ACTUALLY_PUBLIC, deps = [ ":stardoc_output_proto_py_pb2", ], 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.bzl b/sphinxdocs/sphinxdocs/private/sphinx.bzl index b7c051a154..24c5e9f9d1 100644 --- a/sphinxdocs/sphinxdocs/private/sphinx.bzl +++ b/sphinxdocs/sphinxdocs/private/sphinx.bzl @@ -100,7 +100,7 @@ def sphinx_docs( sphinx, config, formats, - strip_prefix = "", + strip_prefix = None, extra_opts = [], tools = [], allow_persistent_workers = True, @@ -133,10 +133,10 @@ def sphinx_docs( config: {type}`label` the Sphinx config file (`conf.py`) to use. formats: (list of str) the formats (`-b` flag) to generate documentation in. Each format will become an output group. - strip_prefix: {type}`str` A prefix to remove from the file paths of the - source files. e.g., given `//sphinxdocs/docs:foo.md`, stripping `docs/` makes - Sphinx see `foo.md` in its generated source directory. If not - specified, then {any}`native.package_name` is used. + strip_prefix: {type}`str | None` A prefix to remove from the file paths of the + source files. An empty string (`""`) means no prefix stripping; `None` + means {any}`native.package_name` is used. e.g., given `//docs:foo.md`, + stripping `docs/` makes Sphinx see `foo.md` in its generated source directory. extra_opts: {type}`list[str]` Additional options to pass onto Sphinx building. On each provided option, a location expansion is performed. See {any}`ctx.expand_location`. @@ -148,6 +148,7 @@ def sphinx_docs( This can improve incremental building of docs. **kwargs: {type}`dict` Common attributes to pass onto rules. """ + strip_prefix = strip_prefix if strip_prefix != None else native.package_name() add_tag(kwargs, "//sphinxdocs:sphinx_docs") common_kwargs = copy_propagating_kwargs(kwargs) @@ -355,17 +356,24 @@ def _sphinx_source_tree_impl(ctx): # Materialize a file under the `_sources` dir def _relocate(source_file, dest_path = None): if not dest_path: - dest_path = source_file.short_path.removeprefix(ctx.attr.strip_prefix) + # Strip leading slash if strip_prefix lacks a trailing slash, + # preventing paths.join from treating it as an absolute root. + dest_path = source_file.short_path.removeprefix(ctx.attr.strip_prefix).lstrip("/") dest_path = paths.join(source_prefix, dest_path) if source_file.is_directory: dest_file = ctx.actions.declare_directory(dest_path) + progress_message = "Symlinking Sphinx source directory %{input} to %{output}" else: dest_file = ctx.actions.declare_file(dest_path) + progress_message = "Symlinking Sphinx source %{input} to %{output}" + + # NOTE: Sphinx/MyST will read through symlinks, which can break relative + # xref lookup. Files are copied during the action phase to prevent this. ctx.actions.symlink( output = dest_file, target_file = source_file, - progress_message = "Symlinking Sphinx source %{input} to %{output}", + progress_message = progress_message, ) sphinx_source_files.append(dest_file) return dest_file @@ -373,7 +381,9 @@ def _sphinx_source_tree_impl(ctx): # Though Sphinx has a -c flag, we move the config file into the sources # directory to make the config more intuitive because some configuration # options are relative to the config location, not the sources directory. - source_conf_file = _relocate(ctx.file.config) + # Sphinx requires the configuration file to be named conf.py: + # https://www.sphinx-doc.org/en/master/usage/configuration.html#module-conf + source_conf_file = _relocate(ctx.file.config, "conf.py") sphinx_source_dir_path = paths.dirname(source_conf_file.path) for src in ctx.attr.srcs: diff --git a/sphinxdocs/sphinxdocs/private/sphinx_build.py b/sphinxdocs/sphinxdocs/private/sphinx_build.py index 3605b4579d..f20fbd676a 100644 --- a/sphinxdocs/sphinxdocs/private/sphinx_build.py +++ b/sphinxdocs/sphinxdocs/private/sphinx_build.py @@ -1,18 +1,66 @@ +from __future__ import annotations + +import concurrent.futures import contextlib import io import json import logging import os +import pathlib import shutil +import stat 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). + """ + + id: int + requestId: int + arguments: list[str] + inputs: list[WorkRequestInput] + cancel: bool + + +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 -import sphinx.application -from sphinx.cmd.build import main -WorkRequest = object -WorkResponse = object +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): @@ -29,10 +77,173 @@ def __init__(self, message, exit_code): _REQUEST_INFO_CONFIG_NAME = "bazel_worker_request_info_path" -class Worker: +class DirectorySyncerError(Exception): + """Raised when one or more errors occur during directory synchronization.""" + + 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 + ) + super().__init__(message) + + +class DirectorySyncer: + """Synchronizes a working destination directory from a source directory. + + Supports concurrent SHA-aware incremental updates (via sync()) for worker + mode and concurrent full directory copying (via copytree()) for non-worker + mode, ensuring physical file materialization to prevent relative + cross-reference resolution failures. + """ + def __init__( - self, instream: "typing.TextIO", outstream: "typing.TextIO", exec_root: str + self, + srcdir: pathlib.Path, + destdir: pathlib.Path, + 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: dict[str, str] = {} + self._lock = threading.Lock() + self._finished_cond = threading.Condition(self._lock) + self._remaining = 0 + self._errors: list[BaseException] = [] + self._executor: concurrent.futures.ThreadPoolExecutor | None = None + + def _reset_state(self) -> None: + with self._lock: + self._errors.clear() + self._remaining = 0 + + def _wait_for_completion(self) -> None: + with self._lock: + while self._remaining > 0: + self._finished_cond.wait() + if self._errors: + raise DirectorySyncerError(list(self._errors)) + + 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) + + def _handle_task_done(self, future: concurrent.futures.Future) -> None: + exc = future.exception() + if exc: + with self._lock: + self._errors.append(exc) + + def _task_finished(self) -> None: + with self._lock: + self._remaining -= 1 + if self._remaining == 0: + self._finished_cond.notify_all() + + @contextlib.contextmanager + def _create_executor(self): + with concurrent.futures.ThreadPoolExecutor( + max_workers=self._max_workers + ) as executor: + self._executor = executor + try: + yield + finally: + self._executor = None + + def copytree(self) -> None: + """Concurrently copies srcdir to destdir without SHA tracking.""" + self._reset_state() + shutil.rmtree(self._destdir, ignore_errors=True) + with self._create_executor(): + self._submit_task(self._copy_dir, self._srcdir, self._destdir) + self._wait_for_completion() + + def sync(self, entries: dict[str, str]) -> None: + """Synchronizes destdir to match entries {relative_path: sha} concurrently.""" + self._reset_state() + + to_remove = set(self._current_shas.keys()) - set(entries.keys()) + to_copy = { + path: sha + for path, sha in entries.items() + if self._current_shas.get(path) != sha + } + + if not to_remove and not to_copy: + self._current_shas = dict(entries) + return + + with self._create_executor(): + # 1. Submit stale path removals concurrently ASAP + for rel_path in to_remove: + dest_path = self._destdir / rel_path + self._submit_task(self._remove_path, dest_path) + + # 2. Submit created/updated item copies concurrently ASAP + for rel_path in to_copy: + src_path = self._srcdir / rel_path + dest_path = self._destdir / rel_path + if src_path.is_dir(): + self._submit_task(self._copy_dir, src_path, dest_path) + else: + self._submit_task(self._copy_file, src_path, dest_path) + + self._wait_for_completion() + + self._current_shas = dict(entries) + + def _remove_path(self, dest_path: pathlib.Path) -> None: + try: + if dest_path.is_dir() and not dest_path.is_symlink(): + shutil.rmtree(dest_path) + else: + dest_path.unlink(missing_ok=True) + except BaseException as e: + e.add_note(f"Failed removing path: dest_path={dest_path}") + raise + finally: + self._task_finished() + + def _copy_file(self, src: pathlib.Path, dest: pathlib.Path) -> None: + try: + dest.parent.mkdir(parents=True, exist_ok=True) + shutil.copy2(src, dest) + mode = dest.stat().st_mode + dest.chmod(mode | stat.S_IWUSR) + except BaseException as e: + e.add_note(f"Failed copying file: src={src}, dest={dest}") + raise + finally: + self._task_finished() + + def _copy_dir(self, src: pathlib.Path, dest: pathlib.Path) -> None: + """Recursively creates destination directory and submits tasks for its entries.""" + try: + dest.mkdir(parents=True, exist_ok=True) + with os.scandir(src) as scanner: + for entry in scanner: + c_dest = dest / entry.name + c_src = pathlib.Path(entry.path) + if entry.is_dir(): + self._submit_task(self._copy_dir, c_src, c_dest) + else: + self._submit_task(self._copy_file, c_src, c_dest) + except BaseException as e: + e.add_note(f"Failed copying directory: src={src}, dest={dest}") + raise + finally: + self._task_finished() + + +class Worker: + """A Bazel persistent worker for Sphinx builds.""" + + 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 @@ -51,6 +262,7 @@ def __init__( # dict[str srcdir, dict[str path, str digest]] self._digests = {} + self._syncers: dict[pathlib.Path, DirectorySyncer] = {} # Internal output directories the worker gives to Sphinx that need # to be cleaned up upon exit. @@ -97,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, @@ -112,29 +325,40 @@ 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 = sphinx_args[0] + srcdir = pathlib.Path(sphinx_args[0]) + destdir = pathlib.Path(f"{srcdir}.worker-in.d") incoming_digests = {} - current_digests = self._digests.setdefault(srcdir, {}) + 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"]} - for entry in request["inputs"]: + request_info: RequestInfo = { + "exec_root": self._exec_root, + "inputs": request.get("inputs", []), + } + srcdir_prefix = str(srcdir) + "/" + 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 + # live outside srcdir. Only synchronize documentation sources inside srcdir. + if not path.startswith(srcdir_prefix): + continue digest = entry["digest"] # Make the path srcdir-relative so Sphinx understands it. - path = path.removeprefix(srcdir + "/") + path = path.removeprefix(srcdir_prefix) incoming_digests[path] = digest if path not in current_digests: @@ -144,16 +368,37 @@ def _prepare_sphinx(self, request): logger.info("path %s changed", path) changed_paths.append(path) - self._digests[srcdir] = incoming_digests - self._extension.changed_paths = changed_paths + self._digests[str(srcdir)] = incoming_digests + self._extension.changed_paths = set(changed_paths) request_info["changed_sources"] = changed_paths bazel_outdir = sphinx_args[1] worker_outdir = bazel_outdir + ".worker-out.d" + # The doctree dir deliberately lives outside the declared outputs so + # it survives between invocations (that is what makes worker builds + # incremental). A new worker has no digest history: it reports every + # file as changed and re-reads all docs. Doing that against the stale + # Sphinx environment of a previous worker produces spurious warnings + # (e.g. duplicate labels), which --fail-on-warning turns into build + # failures. So on the first request start from a clean slate. + if is_first_request: + shutil.rmtree(worker_outdir, ignore_errors=True) + shutil.rmtree(destdir, ignore_errors=True) + for arg in sphinx_args: + if arg.startswith("--doctree-dir="): + shutil.rmtree(arg.partition("=")[2], ignore_errors=True) self._worker_outdirs.add(worker_outdir) sphinx_args[1] = worker_outdir - request_info_path = os.path.join(srcdir, "_bazel_worker_request_info.json") + if srcdir not in self._syncers: + self._syncers[srcdir] = DirectorySyncer(srcdir, destdir) + syncer = self._syncers[srcdir] + syncer.sync(incoming_digests) + + sphinx_args[0] = str(destdir) + request_info_path = os.path.join( + sphinx_args[0], "_bazel_worker_request_info.json" + ) with open(request_info_path, "w") as fp: json.dump(request_info, fp) sphinx_args.append(f"--define={_REQUEST_INFO_CONFIG_NAME}={request_info_path}") @@ -167,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 @@ -188,6 +433,12 @@ def _process_request(self, request: "WorkRequest") -> "WorkResponse | None": stdout.truncate(0) stderr.seek(0) stderr.truncate(0) + # If Sphinx cache (`--doctree-dir`) becomes corrupted across incremental + # updates or branch checkouts, exit code 2 is returned. Wiping out the cached + # doctrees before retrying allows Sphinx to recover cleanly from scratch. + for arg in sphinx_args: + if arg.startswith("--doctree-dir="): + shutil.rmtree(arg.split("=", 1)[1], ignore_errors=True) exit_code = main(sphinx_args) if exit_code: @@ -224,22 +475,31 @@ def _process_request(self, request: "WorkRequest") -> "WorkResponse | None": # implicily bring along what the symlinks point to. shutil.copytree(worker_outdir, bazel_outdir, dirs_exist_ok=True) + # Include both stdout and stderr in the response output so that Sphinx + # warnings or diagnostic messages written to stderr are reported to the + # Bazel console even when the build succeeds. + stdout_output = stdout.getvalue() + stderr_output = stderr.getvalue() + if stderr_output: + output = f"--- STDOUT ---\n{stdout_output}\n--- STDERR ---\n{stderr_output}" + else: + output = stdout_output + response = { "requestId": request.get("requestId", 0), - "output": stdout.getvalue(), + "output": output, "exitCode": 0, } 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, "", "") @@ -247,14 +507,31 @@ def setup(self, app): return {"parallel_read_safe": True, "parallel_write_safe": True} def _handle_env_get_outdated(self, app, env, added, changed, removed): - changed = { - # NOTE: path2doc returns None if it's not a doc path - env.path2doc(p) - for p in self.changed_paths - } + changed_docs = set() + for p in self.changed_paths: + # Try multiple path resolutions because depending on how Sphinx and Bazel + # represent inputs (`p`), `env.path2doc` may require relative, srcdir-joined, + # or absolute paths to successfully resolve the document name. + doc = ( + env.path2doc(p) + or env.path2doc(os.path.join(env.srcdir, p)) + or env.path2doc(os.path.abspath(os.path.join(env.srcdir, p))) + ) + if doc: + changed_docs.add(doc) + + # When documents are added or removed across incremental builds or branch checkouts, + # parent documents whose `toctree` includes them (especially via glob patterns or + # explicit references to removed docs) must be invalidated and re-read. Otherwise, + # Sphinx retains stale table of contents entries or throws unresolvable reference errors. + if added or removed: + glob_toctrees = getattr(env, "glob_toctrees", set()) + for doc, includes in getattr(env, "toctree_includes", {}).items(): + if doc in glob_toctrees or not removed.isdisjoint(includes): + changed_docs.add(doc) - logger.info("changed docs: %s", changed) - return changed + logger.info("changed docs: %s", changed_docs) + return changed_docs def _worker_main(stdin, stdout, exec_root): @@ -271,6 +548,11 @@ def _non_worker_main(): args.extend(lines) else: args.append(arg) + if len(args) > 1: + srcdir = pathlib.Path(args[1]) + destdir = pathlib.Path(f"{srcdir}.worker-in.d") + DirectorySyncer(srcdir, destdir).copytree() + args[1] = str(destdir) sys.argv[:] = args return main() diff --git a/sphinxdocs/sphinxdocs/private/sphinx_stardoc.bzl b/sphinxdocs/sphinxdocs/private/sphinx_stardoc.bzl index ac76219299..75a4daf4d0 100644 --- a/sphinxdocs/sphinxdocs/private/sphinx_stardoc.bzl +++ b/sphinxdocs/sphinxdocs/private/sphinx_stardoc.bzl @@ -171,7 +171,7 @@ def sphinx_stardoc( **common_kwargs ) - stardoc_name = internal_name + "_stardoc" + stardoc_name = internal_name + "__stardoc" # NOTE: The .binaryproto suffix is an optimization. It makes the stardoc() # call avoid performing a copy of the output to the desired name. @@ -186,7 +186,7 @@ def sphinx_stardoc( **common_kwargs ) - pb2md_name = internal_name + "_pb2md" + pb2md_name = internal_name + "__pb2md" _stardoc_proto_to_markdown( name = pb2md_name, src = stardoc_pb, diff --git a/sphinxdocs/sphinxdocs/private/visibility.bzl b/sphinxdocs/sphinxdocs/private/visibility.bzl new file mode 100644 index 0000000000..706be45a3b --- /dev/null +++ b/sphinxdocs/sphinxdocs/private/visibility.bzl @@ -0,0 +1,7 @@ +"""Shared code for use with visibility specs.""" + +# Use when a target isn't actually public, but needs public +# visibility to keep Bazel happy. +# Such cases are typically for defaults of rule attributes or macro args that +# get used outside of sphinxdocs itself. +NOT_ACTUALLY_PUBLIC = ["//visibility:public"] 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 new file mode 100644 index 0000000000..ec0878d862 --- /dev/null +++ b/sphinxdocs/tests/sphinx_build/BUILD.bazel @@ -0,0 +1,11 @@ +load("@rules_python//python:py_test.bzl", "py_test") + +py_test( + name = "directory_syncer_test", + srcs = ["directory_syncer_test.py"], + deps = [ + "//sphinxdocs/private:sphinx_build_lib", + "@pypi//absl_py", + "@pypi//sphinx", + ], +) diff --git a/sphinxdocs/tests/sphinx_build/directory_syncer_test.py b/sphinxdocs/tests/sphinx_build/directory_syncer_test.py new file mode 100644 index 0000000000..1365e2b3ce --- /dev/null +++ b/sphinxdocs/tests/sphinx_build/directory_syncer_test.py @@ -0,0 +1,133 @@ +import pathlib +import shutil +import stat +import tempfile + +from absl.testing import absltest +from sphinxdocs.private.sphinx_build import DirectorySyncer, DirectorySyncerError + + +class DirectorySyncerTest(absltest.TestCase): + def setUp(self): + super().setUp() + self.test_dir = pathlib.Path(tempfile.mkdtemp()) + self.addCleanup(shutil.rmtree, self.test_dir, ignore_errors=True) + self.srcdir = self.test_dir / "src" + self.destdir = self.test_dir / "dest" + self.srcdir.mkdir() + + def _write_src(self, rel_path, content, mode=None): + path = self.srcdir / rel_path + path.parent.mkdir(parents=True, exist_ok=True) + if path.exists(): + path.chmod(stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP | stat.S_IROTH) + path.write_text(content) + if mode is not None: + path.chmod(mode) + return path + + def assert_dest_equals(self, rel_path, expected_content): + dest_file = self.destdir / rel_path + self.assertTrue(dest_file.exists(), f"Expected {dest_file} to exist") + self.assertEqual(expected_content, dest_file.read_text()) + + def assert_dest_not_exists(self, rel_path): + dest_file = self.destdir / rel_path + self.assertFalse(dest_file.exists(), f"Expected {dest_file} to not exist") + + def test_copytree(self): + self._write_src("file1.txt", "hello") + self._write_src("sub/file2.txt", "world") + + syncer = DirectorySyncer(self.srcdir, self.destdir) + syncer.copytree() + + self.assert_dest_equals("file1.txt", "hello") + self.assert_dest_equals("sub/file2.txt", "world") + + def test_sync_initial_and_incremental(self): + self._write_src("doc1.md", "v1") + self._write_src("doc2.md", "v1") + self._write_src("doc3.md", "v1") + + syncer = DirectorySyncer(self.srcdir, self.destdir) + # Initial sync + syncer.sync( + { + "doc1.md": "sha-doc1-v1", + "doc2.md": "sha-doc2-v1", + "doc3.md": "sha-doc3-v1", + } + ) + + self.assert_dest_equals("doc1.md", "v1") + self.assert_dest_equals("doc2.md", "v1") + self.assert_dest_equals("doc3.md", "v1") + + # Incremental sync: + # - doc1.md: unchanged SHA + # - doc2.md: updated SHA & content + # - doc3.md: removed from entries + # - doc4.md: newly created file + self._write_src("doc2.md", "v2") + self._write_src("doc4.md", "v1") + + syncer.sync( + { + "doc1.md": "sha-doc1-v1", + "doc2.md": "sha-doc2-v2", + "doc4.md": "sha-doc4-v1", + } + ) + + self.assert_dest_equals("doc1.md", "v1") + self.assert_dest_equals("doc2.md", "v2") + self.assert_dest_not_exists("doc3.md") + self.assert_dest_equals("doc4.md", "v1") + + def test_read_only_file_becomes_writable(self): + read_only_mode = stat.S_IRUSR | stat.S_IRGRP | stat.S_IROTH + self._write_src("readonly.txt", "version 1", mode=read_only_mode) + syncer = DirectorySyncer(self.srcdir, self.destdir) + syncer.sync({"readonly.txt": "sha-v1"}) + + dest_file = self.destdir / "readonly.txt" + self.assert_dest_equals("readonly.txt", "version 1") + self.assertTrue( + dest_file.stat().st_mode & stat.S_IWUSR, + "Destination file should be writable", + ) + + # Ensure subsequent incremental updates can overwrite the file without permission error + self._write_src("readonly.txt", "version 2", mode=read_only_mode) + syncer.sync({"readonly.txt": "sha-v2"}) + self.assert_dest_equals("readonly.txt", "version 2") + + def test_sync_directory_artifact(self): + dir_artifact = self.srcdir / "tree_art" + dir_artifact.mkdir() + (dir_artifact / "page.md").write_text("content") + + syncer = DirectorySyncer(self.srcdir, self.destdir) + syncer.sync( + { + "tree_art": "sha-dir-art", + } + ) + + self.assert_dest_equals("tree_art/page.md", "content") + + def test_errors_bubble_up(self): + syncer = DirectorySyncer(self.srcdir, self.destdir) + with self.assertRaises(DirectorySyncerError) as cm: + syncer.sync( + { + "non_existent_1.txt": "sha1", + "non_existent_2.txt": "sha2", + } + ) + self.assertGreaterEqual(len(cm.exception.errors), 2) + + +if __name__ == "__main__": + absltest.main() diff --git a/sphinxdocs/tests/sphinx_docs/BUILD.bazel b/sphinxdocs/tests/sphinx_docs/BUILD.bazel index 33b98ec585..71bc1f3d79 100644 --- a/sphinxdocs/tests/sphinx_docs/BUILD.bazel +++ b/sphinxdocs/tests/sphinx_docs/BUILD.bazel @@ -1,4 +1,5 @@ load("@bazel_skylib//rules:build_test.bzl", "build_test") +load("@rules_python//python:py_test.bzl", "py_test") load("//sphinxdocs:sphinx.bzl", "sphinx_build_binary", "sphinx_docs") load(":defs.bzl", "gen_directory") @@ -21,10 +22,20 @@ sphinx_docs( ], config = "conf.py", formats = ["html"], + renamed_srcs = { + ":gen_binary_asset": "binary_asset.bin", + }, sphinx = ":sphinx-build", + strip_prefix = package_name() + "/", target_compatible_with = _TARGET_COMPATIBLE_WITH, ) +genrule( + name = "gen_binary_asset", + outs = ["binary_asset.bin"], + cmd = "printf '\\x00\\xff\\xfe\\xfd\\x80\\x90\\x00\\x01\\x02\\x03\\x04' > $@", +) + gen_directory( name = "generated_directory", ) @@ -33,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", ], ) @@ -42,3 +53,10 @@ build_test( name = "docs_build_test", targets = [":docs"], ) + +py_test( + name = "sphinx_docs_output_test", + srcs = ["sphinx_docs_output_test.py"], + data = [":docs"], + deps = ["@pypi//absl_py"], +) diff --git a/sphinxdocs/tests/sphinx_docs/defs.bzl b/sphinxdocs/tests/sphinx_docs/defs.bzl index 2e47ecc0f7..36fd1dba4f 100644 --- a/sphinxdocs/tests/sphinx_docs/defs.bzl +++ b/sphinxdocs/tests/sphinx_docs/defs.bzl @@ -6,7 +6,9 @@ def _gen_directory_impl(ctx): ctx.actions.run_shell( outputs = [out], command = """ -echo "# Hello" > {outdir}/index.md +printf '# Hello\\n' > {outdir}/index.md +printf '# Dir Page 1\\n\\n[Dir Page 2](dir_page2.md)\\n' > {outdir}/dir_page1.md +printf '# Dir Page 2\\n' > {outdir}/dir_page2.md """.format( outdir = out.path, ), diff --git a/sphinxdocs/tests/sphinx_docs/index.md b/sphinxdocs/tests/sphinx_docs/index.md index cdce641fa1..68a5fb38c3 100644 --- a/sphinxdocs/tests/sphinx_docs/index.md +++ b/sphinxdocs/tests/sphinx_docs/index.md @@ -3,6 +3,7 @@ :::{toctree} :glob: +generated_directory/dir_page1 ** genindex ::: diff --git a/sphinxdocs/tests/sphinx_docs/sphinx_docs_output_test.py b/sphinxdocs/tests/sphinx_docs/sphinx_docs_output_test.py new file mode 100644 index 0000000000..5d00817926 --- /dev/null +++ b/sphinxdocs/tests/sphinx_docs/sphinx_docs_output_test.py @@ -0,0 +1,28 @@ +import importlib.resources +import os +from xml.etree import ElementTree + +import tests.sphinx_docs as sphinx_docs +from absl.testing import absltest + + +class SphinxDocsOutputTest(absltest.TestCase): + def test_directory_artifact_relative_xref(self): + page1_path = importlib.resources.files(sphinx_docs).joinpath( + "docs/_build/html/generated_directory/dir_page1.html" + ) + self.assertTrue(os.path.exists(str(page1_path)), f"Not found at {page1_path}") + with open(str(page1_path)) as f: + xml = f.read() + doc_elem = ElementTree.fromstring(xml) + actual = None + for elem in doc_elem.iter(): + if "href" in elem.attrib: + if "".join(elem.itertext()).strip() == "Dir Page 2": + actual = elem.attrib["href"] + break + self.assertEqual("dir_page2.html", actual) + + +if __name__ == "__main__": + absltest.main() diff --git a/sphinxdocs/tests/sphinx_docs_conf_in_other_dir/BUILD.bazel b/sphinxdocs/tests/sphinx_docs_conf_in_other_dir/BUILD.bazel new file mode 100644 index 0000000000..78f7d5e4bc --- /dev/null +++ b/sphinxdocs/tests/sphinx_docs_conf_in_other_dir/BUILD.bazel @@ -0,0 +1,38 @@ +load("@bazel_skylib//rules:build_test.bzl", "build_test") +load("//sphinxdocs:sphinx.bzl", "sphinx_build_binary", "sphinx_docs") + +_TARGET_COMPATIBLE_WITH = select({ + "@platforms//os:linux": [], + "@platforms//os:macos": [], + "//conditions:default": ["@platforms//:incompatible"], +}) + +genrule( + name = "gen_conf", + srcs = ["src_conf.py"], + outs = ["other_dir/conf.py"], + cmd = "cp $(location src_conf.py) $@", +) + +sphinx_docs( + name = "docs", + srcs = ["index.md"], + config = ":gen_conf", + formats = ["html"], + sphinx = ":sphinx-build", + target_compatible_with = _TARGET_COMPATIBLE_WITH, +) + +sphinx_build_binary( + name = "sphinx-build", + tags = ["manual"], + deps = [ + "@pypi//myst_parser", + "@pypi//sphinx", + ], +) + +build_test( + name = "docs_build_test", + targets = [":docs"], +) diff --git a/sphinxdocs/tests/sphinx_docs_conf_in_other_dir/index.md b/sphinxdocs/tests/sphinx_docs_conf_in_other_dir/index.md new file mode 100644 index 0000000000..cd7d8b4188 --- /dev/null +++ b/sphinxdocs/tests/sphinx_docs_conf_in_other_dir/index.md @@ -0,0 +1,3 @@ +# Test Documentation + +This is a test document for reproducing conf.py in a separate directory. diff --git a/sphinxdocs/tests/sphinx_docs_conf_in_other_dir/src_conf.py b/sphinxdocs/tests/sphinx_docs_conf_in_other_dir/src_conf.py new file mode 100644 index 0000000000..d4f2f45d31 --- /dev/null +++ b/sphinxdocs/tests/sphinx_docs_conf_in_other_dir/src_conf.py @@ -0,0 +1,4 @@ +project = "Repro Test" +copyright = "2026, Test" +author = "Test" +extensions = ["myst_parser"] diff --git a/sphinxdocs/tests/sphinx_stardoc/BUILD.bazel b/sphinxdocs/tests/sphinx_stardoc/BUILD.bazel index a5d402b809..ffc9697e7f 100644 --- a/sphinxdocs/tests/sphinx_stardoc/BUILD.bazel +++ b/sphinxdocs/tests/sphinx_stardoc/BUILD.bazel @@ -69,7 +69,6 @@ sphinx_stardoc( deps = [":func_and_providers_bzl"], ) -# A bzl_library with multiple sources bzl_library( name = "func_and_providers_bzl", srcs = [ @@ -89,14 +88,15 @@ bzl_library( srcs = ["bzl_typedef.bzl"], ) +# A bzl_library with multiple sources sphinx_build_binary( name = "sphinx-build", 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/sphinx_stardoc/sphinx_output_test.py b/sphinxdocs/tests/sphinx_stardoc/sphinx_output_test.py index 650e0134d3..e6653b1656 100644 --- a/sphinxdocs/tests/sphinx_stardoc/sphinx_output_test.py +++ b/sphinxdocs/tests/sphinx_stardoc/sphinx_output_test.py @@ -13,7 +13,10 @@ def setUp(self): self._xmls = {} def assert_xref(self, doc, *, text, href): - match = self._doc_element(doc).find(f".//*[.='{text}']") + # Find an element with an 'href' attribute whose string content (including + # descendants like ) equals `text`. [@href] filters out ancestor + # elements like
  • or

    which also match [.='{text}']. + match = self._doc_element(doc).find(f".//*[@href][.='{text}']") if not match: self.fail(f"No element found with {text=}") actual = match.attrib.get("href", "") @@ -113,6 +116,7 @@ def _doc_element(self, doc): ("file_with_repo", "@testrepo//lang:rule.bzl", "rule.html"), ("package_absolute", "//lang", "target.html"), ("package_basename", "lang", "target.html"), + ("relative_doc_link", "Rule documentation", "rule.html"), # fmt: on ) def test_xrefs(self, text, href): diff --git a/sphinxdocs/tests/sphinx_stardoc/xrefs.md b/sphinxdocs/tests/sphinx_stardoc/xrefs.md index 9893c32023..e3a7d7b21e 100644 --- a/sphinxdocs/tests/sphinx_stardoc/xrefs.md +++ b/sphinxdocs/tests/sphinx_stardoc/xrefs.md @@ -7,6 +7,7 @@ Various tests of cross referencing support ## Short name +* [Rule documentation](rule.md) * function: {obj}`myfunc` * function arg: {obj}`myfunc.arg1` * rule: {obj}`my_rule` 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/BUILD.bazel b/tests/BUILD.bazel index e7dbef65d8..927fc30566 100644 --- a/tests/BUILD.bazel +++ b/tests/BUILD.bazel @@ -22,6 +22,7 @@ build_test( "//python:py_test_bzl", "//python/cc:py_cc_toolchain_bzl", "//python/cc:py_cc_toolchain_info_bzl", + "//python/cc:py_extension", "//python/entry_points:py_console_script_binary_bzl", ], ) diff --git a/tests/base_rules/py_executable_base_tests.bzl b/tests/base_rules/py_executable_base_tests.bzl index dff0399bc2..f6b3c9bb60 100644 --- a/tests/base_rules/py_executable_base_tests.bzl +++ b/tests/base_rules/py_executable_base_tests.bzl @@ -109,6 +109,29 @@ def _test_basic_zip_impl(env, target): _tests.append(_test_basic_zip) +def _test_config_settings_extra_toolchains(name, config): + rt_util.helper_target( + config.rule, + name = name + "_subject", + srcs = ["main.py"], + main = "main.py", + config_settings = { + "//command_line_option:extra_toolchains": "{tc},{tc}".format(tc = CC_TOOLCHAIN), + }, + ) + analysis_test( + name = name, + impl = _test_config_settings_extra_toolchains_impl, + target = name + "_subject", + ) + +def _test_config_settings_extra_toolchains_impl(env, target): + # If we got here, it means analysis succeeded, which implies the transition + # successfully parsed the CSV string into a list. + env.expect.that_target(target).has_provider(PyInfo) + +_tests.append(_test_config_settings_extra_toolchains) + def _test_cross_compile_to_unix(name, config): rt_util.helper_target( config.rule, @@ -496,6 +519,45 @@ def _test_py_runtime_info_provided_impl(env, target): _tests.append(_test_py_runtime_info_provided) +def _test_venv_output_prefix_with_path_separators(name, config): + rt_util.helper_target( + config.rule, + name = name + "/foo/tool", + srcs = ["main.py"], + main = "main.py", + ) + rt_util.helper_target( + config.rule, + name = name + "/bar/tool", + srcs = ["main.py"], + main = "main.py", + ) + rt_util.helper_target( + config.rule, + name = name + "/foo_tool", + srcs = ["main.py"], + main = "main.py", + ) + analysis_test( + name = name, + impl = _test_venv_output_prefix_with_path_separators_impl, + targets = { + "bar": name + "/bar/tool", + "foo": name + "/foo/tool", + "foo_underscore": name + "/foo_tool", + }, + ) + +def _test_venv_output_prefix_with_path_separators_impl(env, targets): + for target in [targets.foo, targets.bar, targets.foo_underscore]: + target = env.expect.that_target(target) + venv_file = "*/_{}.venv/*site-packages/bazel.pth".format( + target.meta.format_str("{name}"), + ) + target.runfiles().contains_predicate(matching.str_matches(venv_file)) + +_tests.append(_test_venv_output_prefix_with_path_separators) + def _test_windows_target_with_path_separators(name, config): rt_util.helper_target( config.rule, diff --git a/tests/base_rules/py_info/py_info_tests.bzl b/tests/base_rules/py_info/py_info_tests.bzl index 273959b957..0e1a96548c 100644 --- a/tests/base_rules/py_info/py_info_tests.bzl +++ b/tests/base_rules/py_info/py_info_tests.bzl @@ -16,7 +16,7 @@ load("@rules_testing//lib:analysis_test.bzl", "analysis_test") load("@rules_testing//lib:test_suite.bzl", "test_suite") load("@rules_testing//lib:util.bzl", rt_util = "util") -load("//python:py_info.bzl", "PyInfo") +load("//python:py_info.bzl", "PyInfo", "VenvSymlinkKind") load("//python/private:py_info.bzl", "PyInfoBuilder") # buildifier: disable=bzl-visibility load("//python/private:reexports.bzl", "BuiltinPyInfo") # buildifier: disable=bzl-visibility load("//tests/support:py_info_subject.bzl", "py_info_subject") @@ -170,6 +170,12 @@ def _test_py_info_builder_impl(env, targets): builder.transitive_sources.add(trans) builder.merge_uses_shared_libraries(True) + symlink = builder.add_venv_symlink() + symlink.set_kind(VenvSymlinkKind.LIB) + symlink.set_venv_path("test_path") + symlink.set_link_to_path("test_target") + symlink.files.add(trans) + builder.merge_target(targets.py1) builder.merge_targets([targets.py2]) @@ -247,6 +253,21 @@ def _test_py_info_builder_impl(env, targets): "tests/base_rules/py_info/py6-trans.pyi", ]) + if hasattr(actual, "venv_symlinks"): + entries = actual.venv_symlinks.to_list() + env.expect.that_int(len(entries)).equals(1) + entry = entries[0] + env.expect.that_str(entry.venv_path).equals("test_path") + env.expect.that_str(entry.link_to_path).equals("test_target") + env.expect.that_str(entry.kind).equals(VenvSymlinkKind.LIB) + env.expect.that_collection(entry.files.to_list()).contains_exactly([trans]) + env.expect.that_bool(entry.package == None).equals(True) + env.expect.that_bool(entry.version == None).equals(True) + env.expect.that_bool(entry.link_to_file == None).equals(True) + + check(builder.build()) + + # Call build() again to verify it doesn't duplicate/leak state check(builder.build()) if BuiltinPyInfo != None: check(builder.build_builtin_py_info()) diff --git a/tests/base_rules/py_library/py_library_tests.bzl b/tests/base_rules/py_library/py_library_tests.bzl index 3726ff1f41..c375e72794 100644 --- a/tests/base_rules/py_library/py_library_tests.bzl +++ b/tests/base_rules/py_library/py_library_tests.bzl @@ -3,10 +3,12 @@ load("@rules_testing//lib:analysis_test.bzl", "analysis_test") load("@rules_testing//lib:truth.bzl", "matching") load("@rules_testing//lib:util.bzl", rt_util = "util") +load("//python:py_info.bzl", "PyInfo") load("//python:py_library.bzl", "py_library") load("//python:py_runtime_info.bzl", "PyRuntimeInfo") load("//tests/base_rules:base_tests.bzl", "create_base_tests") load("//tests/base_rules:util.bzl", pt_util = "util") +load("//tests/support:py_info_subject.bzl", "py_info_subject") _tests = [] @@ -141,6 +143,63 @@ def _test_files_to_compile_impl(env, target): _tests.append(_test_files_to_compile) +def _test_pyi_srcs_not_propagated_via_srcs(name, config): + rt_util.helper_target( + config.rule, + name = name + "_lib", + srcs = ["lib.py"], + pyi_srcs = ["lib.pyi"], + ) + rt_util.helper_target( + config.rule, + name = name + "_subject", + srcs = [name + "_lib"], + ) + analysis_test( + name = name, + target = name + "_subject", + impl = _test_pyi_srcs_not_propagated_via_srcs_impl, + ) + +def _test_pyi_srcs_not_propagated_via_srcs_impl(env, target): + info = env.expect.that_target(target).provider( + PyInfo, + factory = py_info_subject, + ) + info.transitive_pyi_files().contains_exactly([]) + +_tests.append(_test_pyi_srcs_not_propagated_via_srcs) + +def _test_pyi_srcs_propagated_via_deps(name, config): + rt_util.helper_target( + config.rule, + name = name + "_lib", + srcs = ["lib.py"], + pyi_srcs = ["lib.pyi"], + ) + rt_util.helper_target( + config.rule, + name = name + "_subject", + srcs = [name + "_lib"], + deps = [name + "_lib"], + ) + analysis_test( + name = name, + target = name + "_subject", + impl = _test_pyi_srcs_propagated_via_deps_impl, + ) + +def _test_pyi_srcs_propagated_via_deps_impl(env, target): + info = env.expect.that_target(target).provider( + PyInfo, + factory = py_info_subject, + ) + info.transitive_pyi_files().contains_exactly([ + "{package}/lib.pyi", + ]) + +_tests.append(_test_pyi_srcs_propagated_via_deps) + def py_library_test_suite(name): config = struct(rule = py_library, base_test_rule = py_library) native.test_suite( diff --git a/tests/base_rules/py_test/py_test_tests.bzl b/tests/base_rules/py_test/py_test_tests.bzl index fd284beffd..e16204e9f2 100644 --- a/tests/base_rules/py_test/py_test_tests.bzl +++ b/tests/base_rules/py_test/py_test_tests.bzl @@ -16,12 +16,13 @@ load("@rules_testing//lib:analysis_test.bzl", "analysis_test") load("@rules_testing//lib:util.bzl", rt_util = "util") load("//python:py_test.bzl", "py_test") +load("//python/private:common_labels.bzl", "labels") # buildifier: disable=bzl-visibility load( "//tests/base_rules:py_executable_base_tests.bzl", "create_executable_tests", ) load("//tests/base_rules:util.bzl", pt_util = "util") -load("//tests/support:support.bzl", "CC_TOOLCHAIN", "CROSSTOOL_TOP") +load("//tests/support:support.bzl", "CC_TOOLCHAIN", "CROSSTOOL_TOP", "PY_TOOLCHAINS") load("//tests/support/platforms:platforms.bzl", "platform_targets") # The Windows CI currently runs as root, which breaks when @@ -96,6 +97,61 @@ def _test_non_mac_doesnt_require_darwin_for_execution_impl(env, target): _tests.append(_test_non_mac_doesnt_require_darwin_for_execution) +_VALIDATE_TEST_MAIN_CONFIG_SETTINGS = { + "//command_line_option:extra_toolchains": [PY_TOOLCHAINS, CC_TOOLCHAIN], + labels.EXEC_TOOLS_TOOLCHAIN: "enabled", +} + +def _test_validate_test_main_enabled(name, config): + rt_util.helper_target( + config.rule, + name = name + "_subject", + srcs = [name + "_subject.py"], + ) + analysis_test( + name = name, + impl = _test_validate_test_main_enabled_impl, + target = name + "_subject", + config_settings = _VALIDATE_TEST_MAIN_CONFIG_SETTINGS | { + labels.VALIDATE_TEST_MAIN: "enabled", + }, + attr_values = _SKIP_WINDOWS, + ) + +def _test_validate_test_main_enabled_impl(env, target): + mnemonics = [a.mnemonic for a in target.actions] + env.expect.that_collection(mnemonics).contains("PyValidateTestMain") + env.expect.that_bool( + hasattr(target[OutputGroupInfo], "_validation"), + ).equals(True) + +_tests.append(_test_validate_test_main_enabled) + +def _test_validate_test_main_disabled(name, config): + rt_util.helper_target( + config.rule, + name = name + "_subject", + srcs = [name + "_subject.py"], + ) + analysis_test( + name = name, + impl = _test_validate_test_main_disabled_impl, + target = name + "_subject", + config_settings = _VALIDATE_TEST_MAIN_CONFIG_SETTINGS | { + labels.VALIDATE_TEST_MAIN: "disabled", + }, + attr_values = _SKIP_WINDOWS, + ) + +def _test_validate_test_main_disabled_impl(env, target): + mnemonics = [a.mnemonic for a in target.actions] + env.expect.that_collection(mnemonics).not_contains("PyValidateTestMain") + env.expect.that_bool( + hasattr(target[OutputGroupInfo], "_validation"), + ).equals(False) + +_tests.append(_test_validate_test_main_disabled) + def py_test_test_suite(name): config = struct(rule = py_test) native.test_suite( diff --git a/tests/bootstrap_impls/BUILD.bazel b/tests/bootstrap_impls/BUILD.bazel index ab3148db00..89cd682a6a 100644 --- a/tests/bootstrap_impls/BUILD.bazel +++ b/tests/bootstrap_impls/BUILD.bazel @@ -136,6 +136,32 @@ py_reconfig_test( main = "sys_path_order_test.py", ) +genrule( + name = "stdlib_shadowing_outputs", + outs = [ + "shutil.py", + "types.py", + ], + cmd = """ +cat > $(@D)/shutil.py <<'PY' +raise RuntimeError("target output shutil.py shadowed the stdlib shutil module") +PY +cat > $(@D)/types.py <<'PY' +raise RuntimeError("target output types.py shadowed the stdlib types module") +PY +""", +) + +py_reconfig_test( + name = "stdlib_shadowing_system_python_test", + srcs = [ + "stdlib_shadowing_test.py", + ":stdlib_shadowing_outputs", + ], + bootstrap_impl = "system_python", + main = "stdlib_shadowing_test.py", +) + py_reconfig_test( name = "main_module_test", srcs = ["main_module.py"], 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/stdlib_shadowing_test.py b/tests/bootstrap_impls/stdlib_shadowing_test.py new file mode 100644 index 0000000000..fc913857db --- /dev/null +++ b/tests/bootstrap_impls/stdlib_shadowing_test.py @@ -0,0 +1,20 @@ +# Copyright 2026 The Bazel Authors. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Verifies stage 1 bootstrap stdlib imports cannot be shadowed.""" + + +def test_bootstrap_reached_main(): + # If stage 1 imports target outputs such as shutil.py instead of stdlib + # modules, the process fails before this test module is executed. + pass 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_cc_toolchain/py_cc_toolchain_tests.bzl b/tests/cc/py_cc_toolchain/py_cc_toolchain_tests.bzl index 975f0d56b5..5b6f8e151d 100644 --- a/tests/cc/py_cc_toolchain/py_cc_toolchain_tests.bzl +++ b/tests/cc/py_cc_toolchain/py_cc_toolchain_tests.bzl @@ -47,6 +47,7 @@ def _test_py_cc_toolchain_impl(env, target): meta = env.expect.meta.derive(expr = "py_cc_toolchain_info"), ) toolchain.python_version().equals("3.999") + toolchain.abi_flags().equals("") # ===== Verify headers info ===== headers_providers = toolchain.headers().providers_map() @@ -118,8 +119,40 @@ def _test_py_cc_toolchain_impl(env, target): matching.str_matches("/libpython3."), ) + # ===== Verify PEP 508 platform markers & SOABI ===== + toolchain.sys_platform().equals("linux") + toolchain.platform_machine().equals("x86_64") + toolchain.platform_tag().equals("x86_64-linux-gnu") + toolchain.soabi().equals("cpython-3999-x86_64-linux-gnu") + _tests.append(_test_py_cc_toolchain) +def _test_custom_pep508_markers(name): + py_cc_toolchain( + name = name + "_subject", + headers = "//tests/support/cc_toolchains:py_headers", + platform_machine = "arm64", + python_version = "3.11", + sys_platform = "darwin", + ) + analysis_test( + name = name, + target = name + "_subject", + impl = _test_custom_pep508_markers_impl, + ) + +def _test_custom_pep508_markers_impl(env, target): + toolchain = PyCcToolchainInfoSubject.new( + target[platform_common.ToolchainInfo].py_cc_toolchain, + meta = env.expect.meta.derive(expr = "py_cc_toolchain_info"), + ) + toolchain.sys_platform().equals("darwin") + toolchain.platform_machine().equals("arm64") + toolchain.platform_tag().equals("darwin") + toolchain.soabi().equals("cpython-311-darwin") + +_tests.append(_test_custom_pep508_markers) + def _test_libs_optional(name): py_cc_toolchain( name = name + "_subject", diff --git a/tests/cc/py_extension/BUILD.bazel b/tests/cc/py_extension/BUILD.bazel new file mode 100644 index 0000000000..e6edde36e6 --- /dev/null +++ b/tests/cc/py_extension/BUILD.bazel @@ -0,0 +1,194 @@ +load("@rules_cc//cc:cc_library.bzl", "cc_library") +load("@rules_cc//cc:cc_shared_library.bzl", "cc_shared_library") +load("//python:py_test.bzl", "py_test") + +# buildifier: disable=bzl-visibility +load("//python/cc:py_extension.bzl", "py_extension") +load(":py_extension_build_test.bzl", "py_extension_build_test") + +package( + default_visibility = ["//tests/cc/py_extension:__subpackages__"], +) + +licenses(["notice"]) + +##### + +py_extension_build_test( + # An extension defined solely by source files, with no deps + name = "ext_source", + srcs = ["ext_source.c"], +) + +##### + +py_extension_build_test( + # A python extension that explicitly includes current_py_cc_headers in deps. + # Verifies that explicit current_py_cc_headers does not cause a duplicate label error. + name = "ext_explicit_headers", + srcs = ["ext_source.c"], + deps = [ + "@rules_python//python/cc:current_py_cc_headers", + ], +) + +##### + +py_extension_build_test( + # A python extension that includes current_py_cc_headers inside a select() in deps. + # Verifies that select() in deps works cleanly without duplicate label or type error. + name = "ext_explicit_headers_select", + srcs = ["ext_source.c"], + deps = select({ + "//conditions:default": ["@rules_python//python/cc:current_py_cc_headers"], + }), +) + +##### + +py_extension_build_test( + # A python extension that gets its code from a statically-linked library + name = "ext_static", + deps = [":static_dep"], +) + +cc_library( + name = "static_dep", + srcs = ["static_dep.c"], + hdrs = ["static_dep.h"], +) + +##### + +py_extension_build_test( + # An extension that also depends on a data file + name = "ext_with_data", + data = ["some_data.txt"], + deps = [":static_dep"], +) + +##### + +py_extension_build_test( + # A python extension that dynamically links to another shared library + name = "ext_shared", + dynamic_deps = [ + ":add_one_shared", + ], + imports = ["."], + deps = [ + # + ":ext_shared_impl", + ], +) + +cc_library( + name = "ext_shared_impl", + srcs = ["ext_shared.c"], + copts = [ + # Gemini says PIC is needed + "-fPIC", + "-fvisibility=hidden", + ], + deps = [ + #":add_one_headers", + # todo: if we put this here, we statically link add_one into the + # extension. + ":add_one_impl", + "@rules_python//python/cc:current_py_cc_headers", + ], +) + +cc_shared_library( + name = "add_one_shared", + user_link_flags = select({ + "@platforms//os:windows": ["/EXPORT:add_one"], + "//conditions:default": [], + }), + deps = [":add_one_impl"], +) + +cc_library( + name = "add_one_headers", + hdrs = ["add_one.h"], +) + +cc_library( + name = "add_one_impl", + srcs = ["add_one.c"], + deps = [ + ":add_one_headers", + ":add_one_helper", + ], +) + +cc_library( + name = "add_one_helper", + srcs = ["add_one_helper.c"], + hdrs = ["add_one_helper.h"], +) + +##### + +py_extension_build_test( + # An extension that uses the Python limited API + name = "ext_limited", + py_limited_api = "3.8", + deps = [":ext_limited_impl"], +) + +cc_library( + name = "ext_limited_impl", + srcs = ["ext_limited.c"], + copts = [ + "-fPIC", + "-fvisibility=hidden", + ], + defines = ["Py_LIMITED_API=0x3080000"], + deps = [ + "@rules_python//python/cc:current_py_cc_headers", + ], +) + +##### + +py_extension_build_test( + # An extension with its PyInit_* function in a static dependency + name = "ext_init_in_dep", + deps = [":ext_init_in_dep_impl"], +) + +cc_library( + name = "ext_init_in_dep_impl", + srcs = ["ext_init_in_dep.c"], + deps = [ + "@rules_python//python/cc:current_py_cc_headers", + ], +) + +##### + +py_test( + name = "py_extension_test", + srcs = ["py_extension_test.py"], + deps = [ + ":ext_shared", + "@dev_pip//pyelftools", + "@rules_python//python/runfiles", + ], +) + +##### + +py_extension( + name = "ext_pkg_test", + srcs = ["ext_pkg_test.c"], +) + +py_test( + name = "py_extension_pkg_test", + srcs = ["py_extension_pkg_test.py"], + deps = [ + ":ext_pkg_test", + ], +) diff --git a/tests/cc/py_extension/add_one.c b/tests/cc/py_extension/add_one.c new file mode 100644 index 0000000000..7da8f6796e --- /dev/null +++ b/tests/cc/py_extension/add_one.c @@ -0,0 +1,7 @@ + +#include "add_one_helper.h" + +int add_one(int x) { + x = add_one_helper(x); + return x + 1; +} diff --git a/tests/cc/py_extension/add_one.h b/tests/cc/py_extension/add_one.h new file mode 100644 index 0000000000..eda0abf7e5 --- /dev/null +++ b/tests/cc/py_extension/add_one.h @@ -0,0 +1,6 @@ +#ifndef TESTS_CC_PY_EXTENSION_DYN_DEP_A_H_ +#define TESTS_CC_PY_EXTENSION_DYN_DEP_A_H_ + +int add_one(int x); + +#endif // TESTS_CC_PY_EXTENSION_DYN_DEP_A_H_ diff --git a/tests/cc/py_extension/add_one_helper.c b/tests/cc/py_extension/add_one_helper.c new file mode 100644 index 0000000000..21d19a5823 --- /dev/null +++ b/tests/cc/py_extension/add_one_helper.c @@ -0,0 +1,7 @@ + + +#include "add_one_helper.h" + +int add_one_helper(int i) { + return i + 1; +} diff --git a/tests/cc/py_extension/add_one_helper.h b/tests/cc/py_extension/add_one_helper.h new file mode 100644 index 0000000000..5524d3077f --- /dev/null +++ b/tests/cc/py_extension/add_one_helper.h @@ -0,0 +1,2 @@ + +int add_one_helper(int i); diff --git a/tests/cc/py_extension/dependency_graph/BUILD.bazel b/tests/cc/py_extension/dependency_graph/BUILD.bazel new file mode 100644 index 0000000000..47bb5062a0 --- /dev/null +++ b/tests/cc/py_extension/dependency_graph/BUILD.bazel @@ -0,0 +1,12 @@ +load(":dependency_graph_tests.bzl", "dependency_graph_test_suite") + +package( + default_testonly = True, + default_visibility = ["//visibility:private"], +) + +licenses(["notice"]) + +dependency_graph_test_suite( + name = "dependency_graph_tests", +) diff --git a/tests/cc/py_extension/dependency_graph/dependency_graph_tests.bzl b/tests/cc/py_extension/dependency_graph/dependency_graph_tests.bzl new file mode 100644 index 0000000000..269c29eacb --- /dev/null +++ b/tests/cc/py_extension/dependency_graph/dependency_graph_tests.bzl @@ -0,0 +1,236 @@ +# Copyright 2025 The Bazel Authors. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Parity tests comparing cc_shared_library and py_extension behavior.""" + +load("@rules_cc//cc:cc_library.bzl", "cc_library") +load("@rules_cc//cc:cc_shared_library.bzl", "cc_shared_library") +load("@rules_cc//cc/common:cc_shared_library_info.bzl", "CcSharedLibraryInfo") +load("@rules_testing//lib:analysis_test.bzl", "analysis_test", "test_suite") +load("@rules_testing//lib:util.bzl", "util") + +_tests = [] + +# For tests 1 and 2 +def _create_dynamic_deps_helpers(name): + util.helper_target( + cc_library, + name = name + "_libC", + srcs = ["test_lib_c.c"], + hdrs = ["test_symbols.h"], + copts = ["-fPIC"], + ) + util.helper_target( + cc_shared_library, + name = name + "_cslC", + deps = [":" + name + "_libC"], + ) + util.helper_target( + cc_library, + name = name + "_libB", + srcs = ["test_lib_b.c"], + hdrs = ["test_symbols.h"], + copts = ["-fPIC"], + deps = [":" + name + "_libC"], + ) + util.helper_target( + cc_shared_library, + name = name + "_cslB", + deps = [":" + name + "_libB"], + dynamic_deps = [":" + name + "_cslC"], + ) + util.helper_target( + cc_library, + name = name + "_libA", + srcs = ["test_lib_a.c"], + hdrs = ["test_symbols.h"], + copts = ["-fPIC"], + deps = [":" + name + "_libB", ":" + name + "_libC"], + ) + +# Test 1: CSL A -> CSL B -> CSL C (Dynamic deps) +def _test_csl_dynamic_deps_top(name): + _create_dynamic_deps_helpers(name) + util.helper_target( + cc_shared_library, + name = name + "_cslA", + deps = [":" + name + "_libA"], + dynamic_deps = [":" + name + "_cslB", ":" + name + "_cslC"], + ) + analysis_test( + name = name, + target = name + "_cslA", + impl = _csl_dynamic_deps_test_impl, + ) + +_tests.append(_test_csl_dynamic_deps_top) + +def _csl_dynamic_deps_test_impl(env, target): + env.expect.that_target(target).has_provider(CcSharedLibraryInfo) + csl_info = target[CcSharedLibraryInfo] + + # Derive labels + test_name = target.label.name[:-5] # remove "_cslA" + lib_a_label = target.label.same_package_label(test_name + "_libA") + lib_b_label = target.label.same_package_label(test_name + "_libB") + lib_c_label = target.label.same_package_label(test_name + "_libC") + + env.expect.that_collection([str(e) for e in csl_info.exports]).contains_exactly([str(lib_a_label)]) + static_libs = [str(lbl) for lbl in csl_info.link_once_static_libs] + env.expect.that_collection(static_libs).contains(str(lib_a_label)) + env.expect.that_collection(static_libs).contains_none_of([str(lib_b_label), str(lib_c_label)]) + +# Test 2: py_extension A -> CSL B -> CSL C (Dynamic deps) +def _test_pyext_dynamic_deps_cslB(name): + _create_dynamic_deps_helpers(name) + analysis_test( + name = name, + target = name + "_cslB", + impl = _cslB_deps_test_impl, + ) + +_tests.append(_test_pyext_dynamic_deps_cslB) + +def _test_pyext_dynamic_deps_cslC(name): + _create_dynamic_deps_helpers(name) + analysis_test( + name = name, + target = name + "_cslC", + impl = _cslC_deps_test_impl, + ) + +_tests.append(_test_pyext_dynamic_deps_cslC) + +def _cslC_deps_test_impl(env, target): + env.expect.that_target(target).has_provider(CcSharedLibraryInfo) + csl_info = target[CcSharedLibraryInfo] + + # Derive labels + test_name = target.label.name[:-5] # remove "_cslC" + lib_c_label = target.label.same_package_label(test_name + "_libC") + + env.expect.that_collection([str(e) for e in csl_info.exports]).contains_exactly([str(lib_c_label)]) + env.expect.that_collection([str(lbl) for lbl in csl_info.link_once_static_libs]).contains_exactly([str(lib_c_label)]) + +def _cslB_deps_test_impl(env, target): + env.expect.that_target(target).has_provider(CcSharedLibraryInfo) + csl_info = target[CcSharedLibraryInfo] + + # Derive labels + test_name = target.label.name[:-5] # remove "_cslB" + lib_b_label = target.label.same_package_label(test_name + "_libB") + lib_c_label = target.label.same_package_label(test_name + "_libC") + csl_c_label = target.label.same_package_label(test_name + "_cslC") + + env.expect.that_collection([str(e) for e in csl_info.exports]).contains_exactly([str(lib_b_label)]) + static_libs = [str(lbl) for lbl in csl_info.link_once_static_libs] + env.expect.that_collection(static_libs).contains(str(lib_b_label)) + env.expect.that_collection(static_libs).contains_none_of([str(lib_c_label)]) + + dynamic_deps = [str(d.linker_input.owner) for d in csl_info.dynamic_deps.to_list()] + env.expect.that_collection(dynamic_deps).contains(str(csl_c_label)) + +# For tests 3 and 4 +def _create_static_sharing_helpers(name): + util.helper_target( + cc_library, + name = name + "_libC", + srcs = ["test_lib_c.c"], + hdrs = ["test_symbols.h"], + copts = ["-fPIC"], + ) + util.helper_target( + cc_library, + name = name + "_libB", + srcs = ["test_lib_b.c"], + hdrs = ["test_symbols.h"], + copts = ["-fPIC"], + deps = [":" + name + "_libC"], + ) + util.helper_target( + cc_shared_library, + name = name + "_cslB", + deps = [":" + name + "_libB", ":" + name + "_libC"], + ) + util.helper_target( + cc_library, + name = name + "_libA", + srcs = ["test_lib_a.c"], + hdrs = ["test_symbols.h"], + copts = ["-fPIC"], + deps = [":" + name + "_libB", ":" + name + "_libC"], + ) + +# Test 3: CSL A -> CSL B, CL C (Static sharing) +def _test_csl_static_sharing_top(name): + _create_static_sharing_helpers(name) + util.helper_target( + cc_shared_library, + name = name + "_cslA", + deps = [":" + name + "_libA"], + dynamic_deps = [":" + name + "_cslB"], + ) + analysis_test( + name = name, + target = name + "_cslA", + impl = _csl_static_sharing_test_impl, + ) + +_tests.append(_test_csl_static_sharing_top) + +def _csl_static_sharing_test_impl(env, target): + env.expect.that_target(target).has_provider(CcSharedLibraryInfo) + csl_info = target[CcSharedLibraryInfo] + + # Derive labels + test_name = target.label.name[:-5] # remove "_cslA" + lib_a_label = target.label.same_package_label(test_name + "_libA") + lib_b_label = target.label.same_package_label(test_name + "_libB") + lib_c_label = target.label.same_package_label(test_name + "_libC") + + env.expect.that_collection([str(e) for e in csl_info.exports]).contains_exactly([str(lib_a_label)]) + static_libs = [str(lbl) for lbl in csl_info.link_once_static_libs] + env.expect.that_collection(static_libs).contains(str(lib_a_label)) + env.expect.that_collection(static_libs).contains_none_of([str(lib_b_label), str(lib_c_label)]) + +# Test 4: Same as 3, but A is py_extension + +def _test_pyext_static_sharing_cslB(name): + _create_static_sharing_helpers(name) + analysis_test( + name = name, + target = name + "_cslB", + impl = _cslB_static_sharing_test_impl, + ) + +_tests.append(_test_pyext_static_sharing_cslB) + +def _cslB_static_sharing_test_impl(env, target): + env.expect.that_target(target).has_provider(CcSharedLibraryInfo) + csl_info = target[CcSharedLibraryInfo] + + # Derive labels + test_name = target.label.name[:-5] # remove "_cslB" + lib_b_label = target.label.same_package_label(test_name + "_libB") + lib_c_label = target.label.same_package_label(test_name + "_libC") + + env.expect.that_collection([str(e) for e in csl_info.exports]).contains_exactly([str(lib_b_label), str(lib_c_label)]) + static_libs = [str(lbl) for lbl in csl_info.link_once_static_libs] + env.expect.that_collection(static_libs).contains_exactly([str(lib_b_label), str(lib_c_label)]) + +def dependency_graph_test_suite(name): + test_suite( + name = name, + tests = _tests, + ) diff --git a/tests/cc/py_extension/dependency_graph/test_lib_a.c b/tests/cc/py_extension/dependency_graph/test_lib_a.c new file mode 100644 index 0000000000..19e2f489bf --- /dev/null +++ b/tests/cc/py_extension/dependency_graph/test_lib_a.c @@ -0,0 +1,6 @@ +#include "test_symbols.h" + +void fnA() { + fnB(); + fnC(); +} diff --git a/tests/cc/py_extension/dependency_graph/test_lib_b.c b/tests/cc/py_extension/dependency_graph/test_lib_b.c new file mode 100644 index 0000000000..3621587c1c --- /dev/null +++ b/tests/cc/py_extension/dependency_graph/test_lib_b.c @@ -0,0 +1,5 @@ +#include "test_symbols.h" + +void fnB() { + fnC(); +} diff --git a/tests/cc/py_extension/dependency_graph/test_lib_c.c b/tests/cc/py_extension/dependency_graph/test_lib_c.c new file mode 100644 index 0000000000..99941576dd --- /dev/null +++ b/tests/cc/py_extension/dependency_graph/test_lib_c.c @@ -0,0 +1,6 @@ +#include "test_symbols.h" +#include + +void fnC() { + printf("fnC\n"); +} diff --git a/tests/cc/py_extension/dependency_graph/test_symbols.h b/tests/cc/py_extension/dependency_graph/test_symbols.h new file mode 100644 index 0000000000..59ab3b02e7 --- /dev/null +++ b/tests/cc/py_extension/dependency_graph/test_symbols.h @@ -0,0 +1,8 @@ +#ifndef TEST_SYMBOLS_H +#define TEST_SYMBOLS_H + +void fnC(); +void fnB(); +void fnA(); + +#endif // TEST_SYMBOLS_H diff --git a/tests/cc/py_extension/ext_init_in_dep.c b/tests/cc/py_extension/ext_init_in_dep.c new file mode 100644 index 0000000000..a6d32ba0b2 --- /dev/null +++ b/tests/cc/py_extension/ext_init_in_dep.c @@ -0,0 +1,20 @@ + +#include + +// No methods defined; we're just testing the init function. +static PyMethodDef ModuleMethods[] = { + {NULL, NULL, 0, NULL} /* Sentinel */ +}; + +static struct PyModuleDef ext_init_in_dep_module = { + PyModuleDef_HEAD_INIT, + "ext_init_in_dep", /* name of module */ + NULL, /* module documentation, may be NULL */ + -1, /* size of per-interpreter state of the module, + or -1 if the module keeps state in global variables. */ + ModuleMethods +}; + +PyMODINIT_FUNC PyInit_ext_init_in_dep(void) { + return PyModule_Create(&ext_init_in_dep_module); +} diff --git a/tests/cc/py_extension/ext_limited.c b/tests/cc/py_extension/ext_limited.c new file mode 100644 index 0000000000..f3622c5824 --- /dev/null +++ b/tests/cc/py_extension/ext_limited.c @@ -0,0 +1,22 @@ +#include + +static PyObject* get_limited_api_version(PyObject* self, PyObject* args) { + return PyUnicode_FromFormat("0x%08x", Py_LIMITED_API); +} + +static PyMethodDef ModuleMethods[] = { + {"get_limited_api_version", get_limited_api_version, METH_NOARGS, "Get the version of the limited API this extension was compiled against."}, + {NULL, NULL, 0, NULL} +}; + +static struct PyModuleDef ext_limited_module = { + PyModuleDef_HEAD_INIT, + "ext_limited", + NULL, + -1, + ModuleMethods +}; + +PyMODINIT_FUNC PyInit_ext_limited(void) { + return PyModule_Create(&ext_limited_module); +} diff --git a/tests/cc/py_extension/ext_pkg_test.c b/tests/cc/py_extension/ext_pkg_test.c new file mode 100644 index 0000000000..c151f0162b --- /dev/null +++ b/tests/cc/py_extension/ext_pkg_test.c @@ -0,0 +1,22 @@ +#include + +static PyObject* get_magic_number(PyObject* self, PyObject* args) { + return PyLong_FromLong(42); +} + +static PyMethodDef ModuleMethods[] = { + {"get_magic_number", get_magic_number, METH_NOARGS, "Returns 42."}, + {NULL, NULL, 0, NULL} +}; + +static struct PyModuleDef ext_pkg_test_module = { + PyModuleDef_HEAD_INIT, + "ext_pkg_test", + NULL, + -1, + ModuleMethods +}; + +PyMODINIT_FUNC PyInit_ext_pkg_test(void) { + return PyModule_Create(&ext_pkg_test_module); +} diff --git a/tests/cc/py_extension/ext_shared.c b/tests/cc/py_extension/ext_shared.c new file mode 100644 index 0000000000..4b79a6da2f --- /dev/null +++ b/tests/cc/py_extension/ext_shared.c @@ -0,0 +1,33 @@ +#include + +#include "tests/cc/py_extension/add_one.h" + +// A simple function that returns a Python integer. +static PyObject* do_alpha(PyObject* self, PyObject* args) { + return PyLong_FromLong(add_one(41)); +} + +// Method definition object for this extension, these are the functions +// that will be available in the module. +static PyMethodDef ModuleMethods[] = { + {"do_alpha", do_alpha, METH_NOARGS, "A simple C function."}, + {NULL, NULL, 0, NULL} /* Sentinel */ +}; + +// Module definition +// The arguments of this structure tell Python what to call your extension, +// what its methods are and where to look for its method definitions. +static struct PyModuleDef ext_shared_module = { + PyModuleDef_HEAD_INIT, + "ext_shared", /* name of module */ + NULL, /* module documentation, may be NULL */ + -1, /* size of per-interpreter state of the module, + or -1 if the module keeps state in global variables. */ + ModuleMethods +}; + +// The module init function. This must be exported and retained in the +// shared library output. +PyMODINIT_FUNC PyInit_ext_shared(void) { + return PyModule_Create(&ext_shared_module); +} diff --git a/tests/cc/py_extension/ext_source.c b/tests/cc/py_extension/ext_source.c new file mode 100644 index 0000000000..df0239df3a --- /dev/null +++ b/tests/cc/py_extension/ext_source.c @@ -0,0 +1,33 @@ + +#include + + +static PyObject* calc_one_plus_two(PyObject* self, PyObject* args) { + return PyLong_FromLong(1 + 2); +} + + +// Method definition object for this extension, these are the functions +// that will be available in the module. +static PyMethodDef ModuleMethods[] = { + {"calc_one_plus_two", calc_one_plus_two, METH_NOARGS, "A simple C function."}, + {NULL, NULL, 0, NULL} /* Sentinel */ +}; + +// Module definition +// The arguments of this structure tell Python what to call your extension, +// what its methods are and where to look for its method definitions. +static struct PyModuleDef ext_source_module = { + PyModuleDef_HEAD_INIT, + "ext_source", /* name of module */ + NULL, /* module documentation, may be NULL */ + -1, /* size of per-interpreter state of the module, + or -1 if the module keeps state in global variables. */ + ModuleMethods +}; + +// The module init function. This must be exported and retained in the +// shared library output. +PyMODINIT_FUNC PyInit_ext_source(void) { + return PyModule_Create(&ext_source_module); +} diff --git a/tests/cc/py_extension/ext_static.c b/tests/cc/py_extension/ext_static.c new file mode 100644 index 0000000000..500c52db00 --- /dev/null +++ b/tests/cc/py_extension/ext_static.c @@ -0,0 +1 @@ +/* A no-op C extension for static linking tests. */ diff --git a/tests/cc/py_extension/py_extension/BUILD.bazel b/tests/cc/py_extension/py_extension/BUILD.bazel new file mode 100644 index 0000000000..0d64c2bf9d --- /dev/null +++ b/tests/cc/py_extension/py_extension/BUILD.bazel @@ -0,0 +1,12 @@ +load(":py_extension_tests.bzl", "py_extension_analysis_test_suite") + +package( + default_testonly = True, + default_visibility = ["//visibility:private"], +) + +licenses(["notice"]) + +py_extension_analysis_test_suite( + name = "py_extension_analysis_tests", +) diff --git a/tests/cc/py_extension/py_extension/py_extension_tests.bzl b/tests/cc/py_extension/py_extension/py_extension_tests.bzl new file mode 100644 index 0000000000..ef2a2d8b6e --- /dev/null +++ b/tests/cc/py_extension/py_extension/py_extension_tests.bzl @@ -0,0 +1,102 @@ +# Copyright 2025 The Bazel Authors. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Tests for py_extension.""" + +load("@rules_testing//lib:analysis_test.bzl", "analysis_test", "test_suite") +load("@rules_testing//lib:truth.bzl", "matching") +load("//python/private:py_info.bzl", "PyInfo") # buildifier: disable=bzl-visibility + +_tests = [] + +def _test_static_deps_impl(env, target): + env.expect.that_target(target).has_provider(PyInfo) + py_info = target[PyInfo] + + # The extension should be in PyInfo + env.expect.that_collection(py_info.transitive_sources.to_list()).has_size(1) + env.expect.that_depset_of_files(py_info.transitive_sources).contains_predicate( + matching.file_path_matches("ext_static.*311-*"), + ) + +def _test_static_deps(name): + analysis_test( + name = name, + impl = _test_static_deps_impl, + target = "//tests/cc/py_extension:ext_static", + ) + +_tests.append(_test_static_deps) + +def _test_data_deps_impl(env, target): + env.expect.that_target(target).has_provider(PyInfo) + + # Check that data file is in runfiles + default_info = target[DefaultInfo] + env.expect.that_depset_of_files(default_info.default_runfiles.files).contains_predicate( + matching.file_basename_equals("some_data.txt"), + ) + +def _test_data_deps(name): + analysis_test( + name = name, + impl = _test_data_deps_impl, + target = "//tests/cc/py_extension:ext_with_data", + ) + +_tests.append(_test_data_deps) + +def _test_dynamic_deps_impl(env, target): + env.expect.that_target(target).has_provider(PyInfo) + py_info = target[PyInfo] + + # The extension should be in PyInfo + env.expect.that_collection(py_info.transitive_sources.to_list()).has_size(1) + env.expect.that_depset_of_files(py_info.transitive_sources).contains_predicate( + matching.file_path_matches("ext_shared.*311-*"), + ) + +def _test_dynamic_deps(name): + analysis_test( + name = name, + impl = _test_dynamic_deps_impl, + target = "//tests/cc/py_extension:ext_shared", + ) + +_tests.append(_test_dynamic_deps) + +def _test_musl_platform_impl(env, target): + env.expect.that_target(target).has_provider(PyInfo) + py_info = target[PyInfo] + env.expect.that_depset_of_files(py_info.transitive_sources).contains_predicate( + matching.file_path_matches("ext_static.*311-*"), + ) + +def _test_musl_platform(name): + analysis_test( + name = name, + impl = _test_musl_platform_impl, + target = "//tests/cc/py_extension:ext_static", + config_settings = { + str(Label("//python/config_settings:py_linux_libc")): "musl", + }, + ) + +_tests.append(_test_musl_platform) + +def py_extension_analysis_test_suite(name): + test_suite( + name = name, + tests = _tests, + ) diff --git a/tests/cc/py_extension/py_extension_build_test.bzl b/tests/cc/py_extension/py_extension_build_test.bzl new file mode 100644 index 0000000000..18cca2204b --- /dev/null +++ b/tests/cc/py_extension/py_extension_build_test.bzl @@ -0,0 +1,15 @@ +"""Macro helper for py_extension build testing.""" + +load("@bazel_skylib//rules:build_test.bzl", "build_test") +load("//python/cc:py_extension.bzl", "py_extension") + +def py_extension_build_test(name, **kwargs): + """Creates a py_extension target and a build_test verifying it builds.""" + py_extension( + name = name, + **kwargs + ) + build_test( + name = name + "_build_test", + targets = [":" + name], + ) diff --git a/tests/cc/py_extension/py_extension_pkg_test.py b/tests/cc/py_extension/py_extension_pkg_test.py new file mode 100644 index 0000000000..68c6dc3ee6 --- /dev/null +++ b/tests/cc/py_extension/py_extension_pkg_test.py @@ -0,0 +1,18 @@ +import unittest + +from tests.cc.py_extension import ( + ext_pkg_test, # pyrefly: ignore[missing-module-attribute] +) + + +class PyExtensionPkgTest(unittest.TestCase): + def test_import_via_package(self): + self.assertEqual(ext_pkg_test.get_magic_number(), 42) + + def test_direct_import(self): + with self.assertRaises(ModuleNotFoundError): + import ext_pkg_test # pyrefly: ignore[missing-import] # noqa: F401 + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/cc/py_extension/py_extension_test.py b/tests/cc/py_extension/py_extension_test.py new file mode 100644 index 0000000000..7ffcdd3e66 --- /dev/null +++ b/tests/cc/py_extension/py_extension_test.py @@ -0,0 +1,47 @@ +import os +import sys +import unittest + +import ext_shared # pyrefly: ignore[missing-import] +from elftools.elf.dynamic import DynamicSection +from elftools.elf.elffile import ELFFile + + +class PyExtensionTest(unittest.TestCase): + @unittest.skipIf( + sys.platform != "linux", "ELF inspection is only supported on Linux" + ) + def test_inspect_elf(self): + ext_path = ext_shared.__file__ + self.assertTrue( + os.path.exists(ext_path), f"Could not find ext_shared.so at {ext_path}" + ) + + with open(ext_path, "rb") as f: + elf = ELFFile(f) + + # Check for DT_NEEDED entry for the dynamic library + dynamic_section = elf.get_section_by_name(".dynamic") + self.assertIsNotNone(dynamic_section) + self.assertTrue(isinstance(dynamic_section, DynamicSection)) + + needed_libs = [ + tag.needed # pyrefly: ignore[missing-attribute] + for tag in dynamic_section.iter_tags() + if tag.entry.d_tag == "DT_NEEDED" # pyrefly: ignore[missing-attribute] + ] + self.assertIn("libadd_one_shared.so", needed_libs) + + # Check for the PyInit symbol + dynsym_section = elf.get_section_by_name(".dynsym") + self.assertIsNotNone(dynsym_section) + + symbols = [s.name for s in dynsym_section.iter_symbols()] + self.assertIn("PyInit_ext_shared", symbols) + + def test_import_and_call(self): + self.assertEqual(ext_shared.do_alpha(), 43) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/cc/py_extension/py_limited_api/BUILD.bazel b/tests/cc/py_extension/py_limited_api/BUILD.bazel new file mode 100644 index 0000000000..f548f3040f --- /dev/null +++ b/tests/cc/py_extension/py_limited_api/BUILD.bazel @@ -0,0 +1,12 @@ +load(":py_limited_api_tests.bzl", "py_limited_api_test_suite") + +package( + default_testonly = True, + default_visibility = ["//visibility:private"], +) + +licenses(["notice"]) + +py_limited_api_test_suite( + name = "py_limited_api_tests", +) diff --git a/tests/cc/py_extension/py_limited_api/py_limited_api_tests.bzl b/tests/cc/py_extension/py_limited_api/py_limited_api_tests.bzl new file mode 100644 index 0000000000..fbe92b936a --- /dev/null +++ b/tests/cc/py_extension/py_limited_api/py_limited_api_tests.bzl @@ -0,0 +1,140 @@ +# Copyright 2025 The Bazel Authors. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Tests for the py_limited_api attribute for py_extension.""" + +load("@rules_cc//cc:cc_library.bzl", "cc_library") +load("@rules_testing//lib:analysis_test.bzl", "analysis_test", "test_suite") +load("@rules_testing//lib:truth.bzl", "matching") +load("@rules_testing//lib:util.bzl", "util") +load("//python/cc:py_extension.bzl", "py_extension") + +def _test_limited_pass_impl(env, target): + env.expect.that_depset_of_files(target[DefaultInfo].files).contains_predicate( + matching.file_path_matches("tests/cc/py_extension/py_limited_api/{}.abi3.*".format(target.label.name)), + ) + +def _test_limited_same_version(name): + util.helper_target( + cc_library, + name = name + "_csl", + defines = ["Py_LIMITED_API=0x03080000"], + deps = [ + "@rules_python//python/cc:current_py_cc_headers", + ], + ) + py_extension( + name = name + "_pyext", + deps = [":" + name + "_csl"], + py_limited_api = "3.8", + ) + analysis_test( + name = name, + target = name + "_pyext", + impl = _test_limited_pass_impl, + ) + +def _test_limited_older_dep(name): + util.helper_target( + cc_library, + name = name + "_csl", + defines = ["Py_LIMITED_API=0x03080000"], # 3.8 + deps = [ + "@rules_python//python/cc:current_py_cc_headers", + ], + ) + py_extension( + name = name + "_pyext", + deps = [":" + name + "_csl"], + py_limited_api = "3.9", # 3.9 + ) + analysis_test( + name = name, + target = name + "_pyext", + impl = _test_limited_pass_impl, + ) + +def _test_no_limited_api(name): + util.helper_target( + cc_library, + name = name + "_csl", + deps = [ + "@rules_python//python/cc:current_py_cc_headers", + ], + ) + py_extension( + name = name + "_pyext", + deps = [":" + name + "_csl"], + ) + analysis_test( + name = name, + target = name + "_pyext", + impl = _test_no_limited_api_impl, + ) + +def _test_no_limited_api_impl(env, target): + # Should pass, nothing to assert on filename since it is platform-specific + _ = env # @unused + _ = target # @unused + +def _test_no_limited_api_dep_has_limited(name): + util.helper_target( + cc_library, + name = name + "_csl", + defines = ["Py_LIMITED_API=0x03080000"], + deps = [ + "@rules_python//python/cc:current_py_cc_headers", + ], + ) + py_extension( + name = name + "_pyext", + deps = [":" + name + "_csl"], + ) + analysis_test( + name = name, + target = name + "_pyext", + impl = _test_no_limited_api_dep_has_limited_impl, + ) + +def _test_no_limited_api_dep_has_limited_impl(env, target): + _ = env # @unused + _ = target # @unused + +def _test_limited_api_dep_has_no_python(name): + util.helper_target( + cc_library, + name = name + "_csl", + ) + py_extension( + name = name + "_pyext", + deps = [":" + name + "_csl"], + py_limited_api = "3.8", + ) + analysis_test( + name = name, + target = name + "_pyext", + impl = _test_limited_pass_impl, + ) + +def py_limited_api_test_suite(name): + test_suite( + name = name, + tests = [ + _test_limited_same_version, + _test_limited_older_dep, + _test_no_limited_api, + _test_no_limited_api_dep_has_limited, + _test_limited_api_dep_has_no_python, + ], + ) diff --git a/tests/cc/py_extension/some_data.txt b/tests/cc/py_extension/some_data.txt new file mode 100644 index 0000000000..4b5dc1d64c --- /dev/null +++ b/tests/cc/py_extension/some_data.txt @@ -0,0 +1 @@ +This is a data file diff --git a/tests/cc/py_extension/static_dep.c b/tests/cc/py_extension/static_dep.c new file mode 100644 index 0000000000..fa95e4dacc --- /dev/null +++ b/tests/cc/py_extension/static_dep.c @@ -0,0 +1,5 @@ +#include "static_dep.h" + +int my_lib_func() { + return 42; +} diff --git a/tests/cc/py_extension/static_dep.h b/tests/cc/py_extension/static_dep.h new file mode 100644 index 0000000000..d0f272abd7 --- /dev/null +++ b/tests/cc/py_extension/static_dep.h @@ -0,0 +1 @@ +int my_lib_func(); diff --git a/tests/config_settings/transition/BUILD.bazel b/tests/config_settings/transition/BUILD.bazel index 19d4958669..093de50959 100644 --- a/tests/config_settings/transition/BUILD.bazel +++ b/tests/config_settings/transition/BUILD.bazel @@ -1,6 +1,3 @@ load(":multi_version_tests.bzl", "multi_version_test_suite") -load(":py_args_tests.bzl", "py_args_test_suite") - -py_args_test_suite(name = "py_args_tests") multi_version_test_suite(name = "multi_version_tests") diff --git a/tests/config_settings/transition/py_args_tests.bzl b/tests/config_settings/transition/py_args_tests.bzl deleted file mode 100644 index 4538c88a5c..0000000000 --- a/tests/config_settings/transition/py_args_tests.bzl +++ /dev/null @@ -1,68 +0,0 @@ -# Copyright 2023 The Bazel Authors. All rights reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"" - -load("@rules_testing//lib:test_suite.bzl", "test_suite") -load("//python/config_settings/private:py_args.bzl", "py_args") # buildifier: disable=bzl-visibility - -_tests = [] - -def _test_py_args_default(env): - actual = py_args("foo", {}) - - want = { - "args": None, - "data": None, - "deps": None, - "env": None, - "main": "foo.py", - "srcs": None, - } - env.expect.that_dict(actual).contains_exactly(want) - -_tests.append(_test_py_args_default) - -def _test_kwargs_get_consumed(env): - kwargs = { - "args": ["some", "args"], - "data": ["data"], - "deps": ["deps"], - "env": {"key": "value"}, - "main": "__main__.py", - "srcs": ["__main__.py"], - "visibility": ["//visibility:public"], - } - actual = py_args("bar_bin", kwargs) - - want = { - "args": ["some", "args"], - "data": ["data"], - "deps": ["deps"], - "env": {"key": "value"}, - "main": "__main__.py", - "srcs": ["__main__.py"], - } - env.expect.that_dict(actual).contains_exactly(want) - env.expect.that_dict(kwargs).keys().contains_exactly(["visibility"]) - -_tests.append(_test_kwargs_get_consumed) - -def py_args_test_suite(name): - """Create the test suite. - - Args: - name: the name of the test suite - """ - test_suite(name = name, basic_tests = _tests) diff --git a/tests/coverage_deps/coverage_deps_test.bzl b/tests/coverage_deps/coverage_deps_test.bzl index 12351affde..ddb82cd6c5 100644 --- a/tests/coverage_deps/coverage_deps_test.bzl +++ b/tests/coverage_deps/coverage_deps_test.bzl @@ -12,68 +12,25 @@ # See the License for the specific language governing permissions and # limitations under the License. -"Tests for the warning emitted by coverage_dep when no wheel is available." +"Tests for coverage_dep's handling of platforms with no bundled wheel." load("@rules_testing//lib:test_suite.bzl", "test_suite") load("//python/private:coverage_deps.bzl", "coverage_dep") # buildifier: disable=bzl-visibility -load("//python/private:repo_utils.bzl", "REPO_DEBUG_ENV_VAR", "REPO_VERBOSITY_ENV_VAR", "repo_utils") # buildifier: disable=bzl-visibility _tests = [] -def _capturing_logger(): - """Build a (logger, captured_messages_list) pair. - - The logger has its verbosity set to INFO so WARN messages are captured but - nothing noisier than necessary is emitted. The printer collects the second - positional argument from each printer invocation (the formatted message). - """ - captured = [] - logger = repo_utils.logger( - struct( - getenv = { - REPO_DEBUG_ENV_VAR: None, - REPO_VERBOSITY_ENV_VAR: "INFO", - }.get, - ), - name = "unit-test", - printer = lambda _key, message: captured.append(message), - ) - return logger, captured - -def _test_unsupported_python_version_warns(env): - # cp37 is not in the bundled wheel set; coverage_dep should return None - # and emit a warning describing the misconfiguration. - logger, captured = _capturing_logger() - result = coverage_dep( - name = "unused_for_test", - python_version = "3.7", - platform = "aarch64-apple-darwin", - visibility = ["//visibility:public"], - logger = logger, - ) - env.expect.that_bool(result == None).equals(True) - env.expect.that_int(len(captured)).equals(1) - env.expect.that_str(captured[0]).contains("no wheel for") - env.expect.that_str(captured[0]).contains("python_version=3.7") - env.expect.that_str(captured[0]).contains("platform=aarch64-apple-darwin") - -_tests.append(_test_unsupported_python_version_warns) - -def _test_windows_platform_is_silent(env): - # Windows is intentionally unsupported and not actionable; coverage_dep - # must return None without logging anything. - logger, captured = _capturing_logger() +def _test_windows_platform_returns_none(env): + # Windows is intentionally unsupported: the upstream coverage wrapper is + # written in shell. result = coverage_dep( name = "unused_for_test", python_version = "3.10", platform = "x86_64-pc-windows-msvc", visibility = ["//visibility:public"], - logger = logger, ) env.expect.that_bool(result == None).equals(True) - env.expect.that_int(len(captured)).equals(0) -_tests.append(_test_windows_platform_is_silent) +_tests.append(_test_windows_platform_returns_none) # NOTE: there is intentionally no unit test for the supported-wheel path # (where coverage_dep returns a non-None label and emits no warning). diff --git a/tests/docs/BUILD.bazel b/tests/docs/BUILD.bazel new file mode 100644 index 0000000000..bdc99a290c --- /dev/null +++ b/tests/docs/BUILD.bazel @@ -0,0 +1,10 @@ +load("@bazel_skylib//rules:build_test.bzl", "build_test") + +licenses(["notice"]) + +build_test( + name = "docs_build_test", + targets = [ + "//docs:docs", + ], +) 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/explicit_init_py/BUILD.bazel b/tests/explicit_init_py/BUILD.bazel new file mode 100644 index 0000000000..9ea71dc09e --- /dev/null +++ b/tests/explicit_init_py/BUILD.bazel @@ -0,0 +1,6 @@ +load("//python/private:bzlmod_enabled.bzl", "BZLMOD_ENABLED") # buildifier: disable=bzl-visibility + +test_suite( + name = "explicit_init_py", + tests = ["@other//:explicit_init_py_tests"] if BZLMOD_ENABLED else [], +) diff --git a/tests/get_release_info/get_release_info_tests.bzl b/tests/get_release_info/get_release_info_tests.bzl index c810489c92..0b1b60adc3 100644 --- a/tests/get_release_info/get_release_info_tests.bzl +++ b/tests/get_release_info/get_release_info_tests.bzl @@ -63,6 +63,7 @@ def _test_astral_mirror(env): expected_urls = [ "https://github.com/astral-sh/python-build-standalone/releases/download/20230826/cpython-3.11.5+20230826-x86_64-unknown-linux-gnu-install_only.tar.gz", "https://releases.astral.sh/github/python-build-standalone/releases/download/20230826/cpython-3.11.5+20230826-x86_64-unknown-linux-gnu-install_only.tar.gz", + "https://github.com/indygreg/python-build-standalone/releases/download/20230826/cpython-3.11.5+20230826-x86_64-unknown-linux-gnu-install_only.tar.gz", ] _, urls, _, _, _ = get_release_info( @@ -95,7 +96,7 @@ def _test_astral_mirror_legacy(env): _, urls, _, _, _ = get_release_info( platform = "x86_64-unknown-linux-gnu", python_version = "3.11.5", - base_url = "https://github.com/indygreg/python-build-standalone/releases/download", + base_urls = ["https://github.com/indygreg/python-build-standalone/releases/download"], tool_versions = tool_versions, ) diff --git a/tests/integration/BUILD.bazel b/tests/integration/BUILD.bazel index 9295cbb22f..13b9c2e855 100644 --- a/tests/integration/BUILD.bazel +++ b/tests/integration/BUILD.bazel @@ -13,7 +13,9 @@ # limitations under the License. load("@rules_bazel_integration_test//bazel_integration_test:defs.bzl", "default_test_runner") +load("//python:py_binary.bzl", "py_binary") load("//python:py_library.bzl", "py_library") +load("//tests/support:support.bzl", "NOT_WINDOWS") load(":integration_test.bzl", "rules_python_integration_test") licenses(["notice"]) @@ -40,6 +42,7 @@ default_test_runner( rules_python_integration_test( name = "bzlmod_lockfile_test", bazel_versions = ["9.1.0"], + py_main = "bzlmod_lockfile_test.py", ) test_suite( @@ -48,6 +51,7 @@ test_suite( tests = [ "bzlmod_lockfile_test_bazel_9.1.0", "local_toolchains_test_bazel_self", + "uv_lock_test_bazel_self", ], ) @@ -101,6 +105,10 @@ rules_python_integration_test( workspace_path = "py_cc_toolchain_registered", ) +rules_python_integration_test( + name = "runtime_manifests_test", +) + rules_python_integration_test( name = "custom_commands_test", py_main = "custom_commands_test.py", @@ -111,8 +119,55 @@ rules_python_integration_test( py_main = "toolchain_target_settings_test.py", ) +rules_python_integration_test( + name = "validate_test_main_test", + py_main = "validate_test_main_test.py", +) + +rules_python_integration_test( + name = "unified_pypi_test", + py_main = "unified_pypi_test.py", +) + +rules_python_integration_test( + name = "uv_lock_test", + py_deps = [ + "@pypiserver//pypiserver", + ":uv_lock_pypi_server_lib", + ], + py_main = "uv_lock_test.py", +) + +rules_python_integration_test( + name = "whl_library_test", +) + py_library( name = "runner_lib", srcs = ["runner.py"], imports = ["../../"], ) + +py_library( + name = "uv_lock_pypi_server_lib", + srcs = ["uv_lock_pypi_server.py"], + imports = ["../../"], + # currently windows is not working due to + # https://github.com/pypiserver/pypiserver/blob/main/pypiserver/config.py#L123 + # + # class DEFAULTS: + # .... + # PACKAGE_DIRECTORIES = [pathlib.Path("~/packages").expanduser().resolve()] + # .... + # + # which is loaded through `__init__.py` even though it is not used and breaks because + # in a Windows sandbox one cannot resolve the home directory. + target_compatible_with = NOT_WINDOWS, + deps = ["@pypiserver//pypiserver"], +) + +py_binary( + name = "uv_lock_pypi_server", + srcs = ["uv_lock_pypi_server.py"], + deps = [":uv_lock_pypi_server_lib"], +) diff --git a/tests/integration/bzlmod_lockfile/MODULE.bazel b/tests/integration/bzlmod_lockfile/MODULE.bazel index 6c074bcdd4..bfd43cb895 100644 --- a/tests/integration/bzlmod_lockfile/MODULE.bazel +++ b/tests/integration/bzlmod_lockfile/MODULE.bazel @@ -12,8 +12,8 @@ python.toolchain(python_version = "3.13") # TODO: This test module should also verify that isolate = True works, will do in a followup PR. pip = use_extension("@rules_python//python/extensions:pip.bzl", "pip") pip.parse( - hub_name = "pypi", + hub_name = "pypi_lockfile", python_version = "3.13", requirements_lock = "//:requirements_lock.txt", ) -use_repo(pip, "pypi") +use_repo(pip, pypi = "pypi_lockfile") diff --git a/tests/integration/bzlmod_lockfile/MODULE.bazel.lock b/tests/integration/bzlmod_lockfile/MODULE.bazel.lock index 0dcc4b2a48..6b293283ae 100644 --- a/tests/integration/bzlmod_lockfile/MODULE.bazel.lock +++ b/tests/integration/bzlmod_lockfile/MODULE.bazel.lock @@ -161,6 +161,8 @@ "https://bcr.bazel.build/modules/swift_argument_parser/1.3.1.1/MODULE.bazel": "5e463fbfba7b1701d957555ed45097d7f984211330106ccd1352c6e0af0dcf91", "https://bcr.bazel.build/modules/swift_argument_parser/1.3.1.2/MODULE.bazel": "75aab2373a4bbe2a1260b9bf2a1ebbdbf872d3bd36f80bff058dccd82e89422f", "https://bcr.bazel.build/modules/swift_argument_parser/1.3.1.2/source.json": "5fba48bbe0ba48761f9e9f75f92876cafb5d07c0ce059cc7a8027416de94a05b", + "https://bcr.bazel.build/modules/toml.bzl/0.4.1/MODULE.bazel": "6bc0b938f03ade8d58c2fca0ad5c3fa12b4764e1e1927ad50b0c860286db2167", + "https://bcr.bazel.build/modules/toml.bzl/0.4.1/source.json": "86a90afd8b43c9b69ad31f5c03998c3adcf4b08175e621addb93e3a38eec538b", "https://bcr.bazel.build/modules/upb/0.0.0-20220923-a547704/MODULE.bazel": "7298990c00040a0e2f121f6c32544bab27d4452f80d9ce51349b1a28f3005c43", "https://bcr.bazel.build/modules/zlib/1.2.11/MODULE.bazel": "07b389abc85fdbca459b69e2ec656ae5622873af3f845e1c9d80fe179f3effa0", "https://bcr.bazel.build/modules/zlib/1.3.1.bcr.5/MODULE.bazel": "eec517b5bbe5492629466e11dae908d043364302283de25581e3eb944326c4ca", @@ -250,7 +252,7 @@ }, "@@rules_python+//python/uv:uv.bzl%uv": { "general": { - "bzlTransitiveDigest": "yrEbeCJlv5gbdoRjwwR/EDEkc3pfuQy/FTYKMNoK5Wc=", + "bzlTransitiveDigest": "DafUArm1CsjJZcemT0FvUq9rUch+pq38+JqfOBJCpm8=", "usagesDigest": "6yXGw7XDyXjOfqBL0SBu1YBEMMYPQzCE3jTzUCkxPgg=", "recordedInputs": [ "REPO_MAPPING:rules_python+,bazel_tools bazel_tools", @@ -284,364 +286,394 @@ "dist_hashes": { "https://pypi.org/simple": { "backports-tarfile": { - "https://files.pythonhosted.org/packages/86/72/cd9b395f25e290e633655a100af28cb253e4393396264a98bd5f5951d50f/backports_tarfile-1.2.0.tar.gz": "d75e02c268746e1b8144c278978b6e98e85de6ad16f8e4b0844a154557eca991", - "https://files.pythonhosted.org/packages/b9/fa/123043af240e49752f1c4bd24da5053b6bd00cad78c2be53c0d1e8b975bc/backports.tarfile-1.2.0-py3-none-any.whl": "77e284d754527b01fb1e6fa8a1afe577858ebe4e9dad8919e34c862cb399bc34" + "https://files.pythonhosted.org/packages/86/72/cd9b395f25e290e633655a100af28cb253e4393396264a98bd5f5951d50f/backports_tarfile-1.2.0.tar.gz": "sha256:d75e02c268746e1b8144c278978b6e98e85de6ad16f8e4b0844a154557eca991", + "https://files.pythonhosted.org/packages/b9/fa/123043af240e49752f1c4bd24da5053b6bd00cad78c2be53c0d1e8b975bc/backports.tarfile-1.2.0-py3-none-any.whl": "sha256:77e284d754527b01fb1e6fa8a1afe577858ebe4e9dad8919e34c862cb399bc34" }, "certifi": { - "https://files.pythonhosted.org/packages/4c/5b/b6ce21586237c77ce67d01dc5507039d444b630dd76611bbca2d8e5dcd91/certifi-2025.10.5.tar.gz": "47c09d31ccf2acf0be3f701ea53595ee7e0b8fa08801c6624be771df09ae7b43", - "https://files.pythonhosted.org/packages/e4/37/af0d2ef3967ac0d6113837b44a4f0bfe1328c2b9763bd5b1744520e5cfed/certifi-2025.10.5-py3-none-any.whl": "0f212c2744a9bb6de0c56639a6f68afe01ecd92d91f14ae897c4fe7bbeeef0de" + "https://files.pythonhosted.org/packages/0b/a7/71ac2cff56fec219ed242bb11b8efb69fcc4bec75db06fb7bfe35de520e6/certifi-2026.7.22-py3-none-any.whl": "sha256:62f22742b58a1a33014a2b6b706588a8d7e2a88ae7bd1a6ebe8c992928483775", + "https://files.pythonhosted.org/packages/a3/c2/24167ea9858356b47a87a50d39908bfdb72ceeefe0041586e704e5376b3a/certifi-2026.7.22.tar.gz": "sha256:741e2c3b351ddf169a738da9f2c048608ff7f2c5cc02f1ebc6b118bb090d5d55" }, "cffi": { - "https://files.pythonhosted.org/packages/05/eb/b86f2a2645b62adcfff53b0dd97e8dfafb5c8aa864bd0d9a2c2049a0d551/cffi-2.0.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl": "5eda85d6d1879e692d546a078b44251cdd08dd1cfb98dfb77b670c97cee49ea0", - "https://files.pythonhosted.org/packages/07/e0/267e57e387b4ca276b90f0434ff88b2c2241ad72b16d31836adddfd6031b/cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl": "3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94", - "https://files.pythonhosted.org/packages/0b/28/dd0967a76aab36731b6ebfe64dec4e981aff7e0608f60c2d46b46982607d/cffi-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl": "5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743", - "https://files.pythonhosted.org/packages/12/4a/3dfd5f7850cbf0d06dc84ba9aa00db766b52ca38d8b86e3a38314d52498c/cffi-2.0.0-cp311-cp311-macosx_10_13_x86_64.whl": "b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe", - "https://files.pythonhosted.org/packages/15/12/a7a79bd0df4c3bff744b2d7e52cc1b68d5e7e427b384252c42366dc1ecbc/cffi-2.0.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl": "3f4d46d8b35698056ec29bca21546e1551a205058ae1a181d871e278b0b28165", - "https://files.pythonhosted.org/packages/1f/74/cc4096ce66f5939042ae094e2e96f53426a979864aa1f96a621ad128be27/cffi-2.0.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl": "61d028e90346df14fedc3d1e5441df818d095f3b87d286825dfcbd6459b7ef63", - "https://files.pythonhosted.org/packages/21/7a/13b24e70d2f90a322f2900c5d8e1f14fa7e2a6b3332b7309ba7b2ba51a5a/cffi-2.0.0-cp310-cp310-musllinux_1_2_aarch64.whl": "cf364028c016c03078a23b503f02058f1814320a56ad535686f90565636a9495", - "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl": "7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", - "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl": "737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", - "https://files.pythonhosted.org/packages/2b/c0/015b25184413d7ab0a410775fdb4a50fca20f5589b5dab1dbbfa3baad8ce/cffi-2.0.0-cp311-cp311-win32.whl": "c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5", - "https://files.pythonhosted.org/packages/2b/e7/7c769804eb75e4c4b35e658dba01de1640a351a9653c3d49ca89d16ccc91/cffi-2.0.0-cp39-cp39-musllinux_1_2_x86_64.whl": "89472c9762729b5ae1ad974b777416bfda4ac5642423fa93bd57a09204712322", - "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl": "7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", - "https://files.pythonhosted.org/packages/32/f2/81b63e288295928739d715d00952c8c6034cb6c6a516b17d37e0c8be5600/cffi-2.0.0-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.whl": "cb527a79772e5ef98fb1d700678fe031e353e765d1ca2d409c92263c6d43e09f", - "https://files.pythonhosted.org/packages/33/fa/072dd15ae27fbb4e06b437eb6e944e75b068deb09e2a2826039e49ee2045/cffi-2.0.0-cp310-cp310-win_amd64.whl": "b18a3ed7d5b3bd8d9ef7a8cb226502c6bf8308df1525e1cc676c3680e7176739", - "https://files.pythonhosted.org/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl": "6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", - "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl": "19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", - "https://files.pythonhosted.org/packages/3a/c8/15cb9ada8895957ea171c62dc78ff3e99159ee7adb13c0123c001a2546c1/cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl": "81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037", - "https://files.pythonhosted.org/packages/3d/de/38d9726324e127f727b4ecc376bc85e505bfe61ef130eaf3f290c6847dd4/cffi-2.0.0-cp39-cp39-macosx_11_0_arm64.whl": "de8dad4425a6ca6e4e5e297b27b5c824ecc7581910bf9aee86cb6835e6812aa7", - "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl": "9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", - "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl": "087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", - "https://files.pythonhosted.org/packages/44/64/58f6255b62b101093d5df22dcb752596066c7e89dd725e0afaed242a61be/cffi-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl": "a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9", - "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl": "afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", - "https://files.pythonhosted.org/packages/49/72/ff2d12dbf21aca1b32a40ed792ee6b40f6dc3a9cf1644bd7ef6e95e0ac5e/cffi-2.0.0-cp310-cp310-musllinux_1_2_x86_64.whl": "8ea985900c5c95ce9db1745f7933eeef5d314f0565b27625d9a10ec9881e1bfb", - "https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl": "45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", - "https://files.pythonhosted.org/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl": "00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", - "https://files.pythonhosted.org/packages/4f/27/6933a8b2562d7bd1fb595074cf99cc81fc3789f6a6c05cdabb46284a3188/cffi-2.0.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl": "3e837e369566884707ddaf85fc1744b47575005c0a229de3327f8f9a20f4efeb", - "https://files.pythonhosted.org/packages/4f/8b/f0e4c441227ba756aafbe78f117485b25bb26b1c059d01f137fa6d14896b/cffi-2.0.0-cp311-cp311-macosx_11_0_arm64.whl": "2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c", - "https://files.pythonhosted.org/packages/50/bd/b1a6362b80628111e6653c961f987faa55262b4002fcec42308cad1db680/cffi-2.0.0-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl": "53f77cbe57044e88bbd5ed26ac1d0514d2acf0591dd6bb02a3ae37f76811b80c", - "https://files.pythonhosted.org/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl": "d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", - "https://files.pythonhosted.org/packages/54/8f/a1e836f82d8e32a97e6b29cc8f641779181ac7363734f12df27db803ebda/cffi-2.0.0-cp39-cp39-win_amd64.whl": "b882b3df248017dba09d6b16defe9b5c407fe32fc7c65a9c69798e6175601be9", - "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl": "c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", - "https://files.pythonhosted.org/packages/60/99/c9dc110974c59cc981b1f5b66e1d8af8af764e00f0293266824d9c4254bc/cffi-2.0.0-cp310-cp310-musllinux_1_2_i686.whl": "e11e82b744887154b182fd3e7e8512418446501191994dbf9c9fc1f32cc8efd5", - "https://files.pythonhosted.org/packages/78/2d/7fa73dfa841b5ac06c7b8855cfc18622132e365f5b81d02230333ff26e9e/cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl": "3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba", - "https://files.pythonhosted.org/packages/7b/2b/2b6435f76bfeb6bbf055596976da087377ede68df465419d192acf00c437/cffi-2.0.0-cp312-cp312-win32.whl": "da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18", - "https://files.pythonhosted.org/packages/84/ef/a7b77c8bdc0f77adc3b46888f1ad54be8f3b7821697a7b89126e829e676a/cffi-2.0.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl": "9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664", - "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl": "fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", - "https://files.pythonhosted.org/packages/93/d7/516d984057745a6cd96575eea814fe1edd6646ee6efd552fb7b0921dec83/cffi-2.0.0-cp310-cp310-macosx_10_13_x86_64.whl": "0cf2d91ecc3fcc0625c2c530fe004f82c110405f101548512cce44322fa8ac44", - "https://files.pythonhosted.org/packages/95/31/9f7f93ad2f8eff1dbc1c3656d7ca5bfd8fb52c9d786b4dcf19b2d02217fa/cffi-2.0.0-cp312-cp312-win_arm64.whl": "4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6", - "https://files.pythonhosted.org/packages/95/5c/1b493356429f9aecfd56bc171285a4c4ac8697f76e9bbbbb105e537853a1/cffi-2.0.0-cp311-cp311-win_arm64.whl": "c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d", - "https://files.pythonhosted.org/packages/98/29/9b366e70e243eb3d14a5cb488dfd3a0b6b2f1fb001a203f653b93ccfac88/cffi-2.0.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl": "fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453", - "https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl": "c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", - "https://files.pythonhosted.org/packages/9b/13/c92e36358fbcc39cf0962e83223c9522154ee8630e1df7c0b3a39a8124e2/cffi-2.0.0-cp39-cp39-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl": "4647afc2f90d1ddd33441e5b0e85b16b12ddec4fca55f0d9671fef036ecca27c", - "https://files.pythonhosted.org/packages/9e/84/ad6a0b408daa859246f57c03efd28e5dd1b33c21737c2db84cae8c237aa5/cffi-2.0.0-cp310-cp310-macosx_11_0_arm64.whl": "f73b96c41e3b2adedc34a7356e64c8eb96e03a3782b535e043a986276ce12a49", - "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl": "dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", - "https://files.pythonhosted.org/packages/9f/e0/6cbe77a53acf5acc7c08cc186c9928864bd7c005f9efd0d126884858a5fe/cffi-2.0.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl": "9332088d75dc3241c702d852d4671613136d90fa6881da7d770a483fd05248b4", - "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl": "1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", - "https://files.pythonhosted.org/packages/a3/ad/5c51c1c7600bdd7ed9a24a203ec255dccdd0ebf4527f7b922a0bde2fb6ed/cffi-2.0.0-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl": "e6e73b9e02893c764e7e8d5bb5ce277f1a009cd5243f8228f75f842bf937c534", - "https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl": "d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", - "https://files.pythonhosted.org/packages/aa/d9/6218d78f920dcd7507fc16a766b5ef8f3b913cc7aa938e7fc80b9978d089/cffi-2.0.0-cp39-cp39-win32.whl": "2081580ebb843f759b9f617314a24ed5738c51d2aee65d31e02f6f7a2b97707a", - "https://files.pythonhosted.org/packages/ab/49/fa72cebe2fd8a55fbe14956f9970fe8eb1ac59e5df042f603ef7c8ba0adc/cffi-2.0.0-cp311-cp311-musllinux_1_2_i686.whl": "94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414", - "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl": "0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", - "https://files.pythonhosted.org/packages/ae/8f/dc5531155e7070361eb1b7e4c1a9d896d0cb21c49f807a6c03fd63fc877e/cffi-2.0.0-cp311-cp311-win_amd64.whl": "66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5", - "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl": "07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", - "https://files.pythonhosted.org/packages/b1/b7/1200d354378ef52ec227395d95c2576330fd22a869f7a70e88e1447eb234/cffi-2.0.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl": "baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92", - "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl": "12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", - "https://files.pythonhosted.org/packages/b6/75/1f2747525e06f53efbd878f4d03bac5b859cbc11c633d0fb81432d98a795/cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl": "2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187", - "https://files.pythonhosted.org/packages/b8/56/6033f5e86e8cc9bb629f0077ba71679508bdf54a9a5e112a3c0b91870332/cffi-2.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl": "730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93", - "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl": "203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", - "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl": "d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", - "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl": "7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", - "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl": "d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", - "https://files.pythonhosted.org/packages/c0/cc/08ed5a43f2996a16b462f64a7055c6e962803534924b9b2f1371d8c00b7b/cffi-2.0.0-cp39-cp39-macosx_10_13_x86_64.whl": "fe562eb1a64e67dd297ccc4f5addea2501664954f2692b69a76449ec7913ecbf", - "https://files.pythonhosted.org/packages/c2/95/7a135d52a50dfa7c882ab0ac17e8dc11cec9d55d2c18dda414c051c5e69e/cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl": "1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e", - "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl": "38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", - "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl": "256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", - "https://files.pythonhosted.org/packages/cb/1e/a5a1bd6f1fb30f22573f76533de12a00bf274abcdc55c8edab639078abb6/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl": "dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", - "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl": "28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", - "https://files.pythonhosted.org/packages/d5/72/12b5f8d3865bf0f87cf1404d8c374e7487dcf097a1c91c436e72e6badd83/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl": "b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062", - "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl": "24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", - "https://files.pythonhosted.org/packages/d7/91/500d892b2bf36529a75b77958edfcd5ad8e2ce4064ce2ecfeab2125d72d1/cffi-2.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl": "8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26", - "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl": "b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", - "https://files.pythonhosted.org/packages/dc/7f/55fecd70f7ece178db2f26128ec41430d8720f2d12ca97bf8f0a628207d5/cffi-2.0.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl": "6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5", - "https://files.pythonhosted.org/packages/df/a2/781b623f57358e360d62cdd7a8c681f074a71d445418a776eef0aadb4ab4/cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl": "8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c", - "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl": "92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", - "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl": "6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", - "https://files.pythonhosted.org/packages/e2/cc/027d7fb82e58c48ea717149b03bcadcbdc293553edb283af792bd4bcbb3f/cffi-2.0.0-cp310-cp310-win32.whl": "1f72fb8906754ac8a2cc3f9f5aaa298070652a0ffae577e0ea9bd480dc3c931a", - "https://files.pythonhosted.org/packages/e8/be/f6424d1dc46b1091ffcc8964fa7c0ab0cd36839dd2761b49c90481a6ba1b/cffi-2.0.0-cp39-cp39-musllinux_1_2_aarch64.whl": "0f6084a0ea23d05d20c3edcda20c3d006f9b6f3fefeac38f59262e10cef47ee2", - "https://files.pythonhosted.org/packages/ea/47/4f61023ea636104d4f16ab488e268b93008c3d0bb76893b1b31db1f96802/cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl": "6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d", - "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz": "44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", - "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl": "74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", - "https://files.pythonhosted.org/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl": "f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", - "https://files.pythonhosted.org/packages/f7/e0/dda537c2309817edf60109e39265f24f24aa7f050767e22c98c53fe7f48b/cffi-2.0.0-cp39-cp39-musllinux_1_2_i686.whl": "1cd13c99ce269b3ed80b417dcd591415d3372bcac067009b6e0f59c7d4015e65", - "https://files.pythonhosted.org/packages/f8/ed/13bd4418627013bec4ed6e54283b1959cf6db888048c7cf4b4c3b5b36002/cffi-2.0.0-cp312-cp312-win_amd64.whl": "da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5", - "https://files.pythonhosted.org/packages/ff/df/a4f0fbd47331ceeba3d37c2e51e9dfc9722498becbeec2bd8bc856c9538a/cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl": "21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe" + "https://files.pythonhosted.org/packages/04/8c/b925975448cf20634a9fbd5efceb807219db452653648d2897c0989cab2d/cffi-2.1.0-cp311-cp311-musllinux_1_2_x86_64.whl": "sha256:89095c1968b4ba8285840e131bf2891b09ae137fe2146905acae0354fbce1b5e", + "https://files.pythonhosted.org/packages/05/ef/6cd4f8c671517162379dc79cfae5aea9106bc38abb89628d5c16adf6a838/cffi-2.1.0-cp315-cp315-win_arm64.whl": "sha256:8d35c139744adb3e727cd51b1a18324bbe44b8bd41bf8322bca4d41289f48eda", + "https://files.pythonhosted.org/packages/0f/6f/ade5ce9863a57992a6ea3d0d10d7e29b8749fc127204b3d493d667b2815f/cffi-2.1.0-cp314-cp314-win32.whl": "sha256:1854b724d00f6654c742097d5387569021be12d3a0f770eae1df8f8acfcc6acd", + "https://files.pythonhosted.org/packages/11/b6/12fc55092817a5faa26fb8c40c7f9d662e11a46ee248c137aafc42517d92/cffi-2.1.0-cp315-cp315t-macosx_10_15_x86_64.whl": "sha256:f9912624a0c0b834b7520d7769b3644453aabc0a7e1c839da7359f050750e9bc", + "https://files.pythonhosted.org/packages/14/d0/117dcd9209255ad8571fbc8c92ef32593a1d294dcec91ddc4e4db50606f2/cffi-2.1.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl": "sha256:eb4e8997a49aa2c08a3e43c9045d224448b8941d88e7ac163c7d383e560cbf98", + "https://files.pythonhosted.org/packages/14/f0/134c00ce0779ec86dea2aa1aac69339c2741a8045072676763512363a2ea/cffi-2.1.0-cp314-cp314t-macosx_10_15_x86_64.whl": "sha256:7ea6b3e2c4250ff1de21c630fe72d0f63eb95c2c32ffbf64a358cf4a8836d714", + "https://files.pythonhosted.org/packages/19/e5/d3cc82a4a0be7902af279c04181ad038449c096734464a5ae1de3e1401bd/cffi-2.1.0-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl": "sha256:0611e7ebf90573a535ebdc33ae9da222d037853983e13359f580fab781ca017f", + "https://files.pythonhosted.org/packages/1b/dc/5620cf930688be01f2d673804291de757a934c90b946dbdc3d84130c2ea4/cffi-2.1.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl": "sha256:b6422532152adf4e59b110cb2808cee7a033800952f5c036b4af047ee43199e7", + "https://files.pythonhosted.org/packages/1e/85/990925db5df586ec90beb97529c853497e7f85ba0234830447faf41c3057/cffi-2.1.0-cp312-cp312-macosx_10_15_x86_64.whl": "sha256:df2b82571a1b30f58a87bf4e5a9e78d2b1eff6c6ce8fd3aa3757221f93f0863f", + "https://files.pythonhosted.org/packages/20/71/7c8372d30e42415602ed9f268f7cfd66f1b855fed881ecd168bcb45dbc0b/cffi-2.1.0-cp314-cp314-macosx_10_15_x86_64.whl": "sha256:1ff3456eab0d889592d1936d6125bbfbc7ae4d3354a700f8bd80450a66445d4d", + "https://files.pythonhosted.org/packages/22/d7/1a74539db16d8bfd839ff1515948948efbb162e574650fd3d846896eea95/cffi-2.1.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl": "sha256:88023dfe18799507b73f1dbb0d14326a17465de1bc9c9c7655c22845e9ddc3a2", + "https://files.pythonhosted.org/packages/22/f0/a2fc43084c0433caf7f461bccc013e28f848d04ee1c5ed7fce71423cf4d9/cffi-2.1.0-cp311-cp311-musllinux_1_2_i686.whl": "sha256:7762faa47e8ff7eb80bd261d9a7d8eea2d8baa69de5e95b70c1f338bbe712f02", + "https://files.pythonhosted.org/packages/28/3b/fad54de07260b93ddeef4b96d0131d57ea900675df1d410ae1deee52d7a6/cffi-2.1.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl": "sha256:33eb1ad83ebe8f313e0df035c406227d55a79456704a863fad9842136af5ad7d", + "https://files.pythonhosted.org/packages/28/ed/c127d3ac36e899c965e3361357c3befacd6578c03f40125183e41c3b219e/cffi-2.1.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl": "sha256:6ca4919c6e4f89aa99c42510b42cf54596892c00b3f9077f6bdd1505e24b9c8d", + "https://files.pythonhosted.org/packages/2c/0e/fac738d73728c6cea2a88a2883dca54892496cbba88a1dc1f2909cb8a6f5/cffi-2.1.0-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.whl": "sha256:2b71d409cccee78310ab5dec549aed052aaea483346e282c7b02362596e01bb0", + "https://files.pythonhosted.org/packages/2c/d8/772b8259bf75749adffb1c546828978381fb516f60cf701f6c83daf60c85/cffi-2.1.0-cp315-cp315-win32.whl": "sha256:0a42c688d19fca6e095a53c6a6e2295a5b050a8b289f109adab02a9e61a25de6", + "https://files.pythonhosted.org/packages/2e/1a/cc6ae6c2913a03aab8898eee57963cf1035b8df5872ed8b9115fcc7e2be8/cffi-2.1.0-cp314-cp314-win_arm64.whl": "sha256:7d28dff1db6764108bc30788d85d61c876beff416d9a49cb9dd7c5a9f34f5804", + "https://files.pythonhosted.org/packages/2e/d2/065fcae1c73979fac8e054462478d0ff8a29c40cdc2ed7ea5676a061df53/cffi-2.1.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl": "sha256:276f20fffd7b396e12516ba8edf9509210ac248cbbc5acbc39cd512f9f59ebe6", + "https://files.pythonhosted.org/packages/2f/dd/afa2191fc6d57fedd26e5844a2fe2fcc0bbfa00961bbaa5a41e4921e7cca/cffi-2.1.0-cp315-cp315-win_amd64.whl": "sha256:bccbbb5ee76a61f9d99b5bf3846a51d7fca4b6a732fe46f89295610edaf41853", + "https://files.pythonhosted.org/packages/38/37/04f54b8e63a02f3d908332c9effbf8c366167c6f733ed8a3d4f79b7e2a1e/cffi-2.1.0-cp313-cp313-musllinux_1_2_aarch64.whl": "sha256:961be50688f7fba2fa65f63712d3b9b341a22311f5253460ce933f52f0de1c8c", + "https://files.pythonhosted.org/packages/38/66/04781a77b411f0bb5b234d62c1814754ab75ebe455ccff1b08e8d7aae98f/cffi-2.1.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl": "sha256:4d433a51f1870e43a13b6732f92aaf540ff77c2015097c78556f75a2d6c030e0", + "https://files.pythonhosted.org/packages/3a/a6/e879bb68cc23a2bc9ba8f4b7d8019f0c2694bad2ab6c4a3701d429439f58/cffi-2.1.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl": "sha256:ff067a8d8d880e7809e4ac88eb009bb848870115317b306666502ccad30b147f", + "https://files.pythonhosted.org/packages/3b/30/c806937ed5e4c2c7ac30d9d6b76b5dc57ff8b75d83800d9bb11a8253cf2a/cffi-2.1.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl": "sha256:a016194dbe13d14ee9556e734b772d8d67b947092b268d757fd4290e3ba2dfc2", + "https://files.pythonhosted.org/packages/41/aa/3c1409cdd26094efacd1c36c66e0a6eb9d4296e4fd4f9901b8b2042f4323/cffi-2.1.0-cp310-cp310-musllinux_1_2_i686.whl": "sha256:c5f5df567f6eb216de69be06ce55c8b714090fae02b18a3b40da8163b8c5fa9c", + "https://files.pythonhosted.org/packages/41/de/92b9eeed4ae4a21d6fd9b2a2c8505cbed573299902ea73981cc13f7ff62c/cffi-2.1.0-cp314-cp314-win_amd64.whl": "sha256:1b96bfe2c4bd825681b7d311ad6d9b7280a091f43e8f63da5729638083cd3bfb", + "https://files.pythonhosted.org/packages/45/ca/f91641185cdd90c36d317a9dc7f85e88ef8682d8b300977baff5e23c35d8/cffi-2.1.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl": "sha256:19c54ac121cad98450b4896fa9a43ee0180d57bc4bc911a33db6cab1efab6cd3", + "https://files.pythonhosted.org/packages/4b/92/e7bb136ad6b5352603732cf907ef862ca103f20f2031c1735a46300c20c9/cffi-2.1.0-cp312-cp312-macosx_11_0_arm64.whl": "sha256:78474632761faa0fb96f30b1c928c84ebcf68713cbb80d15bab09dfe61640fde", + "https://files.pythonhosted.org/packages/4b/a4/77b53abbf7a1e0beb9637edbef2a94d15f9c822f591e85d439ffd91519a6/cffi-2.1.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl": "sha256:46b1c8db8f6122420f32d02fffb924c2fe9bc772d228c7c711748fff56aabb2b", + "https://files.pythonhosted.org/packages/50/d8/3b86aba791cb610d24e8a3e1b2cd529e71fa15096b04e4d4e360049d4a4c/cffi-2.1.0-cp314-cp314t-macosx_11_0_arm64.whl": "sha256:6af371f3767faeffc6ac1ef57cdfd25844403e9d3f476c5537caee499de96376", + "https://files.pythonhosted.org/packages/55/c7/8c8c50cb11c6750051daf12164098a9a6f027ac4356967fd4d800a07f242/cffi-2.1.0-cp315-cp315-ios_13_0_arm64_iphoneos.whl": "sha256:2e9dabb9abcb7ad15938c7196ad5c1718a4e6d33cc79b4c0209bdb64c4a54a5c", + "https://files.pythonhosted.org/packages/57/5f/ff100cae70ebe9d8df1c01a00e510e45d9adb5c1fdda84791b199141de97/cffi-2.1.0.tar.gz": "sha256:efc1cdd798b1aaf39b4610bba7aad28c9bea9b910f25c784ccf9ec1fa719d1f9", + "https://files.pythonhosted.org/packages/58/0c/f528df19cc94b675087324d4760d9e6d5bfae97d6217aa4fac43de4f5fcc/cffi-2.1.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl": "sha256:d9fafc5aa2e2a39aaf7f8cc0c1f044a9b07fca12e558dca53a3cc5c654ad67a7", + "https://files.pythonhosted.org/packages/58/85/7ae00d5c8dd6266f4e944c3db630f3c5c9a98b61d469c714d848b1d8138a/cffi-2.1.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl": "sha256:a95b05f9baf29b91171b3a8bd2020b028835243e7b0ff6bb23e2a3c228518b1b", + "https://files.pythonhosted.org/packages/5a/02/d5e6c43ea85c41bda2a184a3418f195fe7cf602967a8d2b94e085b83deef/cffi-2.1.0-cp315-cp315-musllinux_1_2_x86_64.whl": "sha256:af5e2915d41fe6c961694d7bfdc8562942638200f3ce2765dfb8b745cf997629", + "https://files.pythonhosted.org/packages/5a/47/59eb7975cb0e4ef0afa764ea945b29a5bb4537a9f771cb7d6c8a5dd74c95/cffi-2.1.0-cp313-cp313-win_amd64.whl": "sha256:8e74a6135550c4748af665b1b1118b6aab33b1fc6a16f9aff630af107c3b4512", + "https://files.pythonhosted.org/packages/5a/67/9e6e09409336d9e515c58367e7cfcf4f89df06ad25252675595a58eb59d5/cffi-2.1.0-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.whl": "sha256:762f99479dcb369f60ab9017ad4ab97a36a1dd7c1ee5a3b15db0f4b8659120cd", + "https://files.pythonhosted.org/packages/5a/af/34fee85c48f8d94efc8597bc09470c9dd274c145f1c12e0fbc6ab6d38d74/cffi-2.1.0-cp313-cp313-win_arm64.whl": "sha256:2282cd5e38aa8accd03e99d1256af8411c84cdbee6a89d841b563fdbd1f3e50f", + "https://files.pythonhosted.org/packages/5d/7c/b7379a5704c79eda57ce075869ba70a0368d1c850f803b3c0d078d39dcaf/cffi-2.1.0-cp315-cp315-musllinux_1_2_aarch64.whl": "sha256:8f9ec95b8a043d3dfbc74d9abc6f7baf524dd27a8dc160b0a32ff9cdab650c28", + "https://files.pythonhosted.org/packages/62/f2/c9522a81c32132799a1972c39f5c5f8b4c8b9f00488a23feaa6c06f07741/cffi-2.1.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl": "sha256:1e9f50d192a3e525b15a75ab5114e442d83d657b7ec29182a991bc9a88fd3a66", + "https://files.pythonhosted.org/packages/65/68/9f3ef890cf3c6ab97bd531c5677f67613d302165d16f8142b2811782a614/cffi-2.1.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl": "sha256:30b65779d598c370374fefabf138d456fd6f3216bfa7bedfab1ba82025b0cd93", + "https://files.pythonhosted.org/packages/68/5a/e536c528bc8057496c360c0978559a2dc45653f89dd6151078aa7d8fca1a/cffi-2.1.0-cp315-cp315t-win32.whl": "sha256:cb96698e3c7413d906ce83f8ffd245ec1bd94707541f299d0ce4d6b0193e982b", + "https://files.pythonhosted.org/packages/69/aa/24580a278de21fd7322635556334d9b535f1cbc00b0a3919447cdf464c65/cffi-2.1.0-cp310-cp310-macosx_11_0_arm64.whl": "sha256:164bff1657b2a74f0b6d54e11c9b375bc97b931f2ca9c43fcf875838da1570dd", + "https://files.pythonhosted.org/packages/6c/d0/47e338384ab6b1004241002fa616301020cea4fc95f283506565d252f276/cffi-2.1.0-cp314-cp314t-musllinux_1_2_aarch64.whl": "sha256:c16914df9fb7f500e440e6875fa23ff5e0b31db01fa9c06af98d59a91f0dc2e4", + "https://files.pythonhosted.org/packages/6e/28/bd53988b9833e8f8ad539d26f4c07a6b3f6bcb1e9e02e7ca038250b3428d/cffi-2.1.0-cp312-cp312-musllinux_1_2_aarch64.whl": "sha256:98fff996e983a36d3aa2eca83af40c5821202e7e6f32d13ae94e3d2286f10cfe", + "https://files.pythonhosted.org/packages/6f/08/f2e7d62c460faae0926f2d6e423694aa409ced3bc1fe2927a0a6e5f05416/cffi-2.1.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl": "sha256:799416bae98336e400981ff6e532d67d5c709cfb30afb79865a1315f94b0e224", + "https://files.pythonhosted.org/packages/70/25/65bd5b58ea4bfdfc15cde02cb5365f89ef8ab8b2adfb8fe5c4bd4233382f/cffi-2.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl": "sha256:5ecbd0499275d57506d397eebe1981cee87b47fcd9ef5c22cab7ed7644a39a94", + "https://files.pythonhosted.org/packages/70/b6/9003c33a3e7d2c1306f5962e646457dcfe5a8cd8fce6bbe02d7af25db783/cffi-2.1.0-cp310-cp310-win32.whl": "sha256:9d72af0cf10a76a600a9690078fe31c63b9588c8e86bf9fd353f713c84b5db0f", + "https://files.pythonhosted.org/packages/79/99/0d0fd37f055224085f42bbb2c022d002e17dde4a97972822327b07d84101/cffi-2.1.0-cp312-cp312-musllinux_1_2_x86_64.whl": "sha256:379de10ce1ba048b1448599d1b37b24caee16309d1ac98d3982fc997f768700b", + "https://files.pythonhosted.org/packages/7d/95/8de304305cd9204974b0ca051b86d307cafca13aa575a0ef1b44d92c0d8c/cffi-2.1.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl": "sha256:702c436735fbe99d59ada02a1f65cfc0d31c0ee8b7290912f8fbc5cd1e4b16c3", + "https://files.pythonhosted.org/packages/84/4c/82f132cb4418ee6d953d982b19191e87e2a6372c8a4ce36e50b69d6ade4a/cffi-2.1.0-cp313-cp313-macosx_11_0_arm64.whl": "sha256:716ff8ec22f20b4d988b12884086bcef0fc99737043e503f7a3935a6be99b1ea", + "https://files.pythonhosted.org/packages/88/a9/02cae418ec4beb282ace11958d9d4737793439d561fadc7e6d56f2e2b354/cffi-2.1.0-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl": "sha256:c941bb58d5a6e1c3892d86e42927ed6c180302f07e6d395d08c416e594b98b46", + "https://files.pythonhosted.org/packages/88/f6/01890cfd63c08f8eb96a8319b0443690197d240a8bd6346048cf7bde9190/cffi-2.1.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl": "sha256:3b926723c13eba9f81d2ef3820d63aeceec3b2d4639906047bf675cb8a7a500d", + "https://files.pythonhosted.org/packages/8a/26/710688310447531c7a22f857c7f79d9855ec18b03e04494ced723fb37e2f/cffi-2.1.0-cp310-cp310-win_amd64.whl": "sha256:fb62edb5bb52cca65fab91a63afa7561607120d26090a7e8fda6fb9f064726da", + "https://files.pythonhosted.org/packages/8b/31/e115c985105dd7ffb32444505f18ceb874bb42d992af05d5dced7ecf1980/cffi-2.1.0-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl": "sha256:3681e031db29958a7502f5c0c9d6bbc4c36cb20f7b104086fa642d1799631ff8", + "https://files.pythonhosted.org/packages/8c/e9/45c3a76ad8d43ad9261f4c95436da61128d3ca545d72b9612c0ab5be0b1c/cffi-2.1.0-cp313-cp313-macosx_10_15_x86_64.whl": "sha256:15faec4adfff450819f3aee0e2e02c812de6edb88203aa58807955db2003472a", + "https://files.pythonhosted.org/packages/8d/2e/cdac88979f295fde5daa69622c7d2111e56e7ceb94f211357fbe452339e4/cffi-2.1.0-cp315-cp315t-macosx_11_0_arm64.whl": "sha256:df92f2aba50eb4d96718b68ef76f2e57a57b54f2fa62333496d16c6d585a85ca", + "https://files.pythonhosted.org/packages/96/88/a996879e2eeccb815f6e3a5967b12a308257412acec882039d386bd2aa7b/cffi-2.1.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl": "sha256:10537b1df4967ca26d21e5072d7d54188354483b91dc75058968d3f0cf13fbda", + "https://files.pythonhosted.org/packages/99/e2/67680bf19a6b60d2bb7ff83baefa2a4c3d2d7dc0f3277034b802e1fc504c/cffi-2.1.0-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl": "sha256:37f525a7e7e50c017fdebe58b787be310ad59357ae43a053943a6e1a6c526001", + "https://files.pythonhosted.org/packages/9e/4e/e8d7cb5783f1841a3c8fb3a7735838d7484d08ec08c9f984b14cac1ac0e9/cffi-2.1.0-cp311-cp311-win_arm64.whl": "sha256:35aaea0c7ee0e58a5cd8c2fd1a48fdf7ece0d2699b7ecdda08194e9ce5dd9b3d", + "https://files.pythonhosted.org/packages/a0/17/1073b53b68c9b5ca6914adf5f8bf55aacc2d3be102418c90700160ea8605/cffi-2.1.0-cp315-cp315t-win_arm64.whl": "sha256:cbb7640ce37159548d2147b5b8c241f962143d4c71231431820783f4dc78f210", + "https://files.pythonhosted.org/packages/a0/1c/4ed5a0e5bdca6cbc275556de3328dd1b76fd0c11cc13c88fe66d1d8715f2/cffi-2.1.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl": "sha256:63960549e4f8dc41e31accb97b975abaecfc44c03e396c093a6436763c2ea7db", + "https://files.pythonhosted.org/packages/a2/a5/d4fe77b589e5e82d43ebc809bf2e6474afe8e48e32ea050b9357645b6471/cffi-2.1.0-cp311-cp311-musllinux_1_2_aarch64.whl": "sha256:9d8272c0e483b024e1b9ad029821470ed8ec65631dbd90217469da0e7cd89f1c", + "https://files.pythonhosted.org/packages/a6/cf/2b684132056f438567b61e19d690dd31cd0921ace051e0a458be6074369e/cffi-2.1.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl": "sha256:47ff3a8bfd8cb9da1af7524b965127095055654c177fcfc7578debcb015eecd0", + "https://files.pythonhosted.org/packages/a8/eb/f636456ff21a83fc13c032b58cc5dde061691546ac79efa284b2989b7982/cffi-2.1.0-cp312-cp312-win_amd64.whl": "sha256:c97f080ea627e2863524c5af3836e2270b5f5dfff1f104392b959f8df0c5d384", + "https://files.pythonhosted.org/packages/a9/d6/c72eecca433cd3e681c65ed313ab4835d9d4a379704d0f628a6a05f51c2e/cffi-2.1.0-cp313-cp313-musllinux_1_2_x86_64.whl": "sha256:bf5c6cf48238b0eb4c086978c492ad1cbc22373fc5b2d7353b3a598ce6db887a", + "https://files.pythonhosted.org/packages/b0/80/c138990aa2a70b1a269f6e06348729836d733d6f970867943f61d367f8cc/cffi-2.1.0-cp312-cp312-win32.whl": "sha256:9b8f0f26ca4e7513c534d351eca551947d053fac438f2a04ac96d882909b0d3a", + "https://files.pythonhosted.org/packages/b3/c1/6dbd291ee2ae5a50a034aa057207081f545923bbf15dad4511e985aafff5/cffi-2.1.0-cp314-cp314-musllinux_1_2_x86_64.whl": "sha256:dbf7c7a88e2bac086f06d14577332760bdeecc42bdec8ac4077f6260557d9326", + "https://files.pythonhosted.org/packages/b5/9f/d4dc66ca651eb1145a133314cda721abf13cfac3d28c4a0402263ae6ad75/cffi-2.1.0-cp315-cp315t-musllinux_1_2_x86_64.whl": "sha256:ba00f661f8ba35d075c937174e27c2c421cec3942fd2e0ea3e66996757c0fdd9", + "https://files.pythonhosted.org/packages/b6/3d/f20f8b886b254e3ad10e15cd4186d3aed49f3e6a35ab37aab9f8f25f7c03/cffi-2.1.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl": "sha256:bf01d8c84cbea96b944c73b22182e6c7c432b3475632b8111dbfdc95ddad6e13", + "https://files.pythonhosted.org/packages/b9/26/d00496b22de4d4228f32dde94ad996f350c8aad676d63bcca0743c8dea4d/cffi-2.1.0-cp314-cp314t-win_amd64.whl": "sha256:0582a58f3051372229ca8e7f5f589f9e5632678208d8636fea3676711fdf7fe5", + "https://files.pythonhosted.org/packages/b9/65/b434abc97ce7cecc2c640fde160507c0ecc7e21544b483ba3325d2e2ea17/cffi-2.1.0-cp315-cp315t-musllinux_1_2_aarch64.whl": "sha256:86cf8755a791f72c85dc287128cc62d4f24d392e3f1e15837245623f4a33cccc", + "https://files.pythonhosted.org/packages/ba/b0/e131a9c41f10607926278453d9596163594fe1c4ebc46efe3b5e5b34eb84/cffi-2.1.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl": "sha256:a5781494d4d400a3f47f8f1da94b324f6e6b440a53387774002890a2a2f4b50f", + "https://files.pythonhosted.org/packages/c0/e9/6d7724983b3d5a0908dbf74f64038ade77c18646ff6636ec7894fd392ce1/cffi-2.1.0-cp310-cp310-macosx_10_15_x86_64.whl": "sha256:b65f590ef2a44640f9a05dbb548a429b4ade77913ce683ac8b1480777658a6c0", + "https://files.pythonhosted.org/packages/c3/c0/d1ec30ffb370f748f2fb54425972bfef9871e0132e82fb589c46b6676049/cffi-2.1.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl": "sha256:5972433ad71a9e46516584ef60a0fda12d9dc459938d1539c3ddecf9bdc1368d", + "https://files.pythonhosted.org/packages/c6/4b/e706f67279140f92939da3475ad610df18bfd52d50f14953a8e5fede71d5/cffi-2.1.0-cp313-cp313-win32.whl": "sha256:db3eb7d46527159a878ec3460e9d40615bc25ba337d477db681aea6e4f05c5d2", + "https://files.pythonhosted.org/packages/cc/82/3d5c705acb7abbba9bbd7d79b8e62e0f25b6120eb7ae6ac49f1b721722fe/cffi-2.1.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl": "sha256:ac0f1a2d0cfa7eea3f2aaf006ab6e70e8feeb16b75d65b7e5939982ca2f11056", + "https://files.pythonhosted.org/packages/cc/d7/97d3136f81db489ec8d1d67748c110d6c994268fd7528014aa9f2b085e4e/cffi-2.1.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl": "sha256:d53d10f7da99ae46f7373b9150393e9c5eab9b224909982b43832668de4779f5", + "https://files.pythonhosted.org/packages/d0/9a/bb1d5ed9c3fcae158e9f6391bf309c95d98c2ac37ed56573228471d0af5e/cffi-2.1.0-cp310-cp310-musllinux_1_2_aarch64.whl": "sha256:3d7f118b5adbfdfead90c25822690b02bc8074fba949bb7858bec4ebd55adb43", + "https://files.pythonhosted.org/packages/d3/0b/0ffe8b82d3875bced5fa1e7986a7a46b748262a40ab7f60b475eb9fb1bb3/cffi-2.1.0-cp315-cp315t-win_amd64.whl": "sha256:f146d154428a2523f9cc7936c02353c2459b8f6cf07d3cd1ee1c0a611109c5d5", + "https://files.pythonhosted.org/packages/d3/27/93195977168ee63aed233a1a0993a2178798654d1f4bddcdd321d6fd3b21/cffi-2.1.0-cp314-cp314-musllinux_1_2_aarch64.whl": "sha256:c351efb95e832a853a29361675f33a7ce53de1a109cd73fd47af0712213aa4ce", + "https://files.pythonhosted.org/packages/d3/67/85c89a59ba36a671e79638f44d466749f08179266a57e4f2ffdf92174072/cffi-2.1.0-cp311-cp311-macosx_10_15_x86_64.whl": "sha256:02cb7ff33ded4f1532476731f89ede53e2e488a8e6205515a82144246ffa7dcc", + "https://files.pythonhosted.org/packages/d5/dd/0c7dbf815a579ff005008a2d815a55d6bb047c349eef536d9dc53d3f0a8d/cffi-2.1.0-cp314-cp314t-win_arm64.whl": "sha256:510aeeeac94811b138077451da1fb18b308a5feab47dd2b603af55804155e1c8", + "https://files.pythonhosted.org/packages/d6/5c/584e626835f0375c928176c04137c96927165cb8733cdb3150ec04e5ee5e/cffi-2.1.0-cp314-cp314-macosx_11_0_arm64.whl": "sha256:c4165821e131d6d4ca444347c2b694e2311bcfa3fe5a861cc72968f28867beac", + "https://files.pythonhosted.org/packages/d8/f0/81478e482afa03f6d18dc8f2afb5edc45b3080853b634b5ed91961be0998/cffi-2.1.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl": "sha256:d2117334c3af3bdcb9a88522b844a2bdb5efdc4f71c6c822df55486ae1c3347a", + "https://files.pythonhosted.org/packages/dc/78/aa01ac599a8a4322533d45a1f9bc93b338276d2d59dabbe7c6d92a775c81/cffi-2.1.0-cp314-cp314t-win32.whl": "sha256:7d034dcffa09e9a46c93fa3a3be402096cb5354ac6e41ab8e5cc9cd8b642ad76", + "https://files.pythonhosted.org/packages/dd/2c/400ea43e721727dca8a65c4521390e9196757caba4a45643acb2b63271b8/cffi-2.1.0-cp312-cp312-win_arm64.whl": "sha256:6d194185eabd279f1c05ebe3504265ddfc5ad2b58d0714f7db9f01da592e9eb6", + "https://files.pythonhosted.org/packages/e0/27/1d0b408497e41a74795af122d7b603c418c5fed0171450f899afd04e594f/cffi-2.1.0-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl": "sha256:0520e1f4c35f44e209cbbb421b67eec42e6a157f59444dfb6058874ff3610e5d", + "https://files.pythonhosted.org/packages/e5/4b/1f4c36ab273980d7aa75bb126ea4f8971f24a96108acad3a0a084028c57b/cffi-2.1.0-cp315-cp315-macosx_11_0_arm64.whl": "sha256:cdf2448aab5f661c9315308ec8b93f4e8a1a67a3c733f8631067a2b67d5913dc", + "https://files.pythonhosted.org/packages/e6/3f/0b04a700dd64f465c93020253a793a82c9b4dff9961f48facd0df945d9b8/cffi-2.1.0-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.whl": "sha256:7d3538f9c0e50670f4deb93dbb696576e60590369cae2faf7de681e597a8a1f1", + "https://files.pythonhosted.org/packages/ea/dd/e3b0baa2d3d6a857ac72b7efbf18e32e487c9cdafcc13049ad765495b15e/cffi-2.1.0-cp311-cp311-macosx_11_0_arm64.whl": "sha256:f5bce581e6b8c235e566a14768a943b172ada3ed73537bb0c0be1edee312d4e7", + "https://files.pythonhosted.org/packages/eb/d8/df4543cc087245044ed02ef3ad8e0a26619d0075ac7a77a12dc81177851b/cffi-2.1.0-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl": "sha256:6274dcb2d15cef48daa73ed1be5a40d501d74dccd0cd6db364776d12cb6ba022", + "https://files.pythonhosted.org/packages/eb/da/5c4918a2d61d86fa927d716cb3d8e4626ef8dc8f605a599d32f33897f59a/cffi-2.1.0-cp311-cp311-win32.whl": "sha256:64c753a0f87a256020004f37a1c8c02c480e725f910f0b2a0f3f07debd1b2479", + "https://files.pythonhosted.org/packages/ec/d1/9a5b7169499e8e8d8e636de70b97ac7c9447104d2ff1a2cd94790cea5162/cffi-2.1.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl": "sha256:0a96b74cda968eebbad56d973efe5098974f0a9fb323865bf99ea1fd24e3e64c", + "https://files.pythonhosted.org/packages/ed/a5/e8bbb1ce5b3ac2f53ad6a10bde44318a5a8d99d4f4a000d44a6e39aeb3e4/cffi-2.1.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl": "sha256:7d5980a3433d4b71a5e120f9dd551403d7824e31e2e67124fe2769c404c06913", + "https://files.pythonhosted.org/packages/ed/da/4bbe583a3b3a5c8c60892124fe17f3fa3656523faf0d3484eae90f091853/cffi-2.1.0-cp315-cp315-macosx_10_15_x86_64.whl": "sha256:95f2954c2c9473d892eca6e0409f3568b37ab62a8eedb122461f73cc273476e3", + "https://files.pythonhosted.org/packages/ef/c3/ad299dc38f3583f8d916b299f028af418a9ec98bc695fcbebeae7420691c/cffi-2.1.0-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.whl": "sha256:90bec57cf82089383bd06a605b3eb8daebf7e5a668520beaf6e327a83a947699", + "https://files.pythonhosted.org/packages/f9/c8/6c2de1d55cf35ef8b92885d5ef280790f0fb9634d87ea1cc315176aecd61/cffi-2.1.0-cp311-cp311-win_amd64.whl": "sha256:4f26194e3d95e06501b942642855aed4f953d55e95d7d01b7c4483db3ecff458", + "https://files.pythonhosted.org/packages/f9/cf/398272b8bbfd58aa314fda5a7f1cdbb26d1d78ae324a11211521315dd1f0/cffi-2.1.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl": "sha256:03e9810d18c646077e501f661b682fbf5dee4676048527ca3cffe66faa9960dd", + "https://files.pythonhosted.org/packages/fa/75/74dfb7c3fc6ebbd408038476bd4c1d7e925c62614e7b9c534ecc34218288/cffi-2.1.0-cp310-cp310-musllinux_1_2_x86_64.whl": "sha256:11b3fb55f4f8ad92274ed26705f65d8f91457de71f5380061eb6d125a768fecd", + "https://files.pythonhosted.org/packages/fb/d2/4398416cd699b35167947c6e22aca52c47e69ad5695073c9f1f2c52e04aa/cffi-2.1.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl": "sha256:aa7a1b53a2a4452ada2d1b5dade9960b2522f1e61293a811a077439e39029565" }, "charset-normalizer": { - "https://files.pythonhosted.org/packages/00/bd/ef9c88464b126fa176f4ef4a317ad9b6f4d30b2cffbc43386062367c3e2c/charset_normalizer-3.4.3-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl": "8999f965f922ae054125286faf9f11bc6932184b93011d138925a1773830bbe9", - "https://files.pythonhosted.org/packages/02/f7/3611b32318b30974131db62b4043f335861d4d9b49adc6d57c1149cc49d4/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_aarch64.whl": "ccf600859c183d70eb47e05a44cd80a4ce77394d1ac0f79dbd2dd90a69a3a049", - "https://files.pythonhosted.org/packages/04/9a/914d294daa4809c57667b77470533e65def9c0be1ef8b4c1183a99170e9d/charset_normalizer-3.4.3-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl": "fb731e5deb0c7ef82d698b0f4c5bb724633ee2a489401594c5c88b02e6cb15f7", - "https://files.pythonhosted.org/packages/05/35/bb59b1cd012d7196fc81c2f5879113971efc226a63812c9cf7f89fe97c40/charset_normalizer-3.4.3-cp38-cp38-win_amd64.whl": "5d8d01eac18c423815ed4f4a2ec3b439d654e55ee4ad610e153cf02faf67ea40", - "https://files.pythonhosted.org/packages/05/6b/e2539a0a4be302b481e8cafb5af8792da8093b486885a1ae4d15d452bcec/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_ppc64le.whl": "42e5088973e56e31e4fa58eb6bd709e42fc03799c11c42929592889a2e54c491", - "https://files.pythonhosted.org/packages/06/57/84722eefdd338c04cf3030ada66889298eaedf3e7a30a624201e0cbe424a/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_s390x.whl": "30a96e1e1f865f78b030d65241c1ee850cdf422d869e9028e2fc1d5e4db73b92", - "https://files.pythonhosted.org/packages/0c/52/8b0c6c3e53f7e546a5e49b9edb876f379725914e1130297f3b423c7b71c5/charset_normalizer-3.4.3-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl": "c60e092517a73c632ec38e290eba714e9627abe9d301c8c8a12ec32c314a2a4b", - "https://files.pythonhosted.org/packages/16/ab/0233c3231af734f5dfcf0844aa9582d5a1466c985bbed6cedab85af9bfe3/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl": "1606f4a55c0fd363d754049cdf400175ee96c992b1f8018b993941f221221c5f", - "https://files.pythonhosted.org/packages/17/e5/5e67ab85e6d22b04641acb5399c8684f4d37caf7558a53859f0283a650e9/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl": "2001a39612b241dae17b4687898843f254f8748b796a2e16f1051a17078d991d", - "https://files.pythonhosted.org/packages/1a/79/ae516e678d6e32df2e7e740a7be51dc80b700e2697cb70054a0f1ac2c955/charset_normalizer-3.4.3-cp38-cp38-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl": "3653fad4fe3ed447a596ae8638b437f827234f01a8cd801842e43f3d0a6b281b", - "https://files.pythonhosted.org/packages/20/30/5f64fe3981677fe63fa987b80e6c01042eb5ff653ff7cec1b7bd9268e54e/charset_normalizer-3.4.3-cp39-cp39-musllinux_1_2_ppc64le.whl": "2c322db9c8c89009a990ef07c3bcc9f011a3269bc06782f916cd3d9eed7c9312", - "https://files.pythonhosted.org/packages/21/40/5188be1e3118c82dcb7c2a5ba101b783822cfb413a0268ed3be0468532de/charset_normalizer-3.4.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl": "cc9370a2da1ac13f0153780040f465839e6cccb4a1e44810124b4e22483c93fe", - "https://files.pythonhosted.org/packages/22/82/63a45bfc36f73efe46731a3a71cb84e2112f7e0b049507025ce477f0f052/charset_normalizer-3.4.3-cp38-cp38-macosx_10_9_universal2.whl": "0f2be7e0cf7754b9a30eb01f4295cc3d4358a479843b31f328afd210e2c7598c", - "https://files.pythonhosted.org/packages/2a/91/26c3036e62dfe8de8061182d33be5025e2424002125c9500faff74a6735e/charset_normalizer-3.4.3-cp310-cp310-win32.whl": "d79c198e27580c8e958906f803e63cddb77653731be08851c7df0b1a14a8fc0f", - "https://files.pythonhosted.org/packages/2f/36/77da9c6a328c54d17b960c89eccacfab8271fdaaa228305330915b88afa9/charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_x86_64.whl": "1e8ac75d72fa3775e0b7cb7e4629cec13b7514d928d15ef8ea06bca03ef01cae", - "https://files.pythonhosted.org/packages/31/e7/883ee5676a2ef217a40ce0bffcc3d0dfbf9e64cbcfbdf822c52981c3304b/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_s390x.whl": "cc34f233c9e71701040d772aa7490318673aa7164a0efe3172b2981218c26d93", - "https://files.pythonhosted.org/packages/33/9e/eca49d35867ca2db336b6ca27617deed4653b97ebf45dfc21311ce473c37/charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_x86_64.whl": "78deba4d8f9590fe4dae384aeff04082510a709957e968753ff3c48399f6f92a", - "https://files.pythonhosted.org/packages/37/60/5d0d74bc1e1380f0b72c327948d9c2aca14b46a9efd87604e724260f384c/charset_normalizer-3.4.3-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl": "07a0eae9e2787b586e129fdcbe1af6997f8d0e5abaa0bc98c0e20e124d67e601", - "https://files.pythonhosted.org/packages/39/c6/99271dc37243a4f925b09090493fb96c9333d7992c6187f5cfe5312008d2/charset_normalizer-3.4.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl": "23b6b24d74478dc833444cbd927c338349d6ae852ba53a0d02a2de1fce45b96e", - "https://files.pythonhosted.org/packages/39/f5/3b3836ca6064d0992c58c7561c6b6eee1b3892e9665d650c803bd5614522/charset_normalizer-3.4.3-cp312-cp312-win_amd64.whl": "86df271bf921c2ee3818f0522e9a5b8092ca2ad8b065ece5d7d9d0e9f4849bcc", - "https://files.pythonhosted.org/packages/3a/a4/b3b6c76e7a635748c4421d2b92c7b8f90a432f98bda5082049af37ffc8e3/charset_normalizer-3.4.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl": "00237675befef519d9af72169d8604a067d92755e84fe76492fef5441db05b91", - "https://files.pythonhosted.org/packages/3b/38/20a1f44e4851aa1c9105d6e7110c9d020e093dfa5836d712a5f074a12bf7/charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_ppc64le.whl": "4ca4c094de7771a98d7fbd67d9e5dbf1eb73efa4f744a730437d8a3a5cf994f0", - "https://files.pythonhosted.org/packages/45/8c/dcef87cfc2b3f002a6478f38906f9040302c68aebe21468090e39cde1445/charset_normalizer-3.4.3-cp39-cp39-musllinux_1_2_x86_64.whl": "88ab34806dea0671532d3f82d82b85e8fc23d7b2dd12fa837978dad9bb392a34", - "https://files.pythonhosted.org/packages/4c/92/27dbe365d34c68cfe0ca76f1edd70e8705d82b378cb54ebbaeabc2e3029d/charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_ppc64le.whl": "939578d9d8fd4299220161fdd76e86c6a251987476f5243e8864a7844476ba14", - "https://files.pythonhosted.org/packages/50/10/c117806094d2c956ba88958dab680574019abc0c02bcf57b32287afca544/charset_normalizer-3.4.3-cp38-cp38-musllinux_1_2_x86_64.whl": "a2d08ac246bb48479170408d6c19f6385fa743e7157d716e144cad849b2dd94b", - "https://files.pythonhosted.org/packages/50/ee/f4704bad8201de513fdc8aac1cabc87e38c5818c93857140e06e772b5892/charset_normalizer-3.4.3-cp312-cp312-win32.whl": "fb6fecfd65564f208cbf0fba07f107fb661bcd1a7c389edbced3f7a493f70e37", - "https://files.pythonhosted.org/packages/59/c0/a74f3bd167d311365e7973990243f32c35e7a94e45103125275b9e6c479f/charset_normalizer-3.4.3-cp38-cp38-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl": "252098c8c7a873e17dd696ed98bbe91dbacd571da4b87df3736768efa7a792e4", - "https://files.pythonhosted.org/packages/60/f5/4659a4cb3c4ec146bec80c32d8bb16033752574c20b1252ee842a95d1a1e/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl": "1bb60174149316da1c35fa5233681f7c0f9f514509b8e399ab70fea5f17e45c9", - "https://files.pythonhosted.org/packages/61/c5/dc3ba772489c453621ffc27e8978a98fe7e41a93e787e5e5bde797f1dddb/charset_normalizer-3.4.3-cp38-cp38-win32.whl": "ec557499516fc90fd374bf2e32349a2887a876fbf162c160e3c01b6849eaf557", - "https://files.pythonhosted.org/packages/61/f1/190d9977e0084d3f1dc169acd060d479bbbc71b90bf3e7bf7b9927dec3eb/charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_aarch64.whl": "96b2b3d1a83ad55310de8c7b4a2d04d9277d5591f40761274856635acc5fcb30", - "https://files.pythonhosted.org/packages/63/86/9cbd533bd37883d467fcd1bd491b3547a3532d0fbb46de2b99feeebf185e/charset_normalizer-3.4.3-cp39-cp39-win32.whl": "16a8770207946ac75703458e2c743631c79c59c5890c80011d536248f8eaa432", - "https://files.pythonhosted.org/packages/64/d1/f9d141c893ef5d4243bc75c130e95af8fd4bc355beff06e9b1e941daad6e/charset_normalizer-3.4.3-cp38-cp38-musllinux_1_2_ppc64le.whl": "5b413b0b1bfd94dbf4023ad6945889f374cd24e3f62de58d6bb102c4d9ae534a", - "https://files.pythonhosted.org/packages/64/d4/9eb4ff2c167edbbf08cdd28e19078bf195762e9bd63371689cab5ecd3d0d/charset_normalizer-3.4.3-cp311-cp311-win32.whl": "6cf8fd4c04756b6b60146d98cd8a77d0cdae0e1ca20329da2ac85eed779b6849", - "https://files.pythonhosted.org/packages/65/1a/7425c952944a6521a9cfa7e675343f83fd82085b8af2b1373a2409c683dc/charset_normalizer-3.4.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl": "d0e909868420b7049dafd3a31d45125b31143eec59235311fc4c57ea26a4acd2", - "https://files.pythonhosted.org/packages/65/ca/2135ac97709b400c7654b4b764daf5c5567c2da45a30cdd20f9eefe2d658/charset_normalizer-3.4.3-cp313-cp313-macosx_10_13_universal2.whl": "14c2a87c65b351109f6abfc424cab3927b3bdece6f706e4d12faaf3d52ee5efe", - "https://files.pythonhosted.org/packages/70/99/f1c3bdcfaa9c45b3ce96f70b14f070411366fa19549c1d4832c935d8e2c3/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_x86_64.whl": "18343b2d246dc6761a249ba1fb13f9ee9a2bcd95decc767319506056ea4ad4dc", - "https://files.pythonhosted.org/packages/71/11/98a04c3c97dd34e49c7d247083af03645ca3730809a5509443f3c37f7c99/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl": "41d1fc408ff5fdfb910200ec0e74abc40387bccb3252f3f27c0676731df2b2c8", - "https://files.pythonhosted.org/packages/72/2a/aff5dd112b2f14bcc3462c312dce5445806bfc8ab3a7328555da95330e4b/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_x86_64.whl": "d716a916938e03231e86e43782ca7878fb602a125a91e7acb8b5112e2e96ac16", - "https://files.pythonhosted.org/packages/77/d9/cbcf1a2a5c7d7856f11e7ac2d782aec12bdfea60d104e60e0aa1c97849dc/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_ppc64le.whl": "fdabf8315679312cfa71302f9bd509ded4f2f263fb5b765cf1433b39106c3cc9", - "https://files.pythonhosted.org/packages/7a/03/cbb6fac9d3e57f7e07ce062712ee80d80a5ab46614684078461917426279/charset_normalizer-3.4.3-cp38-cp38-musllinux_1_2_aarch64.whl": "d95bfb53c211b57198bb91c46dd5a2d8018b3af446583aab40074bf7988401cb", - "https://files.pythonhosted.org/packages/7d/a8/c6ec5d389672521f644505a257f50544c074cf5fc292d5390331cd6fc9c3/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl": "0cacf8f7297b0c4fcb74227692ca46b4a5852f8f4f24b3c766dd94a1075c4884", - "https://files.pythonhosted.org/packages/7e/61/19b36f4bd67f2793ab6a99b979b4e4f3d8fc754cbdffb805335df4337126/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_ppc64le.whl": "53cd68b185d98dde4ad8990e56a58dea83a4162161b1ea9272e5c9182ce415e0", - "https://files.pythonhosted.org/packages/7e/95/42aa2156235cbc8fa61208aded06ef46111c4d3f0de233107b3f38631803/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl": "416175faf02e4b0810f1f38bcb54682878a4af94059a1cd63b8747244420801f", - "https://files.pythonhosted.org/packages/7f/b5/991245018615474a60965a7c9cd2b4efbaabd16d582a5547c47ee1c7730b/charset_normalizer-3.4.3-cp311-cp311-macosx_10_9_universal2.whl": "b256ee2e749283ef3ddcff51a675ff43798d92d746d1a6e4631bf8c707d22d0b", - "https://files.pythonhosted.org/packages/82/10/0fd19f20c624b278dddaf83b8464dcddc2456cb4b02bb902a6da126b87a1/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl": "3cfb2aad70f2c6debfbcb717f23b7eb55febc0bb23dcffc0f076009da10c6392", - "https://files.pythonhosted.org/packages/83/2d/5fd176ceb9b2fc619e63405525573493ca23441330fcdaee6bef9460e924/charset_normalizer-3.4.3.tar.gz": "6fce4b8500244f6fcb71465d4a4930d132ba9ab8e71a7859e6a5d59851068d14", - "https://files.pythonhosted.org/packages/85/9a/d891f63722d9158688de58d050c59dc3da560ea7f04f4c53e769de5140f5/charset_normalizer-3.4.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl": "74d77e25adda8581ffc1c720f1c81ca082921329452eba58b16233ab1842141c", - "https://files.pythonhosted.org/packages/86/9e/f552f7a00611f168b9a5865a1414179b2c6de8235a4fa40189f6f79a1753/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl": "30d006f98569de3459c2fc1f2acde170b7b2bd265dc1943e87e1a4efe1b67c31", - "https://files.pythonhosted.org/packages/87/df/b7737ff046c974b183ea9aa111b74185ac8c3a326c6262d413bd5a1b8c69/charset_normalizer-3.4.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl": "0e78314bdc32fa80696f72fa16dc61168fda4d6a0c014e0380f9d02f0e5d8a07", - "https://files.pythonhosted.org/packages/8a/1f/f041989e93b001bc4e44bb1669ccdcf54d3f00e628229a85b08d330615c5/charset_normalizer-3.4.3-py3-none-any.whl": "ce571ab16d890d23b5c278547ba694193a45011ff86a9162a71307ed9f86759a", - "https://files.pythonhosted.org/packages/8e/91/b5a06ad970ddc7a0e513112d40113e834638f4ca1120eb727a249fb2715e/charset_normalizer-3.4.3-cp314-cp314-macosx_10_13_universal2.whl": "3cd35b7e8aedeb9e34c41385fda4f73ba609e561faedfae0a9e75e44ac558a15", - "https://files.pythonhosted.org/packages/99/04/baae2a1ea1893a01635d475b9261c889a18fd48393634b6270827869fa34/charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_s390x.whl": "fd10de089bcdcd1be95a2f73dbe6254798ec1bda9f450d5828c96f93e2536b9c", - "https://files.pythonhosted.org/packages/9a/8f/ae790790c7b64f925e5c953b924aaa42a243fb778fed9e41f147b2a5715a/charset_normalizer-3.4.3-cp313-cp313-win_amd64.whl": "cf1ebb7d78e1ad8ec2a8c4732c7be2e736f6e5123a4146c5b89c9d1f585f8cef", - "https://files.pythonhosted.org/packages/a0/e4/5a075de8daa3ec0745a9a3b54467e0c2967daaaf2cec04c845f73493e9a1/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl": "18b97b8404387b96cdbd30ad660f6407799126d26a39ca65729162fd810a99aa", - "https://files.pythonhosted.org/packages/a3/ad/b0081f2f99a4b194bcbb1934ef3b12aa4d9702ced80a37026b7607c72e58/charset_normalizer-3.4.3-cp313-cp313-win32.whl": "6fb70de56f1859a3f71261cbe41005f56a7842cc348d3aeb26237560bfa5e0ce", - "https://files.pythonhosted.org/packages/a4/fa/384d2c0f57edad03d7bec3ebefb462090d8905b4ff5a2d2525f3bb711fac/charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_s390x.whl": "02425242e96bcf29a49711b0ca9f37e451da7c70562bc10e8ed992a5a7a25cc0", - "https://files.pythonhosted.org/packages/ae/02/e29e22b4e02839a0e4a06557b1999d0a47db3567e82989b5bb21f3fbbd9f/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_aarch64.whl": "027b776c26d38b7f15b26a5da1044f376455fb3766df8fc38563b4efbc515154", - "https://files.pythonhosted.org/packages/b0/a8/6f5bcf1bcf63cb45625f7c5cadca026121ff8a6c8a3256d8d8cd59302663/charset_normalizer-3.4.3-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl": "257f26fed7d7ff59921b78244f3cd93ed2af1800ff048c33f624c87475819dd7", - "https://files.pythonhosted.org/packages/b7/8c/9839225320046ed279c6e839d51f028342eb77c91c89b8ef2549f951f3ec/charset_normalizer-3.4.3-cp314-cp314-win32.whl": "c6dbd0ccdda3a2ba7c2ecd9d77b37f3b5831687d8dc1b6ca5f56a4880cc7b7ce", - "https://files.pythonhosted.org/packages/c1/35/6525b21aa0db614cf8b5792d232021dca3df7f90a1944db934efa5d20bb1/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_x86_64.whl": "320e8e66157cc4e247d9ddca8e21f427efc7a04bbd0ac8a9faf56583fa543f9f", - "https://files.pythonhosted.org/packages/c2/a9/3865b02c56f300a6f94fc631ef54f0a8a29da74fb45a773dfd3dcd380af7/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_aarch64.whl": "6aab0f181c486f973bc7262a97f5aca3ee7e1437011ef0c2ec04b5a11d16c927", - "https://files.pythonhosted.org/packages/c2/ca/9a0983dd5c8e9733565cf3db4df2b0a2e9a82659fd8aa2a868ac6e4a991f/charset_normalizer-3.4.3-cp39-cp39-macosx_10_9_universal2.whl": "70bfc5f2c318afece2f5838ea5e4c3febada0be750fcf4775641052bbba14d05", - "https://files.pythonhosted.org/packages/c4/72/d3d0e9592f4e504f9dea08b8db270821c909558c353dc3b457ed2509f2fb/charset_normalizer-3.4.3-cp39-cp39-musllinux_1_2_aarch64.whl": "1ef99f0456d3d46a50945c98de1774da86f8e992ab5c77865ea8b8195341fc19", - "https://files.pythonhosted.org/packages/c5/35/9c99739250742375167bc1b1319cd1cec2bf67438a70d84b2e1ec4c9daa3/charset_normalizer-3.4.3-cp38-cp38-musllinux_1_2_s390x.whl": "b5e3b2d152e74e100a9e9573837aba24aab611d39428ded46f4e4022ea7d1942", - "https://files.pythonhosted.org/packages/c7/2a/ae245c41c06299ec18262825c1569c5d3298fc920e4ddf56ab011b417efd/charset_normalizer-3.4.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl": "13faeacfe61784e2559e690fc53fa4c5ae97c6fcedb8eb6fb8d0a15b475d2c64", - "https://files.pythonhosted.org/packages/ce/d6/7e805c8e5c46ff9729c49950acc4ee0aeb55efb8b3a56687658ad10c3216/charset_normalizer-3.4.3-cp39-cp39-win_amd64.whl": "d22dbedd33326a4a5190dd4fe9e9e693ef12160c77382d9e87919bce54f3d4ca", - "https://files.pythonhosted.org/packages/ce/ec/1edc30a377f0a02689342f214455c3f6c2fbedd896a1d2f856c002fc3062/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl": "b89bc04de1d83006373429975f8ef9e7932534b8cc9ca582e4db7d20d91816db", - "https://files.pythonhosted.org/packages/d6/98/f3b8013223728a99b908c9344da3aa04ee6e3fa235f19409033eda92fb78/charset_normalizer-3.4.3-cp310-cp310-macosx_10_9_universal2.whl": "fb7f67a1bfa6e40b438170ebdc8158b78dc465a5a67b6dde178a46987b244a72", - "https://files.pythonhosted.org/packages/e1/ef/dd08b2cac9284fd59e70f7d97382c33a3d0a926e45b15fc21b3308324ffd/charset_normalizer-3.4.3-cp39-cp39-musllinux_1_2_s390x.whl": "511729f456829ef86ac41ca78c63a5cb55240ed23b4b737faca0eb1abb1c41bc", - "https://files.pythonhosted.org/packages/e2/c6/f05db471f81af1fa01839d44ae2a8bfeec8d2a8b4590f16c4e7393afd323/charset_normalizer-3.4.3-cp310-cp310-win_amd64.whl": "c6e490913a46fa054e03699c70019ab869e990270597018cef1d8562132c2669", - "https://files.pythonhosted.org/packages/e2/e6/63bb0e10f90a8243c5def74b5b105b3bbbfb3e7bb753915fe333fb0c11ea/charset_normalizer-3.4.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl": "585f3b2a80fbd26b048a0be90c5aae8f06605d3c92615911c3a2b03a8a3b796f", - "https://files.pythonhosted.org/packages/e4/69/132eab043356bba06eb333cc2cc60c6340857d0a2e4ca6dc2b51312886b3/charset_normalizer-3.4.3-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl": "34a7f768e3f985abdb42841e20e17b330ad3aaf4bb7e7aeeb73db2e70f077b99", - "https://files.pythonhosted.org/packages/e9/5e/14c94999e418d9b87682734589404a25854d5f5d0408df68bc15b6ff54bb/charset_normalizer-3.4.3-cp312-cp312-macosx_10_13_universal2.whl": "e28e334d3ff134e88989d90ba04b47d84382a828c061d0d1027b1b12a62b39b1", - "https://files.pythonhosted.org/packages/ee/7a/36fbcf646e41f710ce0a563c1c9a343c6edf9be80786edeb15b6f62e17db/charset_normalizer-3.4.3-cp314-cp314-win_amd64.whl": "73dc19b562516fc9bcf6e5d6e596df0b4eb98d87e4f79f3ae71840e6ed21361c", - "https://files.pythonhosted.org/packages/f0/c9/a2c9c2a355a8594ce2446085e2ec97fd44d323c684ff32042e2a6b718e1d/charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_aarch64.whl": "c6f162aabe9a91a309510d74eeb6507fab5fff92337a15acbe77753d88d9dcf0", - "https://files.pythonhosted.org/packages/f1/e5/38421987f6c697ee3722981289d554957c4be652f963d71c5e46a262e135/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl": "8dcfc373f888e4fb39a7bc57e93e3b845e7f462dacc008d9749568b1c4ece096", - "https://files.pythonhosted.org/packages/f4/9c/996a4a028222e7761a96634d1820de8a744ff4327a00ada9c8942033089b/charset_normalizer-3.4.3-cp311-cp311-win_amd64.whl": "31a9a6f775f9bcd865d88ee350f0ffb0e25936a7f930ca98995c05abf1faf21c", - "https://files.pythonhosted.org/packages/f6/42/6f45efee8697b89fda4d50580f292b8f7f9306cb2971d4b53f8914e4d890/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_s390x.whl": "bd28b817ea8c70215401f657edef3a8aa83c29d447fb0b622c35403780ba11d5", - "https://files.pythonhosted.org/packages/fc/eb/a2ffb08547f4e1e5415fb69eb7db25932c52a52bed371429648db4d84fb1/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl": "c6fd51128a41297f5409deab284fecbe5305ebd7e5a1f959bee1c054622b7018" + "https://files.pythonhosted.org/packages/00/5e/17398df3a139985ba9d11ed072531986f408c8fca952835ef1ab1820c02b/charset_normalizer-3.4.9-cp314-cp314t-macosx_10_15_universal2.whl": "sha256:609b3ba8fcc0fb5ab7af00719d0fb6ad0cb518e48e7712d12fd68f1327951198", + "https://files.pythonhosted.org/packages/01/c4/4fa4c8b3097a11f3c5f09a35b72ed6855fb1d332469504962ab7bafcc702/charset_normalizer-3.4.9-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl": "sha256:5e226f6218febc71f6c1fc2fafb91c226f75bdc1d8fb12d66823716e891608fd", + "https://files.pythonhosted.org/packages/01/da/a44bd7a13d426e69e4894557106cd58669097bfad4a8681123b618fbfc5d/charset_normalizer-3.4.9-cp310-cp310-win_arm64.whl": "sha256:375b83ed0aecfce76c16d198fbc21f3b11b337d68662bea0a995046682a11419", + "https://files.pythonhosted.org/packages/0b/e3/85ec501f206fb049259288c1f3506e53876937fb00edb47009348e66756b/charset_normalizer-3.4.9-cp311-cp311-macosx_10_9_universal2.whl": "sha256:0e94703ec9684807f20cfb5eed95c70f67f2a8f21ad620146d7b5a13677b93e5", + "https://files.pythonhosted.org/packages/0c/74/2f62c8821b969ea3bd67cc2e6976834f48ca5d12664d2559ebcd9bcfbed7/charset_normalizer-3.4.9-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl": "sha256:871ff67ea1aad4dfd91736464934d56b32dac49f9fbe16cddba36198a7b3a0db", + "https://files.pythonhosted.org/packages/0c/e7/aaf6da33fc9f4691cda8f7efbc9f69179d3d39ec8a4799baf273ee1d8db0/charset_normalizer-3.4.9-cp310-cp310-musllinux_1_2_armv7l.whl": "sha256:65a7ff3f705e57d392f7261b6d0550fe137c3019477431f1c355e0db0a7d3e15", + "https://files.pythonhosted.org/packages/0e/42/6dbc00b8cd16011691203e33570fa42ed5746599a2e878112d16eab403a3/charset_normalizer-3.4.9-cp312-cp312-win32.whl": "sha256:78841cccf1af7b40f6f716338d50c0902dbe88d9f800b3c973b7a9a0a693a642", + "https://files.pythonhosted.org/packages/10/e0/47c079dd82d217c807479cd59ffd30af56307ea31c108b75758970459ad3/charset_normalizer-3.4.9-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl": "sha256:4d1c96a7a18b9690a4d46df09e3e3382406ae3213727cd1019ebade1c4a81917", + "https://files.pythonhosted.org/packages/14/cb/1db8b96547ee3186cd2dd7f2e59dd560a9b80748f3604171f3c153d62811/charset_normalizer-3.4.9-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl": "sha256:58150c9f9b9a552505912d182ccdf26f6396fb6094816ceebcbb20eecabaed94", + "https://files.pythonhosted.org/packages/17/6d/bff78a4bacc4891bc63ec5bdc6776d8c85e47fab93d0d5f6223068fad0a4/charset_normalizer-3.4.9-cp39-cp39-win32.whl": "sha256:93d59d504b230e83c7a843251681959a0b6a9cd76f6e146ce1b8a80eb8739af9", + "https://files.pythonhosted.org/packages/19/79/55c32d06d76ae4feafe053f061f3e3ab70bcf19f4007797ce8c3efda7830/charset_normalizer-3.4.9-cp314-cp314-manylinux_2_31_armv7l.whl": "sha256:f7fb7d750cfa0a070d2c24e831fd3481019a60dd317ea2b39acbcebc08b6ed81", + "https://files.pythonhosted.org/packages/1d/85/181c652953eb5276d198f375b1dd641047392050098100a3a02d6534f657/charset_normalizer-3.4.9-cp310-cp310-musllinux_1_2_aarch64.whl": "sha256:e9701d0049d92c16703a42771b98d560b95248949f23f8cf7b4eddd201814fb9", + "https://files.pythonhosted.org/packages/20/95/d75e82f8ce9fd323ebf059c16c9aadefb22a1ecde13b7840b35835e4886c/charset_normalizer-3.4.9-cp314-cp314-win_arm64.whl": "sha256:40a126142a56b2dfc0aacbad1de8310cbf60da7656db0e6b16eebd48e3e93519", + "https://files.pythonhosted.org/packages/28/e9/9fb6099b868c82a40698a748ae0fbd4f31ccc13844c176a07158ba2abbfd/charset_normalizer-3.4.9-cp39-cp39-win_arm64.whl": "sha256:476743fe6dfe14a2da12e3ac79125dc84a3b2cf8094369a47a1529b0cd8549fe", + "https://files.pythonhosted.org/packages/2b/f9/ef4a69ea338ad3c0deceea0f5f7d2380ae8b52132b06d652cb0d2cd86706/charset_normalizer-3.4.9-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl": "sha256:8a79d9f4d8001473a30c163556b3c3bfebec837495a412dde78b51672f6134f9", + "https://files.pythonhosted.org/packages/33/9a/895095b83e7907abd6d3d99aad3a38ad0d9686cc186cb0c94c24320fe63e/charset_normalizer-3.4.9-cp312-cp312-musllinux_1_2_aarch64.whl": "sha256:60f44ade2cf573dad7a277e6f8ca9a51a21dda572b13bd7d8539bb3cd5dbedde", + "https://files.pythonhosted.org/packages/36/31/a276bb2e66243072a3fd06fdcab9cbb61a305b02143d70d2bda21d888fa8/charset_normalizer-3.4.9-cp310-cp310-manylinux_2_31_armv7l.whl": "sha256:bcf74c1df76758a395bf0af608c04c82257523f55c9868b334f06270d0f2112b", + "https://files.pythonhosted.org/packages/37/8d/ca39a7559a4797505530d084fd3a49a2c959efbbbff146302fb7be4e3b35/charset_normalizer-3.4.9-cp310-cp310-win_amd64.whl": "sha256:8c041122946b7ba21bb32c45b1aa57b1be35527690aeb3c5c234521085632eee", + "https://files.pythonhosted.org/packages/3a/25/45f30093ae27dd7b92a793b61882a38685f993700113ca36e0c9c14965e1/charset_normalizer-3.4.9-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl": "sha256:a4fbdde9dd4a9ce5fd52c2b3a347bb50cc89483ef783f1cb00d408c13f7a96c0", + "https://files.pythonhosted.org/packages/3d/c6/eee9dca4439b1061f76373f06ea855678cc4a64c1c3c90b50e479edbb8eb/charset_normalizer-3.4.9-cp314-cp314t-win_arm64.whl": "sha256:19ac87f93086ce37b86e098888555c4b4bc48102279bae3350098c0ed664b501", + "https://files.pythonhosted.org/packages/3d/ca/ad1d7c7d3077dab873f539d3e1d083c0845a762cb0bafdfbe3ef93add598/charset_normalizer-3.4.9-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl": "sha256:920079c3f7456fa213e0829ed2073aaa727fd39d889ead5b4f35d0de5460d04f", + "https://files.pythonhosted.org/packages/3d/ef/d96ec496cfea0c21db43b0ad03891308b02388d054cc902cf0e5a1ad6a88/charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl": "sha256:fa36ec09ef71d158186bc79e359ff5fdd6e7996fe8ab638f00d6b93139ba4fcf", + "https://files.pythonhosted.org/packages/42/ab/b9bc2e77d6b44a7e46ef62ec5cac1c9a6ba7b9135a5d560f002696ec9995/charset_normalizer-3.4.9-cp314-cp314-musllinux_1_2_aarch64.whl": "sha256:a4cfde78a9f2880208d16a93b795726a3017d5977e08d1e162a7a31322479c41", + "https://files.pythonhosted.org/packages/44/95/80282cce0fae9c3061203d723ee87da996aed79679e65d8935050ee7ca1f/charset_normalizer-3.4.9-cp311-cp311-manylinux_2_31_armv7l.whl": "sha256:c0323c9daef75ef2e5083624b4585018a0c9d5e3b40f607eed81a311270b934b", + "https://files.pythonhosted.org/packages/48/18/c8f397329c35e32f6a837e488986f4ae03bd2abebc453b48714991630c2f/charset_normalizer-3.4.9-cp313-cp313-musllinux_1_2_aarch64.whl": "sha256:416c229f77e5ea25b3dfd4b582f8d73d7e43c22320302b9ab128a2d3a0b38efe", + "https://files.pythonhosted.org/packages/49/ba/768fa3f36048d81c477a0ce61f813bc1454d80917ccfe550abd9f44f5e24/charset_normalizer-3.4.9-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl": "sha256:f840ed6d8ecba8255df8c42b87fadeda98ddfc6eeec05e2dc66e26d46dd6f58a", + "https://files.pythonhosted.org/packages/49/fd/a1d26144398c67486422a72bf5812cda22cb4ccfcd95a290fb41ceb4b8e2/charset_normalizer-3.4.9-cp314-cp314-win_amd64.whl": "sha256:16b65ea0f2465b6fb52aa22de5eca612aa964ddfec00a912e26f4656cbef890b", + "https://files.pythonhosted.org/packages/4b/4c/5361f9aa7f2cb58d94f2ab831b3d493f69efb1d239654b4744e3c09527cb/charset_normalizer-3.4.9-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl": "sha256:9104ed0bd76a429d46f9ec0dbc9b08ad1d2dcdf2b00a5a0daa1c145329b35b44", + "https://files.pythonhosted.org/packages/4e/6e/de0229a7ef40f6f9d28a837eebf4ec47bdca5dab4e900c84f22919af636a/charset_normalizer-3.4.9-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl": "sha256:4773092f8019072343a7447203308b176e10199920eb02d6195e81bbb3274c29", + "https://files.pythonhosted.org/packages/4f/8d/1569f4d0032d6ba2a4fe4591c35bf87868c600c41a71eb5c2e1ffa8464c2/charset_normalizer-3.4.9-cp311-cp311-win_arm64.whl": "sha256:1d22856ffbe153a602df38e4a5464f0b748a54002e0d69ac6d2ad0a197cc99ec", + "https://files.pythonhosted.org/packages/50/78/ce342ca4ff30b2eb49fe6d9578df85974f90c67d294113e94efdd9664cbd/charset_normalizer-3.4.9-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl": "sha256:7b86a2b16095d250c6f58b3d9b2eee6f4147754344f3dab0922f7c9bf7d226c9", + "https://files.pythonhosted.org/packages/52/94/af74dde74a3996bd959c350709bfe50e297823d70a8c1cbd54b838880863/charset_normalizer-3.4.9-cp314-cp314t-manylinux_2_31_armv7l.whl": "sha256:f86c6358749bd4fda175388691e3ba8c46e24c5347d0afd20f9b7edfc9faf07d", + "https://files.pythonhosted.org/packages/5e/be/7ee4453d7e88dfbc4104ccd34900b9f2c7c17dac22881865fe0e82424a25/charset_normalizer-3.4.9-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl": "sha256:b5314963fce9b0b12743891de876e724997864ee22aa496f903f426c7e2fa5b2", + "https://files.pythonhosted.org/packages/5f/c0/6eec7bdabe6cbbcc274ec04596f6d93865751a0541d33d60d1ce179bd372/charset_normalizer-3.4.9-cp39-cp39-musllinux_1_2_armv7l.whl": "sha256:ad41ba96094304aa090f5a30cb6e4fb3b3f1c264c523394b4c39bbacc4dc92ba", + "https://files.pythonhosted.org/packages/63/01/f2fb3bd3a73be48b173ee0c6aa8d2497af97d5663a8c4c4b491de4c62f7a/charset_normalizer-3.4.9-cp310-cp310-musllinux_1_2_x86_64.whl": "sha256:79580094b00d1789d1f93ea55bc43cb2f611910c72235b7657f3482ddcc1b22d", + "https://files.pythonhosted.org/packages/6a/05/c94d5cd23396289c54c93b02e0273b4dd8921641d9968c4828caf9bbaad9/charset_normalizer-3.4.9-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl": "sha256:df7276909358e5635ae203673ab7e509ddd224225a8d6b0790bf13eb2bde1cc5", + "https://files.pythonhosted.org/packages/6c/ef/2473d3c4d869155be4af1191111d59c4d5c4e0173026f7e85b176e23bf65/charset_normalizer-3.4.9-cp313-cp313-musllinux_1_2_x86_64.whl": "sha256:69b157c5d3292bcd443faca052f3096f637f1e074b98212a933c074ae23dc3b8", + "https://files.pythonhosted.org/packages/6d/46/79847edd07244a4a2d443c6655a7b6ee94203c21539414b059f32713c357/charset_normalizer-3.4.9-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl": "sha256:3c09a49d6cde137258beb3d551994a2927fd35ad5cf96aed573f61bbd67c5f84", + "https://files.pythonhosted.org/packages/6e/fb/d560d1d1555debbfe7849d9cac6145c1b537709d79576bf22557ed803b82/charset_normalizer-3.4.9-cp313-cp313-win_arm64.whl": "sha256:611057cc5d5c0afc743ba8be6bd828c17e0aaa8643f9d0a9b9bb7dea80eb8012", + "https://files.pythonhosted.org/packages/70/4a/ecbd131485c07fcdfad54e28946d513e3da22ef3b4bd854dcafae54ec739/charset_normalizer-3.4.9-cp312-cp312-macosx_10_13_universal2.whl": "sha256:45b0cc4e3556cd875e09102988d1ab8356c998b596c9fced84547c8138b487a0", + "https://files.pythonhosted.org/packages/78/ad/98aae8630ac71f16711968e38a5acfecce41b778bf2f0312851020f565a8/charset_normalizer-3.4.9-cp314-cp314t-musllinux_1_2_armv7l.whl": "sha256:cd6c3d4b783c556fa00bf540854e42f135e2f256abd29669fcd0da0f2dec79c2", + "https://files.pythonhosted.org/packages/7e/8d/496817fa0944239ecae662dd57ea765cfeaec6a735f9f025d4b7b72e7143/charset_normalizer-3.4.9-cp314-cp314-macosx_10_15_universal2.whl": "sha256:0327fcd59a935777d83410750c50600ee9571af2846f71ce40f25b13da1ef380", + "https://files.pythonhosted.org/packages/7f/f4/ffbb83546e1f198ecc70ecd372b65cf2b50f9068b380abd67640f17a8e18/charset_normalizer-3.4.9-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl": "sha256:16d10d789dd9bcca1173c95af82c58433122564b7bc39385124be735a35cbe99", + "https://files.pythonhosted.org/packages/80/33/6c99c1b3e6b8bf730e1bc809b9a2608f224145069114c479a2e9e1494346/charset_normalizer-3.4.9-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl": "sha256:c1225416b463483160e4af85d5fc3a9690ccb53fd4b1865a6437825f5ede3209", + "https://files.pythonhosted.org/packages/80/3f/bd97d3d9c613013d07cb7733d299385b41df37f0471310f5a73dc359f0b8/charset_normalizer-3.4.9-cp314-cp314t-win_amd64.whl": "sha256:9b8e0f3107e2200b76f6054de99016eac3ee6762713587b36baaa7e4bd2ae177", + "https://files.pythonhosted.org/packages/80/cc/f920afd1a23c58ccd53c1d36085a71893a4737ff5e66e0371efab6809850/charset_normalizer-3.4.9-cp312-cp312-win_amd64.whl": "sha256:4b3dac63058cc36820b0dd072f89898604e2d39686fe05321729d00d8ac185a0", + "https://files.pythonhosted.org/packages/83/d5/9096aa3cf532dfad237861544eb47a0f20d5adbf1039760fed8eaae935d9/charset_normalizer-3.4.9-cp311-cp311-win32.whl": "sha256:ac351b3b8014eead140e77e9717e2992c6bbe30b63bc3422422eb84865412e3d", + "https://files.pythonhosted.org/packages/83/dc/9b29fa4412b318bf3bfea985c35d67eb55e04b59a7c3f2237168b0e0be6f/charset_normalizer-3.4.9-cp312-cp312-musllinux_1_2_x86_64.whl": "sha256:03d07803992c6c7bbc976327f34b18b6160327fc81cb82c9d504720ac0be3b62", + "https://files.pythonhosted.org/packages/86/7e/5ce0bba863470fd1902d5e5843968951bddf38abe4742fc97116ef4598b3/charset_normalizer-3.4.9-cp313-cp313-musllinux_1_2_armv7l.whl": "sha256:75286256590a6320cf106a0d28970d3560aad9ee09aa7b34fb40524792436d35", + "https://files.pythonhosted.org/packages/87/3a/ad914516df7e358a81aae018caa5e0470ba827fa6d763b1d2e87d920a5f6/charset_normalizer-3.4.9-cp312-cp312-manylinux_2_31_armv7l.whl": "sha256:90c44bc373b7687f6948b693cceaea1348ae0975d7474746559494468e3c1d84", + "https://files.pythonhosted.org/packages/8c/e7/5ddfd76fc061eb52de219658a4aa431cbacadf0a0219c8854f00da50d289/charset_normalizer-3.4.9-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl": "sha256:33bdcc2a32c0a0e861f60841a512c8acc658c87c2ac59d89e3a46dacf7d866e4", + "https://files.pythonhosted.org/packages/92/8f/3a47a3667c83c2df9483d91644c6c107de3bf8874aa1793da9d3012eb986/charset_normalizer-3.4.9-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl": "sha256:e4fd89cc178bced6ad29cb3e6dd4aa63fa5017c3524dbd0b25998fb64a87cc8b", + "https://files.pythonhosted.org/packages/98/2b/f97f1c193fb855c345d678f5077d6926034db0722df74c8f057020e05a25/charset_normalizer-3.4.9-py3-none-any.whl": "sha256:68e5f26a1ad57ded6d1cfb85331d1c1a195314756471d97758c48498bb4dcdf5", + "https://files.pythonhosted.org/packages/9b/f2/c0d4b8508565a36bc5c624e88ed297f5b0b1095011034d7f5b83a69908b5/charset_normalizer-3.4.9-cp314-cp314-win32.whl": "sha256:c1c948747b03be832dceed96ca815cef7360de9aa19d37c730f8e3f6101aca48", + "https://files.pythonhosted.org/packages/a1/34/ef5c05f412f42520d7709b7d3784d19640839eb7366ded1755511585429f/charset_normalizer-3.4.9-cp312-cp312-musllinux_1_2_armv7l.whl": "sha256:a1786910334ed46ab1dd73222f2cd1e05c2c3bb39f6dddb4f8b36fc382058a39", + "https://files.pythonhosted.org/packages/a2/55/86048bde1c9d0352940bd7b87d825091a52aef67d01cde6c6f7342c5b552/charset_normalizer-3.4.9-cp39-cp39-win_amd64.whl": "sha256:ddf4af30b417d9fe16481e9b81c27ab2a7cde1ff7ba3e85653b02db7d145dc7b", + "https://files.pythonhosted.org/packages/a5/34/49b9060e8418b14fb5cba9cf6bfb383111e2538a03a1fb18e66a95aeb3d5/charset_normalizer-3.4.9-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl": "sha256:04ce310cb89c15df659582aee80a0603788732a5e017d5bd5c81158106ce249c", + "https://files.pythonhosted.org/packages/a5/ff/c946d63bc3786d5b84d960b0f7ab7e25b828486a946b5aa997625bcaf6a6/charset_normalizer-3.4.9-cp311-cp311-musllinux_1_2_armv7l.whl": "sha256:3d92613ec25e43b05f042302531ec0f00b8445190e43325880cbd6ab7c2581da", + "https://files.pythonhosted.org/packages/a6/ec/81e22253f4b7091eca6515bb3da5e45d05a663f7f567bb745695dc60f892/charset_normalizer-3.4.9-cp39-cp39-macosx_10_9_universal2.whl": "sha256:253a4a220747e8b5faf57ec320c4f5efb0cef05f647420bf267143ec15dba10a", + "https://files.pythonhosted.org/packages/ad/81/8e983840c6e5b93b33c2ba81aa3d52c2e42f0e9a690ce7607a2e61da4a5c/charset_normalizer-3.4.9-cp310-cp310-macosx_10_9_universal2.whl": "sha256:cd6280cf040f233bd7d3407b743b4b4c74f70e8e1c4199cb112a62c941c0772a", + "https://files.pythonhosted.org/packages/af/ba/5e5007c370702f85d2ef75791fac7943ed41e080364a673b20142e430e3e/charset_normalizer-3.4.9-cp311-cp311-musllinux_1_2_x86_64.whl": "sha256:280081916dc341820640489a66e4696049401ef1cf6dd672f672e70ad915aca3", + "https://files.pythonhosted.org/packages/b0/3e/faee8f9de92b14ee1198e9163252bb15efee7301b31256a3b6d9ebfdd0dd/charset_normalizer-3.4.9-cp313-cp313-manylinux_2_31_armv7l.whl": "sha256:5b10cd92fc5c498b35a8635df6d5a100207f88b63a4dc1de7ef9a548e1e2cd63", + "https://files.pythonhosted.org/packages/b1/27/693ee5e8a18191eb38647360c51cd505013e2bd3b366aa43fd5344c21e3c/charset_normalizer-3.4.9-cp314-cp314t-win32.whl": "sha256:0d861473f743244d349b50f850d10eb87aeb22bbdcc8e64f79273c94af5a8226", + "https://files.pythonhosted.org/packages/b2/06/97ec2aeae780b31d742b6352218b43841a6871e2564578ca522dce4a45c3/charset_normalizer-3.4.9-cp313-cp313-macosx_10_13_universal2.whl": "sha256:440eede837960000d74978f0eba527be106b5b9aee0daf779d395276ed0b0614", + "https://files.pythonhosted.org/packages/b3/46/03ddc7da576d814fe0a36dd1f0fd3258e95404b4b2e3c026b7923d7e133f/charset_normalizer-3.4.9-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl": "sha256:304b13570067b2547562e308af560b3963857b1fa90bd6afd978130130fe2d6a", + "https://files.pythonhosted.org/packages/bd/2a/23f34ec9d04624958e137efdc394888716353190e75f25dd22c7a2c7a8aa/charset_normalizer-3.4.9.tar.gz": "sha256:673611bbd43f0810bec0b0f028ddeaaa501190339cac411f347ac76917c3ae7b", + "https://files.pythonhosted.org/packages/c3/69/2a5385192e67175f7d8bd5ce4f57c24bc956439adeae5c13a99aa28a53d1/charset_normalizer-3.4.9-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl": "sha256:2a441ea71902098ffe78c5abe6c494f44160b4af614ed16c3d9a3b1d17fd8ee2", + "https://files.pythonhosted.org/packages/c4/02/c57a22739fe05246b0b5783b3bfb6afaac4eebb46f3ececdfb2f048f780e/charset_normalizer-3.4.9-cp310-cp310-win32.whl": "sha256:432786d3561e69aeeae6c7e8648964ce0ad05736120135601f87ac26b9c83381", + "https://files.pythonhosted.org/packages/c8/53/a8c042eb9eee4716f4d42a0f5a571eb32a09ec429be9fb0b8b9d765393ba/charset_normalizer-3.4.9-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl": "sha256:68ce9f4d6b26d5ccbf7fd4459bf75f74a0a146677ebba80597df60cbdb20e6f4", + "https://files.pythonhosted.org/packages/cb/32/2e64bd2be10e89c61e57ebe6a93fd98ae88eb7ebe414b5121f22c96c69eb/charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl": "sha256:cc1b0fff8ead343dae06305f954eb8468ba0ec1a97881f42489d198e4ce3c632", + "https://files.pythonhosted.org/packages/cd/91/7253a32e86b7e1d1239b1b36ba6dd0f021a21107ab33054b53119cc083b9/charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl": "sha256:51447e9aa2684679af07ca5021c3db526e0284347ebf4ffcec1154c3350cfe32", + "https://files.pythonhosted.org/packages/d0/39/8ff066c672434225f8d25f8b739f992af250944392173dcc88362681c9bf/charset_normalizer-3.4.9-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl": "sha256:21e764fd1e70b6a3e205a0e46f3051701f98a8cb3fad66eeb80e48bb502f8698", + "https://files.pythonhosted.org/packages/d0/a3/53ddae3db108a088156aa8ddfafd411ebbc1340f48c5573f697b27f69a39/charset_normalizer-3.4.9-cp313-cp313-win32.whl": "sha256:51307f5c71007673a2bf8232ad973483d281e74cb99c8c5a990af1eefa6277d9", + "https://files.pythonhosted.org/packages/d4/ce/9af95f7876194bd7a14e3dfe4a4de2e0bff02666a3910d72beafd06cc297/charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl": "sha256:df115d4d83168fdf2cae48ef1ff6d1cb4c466364e30861b37121de0f3bf1b990", + "https://files.pythonhosted.org/packages/d7/74/3c12f9755717dfe5c5c87da63f35d765fa0c00382ec26bf23f7fae34f2ba/charset_normalizer-3.4.9-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl": "sha256:9cdef90ae47919cae358d8ab15797a800ed41da7aba5d72419fb510729e2ed4b", + "https://files.pythonhosted.org/packages/d9/8d/feabb82cb49fcad14515b1d7d1ca4787b0da7fc723a212bf89bc9e0fac52/charset_normalizer-3.4.9-cp311-cp311-musllinux_1_2_aarch64.whl": "sha256:67830fc78e67501f47bb950471b2dcb9b35b140084429318e862895a8e89c993", + "https://files.pythonhosted.org/packages/de/d1/b4319dc3229d8272fba305e206fc0a148e2de8d4087917ce62ae6382f359/charset_normalizer-3.4.9-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl": "sha256:aa99adc8f081b475a12843953db36831eaf83ec33eb46a90629ca6a5de45a616", + "https://files.pythonhosted.org/packages/e4/56/6c745619ac397e8871e2bcd3cea1eec86b877488f33888b3aef5c3ed506e/charset_normalizer-3.4.9-cp314-cp314t-musllinux_1_2_aarch64.whl": "sha256:83aed2c10721ddd90f68140685391b50811a880af20654c59af6b6c66c40513c", + "https://files.pythonhosted.org/packages/e8/5f/b98b8da398637b551e427e7be922bdec19177dc54d6811dcdaa503f23aac/charset_normalizer-3.4.9-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl": "sha256:9bb41182d93ea91f60b4bc8fbf4c820c69ef8a12ab2d917f3f1834f1acad07e8", + "https://files.pythonhosted.org/packages/e8/ef/6953a77c7cf2c2ff9998e6f575ab3e380119f100223381565a4f94c1f836/charset_normalizer-3.4.9-cp313-cp313-win_amd64.whl": "sha256:fe2c7201c642b7c308f1675355ad7ff7b66acfe3541625efe5a3ad38f29d6115", + "https://files.pythonhosted.org/packages/ea/f8/72eb13dcabe7257035cea8aefd922caad2f110d252bf9f67c4c2ca763aee/charset_normalizer-3.4.9-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl": "sha256:84fd18bcc17526fc2b3c1af7d2b9217d32c9c04448c16ec693b9b4f1985c3d33", + "https://files.pythonhosted.org/packages/eb/78/59344ff9a4a7b5f6530bf7bec2c980047cc42c3a616596cdbd8cb5c1a1af/charset_normalizer-3.4.9-cp39-cp39-musllinux_1_2_x86_64.whl": "sha256:43b9e366a31fdd1c87d0eb08f579b4a82b723ea54338f040d6b4e518a026ea29", + "https://files.pythonhosted.org/packages/ec/96/5d9364e3342d69f3a045e1777bc47c85c383e6e9466d561b33fdb419d1f9/charset_normalizer-3.4.9-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl": "sha256:9b2aff1c7b3884512b9512c3eaadd9bab39fb45042ffaaa1dd08ff2b9f8109d9", + "https://files.pythonhosted.org/packages/ec/b4/ef5a49b2e77c00deb43bb3256592b115ba9e4346016e82c516b8d215bf68/charset_normalizer-3.4.9-cp39-cp39-manylinux_2_31_armv7l.whl": "sha256:231ddcbb35e2ff8973e1365db41fe0572662893b99a05deb183b68ad4c0c8bd4", + "https://files.pythonhosted.org/packages/ed/61/710738687f90d01c06a04ed52d6ca1e62dd9b1d8cc2567098167c4691034/charset_normalizer-3.4.9-cp39-cp39-musllinux_1_2_aarch64.whl": "sha256:0fa1aec2d32bcc03c8fa0f6f1712caad1adc38509f31142112e5c9daf5b9c833", + "https://files.pythonhosted.org/packages/ed/a1/e29995109e455dc8eff8d0fac6ae509be39561318a7cfeac5d33ad029213/charset_normalizer-3.4.9-cp311-cp311-win_amd64.whl": "sha256:6366a16e1a25018694d6a5d784d09b046edc9eac40ea2b54065c3052672516a1", + "https://files.pythonhosted.org/packages/ee/f0/f1c4fe746c395922961b5916ed1d7d6e7d4c84851d19ed43cc89980ec953/charset_normalizer-3.4.9-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl": "sha256:32286a2c8d167e897177b673176c1e3e00d4057caf5d2b64eef9a3666b03018e", + "https://files.pythonhosted.org/packages/f0/e6/0386d43a261ff4e4b30c5857af7df877254b46bec7b9d1b74b6bf969a90b/charset_normalizer-3.4.9-cp312-cp312-win_arm64.whl": "sha256:78fa18e436a1a0e58dbd7e02fc4473f3f32cceb12df9dfca542d075961c307d2", + "https://files.pythonhosted.org/packages/f1/60/b22cdbee7e4013dab8b0d7647fc6181120fbbbc8f7025c226d15bd5a47fc/charset_normalizer-3.4.9-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl": "sha256:bd47ba7fc3ca94896759ea0109775132d3e7ab921fbf54038e1bab2e46c313c9", + "https://files.pythonhosted.org/packages/f1/78/c9c71d599f5aa2d42bcdd35cbbd46d7f535351a57e40ff7d8e5a7e219401/charset_normalizer-3.4.9-cp314-cp314-musllinux_1_2_armv7l.whl": "sha256:d4d6fcde76f94f5cb9e43e9e9a61f16dacefd228cbbf6f1a09bd9b219a92f1a1", + "https://files.pythonhosted.org/packages/f4/c4/b3e049d2aa3766180c78507110543d9d50894cc97f57de543f1be521dcdc/charset_normalizer-3.4.9-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl": "sha256:c25fe15c70c59eb7c5ce8c06a1f3fa1da0ecc5ea1e7a5922c40fd2fa9b0d5046", + "https://files.pythonhosted.org/packages/f6/39/c914445c321a845097ce4f6ac7de9a18228a77b766272125a1ce00d851eb/charset_normalizer-3.4.9-cp314-cp314-musllinux_1_2_x86_64.whl": "sha256:898f0e9068ca27d37f8e83a5b962821df851532e6c4a7d615c1c033f9da6eedf", + "https://files.pythonhosted.org/packages/f7/40/9593d54209765207a7f11073c06494c1721e4ca4a0a426c597679bf7f91e/charset_normalizer-3.4.9-cp314-cp314t-musllinux_1_2_x86_64.whl": "sha256:ee2f2a527e3c1a6e6411eb4209642e138b544a2d72fe5d0d76daf77b24063534" }, "cryptography": { - "https://files.pythonhosted.org/packages/03/11/5e395f961d6868269835dee1bafec6a1ac176505a167f68b7d8818431068/cryptography-46.0.7-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl": "ebd6daf519b9f189f85c479427bbd6e9c9037862cf8fe89ee35503bd209ed902", - "https://files.pythonhosted.org/packages/0b/5d/4a8f770695d73be252331e60e526291e3df0c9b27556a90a6b47bccca4c2/cryptography-46.0.7-cp311-abi3-macosx_10_9_universal2.whl": "ea42cbe97209df307fdc3b155f1b6fa2577c0defa8f1f7d3be7d31d189108ad4", - "https://files.pythonhosted.org/packages/0f/54/6bbbfc5efe86f9d71041827b793c24811a017c6ac0fd12883e4caa86b8ed/cryptography-46.0.7-cp311-abi3-manylinux_2_28_ppc64le.whl": "cbd5fb06b62bd0721e1170273d3f4d5a277044c47ca27ee257025146c34cbdd1", - "https://files.pythonhosted.org/packages/10/f2/19ceb3b3dc14009373432af0c13f46aa08e3ce334ec6eff13492e1812ccd/cryptography-46.0.7-cp311-abi3-musllinux_1_2_x86_64.whl": "5d1c02a14ceb9148cc7816249f64f623fbfee39e8c03b3650d842ad3f34d637e", - "https://files.pythonhosted.org/packages/16/01/0cd51dd86ab5b9befe0d031e276510491976c3a80e9f6e31810cce46c4ad/cryptography-46.0.7-cp38-abi3-manylinux_2_31_armv7l.whl": "cdfbe22376065ffcf8be74dc9a909f032df19bc58a699456a21712d6e5eabfd0", - "https://files.pythonhosted.org/packages/1a/bb/a5c213c19ee94b15dfccc48f363738633a493812687f5567addbcbba9f6f/cryptography-46.0.7-cp311-abi3-win32.whl": "d23c8ca48e44ee015cd0a54aeccdf9f09004eba9fc96f38c911011d9ff1bd457", - "https://files.pythonhosted.org/packages/20/2a/1b016902351a523aa2bd446b50a5bc1175d7a7d1cf90fe2ef904f9b84ebc/cryptography-46.0.7-pp311-pypy311_pp73-win_amd64.whl": "258514877e15963bd43b558917bc9f54cf7cf866c38aa576ebf47a77ddbc43a4", - "https://files.pythonhosted.org/packages/28/17/b59a741645822ec6d04732b43c5d35e4ef58be7bfa84a81e5ae6f05a1d33/cryptography-46.0.7-cp314-cp314t-musllinux_1_2_aarch64.whl": "fcd8eac50d9138c1d7fc53a653ba60a2bee81a505f9f8850b6b2888555a45d0e", - "https://files.pythonhosted.org/packages/2b/02/7788f9fefa1d060ca68717c3901ae7fffa21ee087a90b7f23c7a603c32ae/cryptography-46.0.7-cp311-abi3-win_amd64.whl": "397655da831414d165029da9bc483bed2fe0e75dde6a1523ec2fe63f3c46046b", - "https://files.pythonhosted.org/packages/2d/cf/054b9d8220f81509939599c8bdbc0c408dbd2bdd41688616a20731371fe0/cryptography-46.0.7-cp311-abi3-manylinux_2_28_x86_64.whl": "420b1e4109cc95f0e5700eed79908cef9268265c773d3a66f7af1eef53d409ef", - "https://files.pythonhosted.org/packages/32/a8/9f0e4ed57ec9cebe506e58db11ae472972ecb0c659e4d52bbaee80ca340a/cryptography-46.0.7-cp314-cp314t-win_amd64.whl": "e06acf3c99be55aa3b516397fe42f5855597f430add9c17fa46bf2e0fb34c9bb", - "https://files.pythonhosted.org/packages/36/5f/313586c3be5a2fbe87e4c9a254207b860155a8e1f3cca99f9910008e7d08/cryptography-46.0.7-cp311-abi3-manylinux_2_34_aarch64.whl": "8a469028a86f12eb7d2fe97162d0634026d92a21f3ae0ac87ed1c4a447886c83", - "https://files.pythonhosted.org/packages/3a/ea/075aac6a84b7c271578d81a2f9968acb6e273002408729f2ddff517fed4a/cryptography-46.0.7-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl": "d3b99c535a9de0adced13d159c5a9cf65c325601aa30f4be08afd680643e9c15", - "https://files.pythonhosted.org/packages/3d/4c/7d258f169ae71230f25d9f3d06caabcff8c3baf0978e2b7d65e0acac3827/cryptography-46.0.7-cp314-cp314t-manylinux_2_31_armv7l.whl": "60627cf07e0d9274338521205899337c5d18249db56865f943cbe753aa96f40f", - "https://files.pythonhosted.org/packages/40/53/8ed1cf4c3b9c8e611e7122fb56f1c32d09e1fff0f1d77e78d9ff7c82653e/cryptography-46.0.7-cp314-cp314t-manylinux_2_28_aarch64.whl": "b7b412817be92117ec5ed95f880defe9cf18a832e8cafacf0a22337dc1981b4d", - "https://files.pythonhosted.org/packages/41/3d/fe14df95a83319af25717677e956567a105bb6ab25641acaa093db79975d/cryptography-46.0.7-cp314-cp314t-manylinux_2_34_ppc64le.whl": "c5b1ccd1239f48b7151a65bc6dd54bcfcc15e028c8ac126d3fada09db0e07ef1", - "https://files.pythonhosted.org/packages/41/52/a8908dcb1a389a459a29008c29966c1d552588d4ae6d43f3a1a4512e0ebe/cryptography-46.0.7-cp38-abi3-musllinux_1_2_x86_64.whl": "a1529d614f44b863a7b480c6d000fe93b59acee9c82ffa027cfadc77521a9f5e", - "https://files.pythonhosted.org/packages/47/93/ac8f3d5ff04d54bc814e961a43ae5b0b146154c89c61b47bb07557679b18/cryptography-46.0.7.tar.gz": "e4cfd68c5f3e0bfdad0d38e023239b96a2fe84146481852dffbcca442c245aa5", - "https://files.pythonhosted.org/packages/4a/9a/1765afe9f572e239c3469f2cb429f3ba7b31878c893b246b4b2994ffe2fe/cryptography-46.0.7-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl": "5ad9ef796328c5e3c4ceed237a183f5d41d21150f972455a9d926593a1dcb308", - "https://files.pythonhosted.org/packages/4b/fa/f0ab06238e899cc3fb332623f337a7364f36f4bb3f2534c2bb95a35b132c/cryptography-46.0.7-cp38-abi3-win32.whl": "f247c8c1a1fb45e12586afbb436ef21ff1e80670b2861a90353d9b025583d246", - "https://files.pythonhosted.org/packages/50/46/cf71e26025c2e767c5609162c866a78e8a2915bbcfa408b7ca495c6140c4/cryptography-46.0.7-cp314-cp314t-manylinux_2_28_ppc64le.whl": "fbfd0e5f273877695cb93baf14b185f4878128b250cc9f8e617ea0c025dfb022", - "https://files.pythonhosted.org/packages/59/6a/bb2e166d6d0e0955f1e9ff70f10ec4b2824c9cfcdb4da772c7dd69cc7d80/cryptography-46.0.7-cp314-cp314t-musllinux_1_2_x86_64.whl": "65814c60f8cc400c63131584e3e1fad01235edba2614b61fbfbfa954082db0ee", - "https://files.pythonhosted.org/packages/5f/45/6d80dc379b0bbc1f9d1e429f42e4cb9e1d319c7a8201beffd967c516ea01/cryptography-46.0.7-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl": "b36a4695e29fe69215d75960b22577197aca3f7a25b9cf9d165dcfe9d80bc325", - "https://files.pythonhosted.org/packages/63/0c/dca8abb64e7ca4f6b2978769f6fea5ad06686a190cec381f0a796fdcaaba/cryptography-46.0.7-pp311-pypy311_pp73-macosx_11_0_arm64.whl": "fc9ab8856ae6cf7c9358430e49b368f3108f050031442eaeb6b9d87e4dcf4e4f", - "https://files.pythonhosted.org/packages/69/33/60dfc4595f334a2082749673386a4d05e4f0cf4df8248e63b2c3437585f2/cryptography-46.0.7-cp311-abi3-manylinux_2_34_ppc64le.whl": "9694078c5d44c157ef3162e3bf3946510b857df5a3955458381d1c7cfc143ddb", - "https://files.pythonhosted.org/packages/6c/7b/1c55db7242b5e5612b29fc7a630e91ee7a6e3c8e7bf5406d22e206875fbd/cryptography-46.0.7-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl": "d02c738dacda7dc2a74d1b2b3177042009d5cab7c7079db74afc19e56ca1b455", - "https://files.pythonhosted.org/packages/74/66/e3ce040721b0b5599e175ba91ab08884c75928fbeb74597dd10ef13505d2/cryptography-46.0.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl": "db0f493b9181c7820c8134437eb8b0b4792085d37dbb24da050476ccb664e59c", - "https://files.pythonhosted.org/packages/7b/56/15619b210e689c5403bb0540e4cb7dbf11a6bf42e483b7644e471a2812b3/cryptography-46.0.7-cp314-cp314t-macosx_10_9_universal2.whl": "d151173275e1728cf7839aaa80c34fe550c04ddb27b34f48c232193df8db5842", - "https://files.pythonhosted.org/packages/80/07/ad9b3c56ebb95ed2473d46df0847357e01583f4c52a85754d1a55e29e4d0/cryptography-46.0.7-cp38-abi3-manylinux_2_34_ppc64le.whl": "935ce7e3cfdb53e3536119a542b839bb94ec1ad081013e9ab9b7cfd478b05006", - "https://files.pythonhosted.org/packages/8a/6c/1a42450f464dda6ffbe578a911f773e54dd48c10f9895a23a7e88b3e7db5/cryptography-46.0.7-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl": "128c5edfe5e5938b86b03941e94fac9ee793a94452ad1365c9fc3f4f62216832", - "https://files.pythonhosted.org/packages/8f/3e/af9246aaf23cd4ee060699adab1e47ced3f5f7e7a8ffdd339f817b446462/cryptography-46.0.7-cp311-abi3-manylinux_2_28_aarch64.whl": "73510b83623e080a2c35c62c15298096e2a5dc8d51c3b4e1740211839d0dea77", - "https://files.pythonhosted.org/packages/92/49/819d6ed3a7d9349c2939f81b500a738cb733ab62fbecdbc1e38e83d45e12/cryptography-46.0.7-cp38-abi3-manylinux_2_34_aarch64.whl": "abad9dac36cbf55de6eb49badd4016806b3165d396f64925bf2999bcb67837ba", - "https://files.pythonhosted.org/packages/95/b6/3da51d48415bcb63b00dc17c2eff3a651b7c4fed484308d0f19b30e8cb2c/cryptography-46.0.7-cp314-cp314t-win32.whl": "fdd1736fed309b4300346f88f74cd120c27c56852c3838cab416e7a166f67298", - "https://files.pythonhosted.org/packages/9a/92/4ed714dbe93a066dc1f4b4581a464d2d7dbec9046f7c8b7016f5286329e2/cryptography-46.0.7-cp38-abi3-manylinux_2_28_aarch64.whl": "5e51be372b26ef4ba3de3c167cd3d1022934bc838ae9eaad7e644986d2a3d163", - "https://files.pythonhosted.org/packages/9c/59/4a479e0f36f8f378d397f4eab4c850b4ffb79a2f0d58704b8fa0703ddc11/cryptography-46.0.7-cp314-cp314t-manylinux_2_34_x86_64.whl": "d5f7520159cd9c2154eb61eb67548ca05c5774d39e9c2c4339fd793fe7d097b2", - "https://files.pythonhosted.org/packages/a5/d0/36a49f0262d2319139d2829f773f1b97ef8aef7f97e6e5bd21455e5a8fb5/cryptography-46.0.7-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl": "84d4cced91f0f159a7ddacad249cc077e63195c36aac40b4150e7a57e84fffe7", - "https://files.pythonhosted.org/packages/a5/ef/649750cbf96f3033c3c976e112265c33906f8e462291a33d77f90356548c/cryptography-46.0.7-cp38-abi3-musllinux_1_2_aarch64.whl": "7bbc6ccf49d05ac8f7d7b5e2e2c33830d4fe2061def88210a126d130d7f71a85", - "https://files.pythonhosted.org/packages/a7/7f/cd42fc3614386bc0c12f0cb3c4ae1fc2bbca5c9662dfed031514911d513d/cryptography-46.0.7-cp38-abi3-macosx_10_9_universal2.whl": "462ad5cb1c148a22b2e3bcc5ad52504dff325d17daf5df8d88c17dda1f75f2a4", - "https://files.pythonhosted.org/packages/b5/2a/2ea0767cad19e71b3530e4cad9605d0b5e338b6a1e72c37c9c1ceb86c333/cryptography-46.0.7-cp314-cp314t-manylinux_2_34_aarch64.whl": "80406c3065e2c55d7f49a9550fe0c49b3f12e5bfff5dedb727e319e1afb9bf99", - "https://files.pythonhosted.org/packages/b7/e6/a26b84096eddd51494bba19111f8fffe976f6a09f132706f8f1bf03f51f7/cryptography-46.0.7-cp38-abi3-manylinux_2_28_ppc64le.whl": "cdf1a610ef82abb396451862739e3fc93b071c844399e15b90726ef7470eeaf2", - "https://files.pythonhosted.org/packages/b8/c7/201d3d58f30c4c2bdbe9b03844c291feb77c20511cc3586daf7edc12a47b/cryptography-46.0.7-cp38-abi3-manylinux_2_34_x86_64.whl": "35719dc79d4730d30f1c2b6474bd6acda36ae2dfae1e3c16f2051f215df33ce0", - "https://files.pythonhosted.org/packages/c0/ea/01276740375bac6249d0a971ebdf6b4dc9ead0ee0a34ef3b5a88c1a9b0d4/cryptography-46.0.7-cp314-cp314t-manylinux_2_28_x86_64.whl": "ffca7aa1d00cf7d6469b988c581598f2259e46215e0140af408966a24cf086ce", - "https://files.pythonhosted.org/packages/c7/08/ffd537b605568a148543ac3c2b239708ae0bd635064bab41359252ef88ed/cryptography-46.0.7-cp38-abi3-manylinux_2_28_x86_64.whl": "1d25aee46d0c6f1a501adcddb2d2fee4b979381346a78558ed13e50aa8a59067", - "https://files.pythonhosted.org/packages/c7/0b/333ddab4270c4f5b972f980adef4faa66951a4aaf646ca067af597f15563/cryptography-46.0.7-cp311-abi3-manylinux_2_34_x86_64.whl": "42a1e5f98abb6391717978baf9f90dc28a743b7d9be7f0751a6f56a75d14065b", - "https://files.pythonhosted.org/packages/cb/da/9870eec4b69c63ef5925bf7d8342b7e13bc2ee3d47791461c4e49ca212f4/cryptography-46.0.7-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl": "04959522f938493042d595a736e7dbdff6eb6cc2339c11465b3ff89343b65f65", - "https://files.pythonhosted.org/packages/d2/14/633913398b43b75f1234834170947957c6b623d1701ffc7a9600da907e89/cryptography-46.0.7-cp311-abi3-musllinux_1_2_aarch64.whl": "91bbcb08347344f810cbe49065914fe048949648f6bd5c2519f34619142bbe85", - "https://files.pythonhosted.org/packages/d2/f1/00ce3bde3ca542d1acd8f8cfa38e446840945aa6363f9b74746394b14127/cryptography-46.0.7-cp38-abi3-win_amd64.whl": "506c4ff91eff4f82bdac7633318a526b1d1309fc07ca76a3ad182cb5b686d6d3", - "https://files.pythonhosted.org/packages/f4/72/05aa5832b82dd341969e9a734d1812a6aadb088d9eb6f0430fc337cc5a8f/cryptography-46.0.7-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl": "3986ac1dee6def53797289999eabe84798ad7817f3e97779b5061a95b0ee4968", - "https://files.pythonhosted.org/packages/f9/46/4e4e9c6040fb01c7467d47217d2f882daddeb8828f7df800cb806d8a2288/cryptography-46.0.7-cp311-abi3-manylinux_2_31_armv7l.whl": "24402210aa54baae71d99441d15bb5a1919c195398a87b563df84468160a65de" + "https://files.pythonhosted.org/packages/09/41/3797cfaf69cae04a13ee78ebd83f0678d9c02b4779d21ce24445326f1a69/cryptography-49.0.0-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl": "sha256:36d1709f992593689b45bda411498d62c6e365f2ca00b84657d4dadd24de16db", + "https://files.pythonhosted.org/packages/11/2d/5e1fb307cb5931881516b464c98774b3f2c36b5d4bb9a2830253cf553cad/cryptography-49.0.0-cp314-cp314t-manylinux_2_34_x86_64.whl": "sha256:d8ecde755e2e91bf773fc94e8c9d730cd7f2007004cb492263a794ec3899a1c8", + "https://files.pythonhosted.org/packages/17/50/983e838c7fd0d87fd8c969bcdd328edaf5f756e38df5281637424c155873/cryptography-49.0.0-cp314-cp314t-manylinux_2_31_armv7l.whl": "sha256:07cab27cc7b7e0fd28e5e26bb9eeedde5c135c868b46de4a27845abe94af6122", + "https://files.pythonhosted.org/packages/19/2a/5bb823f5bedcf80718cea7fbc95ec5515cca3769633c4b01a32be7f30e7c/cryptography-49.0.0-cp39-abi3-macosx_11_0_arm64.whl": "sha256:ec5e529fb80935c94fe7b729f9972b50e351a0e6b50aa294fd5cabb109fcc29a", + "https://files.pythonhosted.org/packages/1f/09/f42b1d190c5ba75f72062a387f8030d1d75f6ab035788f1d9c4b01de6525/cryptography-49.0.0-cp311-abi3-win_amd64.whl": "sha256:e5dfc1e64de5677cec922ffa8da89c546d0415bf6efdf081842e5d44c84e1f0e", + "https://files.pythonhosted.org/packages/1f/99/d1c90d6041656cc6ee229dc99cd67fd0cd5aec3c5f7d72fffc27cc750054/cryptography-49.0.0.tar.gz": "sha256:f89660a348f4f78a92366240a61404e337586ef7f5909a2fef59ca88ef505493", + "https://files.pythonhosted.org/packages/20/2c/0622f20ff02b2ef32558733443805dc82fd4c275be01b2d19d14676f3a1b/cryptography-49.0.0-cp311-abi3-manylinux_2_28_x86_64.whl": "sha256:2afe9051da7ae7bd5905da5a949280c7d2bb75682e188f650a9d0f2756b834c6", + "https://files.pythonhosted.org/packages/24/01/186c825898477d77e2324d5360fefe622ff1d8d1963ec0554e2cada8ec77/cryptography-49.0.0-cp39-abi3-manylinux_2_28_ppc64le.whl": "sha256:9e82dcc8e56052715fb18b2429e3bca4823b1629136a2084fc45a9a5cecb9b64", + "https://files.pythonhosted.org/packages/2c/99/2d13299eb3dd27b02dcfaafcc91d6b5cb3329f7cbd6d8f51921acd566c1a/cryptography-49.0.0-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl": "sha256:35b151772baff2c74cba7fa290ceaff4c3b11c0c881eb93eb5dbc05a7cfbba18", + "https://files.pythonhosted.org/packages/3d/df/40577043ca124e17012f408ddddaeb213b856336ac82ddb3bc915f39e29f/cryptography-49.0.0-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl": "sha256:f78ff2c9ed8dc2d036b0f4d640e22522213d047c1b14e61205a7e55c80a494d4", + "https://files.pythonhosted.org/packages/47/f1/1d3eaa243bfc5de4a187b22aa8c048b3e4980bfbe830ac46e6bac2e66947/cryptography-49.0.0-cp314-cp314t-manylinux_2_28_ppc64le.whl": "sha256:f37d847238971164fdbc68ade6f6574aecc9c0af714190e2083429ff68f4ce9d", + "https://files.pythonhosted.org/packages/4a/91/01ce7303a4579e6d3a6abef01bd322848e9ea7a219adcabc5048b9033571/cryptography-49.0.0-cp311-abi3-manylinux_2_28_aarch64.whl": "sha256:53ecee2e23f7169b6117e99fc8a944e5e50f79e69758a83b52a00cb98ab2b2d2", + "https://files.pythonhosted.org/packages/4c/fe/93ecac273d3738939d023612ad12cca9a3740a5345d69fda04134c43fd96/cryptography-49.0.0-cp314-cp314t-win_amd64.whl": "sha256:33cd0565932807baddb67b96dbee92f2c374b5c89dee09fd74079aeb8c8dba61", + "https://files.pythonhosted.org/packages/4f/01/339573cf1023163a400b0b5d16f6d507de413b9f60be6fd1b77feeaf6737/cryptography-49.0.0-cp311-abi3-musllinux_1_2_aarch64.whl": "sha256:b87e65d263b3e5d3bb92a57e2a6638e2f31110fa7aa890c7b2dbba42248d0a3f", + "https://files.pythonhosted.org/packages/58/39/2d51306721330c486495853eda1c567880ff036de15a14c4b74f399934af/cryptography-49.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl": "sha256:c2bc30226390d60ea19d9f82b19db005fe0452154a23c1c410c12ea801e43561", + "https://files.pythonhosted.org/packages/62/99/a2c95cf8293f07491e9e27c20cc4dcd18176d944e674679adeb1d0173fd6/cryptography-49.0.0-cp311-abi3-manylinux_2_28_ppc64le.whl": "sha256:2eda353d8a27bcbcaa4cbed18994a74ab4d19a2ca897db188ea269ab9b71419b", + "https://files.pythonhosted.org/packages/63/d3/4a83af35d65e3fad632c926fad684c193ea4398569ccb0bbbc7fe8f5dc9a/cryptography-49.0.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl": "sha256:fc1e275c2f1d97b1a6450b8b0ea3ebfa6e087a611c2b26cb2404d48588abab7b", + "https://files.pythonhosted.org/packages/67/d0/a5fcd3515f0bae49a7b6d0413cc1bdccdcc1fc0047037a0d480642cdc5d6/cryptography-49.0.0-cp39-abi3-manylinux_2_34_ppc64le.whl": "sha256:6fc361c34fb6aac015ce19435876635e5c6d21db31998b0920f675f131e043b8", + "https://files.pythonhosted.org/packages/68/28/8a3ad4653662c93fc44dc4e5d8fd374c25c42e07b34bbfbadf49cf57a5a8/cryptography-49.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl": "sha256:7abcee80084cda3f7691f3eb1ce480d8df49cec637b429aa35986c1de71738aa", + "https://files.pythonhosted.org/packages/6c/72/3e798c064bc39e471008075d0f9bc9daf77a80879c092e4a8e170c585ed4/cryptography-49.0.0-cp39-abi3-manylinux_2_31_armv7l.whl": "sha256:8c25ceb16df5b9435f3f6a9829204985b0e0cbee3b48aacd432c7d2c850b44d9", + "https://files.pythonhosted.org/packages/6c/a0/db537264e234f7273a73ec020873d6d6b39dfd8a53db78b550ca8320440e/cryptography-49.0.0-cp39-abi3-musllinux_1_2_aarch64.whl": "sha256:67e1d20ad9ef3a563c59ef22e7a8a0b8210bd26604369ea4a30a7c66aefe504e", + "https://files.pythonhosted.org/packages/6d/88/05563c7fe2e914e87d1a536d06fe83e66b4e1d95cb593e05aea375531da8/cryptography-49.0.0-cp311-abi3-manylinux_2_34_aarch64.whl": "sha256:ccac2bfebc306b862133e3bb71f3f6ee8bb525240089b2d952e4144b3a6d5da7", + "https://files.pythonhosted.org/packages/71/fd/577302e213a1be9468f92d1afef66fcf1ef83d516819d9992ca547f592bd/cryptography-49.0.0-cp311-abi3-musllinux_1_2_x86_64.whl": "sha256:66ec79c3904820572d7e987abdf304281f141d37ad9a489b8e97066e7b9b6459", + "https://files.pythonhosted.org/packages/86/12/c48a424f38db03027be9f7ed5c7dc5de9933dbee992865f98b13727a009d/cryptography-49.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl": "sha256:196ecd6a36e4e9aa10270393bb98d8df88fccee0bf1e5128b91ae4eb4375896d", + "https://files.pythonhosted.org/packages/93/77/8df9eb486495979bccecd1062e2eaf435250e84437040295b57d09048b0b/cryptography-49.0.0-cp39-abi3-musllinux_1_2_x86_64.whl": "sha256:42b0684e0e40cf26122427802486f6d93aea593612603a94fbf260c7eb1e9c1b", + "https://files.pythonhosted.org/packages/94/64/2923570ac1c0bd3a737aa366ac3abbbbde273042308b8cde95e2364a6e6a/cryptography-49.0.0-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl": "sha256:b47db11c2c3525083296069b98ac5221907455e989ae0c2e3008bde851921615", + "https://files.pythonhosted.org/packages/9b/22/adf66990e63584a68dfb50c24f48a125c07b1699899381c8151e63ed458c/cryptography-49.0.0-cp311-abi3-macosx_11_0_arm64.whl": "sha256:966fe0e9c67490071f14c0d2b1cb2dfb3023c5ce39457343931415f08382f2db", + "https://files.pythonhosted.org/packages/a0/84/84fe36f19caf857d61cb7fc9c63035a47ffabd84ea12d1d393148efa3615/cryptography-49.0.0-cp39-abi3-manylinux_2_34_x86_64.whl": "sha256:2400ef9c9e2299a25614eb1dea3db54a69b1349efd043bfac9c67630d136df36", + "https://files.pythonhosted.org/packages/a3/5b/c5246635d5fd3b64e0d45ae10e99fd32fe9676a79915ccfe5a61ba9af1a5/cryptography-49.0.0-cp311-abi3-manylinux_2_31_armv7l.whl": "sha256:0b82e28ee398a386f0807bba7884d30f25218855690f45115831bcce5d90822c", + "https://files.pythonhosted.org/packages/a5/4d/9c0cd02f95e2602dd5e563da149ee0830abef3537be8b34dc56281ebe27a/cryptography-49.0.0-cp39-abi3-manylinux_2_28_aarch64.whl": "sha256:0f21641cf4b30fca7aee061ced0ec7ad7b073518088b7c9969a297c0ae796c69", + "https://files.pythonhosted.org/packages/a7/f5/8f571d7e27c55bce9f76f026143bcb1e040a4233149ecca0bea5fa5dd5f7/cryptography-49.0.0-cp314-cp314t-manylinux_2_34_aarch64.whl": "sha256:b20133d204d2bb56ba047642199603876c872026ca53e79c35b83772ab2cc505", + "https://files.pythonhosted.org/packages/a8/b2/2193fc74f81aee4f9b62733133b73b5176718932ed8f2e4b03fa040480a6/cryptography-49.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl": "sha256:4ae387c9cb68ea569ca17e490d66d8142b81c3cc814bf179974b7d146e490bbb", + "https://files.pythonhosted.org/packages/a9/3c/f3ad17eecc1a57b0ba236dc01f90e783c51f4a2f35f64777cc4f47a184b2/cryptography-49.0.0-cp311-abi3-manylinux_2_34_x86_64.whl": "sha256:cbc77da8c523d5abd028635ba850a6966fcee2c82e2bf65a41d1d8afe0f98be9", + "https://files.pythonhosted.org/packages/aa/50/a9caea39ad19c431c1a3f8a31114df65b260cdfe67786b6c7e7c040c4c44/cryptography-49.0.0-pp311-pypy311_pp73-win_amd64.whl": "sha256:be9fcb48a55f023493482827d4f459bd263cc20efde64f204b97c123201850c6", + "https://files.pythonhosted.org/packages/ab/f8/614dc7e051418cfe53d55173c1e24c6b0085e89996fe90508c2fdf769aef/cryptography-49.0.0-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl": "sha256:084ef1af862eb07ec46d25f68689f2102a9fc0e05ce7b80f14f5fe51e4eef0f6", + "https://files.pythonhosted.org/packages/b8/7b/62cbbab75d0659865bf0273790031544a0b16c8072d258f9428dcd8190dc/cryptography-49.0.0-cp39-abi3-manylinux_2_28_x86_64.whl": "sha256:6f2debedf9ca60cf1d5bd466475638af5130f89965605cd818484d19987d3a21", + "https://files.pythonhosted.org/packages/b9/26/814681d14248d95d73d5c3eea0c39a94eb8302df966f670a2c60de90974b/cryptography-49.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl": "sha256:32703d93296f5c1f4b53349ad3a250c2cae0fdecd3a3dd5d47e616d8d616af27", + "https://files.pythonhosted.org/packages/c2/e6/f60198ea8d9dfa15fff9ed4ca02ce362f6eadd9ba757dcc50634c4257b63/cryptography-49.0.0-cp39-abi3-win_amd64.whl": "sha256:026ac7423e6fa66872d3bf889be5974507da3944f866f704fa200eadacd00001", + "https://files.pythonhosted.org/packages/c4/b6/d7696e4e890d6ae1469935164c9e5215c557671cb78d6e3f458ccceaa632/cryptography-49.0.0-cp311-abi3-manylinux_2_34_ppc64le.whl": "sha256:d0527ce944105f257f605a827d6ebead966c752038b6e8656abb9c5edee6fc68", + "https://files.pythonhosted.org/packages/d6/a7/f9dac0ab7f80368c56993a7bf638ef9935f825c91902798481fac0898138/cryptography-49.0.0-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl": "sha256:c83782480a4a9da4d0feb51950131ba32e12e70813848b3343f6e18c28a66838", + "https://files.pythonhosted.org/packages/d7/70/2ba3769dd0ae167e2f33dfa9592d45db6ff9a61d62ca1a5b3d1bdd09068f/cryptography-49.0.0-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl": "sha256:b39efa323140595abd3ecca8529d321ae50f55f3aa3ba9cc81ea56a6011953d5", + "https://files.pythonhosted.org/packages/e4/c0/bff5a02ee731d207d6a1ed51732549d8c53d2bc8da1d10ec6f2844201d68/cryptography-49.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl": "sha256:e3fb64c420688e5319ae25113a354015abbd8dffbfbc41781a1ea66fc7622ac3", + "https://files.pythonhosted.org/packages/e6/8b/43011f7ebe515a8aa20d61f290a326cd890c2e738e16e59eaff8d9c3a412/cryptography-49.0.0-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl": "sha256:0e959b578856a3924bc0cbb710fc12c387b9412a951389f3ca61704a9e25f325", + "https://files.pythonhosted.org/packages/e7/84/0e27016a6fc5a0886f797018b26aa42f40c09a82332bff77822a451deaaa/cryptography-49.0.0-cp314-cp314t-manylinux_2_34_ppc64le.whl": "sha256:b970c6da94d5bb18629db453d14f2a1300f6bf59b61e9b82377931ef95504866", + "https://files.pythonhosted.org/packages/ec/9e/db72b3ae7fc9cfad53e630e56c6ae83b9b6ff0bf3718ffb8012d20b3aabf/cryptography-49.0.0-cp314-cp314t-macosx_11_0_arm64.whl": "sha256:73a205dce83953d131a4aa1e0fd917a2fd1c5b1eef251e9d7152efefcbf5caf7", + "https://files.pythonhosted.org/packages/f0/ee/6fca21d1ac73e06f8bef71940abfd4d2f6472b4bca284d770f32bd4086f6/cryptography-49.0.0-cp39-abi3-manylinux_2_34_aarch64.whl": "sha256:28d8b15e6275f12c8a207dc309dfa957903c927d08d0cc937ee3f63f200693cc" }, "docutils": { - "https://files.pythonhosted.org/packages/4a/c0/89fe6215b443b919cb98a5002e107cb5026854ed1ccb6b5833e0768419d1/docutils-0.22.2.tar.gz": "9fdb771707c8784c8f2728b67cb2c691305933d68137ef95a75db5f4dfbc213d", - "https://files.pythonhosted.org/packages/66/dd/f95350e853a4468ec37478414fc04ae2d61dad7a947b3015c3dcc51a09b9/docutils-0.22.2-py3-none-any.whl": "b0e98d679283fc3bb0ead8a5da7f501baa632654e7056e9c5846842213d674d8" + "https://files.pythonhosted.org/packages/32/91/30151a39f7570f448ed84529390628a651d7f27c87d73c9b887f8189695e/docutils-0.23-py3-none-any.whl": "sha256:25d013af9bf23bc1c7b2b093dff4208166c53a94786c9e447808335ef1185fea", + "https://files.pythonhosted.org/packages/39/a4/5180d9afc57e8fca05601dd652bdff19604c218814037fe90ffc7625a50a/docutils-0.23.tar.gz": "sha256:746f5060322511280a1e50eb76846ed6bf2342984b2ac04dc42caa1a8d78799e" + }, + "id": { + "https://files.pythonhosted.org/packages/42/77/de194443bf38daed9452139e960c632b0ef9f9a5dd9ce605fdf18ca9f1b1/id-1.6.1-py3-none-any.whl": "sha256:f5ec41ed2629a508f5d0988eda142e190c9c6da971100612c4de9ad9f9b237ca", + "https://files.pythonhosted.org/packages/6d/04/c2156091427636080787aac190019dc64096e56a23b7364d3c1764ee3a06/id-1.6.1.tar.gz": "sha256:d0732d624fb46fd4e7bc4e5152f00214450953b9e772c182c1c22964def1a069" }, "idna": { - "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl": "946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", - "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz": "12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9" + "https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl": "sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2", + "https://files.pythonhosted.org/packages/cd/63/9496c57188a2ee585e0f1db071d75089a11e98aa86eb99d9d7618fc1edce/idna-3.18.tar.gz": "sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848" }, "importlib-metadata": { - "https://files.pythonhosted.org/packages/20/b0/36bd937216ec521246249be3bf9855081de4c5e06a0c9b4219dbeda50373/importlib_metadata-8.7.0-py3-none-any.whl": "e5dd1551894c77868a30651cef00984d50e1002d06942a7101d34870c5f02afd", - "https://files.pythonhosted.org/packages/76/66/650a33bd90f786193e4de4b3ad86ea60b53c89b669a5c7be931fac31cdb0/importlib_metadata-8.7.0.tar.gz": "d13b81ad223b890aa16c5471f2ac3056cf76c5f10f82d6f9292f0b415f389000" + "https://files.pythonhosted.org/packages/38/3d/2d244233ac4f76e38533cfcb2991c9eb4c7bf688ae0a036d30725b8faafe/importlib_metadata-9.0.0-py3-none-any.whl": "sha256:2d21d1cc5a017bd0559e36150c21c830ab1dc304dedd1b7ea85d20f45ef3edd7", + "https://files.pythonhosted.org/packages/a9/01/15bb152d77b21318514a96f43af312635eb2500c96b55398d020c93d86ea/importlib_metadata-9.0.0.tar.gz": "sha256:a4f57ab599e6a2e3016d7595cfd72eb4661a5106e787a95bcc90c7105b831efc" }, "jaraco-classes": { - "https://files.pythonhosted.org/packages/06/c0/ed4a27bc5571b99e3cff68f8a9fa5b56ff7df1c2251cc715a652ddd26402/jaraco.classes-3.4.0.tar.gz": "47a024b51d0239c0dd8c8540c6c7f484be3b8fcf0b2d85c13825780d3b3f3acd", - "https://files.pythonhosted.org/packages/7f/66/b15ce62552d84bbfcec9a4873ab79d993a1dd4edb922cbfccae192bd5b5f/jaraco.classes-3.4.0-py3-none-any.whl": "f662826b6bed8cace05e7ff873ce0f9283b5c924470fe664fff1c2f00f581790" + "https://files.pythonhosted.org/packages/06/c0/ed4a27bc5571b99e3cff68f8a9fa5b56ff7df1c2251cc715a652ddd26402/jaraco.classes-3.4.0.tar.gz": "sha256:47a024b51d0239c0dd8c8540c6c7f484be3b8fcf0b2d85c13825780d3b3f3acd", + "https://files.pythonhosted.org/packages/7f/66/b15ce62552d84bbfcec9a4873ab79d993a1dd4edb922cbfccae192bd5b5f/jaraco.classes-3.4.0-py3-none-any.whl": "sha256:f662826b6bed8cace05e7ff873ce0f9283b5c924470fe664fff1c2f00f581790" }, "jaraco-context": { - "https://files.pythonhosted.org/packages/df/ad/f3777b81bf0b6e7bc7514a1656d3e637b2e8e15fab2ce3235730b3e7a4e6/jaraco_context-6.0.1.tar.gz": "9bae4ea555cf0b14938dc0aee7c9f32ed303aa20a3b73e7dc80111628792d1b3", - "https://files.pythonhosted.org/packages/ff/db/0c52c4cf5e4bd9f5d7135ec7669a3a767af21b3a308e1ed3674881e52b62/jaraco.context-6.0.1-py3-none-any.whl": "f797fc481b490edb305122c9181830a3a5b76d84ef6d1aef2fb9b47ab956f9e4" + "https://files.pythonhosted.org/packages/af/50/4763cd07e722bb6285316d390a164bc7e479db9d90daa769f22578f698b4/jaraco_context-6.1.2.tar.gz": "sha256:f1a6c9d391e661cc5b8d39861ff077a7dc24dc23833ccee564b234b81c82dfe3", + "https://files.pythonhosted.org/packages/f2/58/bc8954bda5fcda97bd7c19be11b85f91973d67a706ed4a3aec33e7de22db/jaraco_context-6.1.2-py3-none-any.whl": "sha256:bf8150b79a2d5d91ae48629d8b427a8f7ba0e1097dd6202a9059f29a36379535" }, "jaraco-functools": { - "https://files.pythonhosted.org/packages/b4/09/726f168acad366b11e420df31bf1c702a54d373a83f968d94141a8c3fde0/jaraco_functools-4.3.0-py3-none-any.whl": "227ff8ed6f7b8f62c56deff101545fa7543cf2c8e7b82a7c2116e672f29c26e8", - "https://files.pythonhosted.org/packages/f7/ed/1aa2d585304ec07262e1a83a9889880701079dde796ac7b1d1826f40c63d/jaraco_functools-4.3.0.tar.gz": "cfd13ad0dd2c47a3600b439ef72d8615d482cedcff1632930d6f28924d92f294" + "https://files.pythonhosted.org/packages/02/36/ecc85bc96c273dc8a11273ed4782272975e6338d4a3e9228621175edf0e3/jaraco_functools-4.6.0-py3-none-any.whl": "sha256:99e3dc0060c5cbe8fcd1cdb36258e2a65ca40f1566b2033b12abb1bb44dd3c30", + "https://files.pythonhosted.org/packages/6c/1f/c23395957d41ccf27c4e535c3d334c4051e5395b3752057ba4cbaec35c56/jaraco_functools-4.6.0.tar.gz": "sha256:880c577ec9720b3a052d5bc611fb9f2269b3d87902ef42440df443b88e443280" }, "jeepney": { - "https://files.pythonhosted.org/packages/7b/6f/357efd7602486741aa73ffc0617fb310a29b588ed0fd69c2399acbb85b0c/jeepney-0.9.0.tar.gz": "cf0e9e845622b81e4a28df94c40345400256ec608d0e55bb8a3feaa9163f5732", - "https://files.pythonhosted.org/packages/b2/a3/e137168c9c44d18eff0376253da9f1e9234d0239e0ee230d2fee6cea8e55/jeepney-0.9.0-py3-none-any.whl": "97e5714520c16fc0a45695e5365a2e11b81ea79bba796e26f9f1d178cb182683" + "https://files.pythonhosted.org/packages/7b/6f/357efd7602486741aa73ffc0617fb310a29b588ed0fd69c2399acbb85b0c/jeepney-0.9.0.tar.gz": "sha256:cf0e9e845622b81e4a28df94c40345400256ec608d0e55bb8a3feaa9163f5732", + "https://files.pythonhosted.org/packages/b2/a3/e137168c9c44d18eff0376253da9f1e9234d0239e0ee230d2fee6cea8e55/jeepney-0.9.0-py3-none-any.whl": "sha256:97e5714520c16fc0a45695e5365a2e11b81ea79bba796e26f9f1d178cb182683" }, "keyring": { - "https://files.pythonhosted.org/packages/70/09/d904a6e96f76ff214be59e7aa6ef7190008f52a0ab6689760a98de0bf37d/keyring-25.6.0.tar.gz": "0b39998aa941431eb3d9b0d4b2460bc773b9df6fed7621c2dfb291a7e0187a66", - "https://files.pythonhosted.org/packages/d3/32/da7f44bcb1105d3e88a0b74ebdca50c59121d2ddf71c9e34ba47df7f3a56/keyring-25.6.0-py3-none-any.whl": "552a3f7af126ece7ed5c89753650eec89c7eaae8617d0aa4d9ad2b75111266bd" + "https://files.pythonhosted.org/packages/43/4b/674af6ef2f97d56f0ab5153bf0bfa28ccb6c3ed4d1babf4305449668807b/keyring-25.7.0.tar.gz": "sha256:fe01bd85eb3f8fb3dd0405defdeac9a5b4f6f0439edbb3149577f244a2e8245b", + "https://files.pythonhosted.org/packages/81/db/e655086b7f3a705df045bf0933bdd9c2f79bb3c97bfef1384598bb79a217/keyring-25.7.0-py3-none-any.whl": "sha256:be4a0b195f149690c166e850609a477c532ddbfbaed96a404d4e43f8d5e2689f" }, "markdown-it-py": { - "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz": "cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", - "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl": "87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147" + "https://files.pythonhosted.org/packages/06/ff/7841249c247aa650a76b9ee4bbaeae59370dc8bfd2f6c01f3630c35eb134/markdown_it_py-4.2.0.tar.gz": "sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49", + "https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl": "sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a" }, "mdurl": { - "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl": "84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", - "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz": "bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba" + "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl": "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", + "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz": "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba" }, "more-itertools": { - "https://files.pythonhosted.org/packages/a4/8e/469e5a4a2f5855992e425f3cb33804cc07bf18d48f2db061aec61ce50270/more_itertools-10.8.0-py3-none-any.whl": "52d4362373dcf7c52546bc4af9a86ee7c4579df9a8dc268be0a2f949d376cc9b", - "https://files.pythonhosted.org/packages/ea/5d/38b681d3fce7a266dd9ab73c66959406d565b3e85f21d5e66e1181d93721/more_itertools-10.8.0.tar.gz": "f638ddf8a1a0d134181275fb5d58b086ead7c6a72429ad725c67503f13ba30bd" + "https://files.pythonhosted.org/packages/de/1d/f4da6f02cdffe04d6362210b807146a26044c88d839208aec273bb0d9184/more_itertools-11.1.0.tar.gz": "sha256:48e8f4d9e7e5878571ecf6f2b4e57634f93cd474cc8cfbd2376f2d11b396e30d", + "https://files.pythonhosted.org/packages/e8/3d/1087453384dbde46a8c7f9356eead2c58be8a7bf156bca40243377c85715/more_itertools-11.1.0-py3-none-any.whl": "sha256:4b65538ae22f6fed0ce4874efd317463a7489796a0939fa66824dd542125a192" }, "nh3": { - "https://files.pythonhosted.org/packages/0c/e0/cf1543e798ba86d838952e8be4cb8d18e22999be2a24b112a671f1c04fd6/nh3-0.3.0-cp38-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl": "ec6cfdd2e0399cb79ba4dcffb2332b94d9696c52272ff9d48a630c5dca5e325a", - "https://files.pythonhosted.org/packages/10/71/2fb1834c10fab6d9291d62c95192ea2f4c7518bd32ad6c46aab5d095cb87/nh3-0.3.0-cp313-cp313t-musllinux_1_2_i686.whl": "0649464ac8eee018644aacbc103874ccbfac80e3035643c3acaab4287e36e7f5", - "https://files.pythonhosted.org/packages/23/1e/80a8c517655dd40bb13363fc4d9e66b2f13245763faab1a20f1df67165a7/nh3-0.3.0-cp313-cp313t-win_amd64.whl": "423201bbdf3164a9e09aa01e540adbb94c9962cc177d5b1cbb385f5e1e79216e", - "https://files.pythonhosted.org/packages/2f/d6/f1c6e091cbe8700401c736c2bc3980c46dca770a2cf6a3b48a175114058e/nh3-0.3.0-cp313-cp313t-win32.whl": "7275fdffaab10cc5801bf026e3c089d8de40a997afc9e41b981f7ac48c5aa7d5", - "https://files.pythonhosted.org/packages/33/c1/8f8ccc2492a000b6156dce68a43253fcff8b4ce70ab4216d08f90a2ac998/nh3-0.3.0-cp313-cp313t-musllinux_1_2_x86_64.whl": "1adeb1062a1c2974bc75b8d1ecb014c5fd4daf2df646bbe2831f7c23659793f9", - "https://files.pythonhosted.org/packages/39/2c/6394301428b2017a9d5644af25f487fa557d06bc8a491769accec7524d9a/nh3-0.3.0-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl": "f416c35efee3e6a6c9ab7716d9e57aa0a49981be915963a82697952cba1353e1", - "https://files.pythonhosted.org/packages/4c/3c/cba7b26ccc0ef150c81646478aa32f9c9535234f54845603c838a1dc955c/nh3-0.3.0-cp313-cp313t-musllinux_1_2_aarch64.whl": "80fe20171c6da69c7978ecba33b638e951b85fb92059259edd285ff108b82a6d", - "https://files.pythonhosted.org/packages/4e/9a/344b9f9c4bd1c2413a397f38ee6a3d5db30f1a507d4976e046226f12b297/nh3-0.3.0-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.whl": "37d3003d98dedca6cd762bf88f2e70b67f05100f6b949ffe540e189cc06887f9", - "https://files.pythonhosted.org/packages/5b/76/3165e84e5266d146d967a6cc784ff2fbf6ddd00985a55ec006b72bc39d5d/nh3-0.3.0-cp38-abi3-win_arm64.whl": "d97d3efd61404af7e5721a0e74d81cdbfc6e5f97e11e731bb6d090e30a7b62b2", - "https://files.pythonhosted.org/packages/5c/86/a96b1453c107b815f9ab8fac5412407c33cc5c7580a4daf57aabeb41b774/nh3-0.3.0-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl": "ce5e7185599f89b0e391e2f29cc12dc2e206167380cea49b33beda4891be2fe1", - "https://files.pythonhosted.org/packages/63/da/c5fd472b700ba37d2df630a9e0d8cc156033551ceb8b4c49cc8a5f606b68/nh3-0.3.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl": "ba0caa8aa184196daa6e574d997a33867d6d10234018012d35f86d46024a2a95", - "https://files.pythonhosted.org/packages/66/3f/cd37f76c8ca277b02a84aa20d7bd60fbac85b4e2cbdae77cb759b22de58b/nh3-0.3.0-cp38-abi3-musllinux_1_2_aarch64.whl": "634e34e6162e0408e14fb61d5e69dbaea32f59e847cfcfa41b66100a6b796f62", - "https://files.pythonhosted.org/packages/6a/1b/b15bd1ce201a1a610aeb44afd478d55ac018b4475920a3118ffd806e2483/nh3-0.3.0-cp38-abi3-manylinux_2_17_ppc64.manylinux2014_ppc64.whl": "e9e6a7e4d38f7e8dda9edd1433af5170c597336c1a74b4693c5cb75ab2b30f2a", - "https://files.pythonhosted.org/packages/8c/ae/324b165d904dc1672eee5f5661c0a68d4bab5b59fbb07afb6d8d19a30b45/nh3-0.3.0-cp38-abi3-win_amd64.whl": "bae63772408fd63ad836ec569a7c8f444dd32863d0c67f6e0b25ebbd606afa95", - "https://files.pythonhosted.org/packages/8f/14/079670fb2e848c4ba2476c5a7a2d1319826053f4f0368f61fca9bb4227ae/nh3-0.3.0-cp38-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl": "7852f038a054e0096dac12b8141191e02e93e0b4608c4b993ec7d4ffafea4e49", - "https://files.pythonhosted.org/packages/97/03/03f79f7e5178eb1ad5083af84faff471e866801beb980cc72943a4397368/nh3-0.3.0-cp38-abi3-musllinux_1_2_i686.whl": "c7a32a7f0d89f7d30cb8f4a84bdbd56d1eb88b78a2434534f62c71dac538c450", - "https://files.pythonhosted.org/packages/97/33/11e7273b663839626f714cb68f6eb49899da5a0d9b6bc47b41fe870259c2/nh3-0.3.0-cp38-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl": "389d93d59b8214d51c400fb5b07866c2a4f79e4e14b071ad66c92184fec3a392", - "https://files.pythonhosted.org/packages/9a/e0/af86d2a974c87a4ba7f19bc3b44a8eaa3da480de264138fec82fe17b340b/nh3-0.3.0-cp313-cp313t-win_arm64.whl": "16f8670201f7e8e0e05ed1a590eb84bfa51b01a69dd5caf1d3ea57733de6a52f", - "https://files.pythonhosted.org/packages/a3/e5/ac7fc565f5d8bce7f979d1afd68e8cb415020d62fa6507133281c7d49f91/nh3-0.3.0-cp38-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl": "af5aa8127f62bbf03d68f67a956627b1bd0469703a35b3dad28d0c1195e6c7fb", - "https://files.pythonhosted.org/packages/ad/7f/7c6b8358cf1222921747844ab0eef81129e9970b952fcb814df417159fb9/nh3-0.3.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl": "7c915060a2c8131bef6a29f78debc29ba40859b6dbe2362ef9e5fd44f11487c2", - "https://files.pythonhosted.org/packages/b4/11/340b7a551916a4b2b68c54799d710f86cf3838a4abaad8e74d35360343bb/nh3-0.3.0-cp313-cp313t-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl": "a537ece1bf513e5a88d8cff8a872e12fe8d0f42ef71dd15a5e7520fecd191bbb", - "https://files.pythonhosted.org/packages/c3/a4/96cff0977357f60f06ec4368c4c7a7a26cccfe7c9fcd54f5378bf0428fd3/nh3-0.3.0.tar.gz": "d8ba24cb31525492ea71b6aac11a4adac91d828aadeff7c4586541bf5dc34d2f", - "https://files.pythonhosted.org/packages/c9/50/76936ec021fe1f3270c03278b8af5f2079038116b5d0bfe8538ffe699d69/nh3-0.3.0-cp38-abi3-win32.whl": "6d68fa277b4a3cf04e5c4b84dd0c6149ff7d56c12b3e3fab304c525b850f613d", - "https://files.pythonhosted.org/packages/ce/55/1974bcc16884a397ee699cebd3914e1f59be64ab305533347ca2d983756f/nh3-0.3.0-cp38-abi3-musllinux_1_2_x86_64.whl": "3f1b4f8a264a0c86ea01da0d0c390fe295ea0bcacc52c2103aca286f6884f518", - "https://files.pythonhosted.org/packages/ee/db/7aa11b44bae4e7474feb1201d8dee04fabe5651c7cb51409ebda94a4ed67/nh3-0.3.0-cp38-abi3-musllinux_1_2_armv7l.whl": "b0612ccf5de8a480cf08f047b08f9d3fecc12e63d2ee91769cb19d7290614c23", - "https://files.pythonhosted.org/packages/f3/ba/59e204d90727c25b253856e456ea61265ca810cda8ee802c35f3fadaab00/nh3-0.3.0-cp313-cp313t-musllinux_1_2_armv7l.whl": "e90883f9f85288f423c77b3f5a6f4486375636f25f793165112679a7b6363b35" - }, - "pkginfo": { - "https://files.pythonhosted.org/packages/24/03/e26bf3d6453b7fda5bd2b84029a426553bb373d6277ef6b5ac8863421f87/pkginfo-1.12.1.2.tar.gz": "5cd957824ac36f140260964eba3c6be6442a8359b8c48f4adf90210f33a04b7b", - "https://files.pythonhosted.org/packages/fa/3d/f4f2ba829efb54b6cd2d91349c7463316a9cc55a43fc980447416c88540f/pkginfo-1.12.1.2-py3-none-any.whl": "c783ac885519cab2c34927ccfa6bf64b5a704d7c69afaea583dd9b7afe969343" + "https://files.pythonhosted.org/packages/09/a1/ea83abe738a3fbaa203dfdb836ca7cbab0e7e9609faaee4fe1d4652599c0/nh3-0.3.6-cp314-cp314t-musllinux_1_2_i686.whl": "sha256:edb2b4a1a27523e6cc7c417f8d21ce3d005243548b93e56b762b66b0c7f589f9", + "https://files.pythonhosted.org/packages/0a/13/6f1e302ca674ac74362e150848ad56a1be5145391204f74facdb8e94df12/nh3-0.3.6-cp38-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl": "sha256:455469a29951edc92bc48b47ac2281c3f2609e6c4f6a047056449f8c2c23facf", + "https://files.pythonhosted.org/packages/0e/24/a0d80182a18919665fefd19c1c06f1d1df1c9a6455d0252de40c034a0bc3/nh3-0.3.6-cp38-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl": "sha256:e6b7beece07525dc6e6b0fc2f104442de2ba328360ad00e50cbe2e1fd620447d", + "https://files.pythonhosted.org/packages/11/f9/3966c61455668c08853bf5e33b4bed93c421f3194ce4de896dc248d6f6ce/nh3-0.3.6-cp38-abi3-musllinux_1_2_armv7l.whl": "sha256:f5ed5fe84aee7f39db95c214a7421bf0499fbf500fec6d86a4e29bfc37971438", + "https://files.pythonhosted.org/packages/17/0c/6cdb5ee1e127be50dc8391e54bddc1f64e87bf4bfad0c55633320e2e02db/nh3-0.3.6-cp38-abi3-musllinux_1_2_x86_64.whl": "sha256:36d06341bd501240d320f5942481ed5e6846136b666e1ba4faf802b78ebc875f", + "https://files.pythonhosted.org/packages/19/d3/479cb4ae440424825735d60525b53e3c77fd60fd6e6afc0e984f00eb0178/nh3-0.3.6-cp38-abi3-musllinux_1_2_i686.whl": "sha256:082675ff87b9385ec430ffe6d5847ba7456cc39b73720cd4add472f9f4cffd56", + "https://files.pythonhosted.org/packages/25/bb/431615ba1d1d3eb63cde0f974f2114edf863a8a3f6049a12fed23fc241d3/nh3-0.3.6-cp38-abi3-manylinux_2_17_ppc64.manylinux2014_ppc64.whl": "sha256:44673b27010051ab5a5e438a86ec31bbda61d4a77d7e900af6b7be3037c1abae", + "https://files.pythonhosted.org/packages/30/a8/fb2c38845efb703a9173bffdfc745fc64d2b0e55cfc73a3647d2f028250c/nh3-0.3.6-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.whl": "sha256:2f90d9a0cfdbee218994fdaaeeb5a0fde62d08f35e4eef0378ec1e2200172fd0", + "https://files.pythonhosted.org/packages/36/ea/5542f3c45da4c00290d9d67a65e996702e23e613c4b627de3e09cb9fe357/nh3-0.3.6-cp314-cp314t-win_amd64.whl": "sha256:4713502748f564fee0633b37b3403783ce0a3af3a3d148ad91025a5bdadb7bc6", + "https://files.pythonhosted.org/packages/3c/a6/bfaa00046e58603507dcfc266c4778e3ab7adf68a5dedd73b6274b8d9314/nh3-0.3.6-cp38-abi3-manylinux_2_31_riscv64.whl": "sha256:25c733bee928530556b1db0ea46c52cf5aa686146e38e60a6fc7cb801ef91cec", + "https://files.pythonhosted.org/packages/41/21/e1084ab18eb589506335c7c7576f2d4643e9a0c0e33983ef0e549a256b96/nh3-0.3.6-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl": "sha256:e1b160831c9cdb06a6c79c2f9cdb11386602938f9af260d1c457a85add4f6f69", + "https://files.pythonhosted.org/packages/49/09/0d8e3101636d9ad88cdefb2914e764cb8e876ebdbb4286bfc251277d9c67/nh3-0.3.6-cp314-cp314t-musllinux_1_2_armv7l.whl": "sha256:889932a97fb4abb6f95fef1914c0d269ebfb60011e67121c1163059b9449dbb4", + "https://files.pythonhosted.org/packages/4b/4a/526f199626bfcb496bc01a268051b44737962005553b158e985ed7e64865/nh3-0.3.6-cp314-cp314t-musllinux_1_2_aarch64.whl": "sha256:f2f14b7ae1fca99c4a66c981aac3974e7fbc1ca30a12673d223ae1df76680917", + "https://files.pythonhosted.org/packages/59/62/5b6108bedaef2b2637fed04c87bdbcb5967b9961758b41f0e466ef22a022/nh3-0.3.6-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl": "sha256:34d2b0d934156b87ee114f599a3ba9b8b9e17b5d79652ba3a13fa50903de965e", + "https://files.pythonhosted.org/packages/5b/67/314f6151bad77a93d751978a344033e1fc890822f05f0416079338e34231/nh3-0.3.6-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl": "sha256:905f877dc66dd7aea4a76e54bcb26acb5ff8216f720c0017ccf63e0e6035698e", + "https://files.pythonhosted.org/packages/5e/1b/ef84624f14954d270f74060a19fc550dd4f06656399447569afb584d8c06/nh3-0.3.6.tar.gz": "sha256:f3736c9dd3d1856f80cd031715b84ca75cda2bbb1ac802c3da26bfce590838d7", + "https://files.pythonhosted.org/packages/66/35/26bd47e6af5915a628281dccdac354ddf4e32f7397047894270acd8c9870/nh3-0.3.6-cp314-cp314t-win_arm64.whl": "sha256:69bbb92865a693d909db3a700d3c01537533844d0948c1e9323561ce06ecda41", + "https://files.pythonhosted.org/packages/66/69/0654482b8635012fbae67826bd6c381abb05d841ac7388b9b4666300fdad/nh3-0.3.6-cp314-cp314t-musllinux_1_2_x86_64.whl": "sha256:43bc1ed3fa0716295fabee29ba42b2667e4a51d140b0a68e092170a765474fa6", + "https://files.pythonhosted.org/packages/68/17/06e72a18ee9b572914447338237ca7eb164c0df901f141bc10d1282247a2/nh3-0.3.6-cp38-abi3-musllinux_1_2_aarch64.whl": "sha256:82ca5bf427ad1b216b65ede1a2e2d87dc49bec417ceba0f297213107d3cd9d78", + "https://files.pythonhosted.org/packages/7b/e5/7cafee2f0413ca4cb0ef3bd111e94d408a48810008b283ad8aee00dd1809/nh3-0.3.6-cp38-abi3-win_arm64.whl": "sha256:69f365963f63a1e9bff53bdbb3c542c7c2efed3e163c9d5d83a772a2ac468c21", + "https://files.pythonhosted.org/packages/82/fa/2b5d684e3edf1e81bfd02d298c78c3e3da77ca1d8a2be3183a79544a7548/nh3-0.3.6-cp38-abi3-win_amd64.whl": "sha256:f338ac7d594c067679f1e99b4f5ec3906842979560f9d8f15d6bdfa39a353b10", + "https://files.pythonhosted.org/packages/99/3e/6506aa4f23dc7b7993a2d0a45dca3ce864ec48380adfe15a173e643c63e8/nh3-0.3.6-cp314-cp314t-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl": "sha256:2411e8c3cee81a1ddd62c2a5d50585c28aa5566d373ad1db92536b95ddb24ef2", + "https://files.pythonhosted.org/packages/b0/94/f48d08e6f72a406300fa11d8acd929fea1a80d4bf750fa292cb10785f126/nh3-0.3.6-cp38-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl": "sha256:d14bf7982e7a77c0c775634c29c07ce08b38a046df73e1c1f139b3e82f18a38e", + "https://files.pythonhosted.org/packages/e3/e1/e96e7864a7a53bd6b6fab7e9632467382a2a2c1f3fed951918ad131542fb/nh3-0.3.6-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl": "sha256:e196fa70c2ff2eb4de7d3df3108f8f358c1d69dff20d45b11f20a5aa227ffb6d", + "https://files.pythonhosted.org/packages/e9/55/9de666ad975d6ccd77d799ea0add55ee2347aa81286ce21b2a97c070746b/nh3-0.3.6-cp38-abi3-win32.whl": "sha256:5276ef17bdba9ad8040575c74072008b13aae429436e9d0429e718bb5f90f4da", + "https://files.pythonhosted.org/packages/ed/a6/1f7285ffadc8307c4dbeb08d21b920536d5117785056d1079e998c4dfa44/nh3-0.3.6-cp314-cp314t-win32.whl": "sha256:597a8e843bea00b2eb5520658dc24a9bb032e7fc9e7c2c0c4cd29420220c9796", + "https://files.pythonhosted.org/packages/f3/ab/a7653bce9a3b204be6a6931767a9e23595807bb84790ce6685e4d7e5bd08/nh3-0.3.6-cp38-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl": "sha256:a43ebd7543555c3ac1bc353023d0794e75cb76f6f18f19c32e95441496c0cc25" + }, + "packaging": { + "https://files.pythonhosted.org/packages/d7/f1/e7a6dd94a8d4a5626c03e4e99c87f241ba9e350cd9e6d75123f992427270/packaging-26.2.tar.gz": "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661", + "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl": "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e" }, "pycparser": { - "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl": "b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", - "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz": "600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", - "https://files.pythonhosted.org/packages/a0/e3/59cd50310fc9b59512193629e1984c1f95e5c8ae6e5d8c69532ccc65a7fe/pycparser-2.23-py3-none-any.whl": "e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934", - "https://files.pythonhosted.org/packages/fe/cf/d2d3b9f5699fb1e4615c8e32ff220203e43b248e1dfcc6736ad9057731ca/pycparser-2.23.tar.gz": "78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2" + "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl": "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", + "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz": "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29" }, "pygments": { - "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz": "636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", - "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl": "86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b" + "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz": "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", + "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl": "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176" }, "pywin32-ctypes": { - "https://files.pythonhosted.org/packages/85/9f/01a1a99704853cb63f253eea009390c88e7131c67e66a0a02099a8c917cb/pywin32-ctypes-0.2.3.tar.gz": "d162dc04946d704503b2edc4d55f3dba5c1d539ead017afa00142c38b9885755", - "https://files.pythonhosted.org/packages/de/3d/8161f7711c017e01ac9f008dfddd9410dff3674334c233bde66e7ba65bbf/pywin32_ctypes-0.2.3-py3-none-any.whl": "8a1513379d709975552d202d942d9837758905c8d01eb82b8bcc30918929e7b8" + "https://files.pythonhosted.org/packages/85/9f/01a1a99704853cb63f253eea009390c88e7131c67e66a0a02099a8c917cb/pywin32-ctypes-0.2.3.tar.gz": "sha256:d162dc04946d704503b2edc4d55f3dba5c1d539ead017afa00142c38b9885755", + "https://files.pythonhosted.org/packages/de/3d/8161f7711c017e01ac9f008dfddd9410dff3674334c233bde66e7ba65bbf/pywin32_ctypes-0.2.3-py3-none-any.whl": "sha256:8a1513379d709975552d202d942d9837758905c8d01eb82b8bcc30918929e7b8" }, "readme-renderer": { - "https://files.pythonhosted.org/packages/5a/a9/104ec9234c8448c4379768221ea6df01260cd6c2ce13182d4eac531c8342/readme_renderer-44.0.tar.gz": "8712034eabbfa6805cacf1402b4eeb2a73028f72d1166d6f5cb7f9c047c5d1e1", - "https://files.pythonhosted.org/packages/e1/67/921ec3024056483db83953ae8e48079ad62b92db7880013ca77632921dd0/readme_renderer-44.0-py3-none-any.whl": "2fbca89b81a08526aadf1357a8c2ae889ec05fb03f5da67f9769c9a592166151" + "https://files.pythonhosted.org/packages/02/51/d3a6ea424652c60f05600d8c2e01a55c913755e7cdad64afabbd1aa16f44/readme_renderer-45.0.tar.gz": "sha256:030a8fac74904f8fba11ad1bb6964e3f76e896dc7e5e71f16af190c9056696d1", + "https://files.pythonhosted.org/packages/97/1b/295bf2fa3e740131778065e5ffa2c481f0e7210182d408e9a2c244ff5b0c/readme_renderer-45.0-py3-none-any.whl": "sha256:3385ed220117104a2bceb4a9dac8c5fdf6d1f96890d7ea2a9c7174fd5c84091f" }, "requests": { - "https://files.pythonhosted.org/packages/34/64/8860370b167a9721e8956ae116825caff829224fbca0ca6e7bf8ddef8430/requests-2.33.0.tar.gz": "c7ebc5e8b0f21837386ad0e1c8fe8b829fa5f544d8df3b2253bff14ef29d7652", - "https://files.pythonhosted.org/packages/56/5d/c814546c2333ceea4ba42262d8c4d55763003e767fa169adc693bd524478/requests-2.33.0-py3-none-any.whl": "3324635456fa185245e24865e810cecec7b4caf933d7eb133dcde67d48cee69b" + "https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl": "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0", + "https://files.pythonhosted.org/packages/ac/c3/e2a2b89f2d3e2179abd6d00ebd70bff6273f37fb3e0cc209f48b39d00cbf/requests-2.34.2.tar.gz": "sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed" }, "requests-toolbelt": { - "https://files.pythonhosted.org/packages/3f/51/d4db610ef29373b879047326cbf6fa98b6c1969d6f6dc423279de2b1be2c/requests_toolbelt-1.0.0-py2.py3-none-any.whl": "cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06", - "https://files.pythonhosted.org/packages/f3/61/d7545dafb7ac2230c70d38d31cbfe4cc64f7144dc41f6e4e4b78ecd9f5bb/requests-toolbelt-1.0.0.tar.gz": "7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6" + "https://files.pythonhosted.org/packages/3f/51/d4db610ef29373b879047326cbf6fa98b6c1969d6f6dc423279de2b1be2c/requests_toolbelt-1.0.0-py2.py3-none-any.whl": "sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06", + "https://files.pythonhosted.org/packages/f3/61/d7545dafb7ac2230c70d38d31cbfe4cc64f7144dc41f6e4e4b78ecd9f5bb/requests-toolbelt-1.0.0.tar.gz": "sha256:7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6" }, "rfc3986": { - "https://files.pythonhosted.org/packages/85/40/1520d68bfa07ab5a6f065a186815fb6610c86fe957bc065754e47f7b0840/rfc3986-2.0.0.tar.gz": "97aacf9dbd4bfd829baad6e6309fa6573aaf1be3f6fa735c8ab05e46cecb261c", - "https://files.pythonhosted.org/packages/ff/9a/9afaade874b2fa6c752c36f1548f718b5b83af81ed9b76628329dab81c1b/rfc3986-2.0.0-py2.py3-none-any.whl": "50b1502b60e289cb37883f3dfd34532b8873c7de9f49bb546641ce9cbd256ebd" + "https://files.pythonhosted.org/packages/85/40/1520d68bfa07ab5a6f065a186815fb6610c86fe957bc065754e47f7b0840/rfc3986-2.0.0.tar.gz": "sha256:97aacf9dbd4bfd829baad6e6309fa6573aaf1be3f6fa735c8ab05e46cecb261c", + "https://files.pythonhosted.org/packages/ff/9a/9afaade874b2fa6c752c36f1548f718b5b83af81ed9b76628329dab81c1b/rfc3986-2.0.0-py2.py3-none-any.whl": "sha256:50b1502b60e289cb37883f3dfd34532b8873c7de9f49bb546641ce9cbd256ebd" }, "rich": { - "https://files.pythonhosted.org/packages/e3/30/3c4d035596d3cf444529e0b2953ad0466f6049528a879d27534700580395/rich-14.1.0-py3-none-any.whl": "536f5f1785986d6dbdea3c75205c473f970777b4a0d6c6dd1b696aa05a3fa04f", - "https://files.pythonhosted.org/packages/fe/75/af448d8e52bf1d8fa6a9d089ca6c07ff4453d86c65c145d0a300bb073b9b/rich-14.1.0.tar.gz": "e497a48b844b0320d45007cdebfeaeed8db2a4f4bcf49f15e455cfc4af11eaa8" + "https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl": "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb", + "https://files.pythonhosted.org/packages/c0/8f/0722ca900cc807c13a6a0c696dacf35430f72e0ec571c4275d2371fca3e9/rich-15.0.0.tar.gz": "sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36" }, "secretstorage": { - "https://files.pythonhosted.org/packages/53/a4/f48c9d79cb507ed1373477dbceaba7401fd8a23af63b837fa61f1dcd3691/SecretStorage-3.3.3.tar.gz": "2403533ef369eca6d2ba81718576c5e0f564d5cca1b58f73a8b23e7d4eeebd77", - "https://files.pythonhosted.org/packages/54/24/b4293291fa1dd830f353d2cb163295742fa87f179fcc8a20a306a81978b7/SecretStorage-3.3.3-py3-none-any.whl": "f356e6628222568e3af06f2eba8df495efa13b3b63081dafd4f7d9a7b7bc9f99" + "https://files.pythonhosted.org/packages/1c/03/e834bcd866f2f8a49a85eaff47340affa3bfa391ee9912a952a1faa68c7b/secretstorage-3.5.0.tar.gz": "sha256:f04b8e4689cbce351744d5537bf6b1329c6fc68f91fa666f60a380edddcd11be", + "https://files.pythonhosted.org/packages/b7/46/f5af3402b579fd5e11573ce652019a67074317e18c1935cc0b4ba9b35552/secretstorage-3.5.0-py3-none-any.whl": "sha256:0ce65888c0725fcb2c5bc0fdb8e5438eece02c523557ea40ce0703c266248137" }, "six": { - "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz": "ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", - "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl": "4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274" + "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz": "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", + "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl": "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274" }, "twine": { - "https://files.pythonhosted.org/packages/5d/ec/00f9d5fd040ae29867355e559a94e9a8429225a0284a3f5f091a3878bfc0/twine-5.1.1-py3-none-any.whl": "215dbe7b4b94c2c50a7315c0275d2258399280fbb7d04182c7e55e24b5f93997", - "https://files.pythonhosted.org/packages/77/68/bd982e5e949ef8334e6f7dcf76ae40922a8750aa2e347291ae1477a4782b/twine-5.1.1.tar.gz": "9aa0825139c02b3434d913545c7b847a21c835e11597f5255842d457da2322db" + "https://files.pythonhosted.org/packages/92/3c/58f808a359700f39a967dffede33efeac809262c03303fa3eec6afff8f49/twine-7.0.0.tar.gz": "sha256:85cdb29c518efef867360ae4acd4b0dfd61c8654a22fca08e6f8539f05022177", + "https://files.pythonhosted.org/packages/96/08/ddcdc06225eaad6de0e48e1002b06d919dbde20582d0662c7af51308e5d6/twine-7.0.0-py3-none-any.whl": "sha256:b854164df26db268af05f49aa5c0344b10e27a494343ff05b1e0bad3b135f5a7" }, "urllib3": { - "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl": "bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", - "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz": "1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed" + "https://files.pythonhosted.org/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz": "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", + "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl": "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897" }, "zipp": { - "https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl": "071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e", - "https://files.pythonhosted.org/packages/e3/02/0f2892c661036d50ede074e376733dca2ae7c6eb617489437771209d4180/zipp-3.23.0.tar.gz": "a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166" + "https://files.pythonhosted.org/packages/3a/13/547360d81e6d88d58492968ffda9f9542854f11310ee556fef14260cc886/zipp-4.1.0-py3-none-any.whl": "sha256:25ad4e16390cd314347dd8f1de67a2ac538ae658ed4ab9db16029c07c188e97f", + "https://files.pythonhosted.org/packages/b9/d8/eab98a517c14134c0b2eb4e2387bc5f457334293ec5d2dd3857ec2966802/zipp-4.1.0.tar.gz": "sha256:4cb57381f544315db7688e976e922a2b18cdb513d21cc194eb42232ba2a3e602" } } }, - "fact_version": "v1" + "fact_version": "v2" } } } diff --git a/tests/integration/bzlmod_lockfile_test.py b/tests/integration/bzlmod_lockfile_test.py new file mode 100644 index 0000000000..650f132f0f --- /dev/null +++ b/tests/integration/bzlmod_lockfile_test.py @@ -0,0 +1,59 @@ +import difflib +import os +import pathlib +import unittest + +from tests.integration import runner + + +class BzlmodLockfileTest(runner.TestCase): + def test_bzlmod_lockfile(self): + lockfile_path = self.repo_root / "MODULE.bazel.lock" + self.assertTrue( + lockfile_path.exists(), + f"Expected lockfile at {lockfile_path}", + ) + original_lockfile = lockfile_path.read_text() + + res = self.run_bazel("test", "//...", check=False) + if res.exit_code == 0: + return + + # Generate updated lockfile to compare diff and export artifact + self.run_bazel("mod", "deps", "--lockfile_mode=update", check=False) + updated_lockfile = lockfile_path.read_text() if lockfile_path.exists() else "" + + undeclared_outputs_dir = os.environ.get("TEST_UNDECLARED_OUTPUTS_DIR") + if undeclared_outputs_dir: + out_dir = pathlib.Path(undeclared_outputs_dir) + (out_dir / "MODULE.bazel.lock").write_text(updated_lockfile) + + if original_lockfile != updated_lockfile: + diff_lines = list( + difflib.unified_diff( + original_lockfile.splitlines(keepends=True), + updated_lockfile.splitlines(keepends=True), + fromfile="MODULE.bazel.lock (checked-in)", + tofile="MODULE.bazel.lock (expected/updated)", + ) + ) + diff_str = "".join(diff_lines) + + if undeclared_outputs_dir: + (out_dir / "MODULE.bazel.lock.diff").write_text(diff_str) + + msg = ( + f"MODULE.bazel.lock is out of date.\n\n" + f"--- DIFF ---\n{diff_str}\n" + f"--- END DIFF ---\n\n" + f"To update the lockfile, run:\n" + f" bazel mod deps --lockfile_mode=update\n" + f"inside tests/integration/bzlmod_lockfile, or copy the updated MODULE.bazel.lock artifact.\n" + ) + self.fail(msg) + else: + self.fail(f"bazel test //... failed:\n{res.describe()}") + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/integration/compile_pip_requirements/BUILD.bazel b/tests/integration/compile_pip_requirements/BUILD.bazel index 6df46b8372..b1f398c4a8 100644 --- a/tests/integration/compile_pip_requirements/BUILD.bazel +++ b/tests/integration/compile_pip_requirements/BUILD.bazel @@ -29,6 +29,19 @@ compile_pip_requirements( requirements_txt = "requirements_lock.txt", ) +genquery( + name = "requirements_update_data", + expression = """ +some(labels(data, @compile_pip_requirements//:requirements.update) intersect @compile_pip_requirements//:requirements.in) union +some(labels(data, @compile_pip_requirements//:requirements.update) intersect @compile_pip_requirements//:requirements_extra.in) +""", + scope = [ + "@compile_pip_requirements//:requirements.update", + "@compile_pip_requirements//:requirements.in", + "@compile_pip_requirements//:requirements_extra.in", + ], +) + compile_pip_requirements( name = "requirements_nohashes", src = "requirements.txt", diff --git a/tests/integration/compile_pip_requirements/WORKSPACE b/tests/integration/compile_pip_requirements/WORKSPACE index 0eeab2067c..397d828261 100644 --- a/tests/integration/compile_pip_requirements/WORKSPACE +++ b/tests/integration/compile_pip_requirements/WORKSPACE @@ -3,6 +3,11 @@ local_repository( path = "../../..", ) +local_repository( + name = "compile_pip_requirements", + path = ".", +) + load("@rules_python//python:repositories.bzl", "py_repositories", "python_register_toolchains") py_repositories() diff --git a/tests/integration/compile_pip_requirements_test_from_external_repo/MODULE.bazel b/tests/integration/compile_pip_requirements_test_from_external_repo/MODULE.bazel index 596a0bcfc8..201cb4a1ab 100644 --- a/tests/integration/compile_pip_requirements_test_from_external_repo/MODULE.bazel +++ b/tests/integration/compile_pip_requirements_test_from_external_repo/MODULE.bazel @@ -19,7 +19,7 @@ local_path_override( pip = use_extension("@rules_python//python/extensions:pip.bzl", "pip") pip.parse( - hub_name = "pypi", + hub_name = "pypi_compile", python_version = "3.9", requirements_lock = "@compile_pip_requirements//:requirements_lock.txt", ) diff --git a/tests/integration/integration_test.bzl b/tests/integration/integration_test.bzl index 771976d037..f3d5cb6967 100644 --- a/tests/integration/integration_test.bzl +++ b/tests/integration/integration_test.bzl @@ -21,14 +21,14 @@ load( ) load("//python:py_test.bzl", "py_test") -def _test_runner(*, name, bazel_version, py_main, bzlmod): +def _test_runner(*, name, bazel_version, py_main, bzlmod, py_deps): if py_main: test_runner = "{}_bazel_{}_py_runner".format(name, bazel_version) py_test( name = test_runner, srcs = [py_main], main = py_main, - deps = [":runner_lib"], + deps = [":runner_lib"] + py_deps, # Hide from ... patterns; should only be run as part # of the bazel integration test tags = ["manual"], @@ -46,6 +46,7 @@ def rules_python_integration_test( bzlmod = True, tags = None, py_main = None, + py_deps = None, bazel_versions = None, **kwargs): """Runs a bazel-in-bazel integration test. @@ -60,6 +61,7 @@ def rules_python_integration_test( py_main: Optional `.py` file to run tests using. When specified, a python based test runner is used, and this source file is the main entry point and responsible for executing tests. + py_deps: Optional test runner deps to use for setup. bazel_versions: `list[str] | None`, the bazel versions to test. I not specified, defaults to all configured bazel versions. **kwargs: Passed to the upstream `bazel_integration_tests` rule. @@ -91,6 +93,7 @@ def rules_python_integration_test( name = name, bazel_version = bazel_version, py_main = py_main, + py_deps = py_deps or [], bzlmod = bzlmod, ) bazel_integration_test( diff --git a/tests/integration/local_toolchains/BUILD.bazel b/tests/integration/local_toolchains/BUILD.bazel index 20ee7bcfe6..ae99e7cf2f 100644 --- a/tests/integration/local_toolchains/BUILD.bazel +++ b/tests/integration/local_toolchains/BUILD.bazel @@ -13,9 +13,8 @@ # limitations under the License. load("@bazel_skylib//rules:common_settings.bzl", "string_flag") -load("@rules_cc//cc:cc_library.bzl", "cc_library") load("@rules_python//python:py_test.bzl", "py_test") -load(":py_extension.bzl", "py_extension") +load("@rules_python//python/cc:py_extension.bzl", "py_extension") py_test( name = "local_runtime_test", @@ -60,28 +59,24 @@ string_flag( ) # Build rules to generate a python extension. -cc_library( - name = "echo_ext_cc", - testonly = True, - srcs = ["echo_ext.cc"], - deps = [ - "@rules_python//python/cc:current_py_cc_headers", - ], - alwayslink = True, -) - py_extension( name = "echo_ext", testonly = True, + srcs = ["echo_ext.cc"], copts = select({ "@rules_cc//cc/compiler:msvc-cl": [], "//conditions:default": ["-fvisibility=hidden"], }), - deps = [":echo_ext_cc"], + imports = ["."], ) py_test( name = "echo_test", srcs = ["echo_test.py"], + # Make this test better respect pyenv/local Python DLLs on Windows + env_inherit = [ + "PYENV_VERSION", + "PATH", + ], deps = [":echo_ext"], ) diff --git a/tests/integration/local_toolchains/echo_test.py b/tests/integration/local_toolchains/echo_test.py index 17121e0f17..655205fe29 100644 --- a/tests/integration/local_toolchains/echo_test.py +++ b/tests/integration/local_toolchains/echo_test.py @@ -5,4 +5,4 @@ class ExtensionTest(unittest.TestCase): def test_echo_extension(self): - self.assertEqual(echo_ext.echo(42, "str"), tuple(42, "str")) + self.assertEqual(echo_ext.echo(42, "str"), (42, "str")) diff --git a/tests/integration/local_toolchains/py_extension.bzl b/tests/integration/local_toolchains/py_extension.bzl deleted file mode 100644 index 5d37fd7824..0000000000 --- a/tests/integration/local_toolchains/py_extension.bzl +++ /dev/null @@ -1,154 +0,0 @@ -# Copyright 2025 The Bazel Authors. All rights reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Macro to build a python C/C++ extension. - -There are variants of py_extension in many other projects, such as: -* https://github.com/protocolbuffers/protobuf/tree/main/python/py_extension.bzl -* https://github.com/google/riegeli/blob/master/python/riegeli/py_extension.bzl -* https://github.com/pybind/pybind11_bazel/blob/master/build_defs.bzl - -The issue for a generic verion is: -* https://github.com/bazel-contrib/rules_python/issues/824 -""" - -load("@bazel_skylib//rules:copy_file.bzl", "copy_file") -load("@rules_cc//cc:cc_binary.bzl", "cc_binary") -load("@rules_python//python:defs.bzl", "py_library") - -def py_extension( - *, - name, - deps = None, - linkopts = None, - imports = None, - visibility = None, - **kwargs): - """Creates a Python module implemented in C++. - - A Python extension has 2 essential parts: - 1. An internal shared object / pyd package for the extension, `name.pyd`/`name.so` - 2. The py_library target for the extension.` - - Python modules can depend on a py_extension. - - Args: - name: `str`. Name for this target. This is typically the module name. - deps: `list`. Required. C++ libraries to link into the module. - linkopts: `list`. Linking options for the shared library. - imports: `list`. Additional imports for the py_library rule. - visibility: `str`. Visibility for target. - **kwargs: Additional options for the cc_library rule. - """ - if not name: - fail("py_extension requires a name") - if not deps: - fail("py_extension requires a non-empty deps attribute") - if "linkshared" in kwargs: - fail("py_extension attribute linkshared not allowed") - - if not linkopts: - linkopts = [] - - testonly = kwargs.get("testonly") - tags = kwargs.pop("tags", []) - - cc_binary_so_name = name + ".so" - cc_binary_dll_name = name + ".dll" - cc_binary_pyd_name = name + ".pyd" - linker_script_name = name + ".lds" - linker_script_name_rule = name + "_lds" - shared_objects_name = name + "__shared_objects" - - # On Unix, restrict symbol visibility. - exported_symbol = "PyInit_" + name - - # Generate linker script used on non-macOS unix platforms. - native.genrule( - name = linker_script_name_rule, - outs = [linker_script_name], - cmd = "\n".join([ - "cat <<'EOF' >$@", - "{", - " global: " + exported_symbol + ";", - " local: *;", - "};", - "EOF", - ]), - ) - - for cc_binary_name in [cc_binary_dll_name, cc_binary_so_name]: - cur_linkopts = linkopts - cur_deps = deps - if cc_binary_name == cc_binary_so_name: - cur_linkopts = linkopts + select({ - "@platforms//os:macos": [ - # Avoid undefined symbol errors for CPython symbols that - # will be resolved at runtime. - "-undefined", - "dynamic_lookup", - # On macOS, the linker does not support version scripts. Use - # the `-exported_symbol` option instead to restrict symbol - # visibility. - "-Wl,-exported_symbol", - # On macOS, the symbol starts with an underscore. - "-Wl,_" + exported_symbol, - ], - # On non-macOS unix, use a version script to restrict symbol - # visibility. - "//conditions:default": [ - "-Wl,--version-script", - "-Wl,$(location :" + linker_script_name + ")", - ], - }) - cur_deps = cur_deps + select({ - "@platforms//os:macos": [], - "//conditions:default": [linker_script_name], - }) - - cc_binary( - name = cc_binary_name, - linkshared = True, - visibility = ["//visibility:private"], - deps = cur_deps, - tags = tags + ["manual"], - linkopts = cur_linkopts, - **kwargs - ) - - copy_file( - name = cc_binary_pyd_name + "__pyd_copy", - src = ":" + cc_binary_dll_name, - out = cc_binary_pyd_name, - visibility = visibility, - tags = ["manual"], - testonly = testonly, - ) - - native.filegroup( - name = shared_objects_name, - data = select({ - "@platforms//os:windows": [":" + cc_binary_pyd_name], - "//conditions:default": [":" + cc_binary_so_name], - }), - testonly = testonly, - ) - py_library( - name = name, - data = [":" + shared_objects_name], - imports = imports, - tags = tags, - testonly = testonly, - visibility = visibility, - ) diff --git a/tests/integration/pip_parse_isolated/MODULE.bazel b/tests/integration/pip_parse_isolated/MODULE.bazel index 6c44257acb..c25e9dc024 100644 --- a/tests/integration/pip_parse_isolated/MODULE.bazel +++ b/tests/integration/pip_parse_isolated/MODULE.bazel @@ -12,8 +12,8 @@ python.toolchain(python_version = "3.13") # This test module verifies that dependencies can be used with `isolate = True`. pip = use_extension("@rules_python//python/extensions:pip.bzl", "pip", isolate = True) pip.parse( - hub_name = "pypi", + hub_name = "pypi_isolated", python_version = "3.13", requirements_lock = "//:requirements_lock.txt", ) -use_repo(pip, "pypi") +use_repo(pip, pypi = "pypi_isolated") diff --git a/tests/integration/runner.py b/tests/integration/runner.py index 2534ab2d90..9efcbebb89 100644 --- a/tests/integration/runner.py +++ b/tests/integration/runner.py @@ -80,10 +80,13 @@ def setUp(self): # Put the global tmp not under the test tmp to better match how a real # execution has entirely different directories for these. self.tmp_dir = outer_test_tmpdir / "bit_tmp" + self.test_tmp_dir.mkdir(parents=True, exist_ok=True) + self.tmp_dir.mkdir(parents=True, exist_ok=True) self.bazel_env = { "PATH": os.environ["PATH"], "TEST_TMPDIR": str(self.test_tmp_dir), "TMP": str(self.tmp_dir), + "TEMP": str(self.tmp_dir), # For some reason, this is necessary for Bazel 6.4 to work. # If not present, it can't find some bash helpers in @bazel_tools "RUNFILES_DIR": os.environ["TEST_SRCDIR"], @@ -100,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/runtime_manifests/.bazelrc b/tests/integration/runtime_manifests/.bazelrc new file mode 100644 index 0000000000..d4d45a5ea7 --- /dev/null +++ b/tests/integration/runtime_manifests/.bazelrc @@ -0,0 +1,4 @@ +# Copy of fast-tests config +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 diff --git a/tests/integration/runtime_manifests/BUILD.bazel b/tests/integration/runtime_manifests/BUILD.bazel new file mode 100644 index 0000000000..4746fd419b --- /dev/null +++ b/tests/integration/runtime_manifests/BUILD.bazel @@ -0,0 +1,7 @@ +load("@rules_python//python:py_test.bzl", "py_test") + +py_test( + name = "basic_test", + srcs = ["basic_test.py"], + python_version = "3.11", +) diff --git a/tests/integration/runtime_manifests/MODULE.bazel b/tests/integration/runtime_manifests/MODULE.bazel new file mode 100644 index 0000000000..6891b07818 --- /dev/null +++ b/tests/integration/runtime_manifests/MODULE.bazel @@ -0,0 +1,19 @@ +module(name = "runtime_manifests") + +bazel_dep(name = "rules_python", version = "0.0.0") +local_path_override( + module_name = "rules_python", + path = "../../..", +) + +python = use_extension("@rules_python//python/extensions:python.bzl", "python") +python.override( + add_runtime_manifest_urls = [ + "https://github.com/astral-sh/python-build-standalone/releases/download/20260414/SHA256SUMS", + ], + register_all_versions = True, + runtime_manifest_sha = "ce18fdfd47c66830a40ea9b9e314a14b1636bbfd684501bc5ca1fc6d55a7933f", +) +python.toolchain( + python_version = "3.11.15", +) diff --git a/tests/integration/runtime_manifests/WORKSPACE b/tests/integration/runtime_manifests/WORKSPACE new file mode 100644 index 0000000000..8277ec8090 --- /dev/null +++ b/tests/integration/runtime_manifests/WORKSPACE @@ -0,0 +1 @@ +# Workspace boundary file required by rules_bazel_integration_test diff --git a/tests/integration/runtime_manifests/basic_test.py b/tests/integration/runtime_manifests/basic_test.py new file mode 100644 index 0000000000..35f0e93b6e --- /dev/null +++ b/tests/integration/runtime_manifests/basic_test.py @@ -0,0 +1,27 @@ +import datetime +import platform +import sys +import unittest + + +class BasicTest(unittest.TestCase): + def test_basic(self): + print("Hello World from Python {}!".format(sys.version)) + print("Interpreter executable path: {}".format(sys.executable)) + + # Verify that the hermetic interpreter inside Bazel's output/sandbox tree is used + self.assertIn(".cache/bazel", sys.executable) + + # Verify that the exact custom version (3.11.15) parsed from the manifest is used + self.assertEqual(sys.version_info[:3], (3, 11, 15)) + + # Verify that the exact build version (20260414) parsed from the manifest is used + buildno, builddate = platform.python_build() + date_str = " ".join(builddate.split()[:3]) + dt = datetime.datetime.strptime(date_str, "%b %d %Y") + formatted_date = dt.strftime("%Y%m%d") + self.assertEqual(formatted_date, "20260414") + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/integration/unified_pypi/.bazelrc b/tests/integration/unified_pypi/.bazelrc new file mode 100644 index 0000000000..b3a24e8605 --- /dev/null +++ b/tests/integration/unified_pypi/.bazelrc @@ -0,0 +1 @@ +common --experimental_enable_bzlmod diff --git a/tests/integration/unified_pypi/BUILD.bazel b/tests/integration/unified_pypi/BUILD.bazel new file mode 100644 index 0000000000..c0b9905fa6 --- /dev/null +++ b/tests/integration/unified_pypi/BUILD.bazel @@ -0,0 +1,61 @@ +load("@rules_python//python:py_binary.bzl", "py_binary") +load("@rules_python//python:py_test.bzl", "py_test") + +package(default_visibility = ["//visibility:public"]) + +py_test( + name = "test_default", + srcs = ["test_default.py"], + deps = ["@pypi//colorama"], +) + +py_test( + name = "test_cli", + srcs = ["test_cli.py"], + deps = ["@pypi//colorama"], +) + +py_test( + name = "test_a", + srcs = ["test_a.py"], + config_settings = { + "@rules_python//python/config_settings:venv": "pypi_a", + }, + deps = [ + "@pypi//colorama", + "@pypi//colorama:my_colorama", + ], +) + +# Sibling extra alias failure target (my_colorama is missing in pypi_b): +py_binary( + name = "bin_extra_b", + srcs = ["bin_extra_b.py"], + config_settings = { + "@rules_python//python/config_settings:venv": "pypi_b", + }, + deps = ["@pypi//colorama:my_colorama"], +) + +# Disjoint package failure target (six is missing in pypi_a): +py_binary( + name = "bin_six_a", + srcs = ["bin_six_a.py"], + config_settings = { + "@rules_python//python/config_settings:venv": "pypi_a", + }, + deps = ["@pypi//six"], +) + +py_binary( + name = "bin_declared_only", + srcs = ["bin_declared_only.py"], + deps = ["@pypi//declared_only_pkg"], +) + +py_binary( + name = "bin_declared_only_alias", + srcs = ["bin_declared_only.py"], + main = "bin_declared_only.py", + deps = ["@pypi//declared_only_pkg:declared-only-alias"], +) diff --git a/tests/integration/unified_pypi/MODULE.bazel b/tests/integration/unified_pypi/MODULE.bazel new file mode 100644 index 0000000000..6a4b87e015 --- /dev/null +++ b/tests/integration/unified_pypi/MODULE.bazel @@ -0,0 +1,52 @@ +module(name = "unified_pypi") + +bazel_dep(name = "rules_python", version = "0.0.0") +local_path_override( + module_name = "rules_python", + path = "../../..", +) + +python = use_extension("@rules_python//python/extensions:python.bzl", "python") +python.toolchain(python_version = "3.11") + +pip = use_extension("@rules_python//python/extensions:pip.bzl", "pip") +pip.whl_mods( + additive_build_content = """\ +load("@rules_python//python:defs.bzl", "py_library") + +py_library( + name = "my_colorama", + deps = [":pkg"], +) +""", + hub_name = "whl_mods_hub", + whl_name = "colorama", +) +use_repo(pip, "whl_mods_hub") + +# pypi_a has colorama and an extra alias +pip.parse( + extra_hub_aliases = {"colorama": ["my_colorama"]}, + hub_name = "pypi_a", + python_version = "3.11", + requirements_lock = "//:requirements_a.txt", + whl_modifications = { + "@whl_mods_hub//:colorama.json": "colorama", + }, +) +use_repo(pip, "pypi_a") + +# pypi_b has colorama and six, and acts as designated fallback +pip.parse( + hub_name = "pypi_b", + python_version = "3.11", + requirements_lock = "//:requirements_b.txt", +) +use_repo(pip, "pypi_b") + +pip.default(default_hub = "pypi_b") +pip.dep( + name = "declared-only-pkg", + extra_targets = ["declared-only-alias"], +) +use_repo(pip, "pypi") diff --git a/tests/integration/unified_pypi/WORKSPACE b/tests/integration/unified_pypi/WORKSPACE new file mode 100644 index 0000000000..0a08afe832 --- /dev/null +++ b/tests/integration/unified_pypi/WORKSPACE @@ -0,0 +1 @@ +# Minimal WORKSPACE file diff --git a/tests/integration/unified_pypi/WORKSPACE.bzlmod b/tests/integration/unified_pypi/WORKSPACE.bzlmod new file mode 100644 index 0000000000..7bd1c969b9 --- /dev/null +++ b/tests/integration/unified_pypi/WORKSPACE.bzlmod @@ -0,0 +1 @@ +# Minimal WORKSPACE.bzlmod diff --git a/tests/integration/unified_pypi/bin_declared_only.py b/tests/integration/unified_pypi/bin_declared_only.py new file mode 100644 index 0000000000..0b03607e77 --- /dev/null +++ b/tests/integration/unified_pypi/bin_declared_only.py @@ -0,0 +1,2 @@ +# Dummy file for integration test +print("declared_only") diff --git a/tests/integration/unified_pypi/bin_extra_b.py b/tests/integration/unified_pypi/bin_extra_b.py new file mode 100644 index 0000000000..f900d16fd2 --- /dev/null +++ b/tests/integration/unified_pypi/bin_extra_b.py @@ -0,0 +1 @@ +print("Should not be executed") diff --git a/tests/integration/unified_pypi/bin_six_a.py b/tests/integration/unified_pypi/bin_six_a.py new file mode 100644 index 0000000000..f900d16fd2 --- /dev/null +++ b/tests/integration/unified_pypi/bin_six_a.py @@ -0,0 +1 @@ +print("Should not be executed") diff --git a/tests/integration/unified_pypi/requirements_a.txt b/tests/integration/unified_pypi/requirements_a.txt new file mode 100644 index 0000000000..788f12f818 --- /dev/null +++ b/tests/integration/unified_pypi/requirements_a.txt @@ -0,0 +1,3 @@ +colorama==0.4.6 \ + --hash=sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44 \ + --hash=sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6 diff --git a/tests/integration/unified_pypi/requirements_b.txt b/tests/integration/unified_pypi/requirements_b.txt new file mode 100644 index 0000000000..c69f3631b2 --- /dev/null +++ b/tests/integration/unified_pypi/requirements_b.txt @@ -0,0 +1,6 @@ +colorama==0.4.5 \ + --hash=sha256:854bf444933e37f5824ae7bfc1e98d5bce2ebe4160d46b5edf346a89358e99da \ + --hash=sha256:e6c6b4334fc50988a639d9b98ae42f5c90ec94cb1495b4fe76c5f72cf7f79435 +six==1.17.0 \ + --hash=sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274 \ + --hash=sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81 diff --git a/tests/integration/unified_pypi/test_a.py b/tests/integration/unified_pypi/test_a.py new file mode 100644 index 0000000000..a9127ba0c7 --- /dev/null +++ b/tests/integration/unified_pypi/test_a.py @@ -0,0 +1,3 @@ +import colorama + +assert colorama.__version__ == "0.4.6" diff --git a/tests/integration/unified_pypi/test_cli.py b/tests/integration/unified_pypi/test_cli.py new file mode 100644 index 0000000000..a9127ba0c7 --- /dev/null +++ b/tests/integration/unified_pypi/test_cli.py @@ -0,0 +1,3 @@ +import colorama + +assert colorama.__version__ == "0.4.6" diff --git a/tests/integration/unified_pypi/test_default.py b/tests/integration/unified_pypi/test_default.py new file mode 100644 index 0000000000..559df59961 --- /dev/null +++ b/tests/integration/unified_pypi/test_default.py @@ -0,0 +1,3 @@ +import colorama + +assert colorama.__version__ == "0.4.5" diff --git a/tests/integration/unified_pypi_test.py b/tests/integration/unified_pypi_test.py new file mode 100644 index 0000000000..a19a9fcfbe --- /dev/null +++ b/tests/integration/unified_pypi_test.py @@ -0,0 +1,100 @@ +"""Integration test for Unified PyPI Hub dynamic dependency resolution.""" + +import contextlib +import unittest + +from tests.integration import runner + + +class UnifiedPypiTest(runner.TestCase): + def test_default_fallback_hub(self): + self.run_bazel("test", "//:test_default") + + def test_transitioned_hub(self): + self.run_bazel("test", "//:test_a") + + def test_cli_override(self): + self.run_bazel( + "run", + "--@rules_python//python/config_settings:venv=pypi_a", + "//:test_cli", + ) + + def test_disjoint_package_cquery_succeeds_but_build_fails(self): + self.run_bazel("cquery", "//:bin_six_a") + result = self.run_bazel("build", "//:bin_six_a", check=False) + self.assertNotEqual( + result.exit_code, + 0, + "Expected build to fail during execution phase", + ) + self.assert_result_matches( + result, + 'ERROR: PyPI package "six" is not available when building under PyPI hub "pypi_a"\\. Try adding it to the requirements of this hub', + ) + + def test_sibling_extra_alias_cquery_succeeds_but_build_fails(self): + self.run_bazel("cquery", "//:bin_extra_b") + result = self.run_bazel("build", "//:bin_extra_b", check=False) + self.assertNotEqual( + result.exit_code, + 0, + "Expected build to fail during execution phase", + ) + self.assert_result_matches( + result, + 'ERROR: PyPI package "colorama:my_colorama" is not available when building under PyPI hub "pypi_b"\\. Try adding it to the requirements of this hub', + ) + + @contextlib.contextmanager + def _temp_modify_file(self, path, new_content): + original_content = path.read_text() + path.write_text(new_content) + try: + yield + finally: + path.write_text(original_content) + + def test_invalid_default_hub_fails_evaluation(self): + module_bazel = self.repo_root / "MODULE.bazel" + invalid_content = module_bazel.read_text().replace( + 'pip.default(default_hub = "pypi_b")', + 'pip.default(default_hub = "invalid_hub")', + ) + with self._temp_modify_file(module_bazel, invalid_content): + # Run bazel cquery and expect it to fail during loading/extension phase + result = self.run_bazel("cquery", "//:test_default", check=False) + self.assertNotEqual( + result.exit_code, + 0, + "Expected extension evaluation to fail due to invalid default_hub", + ) + self.assert_result_matches( + result, + "default_hub 'invalid_hub' is not a defined PyPI hub", + ) + + def test_unimplemented_declared_dep_fails_build(self): + # Even though cquery succeeds: + self.run_bazel("cquery", "//:bin_declared_only") + + # Build must fail because the package is not implemented by any concrete hub + result = self.run_bazel("build", "//:bin_declared_only", check=False) + self.assertNotEqual(result.exit_code, 0) + self.assert_result_matches( + result, + 'ERROR: PyPI package "declared_only_pkg" is not available when building under PyPI hub "pypi_b"\\. Try adding it to the requirements of this hub', + ) + + def test_unimplemented_declared_dep_alias_fails_build(self): + # Build must fail for alias too + result = self.run_bazel("build", "//:bin_declared_only_alias", check=False) + self.assertNotEqual(result.exit_code, 0) + self.assert_result_matches( + result, + 'ERROR: PyPI package "declared_only_pkg:declared-only-alias" is not available when building under PyPI hub "pypi_b"\\. Try adding it to the requirements of this hub', + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/integration/uv_lock/.bazelrc b/tests/integration/uv_lock/.bazelrc new file mode 100644 index 0000000000..511f8a2413 --- /dev/null +++ b/tests/integration/uv_lock/.bazelrc @@ -0,0 +1,5 @@ +build --enable_runfiles +common --experimental_isolated_extension_usages +common --action_env=UV_EXTRA_INDEX_URL + +try-import %workspace%/user.bazelrc diff --git a/tests/integration/uv_lock/.bazelversion b/tests/integration/uv_lock/.bazelversion new file mode 100644 index 0000000000..47da986f86 --- /dev/null +++ b/tests/integration/uv_lock/.bazelversion @@ -0,0 +1 @@ +9.1.0 diff --git a/tests/integration/uv_lock/BUILD.bazel b/tests/integration/uv_lock/BUILD.bazel new file mode 100644 index 0000000000..da6482ba08 --- /dev/null +++ b/tests/integration/uv_lock/BUILD.bazel @@ -0,0 +1,26 @@ +load("@bazel_skylib//rules:diff_test.bzl", "diff_test") +load("@rules_python//python/uv:lock.bzl", "lock") +load(":uv_runner.bzl", "uv_runner") + +lock( + name = "requirements", + srcs = ["requirements.in"], + out = "requirements.txt", + tags = ["no-remote-exec"], +) + +uv_runner( + name = "uv", + is_windows = select({ + "@platforms//os:windows": True, + "//conditions:default": False, + }), + tags = ["manual"], +) + +diff_test( + name = "requirements_diff_test", + timeout = "short", + file1 = ":requirements", + file2 = ":requirements.txt", +) diff --git a/tests/integration/uv_lock/MODULE.bazel b/tests/integration/uv_lock/MODULE.bazel new file mode 100644 index 0000000000..0e0978ed36 --- /dev/null +++ b/tests/integration/uv_lock/MODULE.bazel @@ -0,0 +1,18 @@ +module(name = "uv_lock") + +bazel_dep(name = "bazel_skylib", version = "1.7.1") +bazel_dep(name = "platforms", version = "0.0.10") +bazel_dep(name = "rules_python") +local_path_override( + module_name = "rules_python", + path = "../../..", +) + +python = use_extension("@rules_python//python/extensions:python.bzl", "python") +python.toolchain(python_version = "3.13") + +uv = use_extension("@rules_python//python/uv:uv.bzl", "uv") +uv.configure(version = "0.11.2") +use_repo(uv, "uv") + +register_toolchains("@uv//:all") diff --git a/tests/integration/uv_lock/README.md b/tests/integration/uv_lock/README.md new file mode 100644 index 0000000000..cce142852c --- /dev/null +++ b/tests/integration/uv_lock/README.md @@ -0,0 +1,64 @@ +# uv_lock integration test workspace + +This directory is a self-contained Bazel workspace used by the +`//tests/integration:uv_lock_test` integration test. + +It demonstrates how to use the `lock()` macro from `@rules_python//python/uv` +to pin requirements with `uv pip compile`. + +## Targets + +| Target | Description | +|--------|-------------| +| `//:requirements` | Build action that produces the locked requirements file | +| `//:requirements.update` | Update the in-source `requirements.txt` via `bazel run` | +| `//:requirements.run` | Run `uv pip compile` with extra command-line args | +| `//:requirements_diff_test` | Diff test comparing the lock output to the in-source file | +| `//:uv` | The `uv` binary from the registered toolchain | + +## Workflow for debugging + +If you want to debug and play around, you can start the server and then run the uv lock command +manually. + +### Start the local PyPI server + +In a separate terminal, start the pypiserver that serves the `my-local-pkg` test wheel: + +```shell +bazel run //tests/integration:uv_lock_pypi_server [-- --no-auth] +``` + +The server prints the URL to use (with and without authentication) and the +SHA256 of the wheel. Pass `--no-auth` to allow anonymous access. + +### Lock the requirements + +With the server running, lock the requirements from this directory: + +```shell +cd tests/integration/uv_lock +bazel run //:requirements.update \ + --action_env=UV_EXTRA_INDEX_URL="" \ + --action_env=UV_CREDENTIALS_DIR= +``` + +The `` and `` values are printed by the pypi-server. + +### Verify the lock output matches the in-source file + +```shell +bazel test //:requirements_diff_test +``` + +## bazel-in-bazel Testing + +When iterating on changes to `lock.bzl`, the integration test can be run +directly from rules_python: + +```shell +bazel test //tests/integration:uv_lock_test_bazel_self \ + --config=fast-tests \ + --test_output=streamed \ + --test_filter= +``` diff --git a/tests/integration/uv_lock/WORKSPACE b/tests/integration/uv_lock/WORKSPACE new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/integration/uv_lock/requirements.in b/tests/integration/uv_lock/requirements.in new file mode 100644 index 0000000000..fba55a7329 --- /dev/null +++ b/tests/integration/uv_lock/requirements.in @@ -0,0 +1 @@ +my-local-pkg==1.0.0 diff --git a/tests/integration/uv_lock/requirements.txt b/tests/integration/uv_lock/requirements.txt new file mode 100644 index 0000000000..52f5c757f5 --- /dev/null +++ b/tests/integration/uv_lock/requirements.txt @@ -0,0 +1,5 @@ +# This file was autogenerated by uv via the following command: +# bazel run //:requirements.update +my-local-pkg==1.0.0 \ + --hash=sha256:be24d5183a182e8da4465ae1b7e60324864d1a72866ec9aefa6aaf80a4529eb1 + # via -r requirements.in diff --git a/tests/integration/uv_lock/uv_runner.bzl b/tests/integration/uv_lock/uv_runner.bzl new file mode 100644 index 0000000000..3faa3c6fc0 --- /dev/null +++ b/tests/integration/uv_lock/uv_runner.bzl @@ -0,0 +1,30 @@ +"""A rule exposing the uv binary from the registered toolchain as an executable target. + +This allows running ``bazel run //:uv -- `` from the test workspace. +""" + +def _uv_runner_impl(ctx): + toolchain_info = ctx.toolchains["@rules_python//python/uv:uv_toolchain_type"] + original_uv_executable = toolchain_info.uv_toolchain_info.uv[DefaultInfo].files_to_run.executable + + ext = "" + if ctx.attr.is_windows: + ext = ".exe" + + uv_exe = ctx.actions.declare_file("uv" + ext) + ctx.actions.symlink(output = uv_exe, target_file = original_uv_executable) + + return DefaultInfo( + files = depset([uv_exe]), + executable = uv_exe, + runfiles = toolchain_info.default_info.default_runfiles, + ) + +uv_runner = rule( + implementation = _uv_runner_impl, + executable = True, + attrs = { + "is_windows": attr.bool(mandatory = True), + }, + toolchains = ["@rules_python//python/uv:uv_toolchain_type"], +) diff --git a/tests/integration/uv_lock_pypi_server.py b/tests/integration/uv_lock_pypi_server.py new file mode 100644 index 0000000000..1f350b809f --- /dev/null +++ b/tests/integration/uv_lock_pypi_server.py @@ -0,0 +1,148 @@ +import argparse +import hashlib +import io +import os +import sys +import uuid +import zipfile +from wsgiref.simple_server import make_server + +from pypiserver import app_from_config, setup_routes_from_config +from pypiserver.config import Config + + +def _create_wheel_bytes(name, version): + pkg_name_normalized = name.replace("-", "_") + wheel_name = "{}-{}-py3-none-any.whl".format(pkg_name_normalized, version) + dist_info = "{}-{}.dist-info".format(pkg_name_normalized, version) + + metadata = ( + "Metadata-Version: 2.1\n" + "Name: {name}\n" + "Version: {version}\n" + "Summary: A test package\n" + ).format(name=pkg_name_normalized, version=version) + + wheel_file = ( + "Wheel-Version: 1.0\n" + "Generator: test\n" + "Root-Is-Purelib: true\n" + "Tag: py3-none-any\n" + ) + + record_entries = [ + "{}/__init__.py,".format(pkg_name_normalized), + "{}/METADATA,".format(dist_info), + "{}/WHEEL,".format(dist_info), + "{}/RECORD,".format(dist_info), + ] + + buf = io.BytesIO() + with zipfile.ZipFile(buf, "w", zipfile.ZIP_DEFLATED) as zf: + zf.writestr("{}/__init__.py".format(pkg_name_normalized), "# empty\n") + zf.writestr("{}/METADATA".format(dist_info), metadata) + zf.writestr("{}/WHEEL".format(dist_info), wheel_file) + zf.writestr("{}/RECORD".format(dist_info), "\n".join(record_entries)) + + wheel_data = buf.getvalue() + sha256 = hashlib.sha256(wheel_data).hexdigest() + return wheel_data, sha256, wheel_name + + +def main(): + parser = argparse.ArgumentParser( + description="Standalone pypiserver for uv_lock integration tests" + ) + parser.add_argument( + "--packages-dir", + type=str, + default=None, + help="Directory for the test wheels (default: $TEST_TMPDIR/pypi-server-packages)", + ) + parser.add_argument( + "--no-auth", + action="store_true", + default=False, + help="Disable authentication (allows anonymous access)", + ) + parser.add_argument( + "--port", + type=int, + default=0, + help="Port to listen on (0 = find free port)", + ) + parser.add_argument( + "--host", + default="localhost", + help="Host to bind to", + ) + args = parser.parse_args() + + if args.packages_dir is None: + sandbox_root = ( + os.environ.get("TEST_TMPDIR") or os.environ.get("TMPDIR") or "/tmp" + ) + args.packages_dir = os.path.join(sandbox_root, "pypi-server-packages") + packages_dir = args.packages_dir + os.makedirs(packages_dir, exist_ok=True) + + wheel_data, sha256, wheel_name = _create_wheel_bytes("my-local-pkg", "1.0.0") + wheel_path = os.path.join(packages_dir, wheel_name) + with open(wheel_path, "wb") as f: + f.write(wheel_data) + + print("Wheel: {}".format(wheel_path), flush=True) + print("SHA256: {}".format(sha256), flush=True) + + password = uuid.uuid4().hex + username = "testuser" + + if args.no_auth: + authenticate = [] + else: + authenticate = ["download", "list", "update"] + + config = Config.default_with_overrides( + roots=[packages_dir], + port=args.port, + host=args.host, + authenticate=authenticate, + password_file=None, + auther=lambda u, p: u == username and p == password, + disable_fallback=True, + fallback_url="", + server_method="wsgiref", + verbosity=0, + log_stream=None, + ) + app = app_from_config(config) + app = setup_routes_from_config(app, config) + + 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) + auth_url = "http://{}:{}@{}:{}".format(username, password, args.host, port) + + print("\npypiserver listening on:\n", flush=True) + print(" URL (no auth): {}".format(base_url), flush=True) + print(" URL (auth): {}".format(auth_url), flush=True) + print("\nRequired dependency in requirements.in:", flush=True) + print(" my-local-pkg==1.0.0", flush=True) + print("\nTo use from the uv_lock test workspace, run:", flush=True) + print(" cd tests/integration/uv_lock", flush=True) + print(" bazel run //:requirements.update \\", flush=True) + print(' --action_env=UV_EXTRA_INDEX_URL="{}" \\'.format(auth_url), flush=True) + print(" --action_env=UV_CREDENTIALS_DIR=", flush=True) + print("\nPress Ctrl+C to stop the server.\n", flush=True) + + try: + server.serve_forever() + except KeyboardInterrupt: + print("\nShutting down...", flush=True) + server.shutdown() + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tests/integration/uv_lock_test.py b/tests/integration/uv_lock_test.py new file mode 100644 index 0000000000..8efd3cb84f --- /dev/null +++ b/tests/integration/uv_lock_test.py @@ -0,0 +1,288 @@ +import base64 +import hashlib +import os +import re +import threading +import time +import unittest +import uuid +from pathlib import Path +from urllib.error import URLError +from urllib.request import Request, urlopen +from wsgiref.simple_server import make_server + +from pypiserver import app_from_config, setup_routes_from_config +from pypiserver.config import Config + +from tests.integration import runner +from tests.integration.uv_lock_pypi_server import _create_wheel_bytes + + +def _make_server_on_free_port(app): + server = make_server("localhost", 0, app) + port = server.server_address[1] + return server, port + + +class UvLockIntegrationTest(runner.TestCase): + def setUp(self): + super().setUp() + + self.username = "testuser" + self.password = uuid.uuid4().hex + + self.dir = Path(os.environ["TEST_TMPDIR"]) + self.docroot = self.dir / "simple" + self.docroot.mkdir(exist_ok=True) + + self.wheel_data, self.wheel_sha256, wheel_name = _create_wheel_bytes( + "my-local-pkg", + "1.0.0", + ) + + packages_dir = self.docroot / "packages" + packages_dir.mkdir(exist_ok=True) + self.wheel_path = packages_dir / wheel_name + self.wheel_path.write_bytes(self.wheel_data) + + config = Config.default_with_overrides( + roots=[packages_dir], + port=0, + host="localhost", + authenticate=["download", "list", "update"], + password_file=None, + auther=lambda u, p: u == self.username and p == self.password, + disable_fallback=True, + fallback_url="", + server_method="wsgiref", + verbosity=0, + log_stream=None, + ) + app = app_from_config(config) + app = setup_routes_from_config(app, config) + + self._server, self.port = _make_server_on_free_port(app) + self.server_url = "http://localhost:{port}".format(port=self.port) + self.auth_url = "http://{user}:{passwd}@localhost:{port}".format( + user=self.username, + passwd=self.password, + port=self.port, + ) + + self._thread = threading.Thread(target=self._server.serve_forever) + self._thread.daemon = True + self._thread.start() + + interval = 0.1 + wait_seconds = 40 + for _ in range(int(wait_seconds / interval)): + try: + req = Request(self.server_url) + with urlopen(req, timeout=1) as response: + if response.status in (200, 401): + break + except (URLError, OSError): + pass + time.sleep(interval) + else: + raise RuntimeError( + "Could not start the server, waited for {}s".format(wait_seconds) + ) + + # Set a default value for UV_EXTRA_INDEX_URL in the bazel env so that + # the workspace .bazelrc `--action_env=UV_EXTRA_INDEX_URL` doesn't + # fail on Windows when the variable is unset in the client env. + self.bazel_env.setdefault("UV_EXTRA_INDEX_URL", "") + + # Use a sandbox-local credential store so credentials don't leak + # to the host system. + self.creds_dir = self.repo_root / ".uv-creds" + self.creds_dir.mkdir(parents=True, exist_ok=True) + self.bazel_env["UV_CREDENTIALS_DIR"] = str(self.creds_dir) + + # Log in to uv's credential store so `uv auth helper` can later + # serve the credentials to Bazel or uv itself. + self.run_bazel( + "run", + "//:uv", + "--", + "auth", + "login", + f"--username={self.username}", + f"--password={self.password}", + self.server_url, + ) + + def tearDown(self): + # Clear credentials from uv's credential store to ensure we are not + # logged into the service after the test. + self.run_bazel( + "run", + "//:uv", + "--", + "auth", + "logout", + self.server_url, + check=False, + ) + self._server.shutdown() + + def _assert_server_requires_auth(self): + req = Request(self.server_url + "/my-local-pkg/") + try: + urlopen(req, timeout=5) + self.fail("Expected 401 without auth") + except URLError: + pass + + def _auth_header(self): + return "Basic " + base64.b64encode( + "{user}:{passwd}".format( + user=self.username, + passwd=self.password, + ).encode("utf-8") + ).decode("utf-8") + + def _assert_simple_api_sha256(self): + auth_header = self._auth_header() + req = Request(self.server_url + "/simple/my-local-pkg/") + req.add_header("Authorization", auth_header) + resp = urlopen(req, timeout=5) + html = resp.read().decode("utf-8") + + match = re.search(r"#sha256=([a-f0-9]+)", html) + self.assertIsNotNone(match, "No sha256 found in simple API: {}".format(html)) + pypiserver_sha256 = match.group(1) + disk_sha256 = hashlib.sha256(self.wheel_path.read_bytes()).hexdigest() + self.assertEqual( + pypiserver_sha256, + disk_sha256, + "pypiserver hash {} != disk hash {}".format(pypiserver_sha256, disk_sha256), + ) + + def _creds_auth_args(self): + return [ + "--strategy=PyRequirementsLockUv=local", + "--action_env={key}={value}".format( + key="UV_CREDENTIALS_DIR", + value=str(self.creds_dir), + ), + "--action_env={key}={value}".format( + key="UV_EXTRA_INDEX_URL", + value=self.server_url, + ), + ] + + def _assert_lock_file(self, result): + self.assertEqual( + result.exit_code, + 0, + "Lock update failed:\n{}".format(result.describe()), + ) + lock_file = self.repo_root / "requirements.txt" + self.assertTrue(lock_file.exists(), "Lock file was not created") + contents = lock_file.read_text() + self.assertIn("my-local-pkg", contents) + self.assertIn("--hash=sha256:", contents) + + def test_lock_update_with_custom_index(self): + self._assert_server_requires_auth() + self._assert_simple_api_sha256() + + result = self.run_bazel( + "run", + "--action_env={key}={value}".format( + key="UV_EXTRA_INDEX_URL", + value=self.auth_url, + ), + "//:requirements.update", + ) + self._assert_lock_file(result) + + def test_update_with_credential_helper(self): + """Use a credential helper for authentication.""" + self._assert_server_requires_auth() + result = self.run_bazel( + "run", + *self._creds_auth_args(), + "//:requirements.update", + ) + self._assert_lock_file(result) + + def test_update_with_uv_auth_helper(self): + """Use the uv auth helper for authentication.""" + self._assert_server_requires_auth() + result = self.run_bazel( + "run", + *self._creds_auth_args(), + "//:requirements.update", + ) + self._assert_lock_file(result) + + def test_diff_test_with_requirements(self): + """Verify that ``diff_test`` can verify the generated lock file.""" + self._assert_server_requires_auth() + + # First generate the lock file + result = self.run_bazel( + "run", + *self._creds_auth_args(), + "//:requirements.update", + ) + self._assert_lock_file(result) + + # Copy the generated lock file to the expected location. The inner + # Bazel workspace is writable because it is a temporary copy created + # by the integration test framework. + generated = self.repo_root / "requirements.txt" + expected = self.repo_root / "requirements_expected.txt" + expected.write_text(generated.read_text()) + + # Run the diff_test: it builds the lock action, then compares the + # output to our expected file. + result = self.run_bazel( + "test", + *self._creds_auth_args(), + "//:requirements_diff_test", + ) + self.assertEqual( + result.exit_code, + 0, + "diff_test failed:\n{}".format(result.describe()), + ) + + def test_no_existing_requirements(self): + """Verify that ``bazel run`` and ``diff_test`` work when + ``requirements.txt`` does not yet exist.""" + self._assert_server_requires_auth() + + # Remove the existing lock file to simulate a fresh checkout + existing = self.repo_root / "requirements.txt" + existing.unlink() + self.assertFalse(existing.exists()) + + # Run ``requirements.update`` to generate the lock from scratch. The + # underlying lock rule will have no ``existing_output`` to copy, but + # ``uv pip compile`` should still produce the output. + result = self.run_bazel( + "run", + *self._creds_auth_args(), + "//:requirements.update", + ) + self._assert_lock_file(result) + + # diff_test should pass now that ``requirements.txt`` exists again + result = self.run_bazel( + "test", + *self._creds_auth_args(), + "//:requirements_diff_test", + ) + self.assertEqual( + result.exit_code, + 0, + "diff_test failed:\n{}".format(result.describe()), + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/integration/validate_test_main/BUILD.bazel b/tests/integration/validate_test_main/BUILD.bazel new file mode 100644 index 0000000000..cf931d3303 --- /dev/null +++ b/tests/integration/validate_test_main/BUILD.bazel @@ -0,0 +1,21 @@ +load("@rules_python//python:py_test.bzl", "py_test") + +# A test whose main module only defines a test case but never runs it. With +# --validate_test_main=enabled this should fail to build. +py_test( + name = "inert_test", + srcs = ["inert_test.py"], +) + +# A test whose main module invokes a runner. This should always build. +py_test( + name = "good_test", + srcs = ["good_test.py"], +) + +# A test whose main module defines nothing and only imports. This is allowed +# even with validation enabled, since it isn't the "defined but never run" case. +py_test( + name = "import_only_test", + srcs = ["import_only_test.py"], +) diff --git a/tests/integration/validate_test_main/MODULE.bazel b/tests/integration/validate_test_main/MODULE.bazel new file mode 100644 index 0000000000..c154ac169e --- /dev/null +++ b/tests/integration/validate_test_main/MODULE.bazel @@ -0,0 +1,10 @@ +module(name = "module_under_test") + +bazel_dep(name = "rules_python", version = "0.0.0") +local_path_override( + module_name = "rules_python", + path = "../../..", +) + +python = use_extension("@rules_python//python/extensions:python.bzl", "python") +python.toolchain(python_version = "3.11") diff --git a/tests/integration/validate_test_main/WORKSPACE b/tests/integration/validate_test_main/WORKSPACE new file mode 100644 index 0000000000..de908549c0 --- /dev/null +++ b/tests/integration/validate_test_main/WORKSPACE @@ -0,0 +1,13 @@ +local_repository( + name = "rules_python", + path = "../../..", +) + +load("@rules_python//python:repositories.bzl", "py_repositories", "python_register_toolchains") + +py_repositories() + +python_register_toolchains( + name = "python_3_11", + python_version = "3.11", +) diff --git a/tests/integration/validate_test_main/WORKSPACE.bzlmod b/tests/integration/validate_test_main/WORKSPACE.bzlmod new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/integration/validate_test_main/good_test.py b/tests/integration/validate_test_main/good_test.py new file mode 100644 index 0000000000..46107c96ec --- /dev/null +++ b/tests/integration/validate_test_main/good_test.py @@ -0,0 +1,10 @@ +import unittest + + +class GoodTest(unittest.TestCase): + def test_something(self): + self.assertTrue(True) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/integration/validate_test_main/import_only_test.py b/tests/integration/validate_test_main/import_only_test.py new file mode 100644 index 0000000000..1bd46dead7 --- /dev/null +++ b/tests/integration/validate_test_main/import_only_test.py @@ -0,0 +1,3 @@ +# This module defines no classes or functions; it only imports. The validation +# action allows it even when enabled. +import os # noqa: F401 diff --git a/tests/integration/validate_test_main/inert_test.py b/tests/integration/validate_test_main/inert_test.py new file mode 100644 index 0000000000..8decf676d4 --- /dev/null +++ b/tests/integration/validate_test_main/inert_test.py @@ -0,0 +1,7 @@ +import unittest + + +class InertTest(unittest.TestCase): + def test_nothing_runs(self): + # This test case is never executed because nothing invokes a runner. + self.assertTrue(True) diff --git a/tests/integration/validate_test_main_test.py b/tests/integration/validate_test_main_test.py new file mode 100644 index 0000000000..4d108411d5 --- /dev/null +++ b/tests/integration/validate_test_main_test.py @@ -0,0 +1,40 @@ +import unittest + +from tests.integration import runner + +_FLAG = "--@rules_python//python/config_settings:validate_test_main" + + +class ValidateTestMainTest(runner.TestCase): + def test_inert_test_fails_when_enabled(self): + """An inert test main should fail the build when validation is on.""" + result = self.run_bazel( + "build", + f"{_FLAG}=enabled", + "//:inert_test", + check=False, + ) + self.assertNotEqual(result.exit_code, 0, "Expected build to fail") + self.assert_result_matches(result, r"will not run any tests") + + def test_good_test_builds_when_enabled(self): + """A main that invokes a runner should build when validation is on.""" + self.run_bazel("build", f"{_FLAG}=enabled", "//:good_test") + + def test_import_only_test_builds_when_enabled(self): + """A main that only imports (defines nothing) is allowed when on.""" + self.run_bazel("build", f"{_FLAG}=enabled", "//:import_only_test") + + def test_inert_test_builds_when_disabled(self): + """Validation is off by default, so even an inert test builds.""" + self.run_bazel("build", f"{_FLAG}=disabled", "//:inert_test") + + def test_inert_test_builds_by_default(self): + """The default (auto) resolves to disabled, so an inert test builds.""" + self.run_bazel("build", "//:inert_test") + + +if __name__ == "__main__": + # Enabling this makes the runner log subprocesses as the test goes along. + # logging.basicConfig(level = "INFO") + unittest.main() diff --git a/tests/integration/whl_library/.bazelrc b/tests/integration/whl_library/.bazelrc new file mode 100644 index 0000000000..eee47a8f19 --- /dev/null +++ b/tests/integration/whl_library/.bazelrc @@ -0,0 +1,2 @@ +common --incompatible_default_to_explicit_init_py +test --test_output=errors diff --git a/tests/integration/whl_library/BUILD.bazel b/tests/integration/whl_library/BUILD.bazel new file mode 100644 index 0000000000..0c26fee4cf --- /dev/null +++ b/tests/integration/whl_library/BUILD.bazel @@ -0,0 +1,65 @@ +load("@rules_python//python:py_test.bzl", "py_test") + +[ + alias( + name = "{}_pkg".format(pkg), + actual = "@rules_python//python:none", + ) + for pkg in [ + "charset_normalizer", + "certifi", + "idna", + "urllib3", + ] +] + +[ + filegroup( + name = "{}_whl".format(pkg), + srcs = [], + visibility = ["//visibility:public"], + ) + for pkg in [ + "charset_normalizer", + "certifi", + "idna", + "urllib3", + ] +] + +# Extract all deps +genquery( + name = "whl_target_deps", + expression = "deps(@whl_archive//:pkg)", + scope = ["@whl_archive//:pkg"], +) + +# Extract all deps +genquery( + name = "whl_deps_target_deps", + expression = "deps(@whl_deps_library//:pkg)", + scope = ["@whl_deps_library//:pkg"], +) + +py_test( + name = "test_contents", + srcs = ["test_contents.py"], + data = [ + ":whl_deps_target_deps", + ":whl_target_deps", + "@pip_archive//:srcs", + "@pip_sdist_archive//:srcs", + "@whl_archive//:srcs", + "@whl_archive//:whl", + "@whl_deps_library//:whl", + ], + env = { + "SDIST_SRC_FILES": "$(locations @pip_sdist_archive//:srcs)", + "SRC_FILES": "$(locations @pip_archive//:srcs)", + "WHL_DEPS": "$(location :whl_target_deps)", + "WHL_DEPS_LOCATION": "$(location @whl_deps_library//:whl)", + "WHL_FILES": "$(locations @whl_archive//:srcs)", + "WHL_LOCATION": "$(location @whl_archive//:whl)", + "WHL_TARGET_DEPS": "$(location :whl_deps_target_deps)", + }, +) diff --git a/tests/integration/whl_library/MODULE.bazel b/tests/integration/whl_library/MODULE.bazel new file mode 100644 index 0000000000..39a579f79e --- /dev/null +++ b/tests/integration/whl_library/MODULE.bazel @@ -0,0 +1,57 @@ +module(name = "integration_test") + +bazel_dep(name = "package_metadata", version = "0.0.7") +bazel_dep(name = "rules_python", version = "0.0.0") +local_path_override( + module_name = "rules_python", + path = "../../..", +) + +python = use_extension("@rules_python//python/extensions:python.bzl", "python") +python.toolchain( + python_version = "3.14", +) +use_repo(python, pbs_host = "python_3_14_host") + +pip_archive = use_repo_rule("@rules_python//python/private/pypi:whl_library.bzl", "pip_archive") + +pip_archive( + name = "pip_sdist_archive", + filename = "requests-2.34.2.tar.gz", + python_interpreter_target = "@pbs_host//:python", + requirement = "requests", + # https://pypi.org/project/requests/#requests-2.34.2.tar.gz + sha256 = "f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed", + urls = [ + "https://files.pythonhosted.org/packages/ac/c3/e2a2b89f2d3e2179abd6d00ebd70bff6273f37fb3e0cc209f48b39d00cbf/requests-2.34.2.tar.gz", + ], +) + +pip_archive( + name = "pip_archive", + python_interpreter_target = "@pbs_host//:python", + requirement = "requests", +) + +whl_archive = use_repo_rule("@rules_python//python/private/pypi:whl_library.bzl", "whl_archive") + +whl_archive( + name = "whl_archive", + dep_template = "@integration_test//:{name}_{target}", + filename = "requests-2.34.2-py3-none-any.whl", + # https://pypi.org/project/requests/#requests-2.34.2-py3-none-any.whl + requirement = "requests", + sha256 = "2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0", + urls = [ + "https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl", + ], +) + +whl_deps_library = use_repo_rule("@rules_python//python/private/pypi:whl_library.bzl", "whl_deps_library") + +whl_deps_library( + name = "whl_deps_library", + dep_template = "@integration_test//:{name}_{target}", + metadata_file = "@whl_archive//:metadata.json", + requirement = "requests", +) diff --git a/tests/integration/whl_library/WORKSPACE b/tests/integration/whl_library/WORKSPACE new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/integration/whl_library/test_contents.py b/tests/integration/whl_library/test_contents.py new file mode 100644 index 0000000000..ab352a5d98 --- /dev/null +++ b/tests/integration/whl_library/test_contents.py @@ -0,0 +1,179 @@ +import os +import re +import unittest +from pathlib import Path + + +class TestContents(unittest.TestCase): + maxDiff = None + + @staticmethod + def _get_files(env_var: str) -> list[str]: + return [ + f.partition("site-packages/")[-1] for f in os.environ[env_var].split(" ") + ] + + def test_sdist_srcs(self): + self.assertEqual( + self._get_files("SDIST_SRC_FILES"), + [ + "requests/__init__.py", + "requests/__version__.py", + "requests/_internal_utils.py", + "requests/_types.py", + "requests/adapters.py", + "requests/api.py", + "requests/auth.py", + "requests/certs.py", + "requests/compat.py", + "requests/cookies.py", + "requests/exceptions.py", + "requests/help.py", + "requests/hooks.py", + "requests/models.py", + "requests/packages.py", + "requests/sessions.py", + "requests/status_codes.py", + "requests/structures.py", + "requests/utils.py", + ], + ) + + def test_srcs(self): + self.assertEqual( + self._get_files("SRC_FILES"), + [ + "requests/__init__.py", + "requests/__version__.py", + "requests/_internal_utils.py", + "requests/_types.py", + "requests/adapters.py", + "requests/api.py", + "requests/auth.py", + "requests/certs.py", + "requests/compat.py", + "requests/cookies.py", + "requests/exceptions.py", + "requests/help.py", + "requests/hooks.py", + "requests/models.py", + "requests/packages.py", + "requests/sessions.py", + "requests/status_codes.py", + "requests/structures.py", + "requests/utils.py", + ], + ) + + def test_whl_srcs(self): + self.assertEqual( + self._get_files("WHL_FILES"), + [ + "requests/__init__.py", + "requests/__version__.py", + "requests/_internal_utils.py", + "requests/_types.py", + "requests/adapters.py", + "requests/api.py", + "requests/auth.py", + "requests/certs.py", + "requests/compat.py", + "requests/cookies.py", + "requests/exceptions.py", + "requests/help.py", + "requests/hooks.py", + "requests/models.py", + "requests/packages.py", + "requests/sessions.py", + "requests/status_codes.py", + "requests/structures.py", + "requests/utils.py", + ], + ) + + def test_whl_location(self): + self.assertTrue( + os.environ["WHL_LOCATION"].endswith("requests-2.34.2-py3-none-any.whl"), + msg=os.environ["WHL_LOCATION"], + ) + self.assertTrue( + os.environ["WHL_DEPS_LOCATION"].endswith( + "requests-2.34.2-py3-none-any.whl" + ), + msg=os.environ["WHL_DEPS_LOCATION"], + ) + + @staticmethod + def _read_file(env_var: str) -> list[str]: + return set(Path(os.environ[env_var]).read_text().splitlines()) + + @staticmethod + def _normalize_label(label: str) -> str: + if not label.startswith("@@"): + return label + repo, _, rest = label.partition("//") + parts = [p for p in re.split(r"[~+]", repo[2:]) if p] + if parts: + return f"@{parts[-1]}//{rest}" + return label + + def test_whl_deps_ar_the_same(self): + for var, main_dep in { + "WHL_DEPS": "@whl_archive//:pkg", + "WHL_TARGET_DEPS": "@whl_deps_library//:pkg", + }.items(): + self.assertEqual( + { + self._normalize_label(x) + for x in self._read_file(var) + if not x.endswith("toolchain_type") + }, + { + main_dep, + "//:certifi_pkg", + "//:charset_normalizer_pkg", + "//:idna_pkg", + "//:urllib3_pkg", + "@whl_archive//:data", + "@whl_archive//:package_metadata", + "@whl_archive//:site-packages/requests-2.34.2.dist-info/INSTALLER", + "@whl_archive//:site-packages/requests-2.34.2.dist-info/METADATA", + "@whl_archive//:site-packages/requests-2.34.2.dist-info/RECORD", + "@whl_archive//:site-packages/requests-2.34.2.dist-info/WHEEL", + "@whl_archive//:site-packages/requests-2.34.2.dist-info/licenses/LICENSE", + "@whl_archive//:site-packages/requests-2.34.2.dist-info/licenses/NOTICE", + "@whl_archive//:site-packages/requests-2.34.2.dist-info/top_level.txt", + "@whl_archive//:site-packages/requests/__init__.py", + "@whl_archive//:site-packages/requests/__version__.py", + "@whl_archive//:site-packages/requests/_internal_utils.py", + "@whl_archive//:site-packages/requests/_types.py", + "@whl_archive//:site-packages/requests/adapters.py", + "@whl_archive//:site-packages/requests/api.py", + "@whl_archive//:site-packages/requests/auth.py", + "@whl_archive//:site-packages/requests/certs.py", + "@whl_archive//:site-packages/requests/compat.py", + "@whl_archive//:site-packages/requests/cookies.py", + "@whl_archive//:site-packages/requests/exceptions.py", + "@whl_archive//:site-packages/requests/help.py", + "@whl_archive//:site-packages/requests/hooks.py", + "@whl_archive//:site-packages/requests/models.py", + "@whl_archive//:site-packages/requests/packages.py", + "@whl_archive//:site-packages/requests/py.typed", + "@whl_archive//:site-packages/requests/sessions.py", + "@whl_archive//:site-packages/requests/status_codes.py", + "@whl_archive//:site-packages/requests/structures.py", + "@whl_archive//:site-packages/requests/utils.py", + "@whl_archive//:srcs", + "@rules_python//python:none", + "@rules_python//python/config_settings:_is_venvs_site_packages_yes", + "@rules_python//python/config_settings:add_srcs_to_runfiles", + "@rules_python//python/config_settings:precompile", + "@rules_python//python/config_settings:precompile_source_retention", + "@rules_python//python/config_settings:venvs_site_packages", + "@rules_python//python/private:sentinel", + }, + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/modules/other/BUILD.bazel b/tests/modules/other/BUILD.bazel index 665049b9f5..2ce7c88325 100644 --- a/tests/modules/other/BUILD.bazel +++ b/tests/modules/other/BUILD.bazel @@ -1,4 +1,5 @@ load("@rules_python//python:py_binary.bzl", "py_binary") +load("@rules_python//tests/support:explicit_init_py_test.bzl", "explicit_init_py_test") load("@rules_python//tests/support:py_reconfig.bzl", "py_reconfig_binary") package( @@ -28,3 +29,28 @@ py_binary( "//nspkg_gamma", ], ) + +explicit_init_py_test( + name = "test_module_dep_no_init", + expect_generated_init = False, + main = "external_main.py", +) + +explicit_init_py_test( + name = "test_legacy_create_init_override", + expect_generated_init = True, + legacy_create_init = 1, + main = "external_main.py", +) + +# These tests are run by @rules_python//tests/explicit_init_py to ensure a +# module's configuration is respected when it's a dependency. +test_suite( + name = "explicit_init_py_tests", + tests = [ + ":test_legacy_create_init_override", + ":test_module_dep_no_init", + "@init_py_test_extension_repo//:test", + "@init_py_test_repo//:test", + ], +) diff --git a/tests/modules/other/MODULE.bazel b/tests/modules/other/MODULE.bazel index 11a633d56b..12ee7672b6 100644 --- a/tests/modules/other/MODULE.bazel +++ b/tests/modules/other/MODULE.bazel @@ -2,4 +2,18 @@ module(name = "other") bazel_dep(name = "rules_python", version = "0") bazel_dep(name = "bazel_skylib", version = "1.7.1") +bazel_dep(name = "rules_testing", version = "0.6.0") bazel_dep(name = "another_module", version = "0") + +# Validate behavior of explicit_init_py configuration extension when used by +# a module that's a dependency. +rules_python_config = use_extension("@rules_python//python/extensions:config.bzl", "config") +rules_python_config.explicit_init_py(default = True) + +init_py_test_repo = use_repo_rule("//:ext.bzl", "init_py_test_repo") + +init_py_test_repo(name = "init_py_test_repo") + +other_ext = use_extension("//:ext.bzl", "other_init_py_test_ext") +other_ext.repo(name = "init_py_test_extension_repo") +use_repo(other_ext, "init_py_test_extension_repo") diff --git a/tests/modules/other/ext.bzl b/tests/modules/other/ext.bzl new file mode 100644 index 0000000000..943909ee71 --- /dev/null +++ b/tests/modules/other/ext.bzl @@ -0,0 +1,37 @@ +"""Module extension declared by 'other' module for testing __init__.py generation.""" + +_BUILD_FILE_CONTENT = """\ +load("@rules_python//tests/support:explicit_init_py_test.bzl", "explicit_init_py_test") + +explicit_init_py_test( + name = "test", + main = "main.py", + expect_generated_init = False, +) +""" + +_MAIN_PY_CONTENT = "print('hello, world')" + +def _repo_impl(rctx): + rctx.file("main.py", _MAIN_PY_CONTENT) + rctx.file("BUILD.bazel", _BUILD_FILE_CONTENT) + +init_py_test_repo = repository_rule(implementation = _repo_impl) + +def _other_init_py_test_ext_impl(module_ctx): + for mod in module_ctx.modules: + for tag in mod.tags.repo: + init_py_test_repo(name = tag.name) + +_repo_tag = tag_class( + attrs = { + "name": attr.string(mandatory = True), + }, +) + +other_init_py_test_ext = module_extension( + implementation = _other_init_py_test_ext_impl, + tag_classes = { + "repo": _repo_tag, + }, +) diff --git a/tests/modules/rules_pyrefly_stub/WORKSPACE b/tests/modules/rules_pyrefly_stub/WORKSPACE new file mode 100644 index 0000000000..48a0b3083d --- /dev/null +++ b/tests/modules/rules_pyrefly_stub/WORKSPACE @@ -0,0 +1 @@ +workspace(name = "rules_pyrefly") diff --git a/tests/modules/rules_pyrefly_stub/pyrefly/BUILD.bazel b/tests/modules/rules_pyrefly_stub/pyrefly/BUILD.bazel new file mode 100644 index 0000000000..2f14f71b3b --- /dev/null +++ b/tests/modules/rules_pyrefly_stub/pyrefly/BUILD.bazel @@ -0,0 +1,3 @@ +package(default_visibility = ["//visibility:public"]) + +exports_files(["pyrefly.bzl"]) diff --git a/tests/modules/rules_pyrefly_stub/pyrefly/pyrefly.bzl b/tests/modules/rules_pyrefly_stub/pyrefly/pyrefly.bzl new file mode 100644 index 0000000000..fc1a44ed31 --- /dev/null +++ b/tests/modules/rules_pyrefly_stub/pyrefly/pyrefly.bzl @@ -0,0 +1,21 @@ +"""Stub implementation of rules_pyrefly for WORKSPACE mode.""" + +# buildifier: disable=unused-variable +def _noop_aspect_impl(_target, _ctx): + return [] + +_noop_aspect = aspect( + implementation = _noop_aspect_impl, + doc = "No-op Pyrefly aspect stub for WORKSPACE mode.", +) + +def pyrefly(**_kwargs): + """Stub pyrefly aspect constructor. + + Args: + **_kwargs: Ignored keyword arguments. + + Returns: + A no-op aspect. + """ + return _noop_aspect 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/BUILD.bazel b/tests/news/BUILD.bazel new file mode 100644 index 0000000000..31abff1a8a --- /dev/null +++ b/tests/news/BUILD.bazel @@ -0,0 +1,8 @@ +load("//python:py_test.bzl", "py_test") + +py_test( + name = "news_test", + srcs = ["news_test.py"], + data = ["//news:news_files"], + deps = ["//python/runfiles"], +) diff --git a/tests/news/news_test.py b/tests/news/news_test.py new file mode 100644 index 0000000000..66476145a2 --- /dev/null +++ b/tests/news/news_test.py @@ -0,0 +1,69 @@ +import pathlib +import unittest + +from python.runfiles import runfiles + + +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) + return None + + +class NewsTest(unittest.TestCase): + def test_all_news_files_are_valid(self): + news_dir = _get_news_dir() + self.assertIsNotNone(news_dir, "Could not locate news directory in runfiles") + self.assertTrue(news_dir.exists(), "News directory does not exist in runfiles") + + allowed_categories = {"added", "changed", "fixed", "removed"} + + for p in news_dir.iterdir(): + if not p.is_file(): + continue + # Ignore BUILD files and .gitkeep if they are in the directory + if p.name in ("BUILD", "BUILD.bazel", ".gitkeep"): + continue + + filename = p.name + + # Collapse extension and filename check into a single assertRegex + self.assertRegex( + filename, + r"^[^.]+\.[^.]+\.md$", + f"News filename {filename} must follow ..md pattern", + ) + + parts = filename.split(".") + category = parts[1].lower() + + # Category must be valid + self.assertIn( + category, + allowed_categories, + f"News file {filename} has invalid category '{category}'. " + f"Must be one of {allowed_categories}", + ) + + # Must be readable as UTF-8 + try: + content = p.read_text(encoding="utf-8").strip() + except (IOError, UnicodeDecodeError) as e: + self.fail(f"Failed to read news file {filename} as UTF-8: {e}") + + # Content must not be empty + self.assertTrue(len(content) > 0, f"News file {filename} must not be empty") + + # Content must NOT start with bullet points (* or -) + self.assertFalse( + content.startswith("* ") or content.startswith("- "), + f"News file {filename} must not start with bullet points (* or -). " + "The release tool adds them automatically.", + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/py_runtime/py_runtime_tests.bzl b/tests/py_runtime/py_runtime_tests.bzl index b8aa1f3fa6..47cd97f142 100644 --- a/tests/py_runtime/py_runtime_tests.bzl +++ b/tests/py_runtime/py_runtime_tests.bzl @@ -20,9 +20,49 @@ load("@rules_testing//lib:util.bzl", rt_util = "util") load("//python:py_runtime.bzl", "py_runtime") load("//python:py_runtime_info.bzl", "PyRuntimeInfo") load("//python/private:common_labels.bzl", "labels") # buildifier: disable=bzl-visibility +load("//python/private:py_runtime_rule.bzl", "coverage_tool_missing_message") # buildifier: disable=bzl-visibility load("//tests/support:py_runtime_info_subject.bzl", "py_runtime_info_subject") _tests = [] +_basic_tests = [] + +def _test_coverage_warning_when_enabled_and_no_tool(env): + # The only case that warrants a warning: coverage is being collected and the + # runtime that was selected cannot produce any. + msg = coverage_tool_missing_message( + coverage_enabled = True, + coverage_tool = None, + label = Label("//fake:runtime"), + ) + env.expect.that_bool(msg == None).equals(False) + env.expect.that_str(msg).contains("has no coverage_tool") + env.expect.that_str(msg).contains("//fake:runtime") + +_basic_tests.append(_test_coverage_warning_when_enabled_and_no_tool) + +def _test_no_coverage_warning_when_coverage_disabled(env): + # Without `bazel coverage` the missing tool has no observable effect, so + # saying anything would be noise on every ordinary build. + msg = coverage_tool_missing_message( + coverage_enabled = False, + coverage_tool = None, + label = Label("//fake:runtime"), + ) + env.expect.that_bool(msg == None).equals(True) + +_basic_tests.append(_test_no_coverage_warning_when_coverage_disabled) + +def _test_no_coverage_warning_when_tool_present(env): + # A configured coverage tool is the working case, whether it came from the + # bundled wheel set or from py_runtime.coverage_tool directly. + msg = coverage_tool_missing_message( + coverage_enabled = True, + coverage_tool = "some-coverage-tool-file", + label = Label("//fake:runtime"), + ) + env.expect.that_bool(msg == None).equals(True) + +_basic_tests.append(_test_no_coverage_warning_when_tool_present) def _simple_binary_impl(ctx): executable = ctx.actions.declare_file(ctx.label.name) @@ -42,6 +82,20 @@ _simple_binary = rule( executable = True, ) +def _source_file_wrapper_impl(ctx): + return [DefaultInfo( + files = depset([ctx.file.src]), + runfiles = ctx.runfiles(files = ctx.files.data), + )] + +_source_file_wrapper = rule( + implementation = _source_file_wrapper_impl, + attrs = { + "data": attr.label_list(allow_files = True), + "src": attr.label(allow_single_file = True, mandatory = True), + }, +) + def _test_bootstrap_template(name): rt_util.helper_target( py_runtime, @@ -195,11 +249,54 @@ def _test_in_build_interpreter(name): def _test_in_build_interpreter_impl(env, target): info = env.expect.that_target(target).provider(PyRuntimeInfo, factory = py_runtime_info_subject) info.python_version().equals("PY3") - info.files().contains_predicate(matching.file_basename_equals("file1.txt")) + info.files().contains_exactly([ + "{package}/fake_interpreter", + "{package}/file1.txt", + ]) info.interpreter().path().contains("fake_interpreter") + env.expect.that_bool(info.actual.interpreter_files_to_run == None).equals(True) _tests.append(_test_in_build_interpreter) +def _test_non_executable_source_file_interpreter_keeps_file_only_behavior(name): + rt_util.helper_target( + _source_file_wrapper, + name = name + "_wrapped_interpreter", + src = "fake_interpreter", + data = ["runfile.txt"], + ) + + rt_util.helper_target( + py_runtime, + name = name + "_subject", + interpreter = name + "_wrapped_interpreter", + python_version = "PY3", + files = ["file1.txt"], + ) + analysis_test( + name = name, + target = name + "_subject", + impl = _test_non_executable_source_file_interpreter_keeps_file_only_behavior_impl, + ) + +def _test_non_executable_source_file_interpreter_keeps_file_only_behavior_impl(env, target): + target = env.expect.that_target(target) + py_runtime_info = target.provider( + PyRuntimeInfo, + factory = py_runtime_info_subject, + ) + py_runtime_info.interpreter().short_path_equals("{package}/fake_interpreter") + env.expect.that_bool(py_runtime_info.actual.interpreter_files_to_run == None).equals(True) + py_runtime_info.files().contains_exactly([ + "{package}/file1.txt", + ]) + + target.default_outputs().contains_exactly([ + "{package}/file1.txt", + ]) + +_tests.append(_test_non_executable_source_file_interpreter_keeps_file_only_behavior) + def _test_interpreter_binary_with_multiple_outputs(name): rt_util.helper_target( _simple_binary, @@ -227,6 +324,9 @@ def _test_interpreter_binary_with_multiple_outputs_impl(env, target): factory = py_runtime_info_subject, ) py_runtime_info.interpreter().short_path_equals("{package}/{test_name}_built_interpreter") + py_runtime_info.interpreter_files_to_run().executable().short_path_equals( + "{package}/{test_name}_built_interpreter", + ) py_runtime_info.files().contains_exactly([ "{package}/extra_default_output.txt", "{package}/runfile.txt", @@ -272,6 +372,9 @@ def _test_interpreter_binary_with_single_output_and_runfiles_impl(env, target): factory = py_runtime_info_subject, ) py_runtime_info.interpreter().short_path_equals("{package}/{test_name}_built_interpreter") + py_runtime_info.interpreter_files_to_run().executable().short_path_equals( + "{package}/{test_name}_built_interpreter", + ) py_runtime_info.files().contains_exactly([ "{package}/runfile.txt", "{package}/{test_name}_built_interpreter", @@ -327,10 +430,12 @@ def _test_system_interpreter(name): ) def _test_system_interpreter_impl(env, target): - env.expect.that_target(target).provider( + info = env.expect.that_target(target).provider( PyRuntimeInfo, factory = py_runtime_info_subject, - ).interpreter_path().equals("/system/python") + ) + info.interpreter_path().equals("/system/python") + env.expect.that_bool(info.actual.interpreter_files_to_run == None).equals(True) _tests.append(_test_system_interpreter) @@ -492,5 +597,6 @@ _tests.append(_test_version_info_from_flag) def py_runtime_test_suite(name): test_suite( name = name, + basic_tests = _basic_tests, tests = _tests, ) diff --git a/tests/py_runtime_info/py_runtime_info_tests.bzl b/tests/py_runtime_info/py_runtime_info_tests.bzl index a44fb60c2f..6b9ecac605 100644 --- a/tests/py_runtime_info/py_runtime_info_tests.bzl +++ b/tests/py_runtime_info/py_runtime_info_tests.bzl @@ -15,7 +15,9 @@ load("@rules_testing//lib:analysis_test.bzl", "analysis_test") load("@rules_testing//lib:test_suite.bzl", "test_suite") +load("@rules_testing//lib:truth.bzl", "matching") load("//python:py_runtime_info.bzl", "PyRuntimeInfo") +load("//tests/support:py_runtime_info_subject.bzl", "py_runtime_info_subject") def _create_py_runtime_info_without_interpreter_version_info_impl(ctx): return [PyRuntimeInfo( @@ -35,6 +37,57 @@ _create_py_runtime_info_without_interpreter_version_info = rule( }, ) +def _simple_binary_impl(ctx): + executable = ctx.actions.declare_file(ctx.label.name) + ctx.actions.write(executable, "", is_executable = True) + return [DefaultInfo( + executable = executable, + files = depset([executable]), + )] + +_simple_binary = rule( + implementation = _simple_binary_impl, + executable = True, +) + +def _file_target_impl(ctx): + output = ctx.actions.declare_file(ctx.label.name + ".txt") + ctx.actions.write(output, "") + return [DefaultInfo(files = depset([output]))] + +_file_target = rule( + implementation = _file_target_impl, +) + +def _create_py_runtime_info_with_interpreter_files_to_run_impl(ctx): + files_to_run = ctx.attr.files_to_run[DefaultInfo].files_to_run + kwargs = dict( + bootstrap_template = ctx.file.bootstrap_template, + interpreter_files_to_run = files_to_run, + python_version = "PY3", + ) + if ctx.attr.use_interpreter_path: + kwargs["interpreter_path"] = "/python" + else: + kwargs["files"] = depset() + kwargs["interpreter"] = ctx.executable.interpreter + + return [PyRuntimeInfo(**kwargs)] + +_create_py_runtime_info_with_interpreter_files_to_run = rule( + implementation = _create_py_runtime_info_with_interpreter_files_to_run_impl, + attrs = { + "bootstrap_template": attr.label(allow_single_file = True, default = "bootstrap.txt"), + "files_to_run": attr.label(mandatory = True), + "interpreter": attr.label( + cfg = "target", + executable = True, + mandatory = True, + ), + "use_interpreter_path": attr.bool(), + }, +) + _tests = [] def _test_can_create_py_runtime_info_without_interpreter_version_info(name): @@ -53,6 +106,112 @@ def _test_can_create_py_runtime_info_without_interpreter_version_info_impl(env, _tests.append(_test_can_create_py_runtime_info_without_interpreter_version_info) +def _test_interpreter_files_to_run_with_interpreter(name): + _simple_binary( + name = name + "_interpreter", + ) + _create_py_runtime_info_with_interpreter_files_to_run( + name = name + "_subject", + files_to_run = name + "_interpreter", + interpreter = name + "_interpreter", + ) + analysis_test( + name = name, + target = name + "_subject", + impl = _test_interpreter_files_to_run_with_interpreter_impl, + ) + +def _test_interpreter_files_to_run_with_interpreter_impl(env, target): + info = env.expect.that_target(target).provider( + PyRuntimeInfo, + factory = py_runtime_info_subject, + ) + info.interpreter().short_path_equals("{package}/{test_name}_interpreter") + info.interpreter_files_to_run().executable().short_path_equals( + "{package}/{test_name}_interpreter", + ) + +_tests.append(_test_interpreter_files_to_run_with_interpreter) + +def _test_interpreter_files_to_run_disallows_interpreter_path(name): + _simple_binary( + name = name + "_interpreter", + ) + _create_py_runtime_info_with_interpreter_files_to_run( + name = name + "_subject", + files_to_run = name + "_interpreter", + interpreter = name + "_interpreter", + tags = ["manual"], + use_interpreter_path = True, + ) + analysis_test( + name = name, + target = name + "_subject", + impl = _test_interpreter_files_to_run_disallows_interpreter_path_impl, + expect_failure = True, + ) + +def _test_interpreter_files_to_run_disallows_interpreter_path_impl(env, target): + env.expect.that_target(target).failures().contains_predicate( + matching.str_matches("*interpreter_files_to_run*interpreter_path*"), + ) + +_tests.append(_test_interpreter_files_to_run_disallows_interpreter_path) + +def _test_interpreter_files_to_run_requires_executable(name): + _simple_binary( + name = name + "_interpreter", + ) + _file_target( + name = name + "_files_to_run", + ) + _create_py_runtime_info_with_interpreter_files_to_run( + name = name + "_subject", + files_to_run = name + "_files_to_run", + interpreter = name + "_interpreter", + tags = ["manual"], + ) + analysis_test( + name = name, + target = name + "_subject", + impl = _test_interpreter_files_to_run_requires_executable_impl, + expect_failure = True, + ) + +def _test_interpreter_files_to_run_requires_executable_impl(env, target): + env.expect.that_target(target).failures().contains_predicate( + matching.str_matches("*interpreter_files_to_run*executable*"), + ) + +_tests.append(_test_interpreter_files_to_run_requires_executable) + +def _test_interpreter_files_to_run_requires_matching_interpreter(name): + _simple_binary( + name = name + "_interpreter", + ) + _simple_binary( + name = name + "_other_interpreter", + ) + _create_py_runtime_info_with_interpreter_files_to_run( + name = name + "_subject", + files_to_run = name + "_other_interpreter", + interpreter = name + "_interpreter", + tags = ["manual"], + ) + analysis_test( + name = name, + target = name + "_subject", + impl = _test_interpreter_files_to_run_requires_matching_interpreter_impl, + expect_failure = True, + ) + +def _test_interpreter_files_to_run_requires_matching_interpreter_impl(env, target): + env.expect.that_target(target).failures().contains_predicate( + matching.str_matches("*interpreter_files_to_run.executable*interpreter*"), + ) + +_tests.append(_test_interpreter_files_to_run_requires_matching_interpreter) + def py_runtime_info_test_suite(name): test_suite( name = name, 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/pypi/extension/extension_tests.bzl b/tests/pypi/extension/extension_tests.bzl index 5a40714b64..97849e882d 100644 --- a/tests/pypi/extension/extension_tests.bzl +++ b/tests/pypi/extension/extension_tests.bzl @@ -24,7 +24,7 @@ load(":pip_parse.bzl", _parse = "pip_parse") _tests = [] -def _pypi_mock_mctx(*modules, os_name = "unittest", arch_name = "exotic", environ = {}, read = None): +def _pypi_mock_mctx(*modules, os_name = "unittest", arch_name = "exotic", environ = {}, read = None, mock_files = {}): _ = read # @unused return mocks.mctx( modules = list(modules), @@ -36,7 +36,7 @@ def _pypi_mock_mctx(*modules, os_name = "unittest", arch_name = "exotic", enviro simple==0.0.1 \ --hash=sha256:deadbeef \ --hash=sha256:deadbaaf""", - }, + } | mock_files, ) def _default( @@ -44,24 +44,28 @@ def _default( arch_name = None, auth_patterns = None, config_settings = None, + default_hub = "", env = None, index_url = None, marker = None, netrc = None, os_name = None, platform = None, + pyproject_toml = None, whl_platform_tags = None, whl_abi_tags = None): return struct( arch_name = arch_name, auth_patterns = auth_patterns or {}, config_settings = config_settings, + default_hub = default_hub, env = env or {}, index_url = index_url or "", marker = marker or "", netrc = netrc, os_name = os_name, platform = platform, + pyproject_toml = pyproject_toml, whl_abi_tags = whl_abi_tags or [], whl_platform_tags = whl_platform_tags or [], ) @@ -88,7 +92,13 @@ _default_tags_default = [ }.items() ] -def _mod(*, name, default = _default_tags_default, parse = [], override = [], whl_mods = [], is_root = True): +def _dep(*, name, extra_targets = []): + return struct( + name = name, + extra_targets = extra_targets, + ) + +def _mod(*, name, default = _default_tags_default, parse = [], override = [], whl_mods = [], dep = [], is_root = True): return struct( name = name, tags = struct( @@ -96,6 +106,7 @@ def _mod(*, name, default = _default_tags_default, parse = [], override = [], wh override = override, whl_mods = whl_mods, default = default, + dep = dep, ), is_root = is_root, ) @@ -104,6 +115,8 @@ def _parse_modules(env, **kwargs): return env.expect.that_struct( parse_modules(**kwargs), attrs = dict( + declared_deps = subjects.dict, + default_hub = subjects.str, exposed_packages = subjects.dict, hub_group_map = subjects.dict, hub_whl_map = subjects.dict, @@ -172,6 +185,58 @@ def _test_simple(env): _tests.append(_test_simple) +def _test_pip_parse_pyproject_toml(env): + # pip.parse() reads the version from pyproject.toml's requires-python when + # python_version is not set explicitly. + pypi = _parse_modules( + env, + module_ctx = _pypi_mock_mctx( + _mod( + name = "rules_python", + parse = [ + _parse( + hub_name = "pypi", + pyproject_toml = "pyproject.toml", + simpleapi_skip = ["simple"], + requirements_lock = "requirements.txt", + ), + ], + ), + os_name = "linux", + arch_name = "x86_64", + mock_files = { + "pyproject.toml": "[project]\nrequires-python = \"==3.15.19\"\n", + }, + ), + available_interpreters = { + "python_3_15_19_host": "unit_test_interpreter_target", + }, + minor_mapping = {"3.15": "3.15.19"}, + ) + + # Resolves identically to passing python_version = "3.15.19" explicitly: + # the full version drives interpreter selection, hub naming uses major.minor. + pypi.exposed_packages().contains_exactly({"pypi": ["simple"]}) + pypi.hub_whl_map().contains_exactly({"pypi": { + "simple": { + "pypi_315_simple": [ + whl_config_setting( + version = "3.15", + ), + ], + }, + }}) + pypi.whl_libraries().contains_exactly({ + "pypi_315_simple": { + "config_load": "@pypi//:config.bzl", + "dep_template": "@pypi//{name}:{target}", + "python_interpreter_target": "unit_test_interpreter_target", + "requirement": "simple==0.0.1 --hash=sha256:deadbeef --hash=sha256:deadbaaf", + }, + }) + +_tests.append(_test_pip_parse_pyproject_toml) + def _test_simple_isolated(env): """Simulate `isolate = True` with parse_modules. @@ -283,6 +348,238 @@ def _test_build_pipstar_platform(env): _tests.append(_test_build_pipstar_platform) +def _test_multiple_default_tags(env): + """Test that multiple pip.default tags do not trigger duplicate default hub failures. + + Only when multiple tags explicitly define default_hub should it fail. + """ + pypi = _parse_modules( + env, + module_ctx = _pypi_mock_mctx( + _mod( + name = "rules_python", + default = _default_tags_default + [ + _default(platform = "extra_custom_platform"), + ], + parse = [ + _parse( + hub_name = "pypi", + python_version = "3.15", + simpleapi_skip = ["simple"], + requirements_lock = "requirements.txt", + ), + ], + ), + os_name = "linux", + arch_name = "x86_64", + ), + available_interpreters = { + "python_3_15_host": "unit_test_interpreter_target", + }, + minor_mapping = {"3.15": "3.15.19"}, + ) + pypi.exposed_packages().contains_exactly({"pypi": ["simple"]}) + +_tests.append(_test_multiple_default_tags) + +def _test_name_collision_no_env(env): + """Test that a hub named 'pypi' is NOT renamed when the env var is not set.""" + pypi = _parse_modules( + env, + module_ctx = _pypi_mock_mctx( + _mod( + name = "rules_python", + parse = [ + _parse( + hub_name = "pypi", + python_version = "3.15", + simpleapi_skip = ["simple"], + requirements_lock = "requirements.txt", + ), + ], + ), + os_name = "linux", + arch_name = "x86_64", + environ = {}, # Env var NOT set + ), + available_interpreters = { + "python_3_15_host": "unit_test_interpreter_target", + }, + minor_mapping = {"3.15": "3.15.19"}, + ) + + # The hub name remains 'pypi' + pypi.exposed_packages().contains_exactly({"pypi": ["simple"]}) + pypi.default_hub().equals(None) + +_tests.append(_test_name_collision_no_env) + +def _test_name_collision_with_env(env): + """Test that a hub named 'pypi' is silently renamed to module_name_pypi and routed as default_hub when the env var is set.""" + pypi = _parse_modules( + env, + module_ctx = _pypi_mock_mctx( + _mod( + name = "rules_python", + parse = [ + _parse( + hub_name = "pypi", + python_version = "3.15", + simpleapi_skip = ["simple"], + requirements_lock = "requirements.txt", + ), + ], + ), + os_name = "linux", + arch_name = "x86_64", + environ = {"RULES_PYTHON_PYPI_HUB_RESERVED": "1"}, + ), + available_interpreters = { + "python_3_15_host": "unit_test_interpreter_target", + }, + minor_mapping = {"3.15": "3.15.19"}, + ) + + # The hub name is renamed to 'rules_python_pypi' + pypi.exposed_packages().contains_exactly({"rules_python_pypi": ["simple"]}) + + # It is used as the default_hub + pypi.default_hub().equals("rules_python_pypi") + +_tests.append(_test_name_collision_with_env) + +def _test_default_hub_precedence(env): + """Test that pip.default(default_hub = ...) has precedence over the fallback renamed default hub.""" + pypi = _parse_modules( + env, + module_ctx = _pypi_mock_mctx( + _mod( + name = "rules_python", + default = _default_tags_default + [ + _default( + platform = "extra_custom_platform", + default_hub = "other_pypi", + ), + ], + parse = [ + _parse( + hub_name = "pypi", + python_version = "3.15", + simpleapi_skip = ["simple"], + requirements_lock = "requirements.txt", + ), + _parse( + hub_name = "other_pypi", + python_version = "3.15", + simpleapi_skip = ["simple"], + requirements_lock = "requirements.txt", + ), + ], + ), + os_name = "linux", + arch_name = "x86_64", + environ = {"RULES_PYTHON_PYPI_HUB_RESERVED": "1"}, + ), + available_interpreters = { + "python_3_15_host": "unit_test_interpreter_target", + }, + minor_mapping = {"3.15": "3.15.19"}, + ) + + # The hub named 'pypi' is renamed to 'rules_python_pypi' + pypi.exposed_packages().contains_exactly({ + "other_pypi": ["simple"], + "rules_python_pypi": ["simple"], + }) + + # But the default_hub remains 'other_pypi' because pip.default has higher precedence! + pypi.default_hub().equals("other_pypi") + +_tests.append(_test_default_hub_precedence) + +def _test_extension_dep(env): + pypi = _parse_modules( + env, + module_ctx = _pypi_mock_mctx( + _mod( + name = "my_module", + dep = [ + _dep( + name = "declared-pkg", + extra_targets = ["declared-alias"], + ), + ], + ), + os_name = "linux", + arch_name = "x86_64", + ), + available_interpreters = {}, + minor_mapping = {}, + ) + + pypi.declared_deps().contains_exactly({"declared_pkg": {"declared-alias": None}}) + pypi.exposed_packages().contains_exactly({}) + pypi.hub_group_map().contains_exactly({}) + pypi.hub_whl_map().contains_exactly({}) + pypi.whl_libraries().contains_exactly({}) + pypi.whl_mods().contains_exactly({}) + +_tests.append(_test_extension_dep) + +def _test_extension_dep_coexists_with_concrete_hub(env): + pypi = _parse_modules( + env, + module_ctx = _pypi_mock_mctx( + _mod( + name = "my_module", + parse = [ + _parse( + hub_name = "pypi_a", + python_version = "3.15", + simpleapi_skip = ["simple"], + requirements_lock = "requirements.txt", + ), + ], + dep = [ + _dep( + name = "simple", + extra_targets = ["extra-target"], + ), + ], + ), + os_name = "linux", + arch_name = "x86_64", + ), + available_interpreters = { + "python_3_15_host": "unit_test_interpreter_target", + }, + minor_mapping = {"3.15": "3.15.19"}, + ) + + pypi.declared_deps().contains_exactly({"simple": {"extra-target": None}}) + pypi.exposed_packages().contains_exactly({"pypi_a": ["simple"]}) + pypi.hub_group_map().contains_exactly({"pypi_a": {}}) + pypi.hub_whl_map().contains_exactly({"pypi_a": { + "simple": { + "pypi_a_315_simple": [ + whl_config_setting( + version = "3.15", + ), + ], + }, + }}) + pypi.whl_libraries().contains_exactly({ + "pypi_a_315_simple": { + "config_load": "@pypi_a//:config.bzl", + "dep_template": "@pypi_a//{name}:{target}", + "python_interpreter_target": "unit_test_interpreter_target", + "requirement": "simple==0.0.1 --hash=sha256:deadbeef --hash=sha256:deadbaaf", + }, + }) + pypi.whl_mods().contains_exactly({}) + +_tests.append(_test_extension_dep_coexists_with_concrete_hub) + def extension_test_suite(name): """Create the test suite. diff --git a/tests/pypi/extension/pip_parse.bzl b/tests/pypi/extension/pip_parse.bzl index 95cf666056..7b5bdfdfd6 100644 --- a/tests/pypi/extension/pip_parse.bzl +++ b/tests/pypi/extension/pip_parse.bzl @@ -3,7 +3,7 @@ def pip_parse( *, hub_name, - python_version, + python_version = None, add_libdir_to_library_search_path = False, auth_patterns = {}, download_only = False, @@ -19,6 +19,7 @@ def pip_parse( netrc = None, parse_all_requirements_files = True, pip_data_exclude = None, + pyproject_toml = None, python_interpreter = None, python_interpreter_target = None, quiet = True, @@ -30,6 +31,7 @@ def pip_parse( target_platforms = [], simpleapi_skip = [], timeout = 600, + uv_lock = None, whl_modifications = {}, **kwargs): """A simple helper for testing to simulate the PyPI extension parse tag class""" @@ -51,6 +53,7 @@ def pip_parse( netrc = netrc, parse_all_requirements_files = parse_all_requirements_files, pip_data_exclude = pip_data_exclude, + pyproject_toml = pyproject_toml, python_interpreter = python_interpreter, python_interpreter_target = python_interpreter_target, python_version = python_version, @@ -61,6 +64,7 @@ def pip_parse( requirements_lock = requirements_lock, requirements_windows = requirements_windows, timeout = timeout, + uv_lock = uv_lock, whl_modifications = whl_modifications, parallel_download = False, experimental_index_url_overrides = {}, diff --git a/tests/pypi/generate_whl_library_build_bazel/generate_whl_library_build_bazel_tests.bzl b/tests/pypi/generate_whl_library_build_bazel/generate_whl_library_build_bazel_tests.bzl index 9586581cad..bb03d9a589 100644 --- a/tests/pypi/generate_whl_library_build_bazel/generate_whl_library_build_bazel_tests.bzl +++ b/tests/pypi/generate_whl_library_build_bazel/generate_whl_library_build_bazel_tests.bzl @@ -19,86 +19,15 @@ load("//python/private/pypi:generate_whl_library_build_bazel.bzl", "generate_whl _tests = [] -def _test_all_legacy(env): - want = """\ -load("@package_metadata//rules:package_metadata.bzl", "package_metadata") -load("@rules_python//python/private/pypi:whl_library_targets.bzl", "whl_library_targets") - -package(default_visibility = ["//visibility:public"]) - -package_metadata( - name = "package_metadata", - purl = None, - visibility = ["//:__subpackages__"], -) - -whl_library_targets( - copy_executables = { - "exec_src": "exec_dest", - }, - copy_files = { - "file_src": "file_dest", - }, - data = ["extra_target"], - data_exclude = [ - "exclude_via_attr", - "data_exclude_all", - ], - dep_template = "@pypi_{name}//:{target}", - dependencies = ["foo"], - dependencies_by_platform = { - "baz": ["bar"], - }, - group_deps = [ - "foo", - "fox", - "qux", - ], - group_name = "qux", - name = "foo.whl", - srcs_exclude = ["srcs_exclude_all"], - tags = ["tag1"], -) - -# SOMETHING SPECIAL AT THE END -""" - actual = generate_whl_library_build_bazel( - dep_template = "@pypi_{name}//:{target}", - name = "foo.whl", - dependencies = ["foo"], - dependencies_by_platform = {"baz": ["bar"]}, - data_exclude = ["exclude_via_attr"], - annotation = struct( - copy_files = {"file_src": "file_dest"}, - copy_executables = {"exec_src": "exec_dest"}, - data = ["extra_target"], - data_exclude_glob = ["data_exclude_all"], - srcs_exclude_glob = ["srcs_exclude_all"], - additive_build_content = """# SOMETHING SPECIAL AT THE END""", - ), - group_name = "qux", - group_deps = ["foo", "fox", "qux"], - tags = ["tag1"], - ) - env.expect.that_str(actual.replace("@@", "@")).equals(want) - -_tests.append(_test_all_legacy) - def _test_all_workspace(env): want = """\ load("@package_metadata//rules:package_metadata.bzl", "package_metadata") load("@pypi//:config.bzl", "packages") -load("@rules_python//python/private/pypi:whl_library_targets.bzl", "whl_library_targets_from_requires") +load("@rules_python//python/private/pypi:whl_library_targets.bzl", "whl_library_targets") package(default_visibility = ["//visibility:public"]) -package_metadata( - name = "package_metadata", - purl = None, - visibility = ["//:__subpackages__"], -) - -whl_library_targets_from_requires( +whl_library_targets( copy_executables = { "exec_src": "exec_dest", }, @@ -118,6 +47,7 @@ whl_library_targets_from_requires( ], group_name = "qux", include = packages, + metadata_name = "foo", name = "foo.whl", requires_dist = [ "foo", @@ -125,11 +55,23 @@ whl_library_targets_from_requires( "qux", ], srcs_exclude = ["srcs_exclude_all"], + tags = [ + "pypi_name=foo", + "pypi_version=0", + ], +) + +package_metadata( + name = "package_metadata", + purl = "foo", + visibility = ["//:__subpackages__"], ) # SOMETHING SPECIAL AT THE END """ actual = generate_whl_library_build_bazel( + metadata_version = "0", + metadata_name = "foo", dep_template = "@pypi//{name}:{target}", name = "foo.whl", requires_dist = ["foo", "bar-baz", "qux"], @@ -145,6 +87,7 @@ whl_library_targets_from_requires( config_load = "@pypi//:config.bzl", group_name = "qux", group_deps = ["foo", "fox", "qux"], + purl = "foo", ) env.expect.that_str(actual.replace("@@", "@")).equals(want) @@ -154,17 +97,11 @@ def _test_all(env): want = """\ load("@package_metadata//rules:package_metadata.bzl", "package_metadata") load("@pypi//:config.bzl", "packages") -load("@rules_python//python/private/pypi:whl_library_targets.bzl", "whl_library_targets_from_requires") +load("@rules_python//python/private/pypi:whl_library_targets.bzl", "whl_library_targets") package(default_visibility = ["//visibility:public"]) -package_metadata( - name = "package_metadata", - purl = None, - visibility = ["//:__subpackages__"], -) - -whl_library_targets_from_requires( +whl_library_targets( copy_executables = { "exec_src": "exec_dest", }, @@ -184,6 +121,7 @@ whl_library_targets_from_requires( ], group_name = "qux", include = packages, + metadata_name = "foo", name = "foo.whl", requires_dist = [ "foo", @@ -191,11 +129,23 @@ whl_library_targets_from_requires( "qux", ], srcs_exclude = ["srcs_exclude_all"], + tags = [ + "pypi_name=foo", + "pypi_version=0", + ], +) + +package_metadata( + name = "package_metadata", + purl = "foo", + visibility = ["//:__subpackages__"], ) # SOMETHING SPECIAL AT THE END """ actual = generate_whl_library_build_bazel( + metadata_version = "0", + metadata_name = "foo", dep_template = "@pypi//{name}:{target}", name = "foo.whl", requires_dist = ["foo", "bar-baz", "qux"], @@ -210,6 +160,7 @@ whl_library_targets_from_requires( ), config_load = "@pypi//:config.bzl", group_name = "qux", + purl = "foo", group_deps = ["foo", "fox", "qux"], ) env.expect.that_str(actual.replace("@@", "@")).equals(want) @@ -220,17 +171,11 @@ def _test_all_with_loads(env): want = """\ load("@package_metadata//rules:package_metadata.bzl", "package_metadata") load("@pypi//:config.bzl", "packages") -load("@rules_python//python/private/pypi:whl_library_targets.bzl", "whl_library_targets_from_requires") +load("@rules_python//python/private/pypi:whl_library_targets.bzl", "whl_library_targets") package(default_visibility = ["//visibility:public"]) -package_metadata( - name = "package_metadata", - purl = None, - visibility = ["//:__subpackages__"], -) - -whl_library_targets_from_requires( +whl_library_targets( copy_executables = { "exec_src": "exec_dest", }, @@ -250,6 +195,7 @@ whl_library_targets_from_requires( ], group_name = "qux", include = packages, + metadata_name = "foo", name = "foo.whl", requires_dist = [ "foo", @@ -257,11 +203,23 @@ whl_library_targets_from_requires( "qux", ], srcs_exclude = ["srcs_exclude_all"], + tags = [ + "pypi_name=foo", + "pypi_version=0", + ], +) + +package_metadata( + name = "package_metadata", + purl = "foo", + visibility = ["//:__subpackages__"], ) # SOMETHING SPECIAL AT THE END """ actual = generate_whl_library_build_bazel( + metadata_version = "0", + metadata_name = "foo", dep_template = "@pypi//{name}:{target}", name = "foo.whl", requires_dist = ["foo", "bar-baz", "qux"], @@ -277,6 +235,7 @@ whl_library_targets_from_requires( group_name = "qux", config_load = "@pypi//:config.bzl", group_deps = ["foo", "fox", "qux"], + purl = "foo", ) env.expect.that_str(actual.replace("@@", "@")).equals(want) diff --git a/tests/pypi/hash/BUILD.bazel b/tests/pypi/hash/BUILD.bazel new file mode 100644 index 0000000000..1e052af141 --- /dev/null +++ b/tests/pypi/hash/BUILD.bazel @@ -0,0 +1,3 @@ +load(":hash_tests.bzl", "hash_test_suite") + +hash_test_suite(name = "hash_tests") diff --git a/tests/pypi/hash/hash_tests.bzl b/tests/pypi/hash/hash_tests.bzl new file mode 100644 index 0000000000..6866a79d07 --- /dev/null +++ b/tests/pypi/hash/hash_tests.bzl @@ -0,0 +1,102 @@ +# Copyright 2026 The Bazel Authors. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"" + +load("@rules_testing//lib:test_suite.bzl", "test_suite") +load("//python/private/pypi:hash.bzl", "hash") # buildifier: disable=bzl-visibility + +_tests = [] + +# The digests of b"rules_python" with the SRI values calculated with: +# python3 -c 'import base64, hashlib; h = hashlib.sha512(b"rules_python").digest(); print(base64.b64encode(h))' +_SHA256 = "a90bb21ddf552d508565aa2f0d78ac13297e6c407e39eb578eeac09e62c5da3a" +_SHA256_SRI = "sha256-qQuyHd9VLVCFZaovDXisEyl+bEB+OetXjurAnmLF2jo=" +_SHA384 = "7407ad30f83623cabf9139bd80edb332516880321ab2a20fa0f281767675bfc10b53555eb5042aa58de4b9345f5f70b6" +_SHA384_SRI = "sha384-dAetMPg2I8q/kTm9gO2zMlFogDIasqIPoPKBdnZ1v8ELU1VetQQqpY3kuTRfX3C2" +_SHA512 = "127af31fba52ce5ac96a4bc0f1b31c31c12367feace2b7dbb81066df479883add9dd6166495b126b76ac8c624d254b6a4ba3fbb628e8dcf0a34e1b0b065f619f" +_SHA512_SRI = "sha512-EnrzH7pSzlrJakvA8bMcMcEjZ/6s4rfbuBBm30eYg63Z3WFmSVsSa3asjGJNJUtqS6P7tijo3PCjThsLBl9hnw==" + +def _test_digest(env): + env.expect.that_str(hash.digest("sha256", _SHA256)).equals("sha256:" + _SHA256) + + # The algorithm name is normalized to lower case. + env.expect.that_str(hash.digest("SHA256", _SHA256)).equals("sha256:" + _SHA256) + + # Unknown algorithms and empty digests yield nothing. + env.expect.that_str(hash.digest("egg", "foo")).equals("") + env.expect.that_str(hash.digest("sha256", "")).equals("") + env.expect.that_str(hash.digest("", "")).equals("") + +_tests.append(_test_digest) + +def _test_hex_to_sri(env): + env.expect.that_str(hash.hex_to_sri("sha256", _SHA256)).equals(_SHA256_SRI) + env.expect.that_str(hash.hex_to_sri("sha384", _SHA384)).equals(_SHA384_SRI) + env.expect.that_str(hash.hex_to_sri("sha512", _SHA512)).equals(_SHA512_SRI) + + # Upper case digests are accepted. + env.expect.that_str(hash.hex_to_sri("sha512", _SHA512.upper())).equals(_SHA512_SRI) + + # Algorithms that SRI does not support yield nothing. + env.expect.that_str(hash.hex_to_sri("md5", "0" * 32)).equals("") + env.expect.that_str(hash.hex_to_sri("blake2b", "0" * 128)).equals("") + + # Invalid digests yield nothing. + env.expect.that_str(hash.hex_to_sri("sha256", "")).equals("") + env.expect.that_str(hash.hex_to_sri("sha256", "abc")).equals("") + env.expect.that_str(hash.hex_to_sri("sha256", "not-hex!")).equals("") + +_tests.append(_test_hex_to_sri) + +def _test_integrity(env): + env.expect.that_str(hash.integrity("")).equals("") + env.expect.that_str(hash.integrity("md5:" + "0" * 32)).equals("") + env.expect.that_str(hash.integrity("sha256:" + _SHA256)).equals(_SHA256_SRI) + env.expect.that_str(hash.integrity("sha384:" + _SHA384)).equals(_SHA384_SRI) + env.expect.that_str(hash.integrity("sha512:" + _SHA512)).equals(_SHA512_SRI) + +_tests.append(_test_integrity) + +def _test_preferred_digest(env): + env.expect.that_str(hash.preferred_digest([])).equals("") + env.expect.that_str(hash.preferred_digest([""])).equals("") + + # sha256 stays preferred so that the repo names remain stable. + env.expect.that_str(hash.preferred_digest([ + "sha256:" + _SHA256, + "sha512:" + _SHA512, + ])).equals("sha256:" + _SHA256) + + # Otherwise the strongest SRI supported algorithm wins. + env.expect.that_str(hash.preferred_digest([ + "sha384:" + _SHA384, + "sha512:" + _SHA512, + ])).equals("sha512:" + _SHA512) + + # And anything else is picked alphabetically for determinism. + env.expect.that_str(hash.preferred_digest([ + "md5:deadb00f", + "blake2b:deadbeef", + ])).equals("blake2b:deadbeef") + +_tests.append(_test_preferred_digest) + +def hash_test_suite(name): + """Create the test suite. + + Args: + name: the name of the test suite + """ + test_suite(name = name, basic_tests = _tests) diff --git a/tests/pypi/hub_builder/hub_builder_tests.bzl b/tests/pypi/hub_builder/hub_builder_tests.bzl index d3ea704c27..f421c7e351 100644 --- a/tests/pypi/hub_builder/hub_builder_tests.bzl +++ b/tests/pypi/hub_builder/hub_builder_tests.bzl @@ -79,6 +79,7 @@ def hub_builder( }, netrc = None, auth_patterns = None, + toml_decode = json.decode, ), whl_overrides = whl_overrides, minor_mapping = minor_mapping or {"3.15": "3.15.19"}, @@ -151,6 +152,30 @@ def _test_simple(env): _tests.append(_test_simple) +def _test_uv_lock_only(env): + builder = hub_builder(env) + builder.pip_parse( + _mock_mctx( + os_name = "osx", + arch_name = "aarch64", + mock_files = { + "uv.lock": """{"package":[{"name":"simple","source":{"registry":"https://pypi.org/simple"},"version":"0.0.1","wheels":[{"hash":"sha256:deadbeef","url":"https://files.pythonhosted.org/packages/simple-0.0.1-py3-none-any.whl"}]}]}""", + }, + ), + _parse( + hub_name = "pypi", + python_version = "3.15", + requirements_lock = None, + uv_lock = "uv.lock", + ), + ) + pypi = builder.build() + + pypi.exposed_packages().contains_exactly(["simple"]) + pypi.group_map().contains_exactly({}) + +_tests.append(_test_uv_lock_only) + def _test_simple_multiple_requirements(env): sub_tests = { ("osx", "aarch64"): "simple==0.0.2 --hash=sha256:deadb00f", @@ -315,8 +340,8 @@ def _test_simple_extras_vs_no_extras_simpleapi(env): "dep_template": "@pypi//{name}:{target}", "filename": "simple-0.0.1-py3-none-any.whl", "index_url": "https://example.com/simple/", + "integrity": "sha256-3q2+7w==", "requirement": "simple[foo]==0.0.1", - "sha256": "deadbeef", "urls": ["/simple-0.0.1-py3-none-any.whl"], }, "pypi_315_simple_py3_none_any_deadbeef_windows_aarch64": { @@ -324,8 +349,8 @@ def _test_simple_extras_vs_no_extras_simpleapi(env): "dep_template": "@pypi//{name}:{target}", "filename": "simple-0.0.1-py3-none-any.whl", "index_url": "https://example.com/simple/", + "integrity": "sha256-3q2+7w==", "requirement": "simple==0.0.1", - "sha256": "deadbeef", "urls": ["/simple-0.0.1-py3-none-any.whl"], }, }) @@ -333,6 +358,77 @@ def _test_simple_extras_vs_no_extras_simpleapi(env): _tests.append(_test_simple_extras_vs_no_extras_simpleapi) +def _test_simple_sha512_simpleapi(env): + """Non-sha256 pins match the index digests and are downloaded with `integrity`.""" + + def mockread_simpleapi(*_, parse_index, **__): + if parse_index: + content = """\ + simple-0.0.1-py3-none-any.whl
    +""" + return struct( + output = parse_simpleapi_html( + content = content, + parse_index = parse_index, + ), + success = True, + ) + + builder = hub_builder( + env, + simpleapi_download_fn = lambda *args, **kwargs: simpleapi_download( + read_simpleapi = mockread_simpleapi, + *args, + **kwargs + ), + ) + builder.pip_parse( + mocks.mctx( + mock_files = { + "win.txt": "simple==0.0.1 --hash=sha512:deadbeef", + }, + ), + _parse( + hub_name = "pypi", + python_version = "3.15", + requirements_windows = "win.txt", + experimental_index_url = "https://example.com", + target_platforms = ["windows_aarch64"], + ), + ) + pypi = builder.build() + + pypi.exposed_packages().contains_exactly(["simple"]) + pypi.whl_map().contains_exactly({ + "simple": { + "pypi_315_simple_py3_none_any_deadbeef": [ + whl_config_setting( + target_platforms = [ + "cp315_windows_aarch64", + ], + version = "3.15", + ), + ], + }, + }) + pypi.whl_libraries().contains_exactly({ + "pypi_315_simple_py3_none_any_deadbeef": { + "config_load": "@pypi//:config.bzl", + "dep_template": "@pypi//{name}:{target}", + "filename": "simple-0.0.1-py3-none-any.whl", + "index_url": "https://example.com/simple/", + "integrity": "sha512-3q2+7w==", + "requirement": "simple==0.0.1", + "urls": ["/simple-0.0.1-py3-none-any.whl"], + }, + }) + +_tests.append(_test_simple_sha512_simpleapi) + def _test_simple_multiple_python_versions(env): builder = hub_builder( env, @@ -647,8 +743,8 @@ torch==2.4.1+cpu ; platform_machine == 'x86_64' \ "dep_template": "@pypi//{name}:{target}", "filename": "torch-2.4.1+cpu-cp312-cp312-linux_x86_64.whl", "index_url": "https://torch.index/torch/", + "integrity": "sha256-iADe7wAmAR1QLAwlbMS2fQAjR/Y8OjjNjkXx9EXGE2Q=", "requirement": "torch==2.4.1+cpu", - "sha256": "8800deef0026011d502c0c256cc4b67d002347f63c3a38cd8e45f1f445c61364", "urls": ["/whl/cpu/torch-2.4.1%2Bcpu-cp312-cp312-linux_x86_64.whl"], }, "pypi_312_torch_cp312_cp312_manylinux_2_17_aarch64_36109432_linux_aarch64": { @@ -656,8 +752,8 @@ torch==2.4.1+cpu ; platform_machine == 'x86_64' \ "dep_template": "@pypi//{name}:{target}", "filename": "torch-2.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", "index_url": "https://torch.index/torch/", + "integrity": "sha256-NhCUMrEL1xY8mzDOiW88LMobhrl2X5VqFZTw/0MJHio=", "requirement": "torch==2.4.1", - "sha256": "36109432b10bd7163c9b30ce896f3c2cca1b86b9765f956a1594f0ff43091e2a", "urls": ["/whl/cpu/torch-2.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl"], }, "pypi_312_torch_cp312_cp312_win_amd64_3a570e5c_windows_x86_64": { @@ -665,8 +761,8 @@ torch==2.4.1+cpu ; platform_machine == 'x86_64' \ "dep_template": "@pypi//{name}:{target}", "filename": "torch-2.4.1+cpu-cp312-cp312-win_amd64.whl", "index_url": "https://torch.index/torch/", + "integrity": "sha256-OlcOXFU0Fc293+Z5IHMns6OAayHGreoU+6d2hNFhnpc=", "requirement": "torch==2.4.1+cpu", - "sha256": "3a570e5c553415cdbddfe679207327b3a3806b21c6adea14fba77684d1619e97", "urls": ["/whl/cpu/torch-2.4.1%2Bcpu-cp312-cp312-win_amd64.whl"], }, "pypi_312_torch_cp312_none_macosx_11_0_arm64_72b484d5_osx_aarch64": { @@ -674,8 +770,8 @@ torch==2.4.1+cpu ; platform_machine == 'x86_64' \ "dep_template": "@pypi//{name}:{target}", "filename": "torch-2.4.1-cp312-none-macosx_11_0_arm64.whl", "index_url": "https://torch.index/torch/", + "integrity": "sha256-crSE1bbOwac1vz+locSIPQF0hpjF6c/b60/6t8eYfg0=", "requirement": "torch==2.4.1", - "sha256": "72b484d5b6cec1a735bf3fa5a1c4883d01748698c5e9cfdbeb4ffab7c7987e0d", "urls": ["/whl/cpu/torch-2.4.1-cp312-none-macosx_11_0_arm64.whl"], }, }) @@ -729,6 +825,31 @@ simple==0.0.1 --hash=sha256:deadb00f ], expect_url = "pypi.org/simple/", ), + # Regression: an unsubstituted ``$VAR`` template with the env var + # unset must expand to "" and fall back to the config default, + # rather than activating the experimental index-url path. + struct( + requirements_txt = "simple==0.0.1 --hash=sha256:deadb00f", + experimental_index_url = "$RULES_PYTHON_PIP_INDEX_URL", + experimental_extra_index_urls = [], + envsubst = ["RULES_PYTHON_PIP_INDEX_URL"], + environ = {}, + expect_index_url = "https://pypi.org/simple", + expect_extra_index_urls = [], + expect_url = "pypi.org/simple/", + ), + # When the env var is set, the resolved value drives the + # experimental index-url path. + struct( + requirements_txt = "simple==0.0.1 --hash=sha256:deadb00f", + experimental_index_url = "${RULES_PYTHON_PIP_INDEX_URL:-}", + experimental_extra_index_urls = [], + envsubst = ["RULES_PYTHON_PIP_INDEX_URL"], + environ = {"RULES_PYTHON_PIP_INDEX_URL": "https://from-env.example.com/simple"}, + expect_index_url = "https://from-env.example.com/simple", + expect_extra_index_urls = [], + expect_url = "from-env.example.com/simple/", + ), ]: got_kwargs = {} @@ -737,15 +858,15 @@ simple==0.0.1 --hash=sha256:deadb00f return { "simple": struct( whls = { - "deadb00f": struct( + "sha256:deadb00f": struct( yanked = None, filename = "simple-0.0.1-py3-none-any.whl", - sha256 = "deadb00f", + digest = "sha256:deadb00f", url = test.expect_url, ), }, sdists = {}, - sha256s_by_version = {}, + hashes_by_version = {}, index_url = test.expect_index_url, ), } @@ -756,6 +877,7 @@ simple==0.0.1 --hash=sha256:deadb00f ) builder.pip_parse( _mock_mctx( + environ = getattr(test, "environ", {}), mock_files = { "requirements.txt": test.requirements_txt, }, @@ -765,6 +887,7 @@ simple==0.0.1 --hash=sha256:deadb00f python_version = "3.15", experimental_index_url = test.experimental_index_url, experimental_extra_index_urls = test.experimental_extra_index_urls, + envsubst = getattr(test, "envsubst", []), requirements_lock = "requirements.txt", target_platforms = [ "linux_x86_64", @@ -785,23 +908,26 @@ simple==0.0.1 --hash=sha256:deadb00f ], }, }) + want_whl_library = { + "config_load": "@pypi//:config.bzl", + "dep_template": "@pypi//{name}:{target}", + "filename": "simple-0.0.1-py3-none-any.whl", + "index_url": test.expect_index_url, + "integrity": "sha256-3q2wDw==", + "requirement": "simple==0.0.1", + "urls": [test.expect_url], + } + if getattr(test, "envsubst", []): + want_whl_library["envsubst"] = test.envsubst pypi.whl_libraries().contains_exactly({ - "pypi_315_simple_py3_none_any_deadb00f": { - "config_load": "@pypi//:config.bzl", - "dep_template": "@pypi//{name}:{target}", - "filename": "simple-0.0.1-py3-none-any.whl", - "index_url": test.expect_index_url, - "requirement": "simple==0.0.1", - "sha256": "deadb00f", - "urls": [test.expect_url], - }, + "pypi_315_simple_py3_none_any_deadb00f": want_whl_library, }) pypi.extra_aliases().contains_exactly({}) env.expect.that_dict(got_kwargs).contains_exactly({ "attr": struct( auth_patterns = {}, - envsubst = {}, + envsubst = getattr(test, "envsubst", []), extra_index_urls = test.expect_extra_index_urls, index_url = test.expect_index_url, index_url_overrides = {}, @@ -922,33 +1048,33 @@ def _test_simple_get_index(env): return { "plat_pkg": struct( whls = { - "deadb44f": struct( + "sha256:deadb44f": struct( yanked = None, filename = "plat-pkg-0.0.4-py3-none-linux_x86_64.whl", - sha256 = "deadb44f", + digest = "sha256:deadb44f", url = "example2.org/index/plat_pkg/", ), }, sdists = {}, - sha256s_by_version = { - "0.0.4": ["deadb44f"], + hashes_by_version = { + "0.0.4": ["sha256:deadb44f"], }, index_url = "https://pypi.org/simple", ), "simple": struct( whls = { - "deadb00f": struct( + "sha256:deadb00f": struct( yanked = None, filename = "simple-0.0.1-py3-none-any.whl", - sha256 = "deadb00f", + digest = "sha256:deadb00f", url = "example2.org", ), }, sdists = { - "deadbeef": struct( + "sha256:deadbeef": struct( yanked = None, filename = "simple-0.0.1.tar.gz", - sha256 = "deadbeef", + digest = "sha256:deadbeef", url = "example.org", ), }, @@ -956,17 +1082,17 @@ def _test_simple_get_index(env): ), "some_other_pkg": struct( whls = { - "deadb33f": struct( + "sha256:deadb33f": struct( yanked = None, filename = "some-other-pkg-0.0.1-py3-none-any.whl", - sha256 = "deadb33f", + digest = "sha256:deadb33f", url = "example2.org/index/some_other_pkg/", ), }, sdists = {}, - sha256s_by_version = { - "0.0.1": ["deadb33f"], - "0.0.3": ["deadbeef"], + hashes_by_version = { + "0.0.1": ["sha256:deadb33f"], + "0.0.3": ["sha256:deadbeef"], }, index_url = "https://with_index_url", ), @@ -1131,18 +1257,17 @@ git_dep @ git+https://git.server/repo/project@deadbeefdeadbeef "dep_template": "@pypi//{name}:{target}", "extra_pip_args": ["--extra-args-for-sdist-building"], "filename": "any-name.tar.gz", + "integrity": "", "python_interpreter_target": "unit_test_interpreter_target", "requirement": "direct_sdist_without_sha @ some-archive/any-name.tar.gz", - "sha256": "", "urls": ["some-archive/any-name.tar.gz"], }, "pypi_315_direct_without_sha_0_0_1_py3_none_any": { "config_load": "@pypi//:config.bzl", "dep_template": "@pypi//{name}:{target}", "filename": "direct_without_sha-0.0.1-py3-none-any.whl", - "python_interpreter_target": "unit_test_interpreter_target", + "integrity": "", "requirement": "direct_without_sha==0.0.1", - "sha256": "", "urls": ["example-direct.org/direct_without_sha-0.0.1-py3-none-any.whl"], "whl_patches": {"my_patch": "1"}, }, @@ -1165,8 +1290,8 @@ git_dep @ git+https://git.server/repo/project@deadbeefdeadbeef "dep_template": "@pypi//{name}:{target}", "filename": "plat-pkg-0.0.4-py3-none-linux_x86_64.whl", "index_url": "https://pypi.org/simple", + "integrity": "sha256-3q20Tw==", "requirement": "plat_pkg==0.0.4", - "sha256": "deadb44f", "urls": ["example2.org/index/plat_pkg/"], }, "pypi_315_simple_py3_none_any_deadb00f": { @@ -1174,16 +1299,16 @@ git_dep @ git+https://git.server/repo/project@deadbeefdeadbeef "dep_template": "@pypi//{name}:{target}", "filename": "simple-0.0.1-py3-none-any.whl", "index_url": "https://pypi.org/simple", + "integrity": "sha256-3q2wDw==", "requirement": "simple==0.0.1", - "sha256": "deadb00f", "urls": ["example2.org"], }, "pypi_315_some_pkg_py3_none_any_deadbaaf": { "config_load": "@pypi//:config.bzl", "dep_template": "@pypi//{name}:{target}", "filename": "some_pkg-0.0.1-py3-none-any.whl", + "integrity": "sha256-3q26rw==", "requirement": "some_pkg==0.0.1", - "sha256": "deadbaaf", "urls": ["example-direct.org/some_pkg-0.0.1-py3-none-any.whl"], }, "pypi_315_some_py3_none_any_deadb33f": { @@ -1191,8 +1316,8 @@ git_dep @ git+https://git.server/repo/project@deadbeefdeadbeef "dep_template": "@pypi//{name}:{target}", "filename": "some-other-pkg-0.0.1-py3-none-any.whl", "index_url": "https://with_index_url", + "integrity": "sha256-3q2zPw==", "requirement": "some_other_pkg==0.0.1", - "sha256": "deadb33f", "urls": ["example2.org/index/some_other_pkg/"], }, }) diff --git a/tests/pypi/index_sources/index_sources_tests.bzl b/tests/pypi/index_sources/index_sources_tests.bzl index 7aa22d164a..3a605b9eb9 100644 --- a/tests/pypi/index_sources/index_sources_tests.bzl +++ b/tests/pypi/index_sources/index_sources_tests.bzl @@ -26,7 +26,7 @@ def _test_no_simple_api_sources(env): requirement_line = "foo @ git+https://github.com/org/foo.git@deadbeef", marker = "", url = "git+https://github.com/org/foo.git@deadbeef", - shas = [], + hashes = [], version = "", filename = "", ), @@ -59,7 +59,7 @@ def _test_no_simple_api_sources(env): requirement_line = "foo==0.0.1 @ https://someurl.org/package.whl --hash=sha256:deadbeef", marker = "", url = "https://someurl.org/package.whl", - shas = ["deadbeef"], + hashes = ["sha256:deadbeef"], version = "0.0.1", filename = "package.whl", ), @@ -68,7 +68,7 @@ def _test_no_simple_api_sources(env): requirement_line = "foo==0.0.1 @ https://someurl.org/package.whl --hash=sha256:deadbeef", marker = "python_version < \"2.7\"", url = "https://someurl.org/package.whl", - shas = ["deadbeef"], + hashes = ["sha256:deadbeef"], version = "0.0.1", filename = "package.whl", ), @@ -80,7 +80,7 @@ def _test_no_simple_api_sources(env): requirement_line = "foo[extra] @ https://example.org/foo-1.0.tar.gz --hash=sha256:deadbe0f", marker = "", url = "https://example.org/foo-1.0.tar.gz", - shas = ["deadbe0f"], + hashes = ["sha256:deadbe0f"], version = "", filename = "foo-1.0.tar.gz", ), @@ -89,14 +89,14 @@ def _test_no_simple_api_sources(env): requirement_line = "torch @ https://download.pytorch.org/whl/cpu/torch-2.6.0%2Bcpu-cp311-cp311-linux_x86_64.whl#sha256=deadbeef", marker = "", url = "https://download.pytorch.org/whl/cpu/torch-2.6.0%2Bcpu-cp311-cp311-linux_x86_64.whl", - shas = ["deadbeef"], + hashes = ["sha256:deadbeef"], version = "", filename = "torch-2.6.0+cpu-cp311-cp311-linux_x86_64.whl", ), } for input, want in inputs.items(): got = index_sources(input) - env.expect.that_collection(got.shas).contains_exactly(want.shas if hasattr(want, "shas") else []) + env.expect.that_collection(got.hashes).contains_exactly(want.hashes if hasattr(want, "hashes") else []) env.expect.that_str(got.version).equals(want.version) env.expect.that_str(got.requirement).equals(want.requirement) env.expect.that_str(got.requirement_line).equals(got.requirement_line) @@ -109,9 +109,9 @@ _tests.append(_test_no_simple_api_sources) def _test_simple_api_sources(env): tests = { "foo==0.0.2 --hash=sha256:deafbeef --hash=sha256:deadbeef": struct( - shas = [ - "deadbeef", - "deafbeef", + hashes = [ + "sha256:deadbeef", + "sha256:deafbeef", ], marker = "", requirement = "foo==0.0.2", @@ -119,9 +119,9 @@ def _test_simple_api_sources(env): url = "", ), "foo[extra]==0.0.2; (python_version < 2.7 or extra == \"@\") --hash=sha256:deafbeef --hash=sha256:deadbeef": struct( - shas = [ - "deadbeef", - "deafbeef", + hashes = [ + "sha256:deadbeef", + "sha256:deafbeef", ], marker = "(python_version < 2.7 or extra == \"@\")", requirement = "foo[extra]==0.0.2", @@ -131,7 +131,7 @@ def _test_simple_api_sources(env): } for input, want in tests.items(): got = index_sources(input) - env.expect.that_collection(got.shas).contains_exactly(want.shas) + env.expect.that_collection(got.hashes).contains_exactly(want.hashes) env.expect.that_str(got.version).equals("0.0.2") env.expect.that_str(got.requirement).equals(want.requirement) env.expect.that_str(got.requirement_line).equals(want.requirement_line) @@ -140,6 +140,62 @@ def _test_simple_api_sources(env): _tests.append(_test_simple_api_sources) +def _test_non_sha256_hashes(env): + tests = { + # A `#sha512=` URL fragment, as any hash algorithm is allowed by PEP 503. + "foo @ https://example.org/foo-0.0.1-py3-none-any.whl#sha512=deadbeef": struct( + hashes = ["sha512:deadbeef"], + marker = "", + requirement = "foo", + requirement_line = "foo @ https://example.org/foo-0.0.1-py3-none-any.whl#sha512=deadbeef", + url = "https://example.org/foo-0.0.1-py3-none-any.whl", + version = "", + filename = "foo-0.0.1-py3-none-any.whl", + ), + # Unknown algorithms are dropped. + "foo==0.0.2 --hash=egg:deadbeef": struct( + hashes = [], + marker = "", + requirement = "foo==0.0.2", + requirement_line = "foo==0.0.2", + url = "", + version = "0.0.2", + filename = "", + ), + "foo==0.0.2 --hash=sha512:deadbeef": struct( + hashes = ["sha512:deadbeef"], + marker = "", + requirement = "foo==0.0.2", + requirement_line = "foo==0.0.2 --hash=sha512:deadbeef", + url = "", + version = "0.0.2", + filename = "", + ), + "foo==0.0.2 --hash=sha512:deafbeef --hash=sha256:deadbeef": struct( + hashes = [ + "sha256:deadbeef", + "sha512:deafbeef", + ], + marker = "", + requirement = "foo==0.0.2", + requirement_line = "foo==0.0.2 --hash=sha512:deafbeef --hash=sha256:deadbeef", + url = "", + version = "0.0.2", + filename = "", + ), + } + for input, want in tests.items(): + got = index_sources(input) + env.expect.that_collection(got.hashes).contains_exactly(want.hashes) + env.expect.that_str(got.version).equals(want.version) + env.expect.that_str(got.requirement).equals(want.requirement) + env.expect.that_str(got.requirement_line).equals(want.requirement_line) + env.expect.that_str(got.marker).equals(want.marker) + env.expect.that_str(got.url).equals(want.url) + env.expect.that_str(got.filename).equals(want.filename) + +_tests.append(_test_non_sha256_hashes) + def index_sources_test_suite(name): """Create the test suite. diff --git a/tests/pypi/parse_requirements/parse_requirements_tests.bzl b/tests/pypi/parse_requirements/parse_requirements_tests.bzl index 2ef2f44764..54f906929a 100644 --- a/tests/pypi/parse_requirements/parse_requirements_tests.bzl +++ b/tests/pypi/parse_requirements/parse_requirements_tests.bzl @@ -92,11 +92,30 @@ foo==0.0.3 --hash=sha256:deadbaaf --hash=sha256:deadb11f --hash=sha256:5d15t --abi=cp39 foo==0.0.3 --hash=sha256:deadbaaf +""", + "requirements_sha512": """\ +foo==0.0.1 --hash=sha512:deadbeef """, "requirements_windows": """\ foo[extra]==0.0.2 --hash=sha256:deadbeef bar==0.0.1 --hash=sha256:deadb00f """, + "uv_lock_empty": """{"package":[]}""", + "uv_lock_foo": """{"package":[{"dependencies":[{"extra":"extra","name":"bar"}],"name":"foo","source":{"registry":"https://pypi.org/simple"},"version":"0.0.1","wheels":[{"hash":"sha256:deadbeef","url":"https://files.pythonhosted.org/packages/foo-0.0.1-py3-none-any.whl"}]}]}""", + "uv_lock_foo_bar": """{"package":[{"name":"bar","version":"0.0.1","source":{"registry":"https://pypi.org/simple"},"sdist":{"hash":"sha256:deadb00f","url":"https://files.pythonhosted.org/packages/bar-0.0.1.tar.gz"}},{"name":"foo","version":"0.0.1","source":{"registry":"https://pypi.org/simple"},"wheels":[{"hash":"sha256:deadbeef","url":"https://files.pythonhosted.org/packages/foo-0.0.1-py3-none-any.whl"}]}]}""", + "uv_lock_foo_dep_extra": """{"package":[{"name":"bar","version":"0.0.2","source":{"registry":"https://pypi.org/simple"},"wheels":[{"hash":"sha256:deadbeef","url":"https://files.pythonhosted.org/packages/bar-0.0.2-py3-none-any.whl"}]},{"name":"foo","version":"0.0.1","source":{"registry":"https://pypi.org/simple"},"dependencies":[{"name":"bar","extra":["extra1"]}],"wheels":[{"hash":"sha256:baadbeef","url":"https://files.pythonhosted.org/packages/foo-0.0.1-py3-none-any.whl"}]}]}""", + "uv_lock_foo_multi_versions": """{"package":[{"name":"foo","source":{"registry":"https://pypi.org/simple"},"version":"0.0.1","wheels":[{"hash":"sha256:deadbeef","url":"https://files.pythonhosted.org/packages/foo-0.0.1-py3-none-any.whl"}]},{"name":"foo","source":{"registry":"https://pypi.org/simple"},"version":"0.0.2","wheels":[{"hash":"sha256:deadb11f","url":"https://files.pythonhosted.org/packages/foo-0.0.2-py3-none-any.whl"}]}]}""", + "uv_lock_foo_multi_wheel_dedup": """{"package":[{"name":"foo","version":"0.0.1","source":{"registry":"https://pypi.org/simple"},"wheels":[{"hash":"sha256:aaa","url":"https://files.pythonhosted.org/packages/foo-0.0.1-cp39-cp39-manylinux_2_17_x86_64.whl"},{"hash":"sha256:bbb","url":"https://files.pythonhosted.org/packages/foo-0.0.1-py3-none-any.whl"}]}]}""", + "uv_lock_foo_only": """{"package":[{"name":"foo","source":{"registry":"https://pypi.org/simple"},"version":"0.0.2"}]}""", + "uv_lock_foo_optional_deps": """{"package":[{"name":"foo","version":"0.0.1","source":{"registry":"https://pypi.org/simple"},"optional-dependencies":{"extra1":[],"extra2":[]},"wheels":[{"hash":"sha256:deadbeef","url":"https://files.pythonhosted.org/packages/foo-0.0.1-py3-none-any.whl"}]}]}""", + "uv_lock_foo_requires_dist_extras": """{"package":[{"name":"foo","version":"0.0.1","source":{"registry":"https://pypi.org/simple"},"wheels":[{"hash":"sha256:deadbeef","url":"https://files.pythonhosted.org/packages/foo-0.0.1-py3-none-any.whl"}]},{"name":"root-pkg","source":{"virtual":"."},"version":"0.0.0","dependencies":[{"name":"foo"}],"metadata":{"requires-dist":[{"name":"foo","extras":["all"]}]}}]}""", + "uv_lock_foo_resolution_markers_dedup": """{"package":[{"name":"foo","source":{"registry":"https://pypi.org/simple"},"version":"0.0.1","resolution-markers":["sys_platform == 'linux'"],"wheels":[{"hash":"sha256:aaa","url":"https://files.pythonhosted.org/packages/foo-0.0.1-cp39-cp39-manylinux_2_17_x86_64.whl"},{"hash":"sha256:bbb","url":"https://files.pythonhosted.org/packages/foo-0.0.1-py3-none-any.whl"}]},{"name":"foo","source":{"registry":"https://pypi.org/simple"},"version":"0.0.2","resolution-markers":["sys_platform == 'darwin'"],"wheels":[{"hash":"sha256:ccc","url":"https://files.pythonhosted.org/packages/foo-0.0.2-cp39-cp39-macosx_11_0_arm64.whl"},{"hash":"sha256:ddd","url":"https://files.pythonhosted.org/packages/foo-0.0.2-py3-none-any.whl"}]}]}""", + "uv_lock_foo_sdist": """{"package":[{"name":"foo","sdist":{"hash":"sha256:feedcafe","url":"https://files.pythonhosted.org/packages/foo-0.0.1.tar.gz"},"source":{"registry":"https://pypi.org/simple"},"version":"0.0.1","wheels":[{"hash":"sha256:deadbeef","url":"https://files.pythonhosted.org/packages/foo-0.0.1-py3-none-any.whl"}]}]}""", + "uv_lock_foo_sha512": """{"package":[{"name":"foo","source":{"registry":"https://pypi.org/simple"},"version":"0.0.1","wheels":[{"hash":"sha512:deadbeef","url":"https://files.pythonhosted.org/packages/foo-0.0.1-py3-none-any.whl"}]}]}""", + "uv_lock_foo_virtual": """{"package":[{"name":"foo","source":{"registry":"https://pypi.org/simple"},"version":"0.0.1","wheels":[{"hash":"sha256:deadbeef","url":"https://files.pythonhosted.org/packages/foo-0.0.1-py3-none-any.whl"}]},{"name":"virtual-pkg","source":{"virtual":true},"version":"0.0.0"}]}""", + "uv_lock_foo_with_extras": """{"package":[{"name":"foo","provides-extras":["extra"],"source":{"registry":"https://pypi.org/simple"},"version":"0.0.1","wheels":[{"hash":"sha256:deadbeef","url":"https://files.pythonhosted.org/packages/foo-0.0.1-py3-none-any.whl"}]}]}""", + "uv_lock_git_vcs": """{"package":[{"name":"foo","source":{"git":"https://github.com/org/foo.git"},"version":"0.1.0"}]}""", + "uv_lock_rules_python_pkg": """{"package":[{"name":"rules_python","source":{"registry":"https://pypi.org/simple"},"version":"0.0.1","wheels":[{"hash":"sha256:deadbeef","url":"https://files.pythonhosted.org/packages/rules_python-0.0.1-py3-none-any.whl"}]}]}""", } return mocks.mctx( @@ -107,7 +126,41 @@ bar==0.0.1 --hash=sha256:deadb00f _tests = [] +def _make_platforms(platform_names): + """Create minimal platform structs for testing, matching py3-none-any wheels.""" + platforms = {} + for name in platform_names: + platforms[name] = struct( + env = pep508_env(python_version = "3.11.0", os = "linux", arch = "x86_64"), + whl_abi_tags = ["none"], + whl_platform_tags = ["any"], + ) + return platforms + def parse_requirements(debug = False, **kwargs): + """Get requirements by calling the original parse_requirements. + + Args: + debug: If True, set verbosity to TRACE. + **kwargs: forwarded to the underlying function. + + Returns: + The result of the underlying parse_requirements call. + """ + kwargs.setdefault("toml_decode", json.decode) + + # Provide default platforms when not specified. + if "platforms" not in kwargs: + if "requirements_by_platform" in kwargs: + platform_names = {} + for _plats in kwargs["requirements_by_platform"].values(): + for _p in _plats: + platform_names[_p] = None + platform_names = sorted(platform_names) + kwargs["platforms"] = _make_platforms(platform_names) + elif "uv_lock" in kwargs: + kwargs["platforms"] = _make_platforms(["linux_x86_64"]) + return _parse_requirements( ctx = _mock_ctx(), logger = repo_utils.logger(struct( @@ -143,7 +196,7 @@ def _test_simple(env): ], url = "", filename = "", - sha256 = "", + digest = "", yanked = None, ), ], @@ -172,7 +225,7 @@ def _test_direct_urls_integration(env): extra_pip_args = [], filename = "foo-1.1.tar.gz", requirement_line = "foo @ https://github.com/org/foo/downloads/foo-1.1.tar.gz", - sha256 = "", + digest = "", target_platforms = ["osx_x86_64"], url = "https://github.com/org/foo/downloads/foo-1.1.tar.gz", yanked = None, @@ -182,7 +235,7 @@ def _test_direct_urls_integration(env): extra_pip_args = [], filename = "package.whl", requirement_line = "foo[extra]", - sha256 = "", + digest = "", target_platforms = ["linux_x86_64"], url = "https://some-url/package.whl", yanked = None, @@ -214,7 +267,7 @@ def _test_direct_urls_no_extract(env): extra_pip_args = [], filename = "", requirement_line = "foo @ https://github.com/org/foo/downloads/foo-1.1.tar.gz", - sha256 = "", + digest = "", target_platforms = ["osx_x86_64"], url = "", yanked = None, @@ -224,7 +277,7 @@ def _test_direct_urls_no_extract(env): extra_pip_args = [], filename = "", requirement_line = "foo[extra] @ https://some-url/package.whl", - sha256 = "", + digest = "", target_platforms = ["linux_x86_64"], url = "", yanked = None, @@ -259,7 +312,7 @@ def _test_extra_pip_args(env): ], url = "", filename = "", - sha256 = "", + digest = "", yanked = None, ), ], @@ -289,7 +342,7 @@ def _test_dupe_requirements(env): target_platforms = ["linux_x86_64"], url = "", filename = "", - sha256 = "", + digest = "", yanked = None, ), ], @@ -321,7 +374,7 @@ def _test_multi_os(env): target_platforms = ["windows_x86_64"], url = "", filename = "", - sha256 = "", + digest = "", yanked = None, ), ], @@ -339,7 +392,7 @@ def _test_multi_os(env): target_platforms = ["linux_x86_64"], url = "", filename = "", - sha256 = "", + digest = "", yanked = None, ), struct( @@ -349,7 +402,7 @@ def _test_multi_os(env): target_platforms = ["windows_x86_64"], url = "", filename = "", - sha256 = "", + digest = "", yanked = None, ), ], @@ -387,7 +440,7 @@ def _test_multi_os_legacy(env): target_platforms = ["cp39_linux_x86_64"], url = "", filename = "", - sha256 = "", + digest = "", yanked = None, ), ], @@ -405,7 +458,7 @@ def _test_multi_os_legacy(env): target_platforms = ["cp39_linux_x86_64"], url = "", filename = "", - sha256 = "", + digest = "", yanked = None, ), struct( @@ -415,7 +468,7 @@ def _test_multi_os_legacy(env): target_platforms = ["cp39_osx_aarch64"], url = "", filename = "", - sha256 = "", + digest = "", yanked = None, ), ], @@ -473,7 +526,7 @@ def _test_env_marker_resolution(env): target_platforms = ["cp311_linux_super_exotic", "cp311_windows_x86_64"], url = "", filename = "", - sha256 = "", + digest = "", yanked = None, ), ], @@ -491,7 +544,7 @@ def _test_env_marker_resolution(env): target_platforms = ["cp311_windows_x86_64"], url = "", filename = "", - sha256 = "", + digest = "", yanked = None, ), ], @@ -522,7 +575,7 @@ def _test_different_package_version(env): target_platforms = ["linux_aarch64"], url = "", filename = "", - sha256 = "", + digest = "", yanked = None, ), struct( @@ -532,7 +585,7 @@ def _test_different_package_version(env): target_platforms = ["linux_x86_64"], url = "", filename = "", - sha256 = "", + digest = "", yanked = None, ), ], @@ -563,7 +616,7 @@ def _test_different_package_extras(env): target_platforms = ["linux_aarch64"], url = "", filename = "", - sha256 = "", + digest = "", yanked = None, ), struct( @@ -573,7 +626,7 @@ def _test_different_package_extras(env): target_platforms = ["linux_x86_64"], url = "", filename = "", - sha256 = "", + digest = "", yanked = None, ), ], @@ -603,7 +656,7 @@ def _test_optional_hash(env): target_platforms = ["linux_x86_64"], url = "https://example.org/bar-0.0.4.whl", filename = "bar-0.0.4.whl", - sha256 = "", + digest = "", yanked = None, ), ], @@ -621,7 +674,7 @@ def _test_optional_hash(env): target_platforms = ["linux_x86_64"], url = "https://example.org/foo-0.0.5.whl", filename = "foo-0.0.5.whl", - sha256 = "deadbeef", + digest = "sha256:deadbeef", yanked = None, ), ], @@ -651,7 +704,7 @@ def _test_git_sources(env): target_platforms = ["linux_x86_64"], url = "", filename = "", - sha256 = "", + digest = "", yanked = None, ), ], @@ -691,23 +744,23 @@ def _test_overlapping_shas_with_index_results(env): "foo": struct( index_url = "https://example.com", sdists = { - "5d15t": struct( + "sha256:5d15t": struct( url = "sdist", - sha256 = "5d15t", + digest = "sha256:5d15t", filename = "foo-0.0.1.tar.gz", yanked = None, ), }, whls = { - "deadb11f": struct( + "sha256:deadb11f": struct( url = "super2", - sha256 = "deadb11f", + digest = "sha256:deadb11f", filename = "foo-0.0.1-py3-none-macosx_14_0_x86_64.whl", yanked = None, ), - "deadbaaf": struct( + "sha256:deadbaaf": struct( url = "super2", - sha256 = "deadbaaf", + digest = "sha256:deadbaaf", filename = "foo-0.0.1-py3-none-any.whl", yanked = None, ), @@ -728,7 +781,7 @@ def _test_overlapping_shas_with_index_results(env): extra_pip_args = [], filename = "foo-0.0.1-py3-none-any.whl", requirement_line = "foo==0.0.3", - sha256 = "deadbaaf", + digest = "sha256:deadbaaf", target_platforms = ["cp39_linux_x86_64"], url = "super2", yanked = None, @@ -738,7 +791,7 @@ def _test_overlapping_shas_with_index_results(env): extra_pip_args = [], filename = "foo-0.0.1-py3-none-macosx_14_0_x86_64.whl", requirement_line = "foo==0.0.3", - sha256 = "deadb11f", + digest = "sha256:deadb11f", target_platforms = ["cp39_osx_x86_64"], url = "super2", yanked = None, @@ -749,6 +802,62 @@ def _test_overlapping_shas_with_index_results(env): _tests.append(_test_overlapping_shas_with_index_results) +def _test_non_sha256_hash_matching(env): + """Test that non-sha256 pins are matched against the index digests.""" + got = parse_requirements( + requirements_by_platform = { + "requirements_sha512": ["cp311_linux_x86_64"], + }, + platforms = { + "cp311_linux_x86_64": struct( + env = pep508_env( + python_version = "3.11.0", + os = "linux", + arch = "x86_64", + ), + whl_abi_tags = ["none"], + whl_platform_tags = ["any"], + ), + }, + get_index_urls = lambda _, __, **kwargs: { + "foo": struct( + index_url = "https://example.com", + sdists = {}, + whls = { + "sha512:deadbeef": struct( + url = "https://example.com/foo-0.0.1-py3-none-any.whl", + digest = "sha512:deadbeef", + filename = "foo-0.0.1-py3-none-any.whl", + yanked = None, + ), + }, + ), + }, + ) + + env.expect.that_collection(got).contains_exactly([ + struct( + name = "foo", + index_url = "https://example.com", + is_exposed = True, + is_multiple_versions = False, + srcs = [ + struct( + distribution = "foo", + extra_pip_args = [], + filename = "foo-0.0.1-py3-none-any.whl", + requirement_line = "foo==0.0.1", + digest = "sha512:deadbeef", + target_platforms = ["cp311_linux_x86_64"], + url = "https://example.com/foo-0.0.1-py3-none-any.whl", + yanked = None, + ), + ], + ), + ]) + +_tests.append(_test_non_sha256_hash_matching) + def _test_get_index_urls_different_versions(env): """Test that different versions from index URLs are matched correctly per platform.""" got = parse_requirements( @@ -783,15 +892,15 @@ def _test_get_index_urls_different_versions(env): index_url = "", sdists = {}, whls = { - "deadb11f": struct( + "sha256:deadb11f": struct( url = "super2", - sha256 = "deadb11f", + digest = "sha256:deadb11f", filename = "foo-0.0.2-py3-none-any.whl", yanked = None, ), - "deadbaaf": struct( + "sha256:deadbaaf": struct( url = "super2", - sha256 = "deadbaaf", + digest = "sha256:deadbaaf", filename = "foo-0.0.1-py3-none-any.whl", yanked = None, ), @@ -812,7 +921,7 @@ def _test_get_index_urls_different_versions(env): extra_pip_args = [], filename = "", requirement_line = "boo==0.0.4 --hash=sha256:deadbaaf", - sha256 = "", + digest = "", target_platforms = ["cp39_linux_x86_64"], url = "", yanked = None, @@ -830,7 +939,7 @@ def _test_get_index_urls_different_versions(env): extra_pip_args = [], filename = "", requirement_line = "foo==0.0.1 --hash=sha256:deadbeef", - sha256 = "", + digest = "", target_platforms = ["cp39_linux_x86_64"], url = "", yanked = None, @@ -840,7 +949,7 @@ def _test_get_index_urls_different_versions(env): extra_pip_args = [], filename = "foo-0.0.2-py3-none-any.whl", requirement_line = "foo==0.0.2", - sha256 = "deadb11f", + digest = "sha256:deadb11f", target_platforms = ["cp310_linux_x86_64"], url = "super2", yanked = None, @@ -920,9 +1029,9 @@ def _test_get_index_urls_single_py_version(env): index_url = "", sdists = {}, whls = { - "deadb11f": struct( + "sha256:deadb11f": struct( url = "super2", - sha256 = "deadb11f", + digest = "sha256:deadb11f", filename = "foo-0.0.2-py3-none-any.whl", yanked = None, ), @@ -943,7 +1052,7 @@ def _test_get_index_urls_single_py_version(env): extra_pip_args = [], filename = "foo-0.0.2-py3-none-any.whl", requirement_line = "foo==0.0.2", - sha256 = "deadb11f", + digest = "sha256:deadb11f", target_platforms = ["cp310_linux_x86_64"], url = "super2", yanked = None, @@ -992,6 +1101,621 @@ def _test_get_index_urls_all_versions(env): _tests.append(_test_get_index_urls_all_versions) +def _test_uv_lock_consistent(env): + """Test that uv_lock with requirements_by_platform uses correct platforms.""" + got = parse_requirements( + requirements_by_platform = { + "requirements_lock": ["linux_x86_64", "windows_x86_64"], + }, + uv_lock = "uv_lock_foo_with_extras", + ) + env.expect.that_collection(got).contains_exactly([ + struct( + name = "foo", + index_url = "https://pypi.org/simple/foo", + is_exposed = True, + is_multiple_versions = False, + srcs = [ + struct( + distribution = "foo", + extra_pip_args = [], + requirement_line = "foo[extra]==0.0.1", + target_platforms = ["linux_x86_64", "windows_x86_64"], + filename = "foo-0.0.1-py3-none-any.whl", + digest = "sha256:deadbeef", + url = "https://files.pythonhosted.org/packages/foo-0.0.1-py3-none-any.whl", + yanked = None, + ), + ], + ), + ]) + +_tests.append(_test_uv_lock_consistent) + +def _test_uv_lock_primary_source(env): + """Test that uv.lock can be used as the sole source without requirements files.""" + got = parse_requirements( + uv_lock = "uv_lock_foo_sdist", + ) + env.expect.that_collection(got).contains_exactly([ + struct( + name = "foo", + index_url = "https://pypi.org/simple/foo", + is_exposed = True, + is_multiple_versions = False, + srcs = [ + struct( + distribution = "foo", + extra_pip_args = [], + requirement_line = "foo==0.0.1", + target_platforms = ["linux_x86_64"], + filename = "foo-0.0.1-py3-none-any.whl", + digest = "sha256:deadbeef", + url = "https://files.pythonhosted.org/packages/foo-0.0.1-py3-none-any.whl", + yanked = None, + ), + ], + ), + ]) + +_tests.append(_test_uv_lock_primary_source) + +def _test_uv_lock_non_sha256_hash(env): + """Test that non-sha256 uv.lock hashes are propagated to the sources.""" + got = parse_requirements( + uv_lock = "uv_lock_foo_sha512", + ) + env.expect.that_collection(got).contains_exactly([ + struct( + name = "foo", + index_url = "https://pypi.org/simple/foo", + is_exposed = True, + is_multiple_versions = False, + srcs = [ + struct( + distribution = "foo", + extra_pip_args = [], + requirement_line = "foo==0.0.1", + target_platforms = ["linux_x86_64"], + filename = "foo-0.0.1-py3-none-any.whl", + digest = "sha512:deadbeef", + url = "https://files.pythonhosted.org/packages/foo-0.0.1-py3-none-any.whl", + yanked = None, + ), + ], + ), + ]) + +_tests.append(_test_uv_lock_non_sha256_hash) + +def _test_uv_lock_primary_source_multiple_versions(env): + """Test that uv.lock with multiple versions of the same package works.""" + got = parse_requirements( + uv_lock = "uv_lock_foo_multi_versions", + ) + env.expect.that_collection(got).contains_exactly([ + struct( + name = "foo", + index_url = "https://pypi.org/simple/foo", + is_exposed = True, + is_multiple_versions = True, + srcs = [ + struct( + distribution = "foo", + extra_pip_args = [], + requirement_line = "foo==0.0.1", + target_platforms = ["linux_x86_64"], + filename = "foo-0.0.1-py3-none-any.whl", + digest = "sha256:deadbeef", + url = "https://files.pythonhosted.org/packages/foo-0.0.1-py3-none-any.whl", + yanked = None, + ), + struct( + distribution = "foo", + extra_pip_args = [], + requirement_line = "foo==0.0.2", + target_platforms = ["linux_x86_64"], + filename = "foo-0.0.2-py3-none-any.whl", + digest = "sha256:deadb11f", + url = "https://files.pythonhosted.org/packages/foo-0.0.2-py3-none-any.whl", + yanked = None, + ), + ], + ), + ]) + +_tests.append(_test_uv_lock_primary_source_multiple_versions) + +def _test_uv_lock_primary_source_with_extras(env): + """Test that uv.lock extras are included in requirement lines.""" + got = parse_requirements( + uv_lock = "uv_lock_foo_with_extras", + ) + env.expect.that_collection(got).contains_exactly([ + struct( + name = "foo", + index_url = "https://pypi.org/simple/foo", + is_exposed = True, + is_multiple_versions = False, + srcs = [ + struct( + distribution = "foo", + extra_pip_args = [], + requirement_line = "foo[extra]==0.0.1", + target_platforms = ["linux_x86_64"], + filename = "foo-0.0.1-py3-none-any.whl", + digest = "sha256:deadbeef", + url = "https://files.pythonhosted.org/packages/foo-0.0.1-py3-none-any.whl", + yanked = None, + ), + ], + ), + ]) + +_tests.append(_test_uv_lock_primary_source_with_extras) + +def _test_uv_lock_primary_source_includes_virtual(env): + """Test that virtual packages in uv.lock are included.""" + got = parse_requirements( + uv_lock = "uv_lock_foo_virtual", + ) + env.expect.that_collection(got).contains_exactly([ + struct( + name = "foo", + index_url = "https://pypi.org/simple/foo", + is_exposed = True, + is_multiple_versions = False, + srcs = [ + struct( + distribution = "foo", + extra_pip_args = [], + requirement_line = "foo==0.0.1", + target_platforms = ["linux_x86_64"], + filename = "foo-0.0.1-py3-none-any.whl", + digest = "sha256:deadbeef", + url = "https://files.pythonhosted.org/packages/foo-0.0.1-py3-none-any.whl", + yanked = None, + ), + ], + ), + struct( + name = "virtual_pkg", + index_url = "", + is_exposed = False, + is_multiple_versions = False, + srcs = [], + ), + ]) + +_tests.append(_test_uv_lock_primary_source_includes_virtual) + +def _test_uv_lock_cross_consistent(env): + """Test that the uv.lock and requirements work together for cross-platform.""" + got = parse_requirements( + requirements_by_platform = { + "requirements_lock": ["linux_x86_64", "windows_x86_64"], + }, + uv_lock = "uv_lock_foo_with_extras", + ) + env.expect.that_collection(got).contains_exactly([ + struct( + name = "foo", + index_url = "https://pypi.org/simple/foo", + is_exposed = True, + is_multiple_versions = False, + srcs = [ + struct( + distribution = "foo", + extra_pip_args = [], + requirement_line = "foo[extra]==0.0.1", + target_platforms = ["linux_x86_64", "windows_x86_64"], + filename = "foo-0.0.1-py3-none-any.whl", + digest = "sha256:deadbeef", + url = "https://files.pythonhosted.org/packages/foo-0.0.1-py3-none-any.whl", + yanked = None, + ), + ], + ), + ]) + +_tests.append(_test_uv_lock_cross_consistent) + +def _test_uv_lock_vcs_entry(env): + """Test that VCS entries in uv.lock are handled without crashing.""" + got = parse_requirements( + uv_lock = "uv_lock_git_vcs", + ) + env.expect.that_collection(got).contains_exactly([ + struct( + name = "foo", + index_url = "", + is_exposed = True, + is_multiple_versions = False, + srcs = [ + struct( + distribution = "foo", + extra_pip_args = [], + requirement_line = "foo==0.1.0", + target_platforms = ["linux_x86_64"], + filename = "foo.git", + digest = "", + url = "https://github.com/org/foo.git", + yanked = None, + ), + ], + ), + ]) + +_tests.append(_test_uv_lock_vcs_entry) + +def _test_uv_lock_rules_python_pkg_not_skipped(env): + """Test that 'rules_python' package is not skipped from uv.lock.""" + got = parse_requirements( + uv_lock = "uv_lock_rules_python_pkg", + ) + env.expect.that_collection(got).contains_exactly([ + struct( + name = "rules_python", + index_url = "https://pypi.org/simple/rules-python", + is_exposed = True, + is_multiple_versions = False, + srcs = [ + struct( + distribution = "rules_python", + extra_pip_args = [], + requirement_line = "rules_python==0.0.1", + target_platforms = ["linux_x86_64"], + filename = "rules_python-0.0.1-py3-none-any.whl", + digest = "sha256:deadbeef", + url = "https://files.pythonhosted.org/packages/rules_python-0.0.1-py3-none-any.whl", + yanked = None, + ), + ], + ), + ]) + +_tests.append(_test_uv_lock_rules_python_pkg_not_skipped) + +def _test_uv_lock_no_consistency_check(env): + """Test that uv.lock is used as the primary source when both uv.lock and requirements exist.""" + got = parse_requirements( + requirements_by_platform = { + "requirements_lock": ["linux_x86_64"], + }, + uv_lock = "uv_lock_foo", + ) + + # The result comes from uv.lock (no extras since uv_lock_foo doesn't have provides-extras) + env.expect.that_collection(got).contains_exactly([ + struct( + name = "foo", + index_url = "https://pypi.org/simple/foo", + is_exposed = True, + is_multiple_versions = False, + srcs = [ + struct( + distribution = "foo", + extra_pip_args = [], + requirement_line = "foo==0.0.1", + target_platforms = ["linux_x86_64"], + filename = "foo-0.0.1-py3-none-any.whl", + digest = "sha256:deadbeef", + url = "https://files.pythonhosted.org/packages/foo-0.0.1-py3-none-any.whl", + yanked = None, + ), + ], + ), + ]) + +_tests.append(_test_uv_lock_no_consistency_check) + +def _test_uv_lock_multiple_packages(env): + """Test that multiple packages from uv.lock are all returned.""" + got = parse_requirements( + uv_lock = "uv_lock_foo_bar", + ) + env.expect.that_collection(got).contains_exactly([ + struct( + name = "bar", + index_url = "https://pypi.org/simple/bar", + is_exposed = True, + is_multiple_versions = False, + srcs = [ + struct( + distribution = "bar", + extra_pip_args = [], + requirement_line = "bar==0.0.1", + target_platforms = ["linux_x86_64"], + filename = "bar-0.0.1.tar.gz", + digest = "sha256:deadb00f", + url = "https://files.pythonhosted.org/packages/bar-0.0.1.tar.gz", + yanked = None, + ), + ], + ), + struct( + name = "foo", + index_url = "https://pypi.org/simple/foo", + is_exposed = True, + is_multiple_versions = False, + srcs = [ + struct( + distribution = "foo", + extra_pip_args = [], + requirement_line = "foo==0.0.1", + target_platforms = ["linux_x86_64"], + filename = "foo-0.0.1-py3-none-any.whl", + digest = "sha256:deadbeef", + url = "https://files.pythonhosted.org/packages/foo-0.0.1-py3-none-any.whl", + yanked = None, + ), + ], + ), + ]) + +_tests.append(_test_uv_lock_multiple_packages) + +def _test_uv_lock_with_extra_pip_args(env): + """Test that extra_pip_args are passed through with uv.lock.""" + got = parse_requirements( + uv_lock = "uv_lock_foo", + extra_pip_args = ["--index-url=example.org"], + ) + env.expect.that_collection(got).contains_exactly([ + struct( + name = "foo", + index_url = "https://pypi.org/simple/foo", + is_exposed = True, + is_multiple_versions = False, + srcs = [ + struct( + distribution = "foo", + extra_pip_args = ["--index-url=example.org"], + requirement_line = "foo==0.0.1", + target_platforms = ["linux_x86_64"], + filename = "foo-0.0.1-py3-none-any.whl", + digest = "sha256:deadbeef", + url = "https://files.pythonhosted.org/packages/foo-0.0.1-py3-none-any.whl", + yanked = None, + ), + ], + ), + ]) + +_tests.append(_test_uv_lock_with_extra_pip_args) + +def _test_uv_lock_multi_os_with_requirements(env): + """Test that uv.lock works with requirements_by_platform for multi-platform.""" + got = parse_requirements( + requirements_by_platform = { + "requirements_foo": ["linux_aarch64"], + "requirements_lock": ["linux_x86_64", "windows_x86_64"], + }, + uv_lock = "uv_lock_foo", + ) + env.expect.that_collection(got).contains_exactly([ + struct( + name = "foo", + index_url = "https://pypi.org/simple/foo", + is_exposed = True, + is_multiple_versions = False, + srcs = [ + struct( + distribution = "foo", + extra_pip_args = [], + requirement_line = "foo==0.0.1", + target_platforms = ["linux_aarch64", "linux_x86_64", "windows_x86_64"], + filename = "foo-0.0.1-py3-none-any.whl", + digest = "sha256:deadbeef", + url = "https://files.pythonhosted.org/packages/foo-0.0.1-py3-none-any.whl", + yanked = None, + ), + ], + ), + ]) + +_tests.append(_test_uv_lock_multi_os_with_requirements) + +def _test_uv_lock_extras_optional_deps(env): + """Test that extras from optional-dependencies in uv.lock are included.""" + got = parse_requirements( + uv_lock = "uv_lock_foo_optional_deps", + ) + env.expect.that_collection(got).contains_exactly([ + struct( + name = "foo", + index_url = "https://pypi.org/simple/foo", + is_exposed = True, + is_multiple_versions = False, + srcs = [ + struct( + distribution = "foo", + extra_pip_args = [], + requirement_line = "foo[extra1,extra2]==0.0.1", + target_platforms = ["linux_x86_64"], + filename = "foo-0.0.1-py3-none-any.whl", + digest = "sha256:deadbeef", + url = "https://files.pythonhosted.org/packages/foo-0.0.1-py3-none-any.whl", + yanked = None, + ), + ], + ), + ]) + +_tests.append(_test_uv_lock_extras_optional_deps) + +def _test_uv_lock_extras_dep_edge(env): + """Test that dep extra edges in uv.lock add extras to the dependency.""" + got = parse_requirements( + uv_lock = "uv_lock_foo_dep_extra", + ) + env.expect.that_collection(got).contains_exactly([ + struct( + name = "bar", + index_url = "https://pypi.org/simple/bar", + is_exposed = True, + is_multiple_versions = False, + srcs = [ + struct( + distribution = "bar", + extra_pip_args = [], + requirement_line = "bar[extra1]==0.0.2", + target_platforms = ["linux_x86_64"], + filename = "bar-0.0.2-py3-none-any.whl", + digest = "sha256:deadbeef", + url = "https://files.pythonhosted.org/packages/bar-0.0.2-py3-none-any.whl", + yanked = None, + ), + ], + ), + struct( + name = "foo", + index_url = "https://pypi.org/simple/foo", + is_exposed = True, + is_multiple_versions = False, + srcs = [ + struct( + distribution = "foo", + extra_pip_args = [], + requirement_line = "foo==0.0.1", + target_platforms = ["linux_x86_64"], + filename = "foo-0.0.1-py3-none-any.whl", + digest = "sha256:baadbeef", + url = "https://files.pythonhosted.org/packages/foo-0.0.1-py3-none-any.whl", + yanked = None, + ), + ], + ), + ]) + +_tests.append(_test_uv_lock_extras_dep_edge) + +def _test_uv_lock_wheel_dedup_single_version(env): + """Test that overlapping wheels for a single version are deduplicated to one per platform.""" + got = parse_requirements( + uv_lock = "uv_lock_foo_multi_wheel_dedup", + platforms = { + "cp39_linux_x86_64": struct( + env = pep508_env(python_version = "3.9.0", os = "linux", arch = "x86_64"), + whl_abi_tags = ["none", "abi3", "cp39"], + whl_platform_tags = ["any", "linux_x86_64", "manylinux_*_x86_64"], + ), + }, + ) + env.expect.that_collection(got).contains_exactly([ + struct( + name = "foo", + index_url = "https://pypi.org/simple/foo", + is_exposed = True, + is_multiple_versions = False, + srcs = [ + struct( + distribution = "foo", + extra_pip_args = [], + requirement_line = "foo==0.0.1", + target_platforms = ["cp39_linux_x86_64"], + filename = "foo-0.0.1-cp39-cp39-manylinux_2_17_x86_64.whl", + digest = "sha256:aaa", + url = "https://files.pythonhosted.org/packages/foo-0.0.1-cp39-cp39-manylinux_2_17_x86_64.whl", + yanked = None, + ), + ], + ), + ]) + +_tests.append(_test_uv_lock_wheel_dedup_single_version) + +def _test_uv_lock_wheel_dedup_resolution_markers(env): + """Test that resolution-markers filtering and wheel dedup work together. + + Two versions of foo with resolution-markers for different platforms. + Each version has a platform-specific wheel and a generic py3-none-any wheel. + The dedup should pick the platform-specific wheel for each platform and + the resolution-markers should split versions across platforms. + """ + got = parse_requirements( + uv_lock = "uv_lock_foo_resolution_markers_dedup", + platforms = { + "cp39_linux_x86_64": struct( + env = pep508_env(python_version = "3.9.0", os = "linux", arch = "x86_64"), + whl_abi_tags = ["none", "abi3", "cp39"], + whl_platform_tags = ["any", "linux_x86_64", "manylinux_*_x86_64"], + ), + "cp39_osx_aarch64": struct( + env = pep508_env(python_version = "3.9.0", os = "osx", arch = "aarch64"), + whl_abi_tags = ["none", "abi3", "cp39"], + whl_platform_tags = ["any", "macosx_*_arm64"], + ), + }, + ) + env.expect.that_collection(got).contains_exactly([ + struct( + name = "foo", + index_url = "https://pypi.org/simple/foo", + is_exposed = True, + is_multiple_versions = True, + srcs = [ + struct( + distribution = "foo", + extra_pip_args = [], + requirement_line = "foo==0.0.1", + target_platforms = ["cp39_linux_x86_64"], + filename = "foo-0.0.1-cp39-cp39-manylinux_2_17_x86_64.whl", + digest = "sha256:aaa", + url = "https://files.pythonhosted.org/packages/foo-0.0.1-cp39-cp39-manylinux_2_17_x86_64.whl", + yanked = None, + ), + struct( + distribution = "foo", + extra_pip_args = [], + requirement_line = "foo==0.0.2", + target_platforms = ["cp39_osx_aarch64"], + filename = "foo-0.0.2-cp39-cp39-macosx_11_0_arm64.whl", + digest = "sha256:ccc", + url = "https://files.pythonhosted.org/packages/foo-0.0.2-cp39-cp39-macosx_11_0_arm64.whl", + yanked = None, + ), + ], + ), + ]) + +_tests.append(_test_uv_lock_wheel_dedup_resolution_markers) + +def _test_uv_lock_requires_dist_extras(env): + """Test that extras from metadata.requires-dist appear in requirement_line.""" + got = parse_requirements( + uv_lock = "uv_lock_foo_requires_dist_extras", + ) + env.expect.that_collection(got).contains_exactly([ + struct( + name = "foo", + index_url = "https://pypi.org/simple/foo", + is_exposed = True, + is_multiple_versions = False, + srcs = [ + struct( + distribution = "foo", + extra_pip_args = [], + requirement_line = "foo[all]==0.0.1", + target_platforms = ["linux_x86_64"], + filename = "foo-0.0.1-py3-none-any.whl", + digest = "sha256:deadbeef", + url = "https://files.pythonhosted.org/packages/foo-0.0.1-py3-none-any.whl", + yanked = None, + ), + ], + ), + struct( + name = "root_pkg", + index_url = "", + is_exposed = False, + is_multiple_versions = False, + srcs = [], + ), + ]) + +_tests.append(_test_uv_lock_requires_dist_extras) + def parse_requirements_test_suite(name): """Create the test suite. diff --git a/tests/pypi/parse_simpleapi_html/parse_simpleapi_html_tests.bzl b/tests/pypi/parse_simpleapi_html/parse_simpleapi_html_tests.bzl index c84140f459..975fcd0ee6 100644 --- a/tests/pypi/parse_simpleapi_html/parse_simpleapi_html_tests.bzl +++ b/tests/pypi/parse_simpleapi_html/parse_simpleapi_html_tests.bzl @@ -78,7 +78,7 @@ def _test_sdist(env): ), struct( filename = "foo-0.0.1.tar.gz", - sha256 = "deadbeefasource", + digest = "sha256:deadbeefasource", url = "https://example.org/full-url/foo-0.0.1.tar.gz", yanked = None, version = "0.0.1", @@ -95,7 +95,7 @@ def _test_sdist(env): ), struct( filename = "foo-0.0.1.tar.gz", - sha256 = "deadbeefasource", + digest = "sha256:deadbeefasource", url = "https://example.org/full-url/foo-0.0.1.tar.gz", version = "0.0.1", yanked = "", @@ -112,7 +112,7 @@ def _test_sdist(env): ), struct( filename = "foo-0.0.1.tar.gz", - sha256 = "deadbeefasource", + digest = "sha256:deadbeefasource", url = "https://example.org/full-url/foo-0.0.1.tar.gz", version = "0.0.1", # NOTE @aignas 2026-03-09: we preserve the white space @@ -130,7 +130,7 @@ def _test_sdist(env): ), struct( filename = "foo-0.0.1.tar.gz", - sha256 = "deadbeefasource", + digest = "sha256:deadbeefasource", url = "https://example.org/full-url/foo-0.0.1.tar.gz", version = "0.0.1", yanked = "", @@ -143,22 +143,22 @@ def _test_sdist(env): got = parse_simpleapi_html(content = html) env.expect.that_collection(got.sdists).has_size(1) env.expect.that_collection(got.whls).has_size(0) - env.expect.that_collection(got.sha256s_by_version).has_size(1) + env.expect.that_collection(got.hashes_by_version).has_size(1) if not got: fail("expected at least one element, but did not get anything from:\n{}".format(html)) actual = env.expect.that_struct( - got.sdists[want.sha256], + got.sdists[want.digest], attrs = dict( filename = subjects.str, - sha256 = subjects.str, + digest = subjects.str, url = subjects.str, yanked = subjects.str, version = subjects.str, ), ) actual.filename().equals(want.filename) - actual.sha256().equals(want.sha256) + actual.digest().equals(want.digest) actual.url().equals(want.url) actual.yanked().equals(want.yanked) actual.version().equals(want.version) @@ -182,7 +182,7 @@ def _test_whls(env): filename = "foo-0.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", metadata_sha256 = "deadb00f", metadata_url = "https://example.org/full-url/foo-0.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.metadata", - sha256 = "deadbeef", + digest = "sha256:deadbeef", url = "https://example.org/full-url/foo-0.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", version = "0.0.2", yanked = None, @@ -202,7 +202,7 @@ def _test_whls(env): filename = "foo-0.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", metadata_sha256 = "deadb00f", metadata_url = "https://example.org/full-url/foo-0.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.metadata", - sha256 = "deadbeef", + digest = "sha256:deadbeef", url = "https://example.org/full-url/foo-0.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", version = "0.0.2", yanked = None, @@ -221,7 +221,7 @@ def _test_whls(env): filename = "foo-0.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", metadata_sha256 = "deadb00f", metadata_url = "https://example.org/full-url/foo-0.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.metadata", - sha256 = "deadbeef", + digest = "sha256:deadbeef", version = "0.0.2", url = "https://example.org/full-url/foo-0.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", yanked = None, @@ -240,7 +240,7 @@ def _test_whls(env): filename = "foo-0.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", metadata_sha256 = "deadb00f", metadata_url = "https://example.org/full-url/foo-0.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.metadata", - sha256 = "deadbeef", + digest = "sha256:deadbeef", version = "0.0.2", url = "https://example.org/full-url/foo-0.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", yanked = None, @@ -258,7 +258,7 @@ def _test_whls(env): filename = "foo-0.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", metadata_sha256 = "", metadata_url = "", - sha256 = "deadbeef", + digest = "sha256:deadbeef", url = "https://example.org/full-url/foo-0.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", version = "0.0.2", yanked = None, @@ -275,12 +275,12 @@ def _test_whls(env): fail("expected at least one element, but did not get anything from:\n{}".format(html)) actual = env.expect.that_struct( - got.whls[want.sha256], + got.whls[want.digest], attrs = dict( filename = subjects.str, metadata_sha256 = subjects.str, metadata_url = subjects.str, - sha256 = subjects.str, + digest = subjects.str, url = subjects.str, yanked = subjects.str, version = subjects.str, @@ -289,13 +289,75 @@ def _test_whls(env): actual.filename().equals(want.filename) actual.metadata_sha256().equals(want.metadata_sha256) actual.metadata_url().equals(want.metadata_url) - actual.sha256().equals(want.sha256) + actual.digest().equals(want.digest) actual.url().equals(want.url) actual.yanked().equals(want.yanked) actual.version().equals(want.version) _tests.append(_test_whls) +def _test_non_sha256_fragment(env): + """The index may advertise digests calculated with any hash algorithm (PEP 503).""" + + html = _generate_html( + struct( + attrs = [ + 'href="https://example.org/full-url/foo-0.0.1-py3-none-any.whl#sha512=deadbeef"', + ], + filename = "foo-0.0.1-py3-none-any.whl", + ), + struct( + attrs = [ + 'href="https://example.org/full-url/foo-0.0.1.tar.gz#sha512=deadb00f"', + ], + filename = "foo-0.0.1.tar.gz", + ), + struct( + # A non-hash fragment is kept as part of the URL. + attrs = [ + 'href="https://example.org/full-url/foo-0.0.2-py3-none-any.whl#egg=foo"', + ], + filename = "foo-0.0.2-py3-none-any.whl", + ), + ) + got = parse_simpleapi_html(content = html) + + env.expect.that_collection(got.whls).has_size(2) + env.expect.that_collection(got.sdists).has_size(1) + env.expect.that_dict(got.hashes_by_version).contains_exactly({ + "0.0.1": ["sha512:deadbeef", "sha512:deadb00f"], + "0.0.2": [""], + }) + + whl = got.whls["sha512:deadbeef"] + env.expect.that_str(whl.digest).equals("sha512:deadbeef") + env.expect.that_str(whl.url).equals("https://example.org/full-url/foo-0.0.1-py3-none-any.whl") + + sdist = got.sdists["sha512:deadb00f"] + env.expect.that_str(sdist.digest).equals("sha512:deadb00f") + + no_hash_whl = got.whls[""] + env.expect.that_str(no_hash_whl.digest).equals("") + env.expect.that_str(no_hash_whl.url).equals("https://example.org/full-url/foo-0.0.2-py3-none-any.whl#egg=foo") + +_tests.append(_test_non_sha256_fragment) + +def _test_sha256_fragment_digest(env): + html = _generate_html( + struct( + attrs = [ + 'href="https://example.org/full-url/foo-0.0.1-py3-none-any.whl#sha256=deadbeef"', + ], + filename = "foo-0.0.1-py3-none-any.whl", + ), + ) + got = parse_simpleapi_html(content = html) + + whl = got.whls["sha256:deadbeef"] + env.expect.that_str(whl.digest).equals("sha256:deadbeef") + +_tests.append(_test_sha256_fragment_digest) + def parse_simpleapi_html_test_suite(name): """Create the test suite. diff --git a/tests/pypi/patch_whl/BUILD.bazel b/tests/pypi/patch_whl/BUILD.bazel index d6c4f47b36..cfdc3cc12a 100644 --- a/tests/pypi/patch_whl/BUILD.bazel +++ b/tests/pypi/patch_whl/BUILD.bazel @@ -1,3 +1,14 @@ +load("//python:py_test.bzl", "py_test") +load("//tests/support:support.bzl", "SUPPORTS_BZLMOD") load(":patch_whl_tests.bzl", "patch_whl_test_suite") patch_whl_test_suite(name = "patch_whl_tests") + +py_test( + name = "patch_whl_patch_test", + srcs = ["patch_whl_patch_test.py"], + target_compatible_with = SUPPORTS_BZLMOD, + deps = [ + "@patch_whl_pkg//:pkg", + ], +) diff --git a/tests/pypi/patch_whl/patch_whl_patch_test.py b/tests/pypi/patch_whl/patch_whl_patch_test.py new file mode 100644 index 0000000000..56dfe1aa55 --- /dev/null +++ b/tests/pypi/patch_whl/patch_whl_patch_test.py @@ -0,0 +1,17 @@ +"""Test that wheel patching works end-to-end.""" + +import unittest + +import pkg + + +class PatchWhlTest(unittest.TestCase): + def test_patched(self): + self.assertEqual(pkg.PATCHED, True) + + def test_data_unchanged(self): + self.assertEqual(pkg.DATA, "hello") + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/pypi/patch_whl/patch_whl_tests.bzl b/tests/pypi/patch_whl/patch_whl_tests.bzl index f93fe459c9..5010f03dc0 100644 --- a/tests/pypi/patch_whl/patch_whl_tests.bzl +++ b/tests/pypi/patch_whl/patch_whl_tests.bzl @@ -15,7 +15,7 @@ "" load("@rules_testing//lib:test_suite.bzl", "test_suite") -load("//python/private/pypi:patch_whl.bzl", "patched_whl_name") # buildifier: disable=bzl-visibility +load("//python/private/pypi:patch_whl.bzl", "fix_record_content", "patched_whl_name") # buildifier: disable=bzl-visibility _tests = [] @@ -31,6 +31,104 @@ def _test_simple_local_version(env): _tests.append(_test_simple_local_version) +def _test_fix_record_adds_missing(env): + record = """\ +foo/__init__.py,sha256=abc,123 +foo-1.0.dist-info/RECORD,, +""" + all_files = { + "foo-1.0.dist-info/METADATA": True, + "foo-1.0.dist-info/RECORD": True, + "foo/__init__.py": True, + "foo/bar.py": True, + } + result = fix_record_content( + record_content = record, + all_files = all_files, + record_rel = "foo-1.0.dist-info/RECORD", + ) + env.expect.that_str(result).contains("foo/bar.py,sha256=0,0") + env.expect.that_str(result).contains("foo-1.0.dist-info/METADATA,sha256=0,0") + +_tests.append(_test_fix_record_adds_missing) + +def _test_fix_record_no_missing(env): + record = """\ +foo/__init__.py,sha256=abc,123 +foo/bar.py,sha256=def,456 +foo-1.0.dist-info/RECORD,, +""" + all_files = { + "foo-1.0.dist-info/RECORD": True, + "foo/__init__.py": True, + "foo/bar.py": True, + } + result = fix_record_content( + record_content = record, + all_files = all_files, + record_rel = "foo-1.0.dist-info/RECORD", + ) + env.expect.that_bool(result == None).equals(True) + +_tests.append(_test_fix_record_no_missing) + +def _test_fix_record_preserves_quoting(env): + record = '''\ +"foo/__init__.py",sha256=abc,123 +"foo-1.0.dist-info/RECORD",, +''' + all_files = { + "foo-1.0.dist-info/RECORD": True, + "foo/__init__.py": True, + "foo/bar.py": True, + } + result = fix_record_content( + record_content = record, + all_files = all_files, + record_rel = "foo-1.0.dist-info/RECORD", + ) + env.expect.that_str(result).contains('"foo/bar.py",sha256=0,0') + +_tests.append(_test_fix_record_preserves_quoting) + +def _test_fix_record_skips_excluded(env): + record = """\ +foo/__init__.py,sha256=abc,123 +foo-1.0.dist-info/RECORD,, +""" + all_files = { + "foo-1.0.dist-info/INSTALLER": True, + "foo-1.0.dist-info/RECORD": True, + "foo/__init__.py": True, + } + result = fix_record_content( + record_content = record, + all_files = all_files, + record_rel = "foo-1.0.dist-info/RECORD", + ) + env.expect.that_bool(result == None).equals(True) + +_tests.append(_test_fix_record_skips_excluded) + +def _test_fix_record_skips_whl_files(env): + record = """\ +foo/__init__.py,sha256=abc,123 +foo-1.0.dist-info/RECORD,, +""" + all_files = { + "foo-1.0.dist-info/RECORD": True, + "foo-1.0.whl": True, + "foo/__init__.py": True, + } + result = fix_record_content( + record_content = record, + all_files = all_files, + record_rel = "foo-1.0.dist-info/RECORD", + ) + env.expect.that_bool(result == None).equals(True) + +_tests.append(_test_fix_record_skips_whl_files) + def patch_whl_test_suite(name): """Create the test suite. diff --git a/tests/pypi/patch_whl/testdata/BUILD.bazel b/tests/pypi/patch_whl/testdata/BUILD.bazel new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/pypi/patch_whl/testdata/patches/BUILD.bazel b/tests/pypi/patch_whl/testdata/patches/BUILD.bazel new file mode 100644 index 0000000000..85f0d05223 --- /dev/null +++ b/tests/pypi/patch_whl/testdata/patches/BUILD.bazel @@ -0,0 +1 @@ +exports_files(["modify_pkg.patch"]) diff --git a/tests/pypi/patch_whl/testdata/patches/modify_pkg.patch b/tests/pypi/patch_whl/testdata/patches/modify_pkg.patch new file mode 100644 index 0000000000..ba8cca54b5 --- /dev/null +++ b/tests/pypi/patch_whl/testdata/patches/modify_pkg.patch @@ -0,0 +1,6 @@ +--- pkg.py ++++ pkg.py +@@ -1,2 +1,2 @@ +-PATCHED = False ++PATCHED = True + DATA = "hello" diff --git a/tests/pypi/patch_whl/testdata/pkg/BUILD.bazel b/tests/pypi/patch_whl/testdata/pkg/BUILD.bazel new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/pypi/patch_whl/testdata/pkg/pkg-1.0.dist-info/METADATA b/tests/pypi/patch_whl/testdata/pkg/pkg-1.0.dist-info/METADATA new file mode 100644 index 0000000000..a102dd73d4 --- /dev/null +++ b/tests/pypi/patch_whl/testdata/pkg/pkg-1.0.dist-info/METADATA @@ -0,0 +1,2 @@ +Name: pkg +Version: 1.0 diff --git a/tests/pypi/patch_whl/testdata/pkg/pkg-1.0.dist-info/RECORD b/tests/pypi/patch_whl/testdata/pkg/pkg-1.0.dist-info/RECORD new file mode 100644 index 0000000000..dba333a1c1 --- /dev/null +++ b/tests/pypi/patch_whl/testdata/pkg/pkg-1.0.dist-info/RECORD @@ -0,0 +1,4 @@ +pkg.py,sha256=abc,123 +pkg-1.0.dist-info/METADATA,sha256=def,456 +pkg-1.0.dist-info/WHEEL,sha256=ghi,789 +pkg-1.0.dist-info/RECORD,, diff --git a/tests/pypi/patch_whl/testdata/pkg/pkg-1.0.dist-info/WHEEL b/tests/pypi/patch_whl/testdata/pkg/pkg-1.0.dist-info/WHEEL new file mode 100644 index 0000000000..a64521a1cc --- /dev/null +++ b/tests/pypi/patch_whl/testdata/pkg/pkg-1.0.dist-info/WHEEL @@ -0,0 +1 @@ +Wheel-Version: 1.0 diff --git a/tests/pypi/patch_whl/testdata/pkg/pkg.py b/tests/pypi/patch_whl/testdata/pkg/pkg.py new file mode 100644 index 0000000000..0985d5a3dd --- /dev/null +++ b/tests/pypi/patch_whl/testdata/pkg/pkg.py @@ -0,0 +1,2 @@ +PATCHED = False +DATA = "hello" diff --git a/tests/pypi/pypi_cache/pypi_cache_tests.bzl b/tests/pypi/pypi_cache/pypi_cache_tests.bzl index 14c12ae6d2..21c2f5bd8c 100644 --- a/tests/pypi/pypi_cache/pypi_cache_tests.bzl +++ b/tests/pypi/pypi_cache/pypi_cache_tests.bzl @@ -11,8 +11,8 @@ def _cache(env, **kwargs): cache = pypi_cache(**kwargs) attrs = { + "hashes_by_version": subjects.dict, "sdists": subjects.dict, - "sha256s_by_version": subjects.dict, "whls": subjects.dict, } @@ -48,14 +48,14 @@ def _test_memory_cache_hit(env): # Mocked parsed result from a PyPI-like index fake_result = struct( sdists = { - "sha_1": struct(version = "1.0.0", filename = "pkg-1.0.0.tar.gz"), + "sha256:sha_1": struct(version = "1.0.0", filename = "pkg-1.0.0.tar.gz"), }, whls = { - "sha_2": struct(version = "1.1.0", filename = "pkg-1.1.0-py3-none-any.whl"), + "sha256:sha_2": struct(version = "1.1.0", filename = "pkg-1.1.0-py3-none-any.whl"), }, - sha256s_by_version = { - "1.0.0": ["sha_1"], - "1.1.0": ["sha_2"], + hashes_by_version = { + "1.0.0": ["sha256:sha_1"], + "1.1.0": ["sha256:sha_2"], }, ) @@ -70,7 +70,7 @@ def _test_memory_cache_hit(env): got.sdists().contains_exactly(fake_result.sdists) got.whls().contains_exactly(fake_result.whls) - got.sha256s_by_version().contains_exactly(fake_result.sha256s_by_version) + got.hashes_by_version().contains_exactly(fake_result.hashes_by_version) # A different key with fewer versions key = ("https://{PYPI_INDEX_URL}/pkg", "https://pypi.org/simple/pkg", ["1.0.0"]) @@ -78,7 +78,7 @@ def _test_memory_cache_hit(env): got = cache.get(key) got.sdists().contains_exactly(fake_result.sdists) got.whls().contains_exactly({}) - got.sha256s_by_version().contains_exactly({"1.0.0": ["sha_1"]}) + got.hashes_by_version().contains_exactly({"1.0.0": ["sha256:sha_1"]}) # A key with no matches key = ("https://{PYPI_INDEX_URL}/pkg", "https://pypi.org/simple/pkg", ["1.2.0"]) @@ -94,31 +94,34 @@ def _test_pypi_cache_writes_to_facts(env): fake_result = struct( sdists = { - "sha_sdist": struct( + "sha256:sha_sdist": struct( version = "1.0.0", filename = "pkg-1.0.0.tar.gz", url = "https://pypi.org/files/pkg-1.0.0.tar.gz", + digest = "sha256:sha_sdist", yanked = "", ), }, whls = { - "sha_whl": struct( + "sha256:sha_whl": struct( version = "1.0.0", filename = "pkg-1.0.0-py3-none-any.whl", url = "https://pypi.org/files/pkg-1.0.0-py3-none-any.whl", + digest = "sha256:sha_whl", yanked = "Security issue", ), # This won't get stored - "sha_whl_2": struct( + "sha256:sha_whl_2": struct( version = "1.1.0", filename = "pkg-1.1.0-py3-none-any.whl", url = "https://pypi.org/files/pkg-1.1.0-py3-none-any.whl", + digest = "sha256:sha_whl_2", yanked = None, ), }, - sha256s_by_version = { - "1.0.0": ["sha_sdist", "sha_whl"], - "1.1.0": ["sha_whl_2"], + hashes_by_version = { + "1.0.0": ["sha256:sha_sdist", "sha256:sha_whl"], + "1.1.0": ["sha256:sha_whl_2"], }, ) @@ -130,11 +133,11 @@ def _test_pypi_cache_writes_to_facts(env): # Then the key returns us the same items got = cache.get(key) got.whls().contains_exactly({ - "sha_whl": fake_result.whls["sha_whl"], + "sha256:sha_whl": fake_result.whls["sha256:sha_whl"], }) got.sdists().contains_exactly(fake_result.sdists) - got.sha256s_by_version().contains_exactly({ - "1.0.0": fake_result.sha256s_by_version["1.0.0"], + got.hashes_by_version().contains_exactly({ + "1.0.0": fake_result.hashes_by_version["1.0.0"], }) # Then when we get facts at the end @@ -143,30 +146,30 @@ def _test_pypi_cache_writes_to_facts(env): # We are not using the real index URL, because we may have credentials in here "https://{PYPI_INDEX_URL}": { "pkg": { - "https://pypi.org/files/pkg-1.0.0-py3-none-any.whl": "sha_whl", - "https://pypi.org/files/pkg-1.0.0.tar.gz": "sha_sdist", + "https://pypi.org/files/pkg-1.0.0-py3-none-any.whl": "sha256:sha_whl", + "https://pypi.org/files/pkg-1.0.0.tar.gz": "sha256:sha_sdist", }, }, }, "dist_yanked": { "https://{PYPI_INDEX_URL}": { "pkg": { - "sha_sdist": "", - "sha_whl": "Security issue", + "sha256:sha_sdist": "", + "sha256:sha_whl": "Security issue", }, }, }, - "fact_version": "v1", # Facts version + "fact_version": "v2", # Facts version }) # When we get the other items cached in memory, they get written to facts got = cache.get((key[0], key[1], ["1.1.0"])) got.whls().contains_exactly({ - "sha_whl_2": fake_result.whls["sha_whl_2"], + "sha256:sha_whl_2": fake_result.whls["sha256:sha_whl_2"], }) got.sdists().contains_exactly({}) - got.sha256s_by_version().contains_exactly({ - "1.1.0": fake_result.sha256s_by_version["1.1.0"], + got.hashes_by_version().contains_exactly({ + "1.1.0": fake_result.hashes_by_version["1.1.0"], }) # Then when we get facts at the end @@ -175,21 +178,21 @@ def _test_pypi_cache_writes_to_facts(env): # We are not using the real index URL, because we may have credentials in here "https://{PYPI_INDEX_URL}": { "pkg": { - "https://pypi.org/files/pkg-1.0.0-py3-none-any.whl": "sha_whl", - "https://pypi.org/files/pkg-1.0.0.tar.gz": "sha_sdist", - "https://pypi.org/files/pkg-1.1.0-py3-none-any.whl": "sha_whl_2", + "https://pypi.org/files/pkg-1.0.0-py3-none-any.whl": "sha256:sha_whl", + "https://pypi.org/files/pkg-1.0.0.tar.gz": "sha256:sha_sdist", + "https://pypi.org/files/pkg-1.1.0-py3-none-any.whl": "sha256:sha_whl_2", }, }, }, "dist_yanked": { "https://{PYPI_INDEX_URL}": { "pkg": { - "sha_sdist": "", - "sha_whl": "Security issue", + "sha256:sha_sdist": "", + "sha256:sha_whl": "Security issue", }, }, }, - "fact_version": "v1", # Facts version + "fact_version": "v2", # Facts version }) _tests.append(_test_pypi_cache_writes_to_facts) @@ -201,20 +204,20 @@ def _test_pypi_cache_reads_from_facts(env): # We are not using the real index URL, because we may have credentials in here "https://{PYPI_INDEX_URL}": { "pkg": { - "https://pypi.org/files/pkg-1.0.0-py3-none-any.whl": "sha_whl", - "https://pypi.org/files/pkg-1.0.0.tar.gz": "sha_sdist", + "https://pypi.org/files/pkg-1.0.0-py3-none-any.whl": "sha256:sha_whl", + "https://pypi.org/files/pkg-1.0.0.tar.gz": "sha256:sha_sdist", }, }, }, "dist_yanked": { "https://{PYPI_INDEX_URL}": { "pkg": { - "sha_sdist": "", - "sha_whl": "Security issue", + "sha256:sha_sdist": "", + "sha256:sha_whl": "Security issue", }, }, }, - "fact_version": "v1", # Facts version + "fact_version": "v2", # Facts version }) cache = _cache(env, mctx = mock_ctx) @@ -229,8 +232,8 @@ def _test_pypi_cache_reads_from_facts(env): expected_result = struct( sdists = { - "sha_sdist": struct( - sha256 = "sha_sdist", + "sha256:sha_sdist": struct( + digest = "sha256:sha_sdist", version = "1.0.0", filename = "pkg-1.0.0.tar.gz", metadata_url = "", @@ -240,8 +243,8 @@ def _test_pypi_cache_reads_from_facts(env): ), }, whls = { - "sha_whl": struct( - sha256 = "sha_whl", + "sha256:sha_whl": struct( + digest = "sha256:sha_whl", version = "1.0.0", filename = "pkg-1.0.0-py3-none-any.whl", url = "https://pypi.org/files/pkg-1.0.0-py3-none-any.whl", @@ -250,14 +253,14 @@ def _test_pypi_cache_reads_from_facts(env): yanked = "Security issue", ), }, - sha256s_by_version = { - "1.0.0": ["sha_sdist", "sha_whl"], + hashes_by_version = { + "1.0.0": ["sha256:sha_sdist", "sha256:sha_whl"], }, ) got.whls().contains_exactly(expected_result.whls) got.sdists().contains_exactly(expected_result.sdists) - got.sha256s_by_version().contains_exactly(expected_result.sha256s_by_version) + got.hashes_by_version().contains_exactly(expected_result.hashes_by_version) # Then when we store the same facts back again, because we accessed the cached keys. cache.get_facts().contains_exactly(mock_ctx.facts) @@ -275,14 +278,14 @@ def _test_pypi_cache_reads_from_facts_drops_unaccessed_dists(env): "dist_hashes": { "https://{PYPI_INDEX_URL}": { "pkg": { - "https://pypi.org/files/pkg-1.0.0-py3-none-any.whl": "sha_whl_1.0.0", - "https://pypi.org/files/pkg-1.0.0.tar.gz": "sha_sdist_1.0.0", - "https://pypi.org/files/pkg-1.1.0-py3-none-any.whl": "sha_whl_1.1.0", - "https://pypi.org/files/pkg-1.1.0.tar.gz": "sha_sdist_1.1.0", + "https://pypi.org/files/pkg-1.0.0-py3-none-any.whl": "sha256:sha_whl_1.0.0", + "https://pypi.org/files/pkg-1.0.0.tar.gz": "sha256:sha_sdist_1.0.0", + "https://pypi.org/files/pkg-1.1.0-py3-none-any.whl": "sha256:sha_whl_1.1.0", + "https://pypi.org/files/pkg-1.1.0.tar.gz": "sha256:sha_sdist_1.1.0", }, }, }, - "fact_version": "v1", + "fact_version": "v2", }) cache = _cache(env, mctx = mock_ctx) @@ -292,8 +295,8 @@ def _test_pypi_cache_reads_from_facts_drops_unaccessed_dists(env): expected = struct( sdists = { - "sha_sdist_1.0.0": struct( - sha256 = "sha_sdist_1.0.0", + "sha256:sha_sdist_1.0.0": struct( + digest = "sha256:sha_sdist_1.0.0", version = "1.0.0", filename = "pkg-1.0.0.tar.gz", metadata_url = "", @@ -303,8 +306,8 @@ def _test_pypi_cache_reads_from_facts_drops_unaccessed_dists(env): ), }, whls = { - "sha_whl_1.0.0": struct( - sha256 = "sha_whl_1.0.0", + "sha256:sha_whl_1.0.0": struct( + digest = "sha256:sha_whl_1.0.0", version = "1.0.0", filename = "pkg-1.0.0-py3-none-any.whl", metadata_url = "", @@ -313,29 +316,93 @@ def _test_pypi_cache_reads_from_facts_drops_unaccessed_dists(env): yanked = None, ), }, - sha256s_by_version = { - "1.0.0": ["sha_sdist_1.0.0", "sha_whl_1.0.0"], + hashes_by_version = { + "1.0.0": ["sha256:sha_sdist_1.0.0", "sha256:sha_whl_1.0.0"], }, ) got.whls().contains_exactly(expected.whls) got.sdists().contains_exactly(expected.sdists) - got.sha256s_by_version().contains_exactly(expected.sha256s_by_version) + got.hashes_by_version().contains_exactly(expected.hashes_by_version) # get_facts() must only contain version 1.0.0 data; 1.1.0 is dropped cache.get_facts().contains_exactly({ "dist_hashes": { "https://{PYPI_INDEX_URL}": { "pkg": { - "https://pypi.org/files/pkg-1.0.0-py3-none-any.whl": "sha_whl_1.0.0", - "https://pypi.org/files/pkg-1.0.0.tar.gz": "sha_sdist_1.0.0", + "https://pypi.org/files/pkg-1.0.0-py3-none-any.whl": "sha256:sha_whl_1.0.0", + "https://pypi.org/files/pkg-1.0.0.tar.gz": "sha256:sha_sdist_1.0.0", }, }, }, - "fact_version": "v1", + "fact_version": "v2", }) _tests.append(_test_pypi_cache_reads_from_facts_drops_unaccessed_dists) +def _test_pypi_cache_facts_non_sha256_round_trip(env): + """Verifies that digests of other hash algorithms survive the facts round trip.""" + mock_ctx = mocks.mctx(facts = {}) + cache = _cache(env, mctx = mock_ctx) + + fake_result = struct( + sdists = {}, + whls = { + "sha512:whl_digest": struct( + version = "1.0.0", + filename = "pkg-1.0.0-py3-none-any.whl", + url = "https://pypi.org/files/pkg-1.0.0-py3-none-any.whl", + digest = "sha512:whl_digest", + yanked = None, + ), + }, + hashes_by_version = { + "1.0.0": ["sha512:whl_digest"], + }, + ) + + key = ("https://{PYPI_INDEX_URL}/pkg/", "https://pypi.org/simple/pkg/", ["1.0.0"]) + cache.setdefault(key, fake_result) + + # The hash algorithm is stored in the facts + cache.get_facts().contains_exactly({ + "dist_hashes": { + "https://{PYPI_INDEX_URL}": { + "pkg": { + "https://pypi.org/files/pkg-1.0.0-py3-none-any.whl": "sha512:whl_digest", + }, + }, + }, + "fact_version": "v2", # Facts version + }) + + # And a cache that only has the facts reconstructs the digests + cache = _cache(env, mctx = mocks.mctx(facts = { + "dist_hashes": { + "https://{PYPI_INDEX_URL}": { + "pkg": { + "https://pypi.org/files/pkg-1.0.0-py3-none-any.whl": "sha512:whl_digest", + }, + }, + }, + "fact_version": "v2", # Facts version + })) + + got = cache.get(key) + got.whls().contains_exactly({ + "sha512:whl_digest": struct( + digest = "sha512:whl_digest", + version = "1.0.0", + filename = "pkg-1.0.0-py3-none-any.whl", + metadata_url = "", + metadata_sha256 = "", + url = "https://pypi.org/files/pkg-1.0.0-py3-none-any.whl", + yanked = None, + ), + }) + got.hashes_by_version().contains_exactly({"1.0.0": ["sha512:whl_digest"]}) + +_tests.append(_test_pypi_cache_facts_non_sha256_round_trip) + def _test_memory_cache_index_urls(env): """Verifies that the cache returns stored values for index_urls.""" store = {} @@ -377,7 +444,7 @@ def _test_pypi_cache_writes_index_urls_to_facts(env): cache.setdefault(key, fake_result) cache.get_facts().contains_exactly({ - "fact_version": "v1", + "fact_version": "v2", "index_urls": { "https://pypi.org/simple/": { "pkg-a": "https://pypi.org/simple/pkg-a/", @@ -389,7 +456,7 @@ def _test_pypi_cache_writes_index_urls_to_facts(env): cache.setdefault(key, fake_result) cache.get_facts().contains_exactly({ - "fact_version": "v1", + "fact_version": "v2", "index_urls": { "https://pypi.org/simple/": { "pkg-a": "https://pypi.org/simple/pkg-a/", @@ -403,7 +470,7 @@ _tests.append(_test_pypi_cache_writes_index_urls_to_facts) def _test_pypi_cache_reads_index_urls_from_facts(env): """Verifies that reading index_urls from facts works correctly.""" mock_ctx = mocks.mctx(facts = { - "fact_version": "v1", + "fact_version": "v2", "index_urls": { "https://pypi.org/simple/": { "pkg-a": "https://pypi.org/simple/pkg-a/", @@ -434,7 +501,7 @@ _tests.append(_test_pypi_cache_reads_index_urls_from_facts) def _test_pypi_cache_reads_index_urls_from_facts_incomplete(env): """Verifies that incomplete index_urls facts returns None (forces fresh download).""" mock_ctx = mocks.mctx(facts = { - "fact_version": "v1", + "fact_version": "v2", "index_urls": { "https://pypi.org/simple/": { "pkg-a": "https://pypi.org/simple/pkg-a/", @@ -457,7 +524,7 @@ def _test_pypi_cache_reads_index_urls_from_facts_drops_unaccessed(env): removed from all requirements files) get cleaned up from the lockfile. """ mock_ctx = mocks.mctx(facts = { - "fact_version": "v1", + "fact_version": "v2", "index_urls": { "https://pypi.org/simple/": { "pkg-a": "https://pypi.org/simple/pkg-a/", @@ -478,7 +545,7 @@ def _test_pypi_cache_reads_index_urls_from_facts_drops_unaccessed(env): # get_facts() must only return the requested (accessed) subset; pkg-c is dropped cache.get_facts().contains_exactly({ - "fact_version": "v1", + "fact_version": "v2", "index_urls": { "https://pypi.org/simple/": { "pkg-a": "https://pypi.org/simple/pkg-a/", diff --git a/tests/pypi/repack_whl/BUILD.bazel b/tests/pypi/repack_whl/BUILD.bazel deleted file mode 100644 index 3f611a2e4f..0000000000 --- a/tests/pypi/repack_whl/BUILD.bazel +++ /dev/null @@ -1,8 +0,0 @@ -load("//python:py_test.bzl", "py_test") - -py_test( - name = "repack_whl_test", - size = "small", - srcs = ["repack_whl_test.py"], - deps = ["//python/private/pypi:repack_whl"], -) diff --git a/tests/pypi/repack_whl/repack_whl_test.py b/tests/pypi/repack_whl/repack_whl_test.py deleted file mode 100644 index 50781cc0e6..0000000000 --- a/tests/pypi/repack_whl/repack_whl_test.py +++ /dev/null @@ -1,37 +0,0 @@ -import unittest - -from python.private.pypi import repack_whl - - -class HasAllQuotedFilenamesTest(unittest.TestCase): - """Tests for _has_all_quoted_filenames detection logic.""" - - def test_all_quoted(self) -> None: - """Returns True when all lines start with quotes (torch-style).""" - record = """\ -"torch/__init__.py",sha256=abc,123 -"torch/utils.py",sha256=def,456 -"torch-2.0.0.dist-info/WHEEL",sha256=ghi,789 -""" - self.assertTrue(repack_whl._has_all_quoted_filenames(record)) - - def test_none_quoted(self) -> None: - """Returns False when no lines are quoted (standard style).""" - record = """\ -torch/__init__.py,sha256=abc,123 -torch/utils.py,sha256=def,456 -torch-2.0.0.dist-info/WHEEL,sha256=ghi,789 -""" - self.assertFalse(repack_whl._has_all_quoted_filenames(record)) - - def test_mixed_quoting(self) -> None: - """Returns False when only some lines are quoted.""" - record = """\ -"file,with,commas.py",sha256=abc,123 -normal_file.py,sha256=def,456 -""" - self.assertFalse(repack_whl._has_all_quoted_filenames(record)) - - -if __name__ == "__main__": - unittest.main() diff --git a/tests/pypi/requirements_files_by_platform/requirements_files_by_platform_tests.bzl b/tests/pypi/requirements_files_by_platform/requirements_files_by_platform_tests.bzl index b1176e6a15..7f4260081b 100644 --- a/tests/pypi/requirements_files_by_platform/requirements_files_by_platform_tests.bzl +++ b/tests/pypi/requirements_files_by_platform/requirements_files_by_platform_tests.bzl @@ -43,7 +43,7 @@ def _test_fail_no_requirements(env): fail_fn = errors.append, ) env.expect.that_str(errors[0]).equals("""\ -A 'requirements_lock' attribute must be specified, a platform-specific lockfiles via 'requirements_by_platform' or an os-specific lockfiles must be specified via 'requirements_*' attributes""") +A 'requirements_lock' or 'uv_lock' attribute must be specified, a platform-specific lockfiles via 'requirements_by_platform' or an os-specific lockfiles must be specified via 'requirements_*' attributes""") _tests.append(_test_fail_no_requirements) @@ -319,6 +319,25 @@ def _test_host_only_os_with_fallback(env): _tests.append(_test_host_only_os_with_fallback) +def _test_uv_lock_only(env): + """Verify that using only uv_lock without requirements_lock succeeds.""" + got = requirements_files_by_platform( + uv_lock = "uv.lock", + ) + env.expect.that_dict(got).contains_exactly({}) + +_tests.append(_test_uv_lock_only) + +def _test_uv_lock_with_platform_arg(env): + """Verify that using uv_lock with --platform argument succeeds.""" + got = requirements_files_by_platform( + uv_lock = "uv.lock", + extra_pip_args = ["--platform", "linux_x86_64"], + ) + env.expect.that_dict(got).contains_exactly({}) + +_tests.append(_test_uv_lock_with_platform_arg) + def requirements_files_by_platform_test_suite(name): """Create the test suite. diff --git a/tests/pypi/simpleapi_download/simpleapi_download_tests.bzl b/tests/pypi/simpleapi_download/simpleapi_download_tests.bzl index 4e86b76e10..dfa33ebe65 100644 --- a/tests/pypi/simpleapi_download/simpleapi_download_tests.bzl +++ b/tests/pypi/simpleapi_download/simpleapi_download_tests.bzl @@ -43,7 +43,7 @@ def _test_simple(env): output = struct( sdists = {"deadbeef": url.strip("/").split("/")[-1]}, whls = {"deadb33f": url.strip("/").split("/")[-1]}, - sha256s_by_version = {"fizz": url.strip("/").split("/")[-1]}, + hashes_by_version = {"fizz": url.strip("/").split("/")[-1]}, ), success = True, ) @@ -71,19 +71,19 @@ def _test_simple(env): "bar": struct( index_url = "https://main.com/bar/", sdists = {"deadbeef": "bar"}, - sha256s_by_version = {"fizz": "bar"}, + hashes_by_version = {"fizz": "bar"}, whls = {"deadb33f": "bar"}, ), "baz": struct( index_url = "https://main.com/baz/", sdists = {"deadbeef": "baz"}, - sha256s_by_version = {"fizz": "baz"}, + hashes_by_version = {"fizz": "baz"}, whls = {"deadb33f": "baz"}, ), "foo": struct( index_url = "https://extra.com/foo/", sdists = {"deadbeef": "foo"}, - sha256s_by_version = {"fizz": "foo"}, + hashes_by_version = {"fizz": "foo"}, whls = {"deadb33f": "foo"}, ), }) @@ -115,7 +115,7 @@ def _test_index_overrides(env): output = struct( sdists = {"deadbeef": url.strip("/").split("/")[-1]}, whls = {"deadb33f": url.strip("/").split("/")[-1]}, - sha256s_by_version = {"fizz": url.strip("/").split("/")[-1]}, + hashes_by_version = {"fizz": url.strip("/").split("/")[-1]}, ), success = True, ) @@ -147,19 +147,19 @@ def _test_index_overrides(env): "ba_z": struct( index_url = "https://main.com/ba-z/", sdists = {"deadbeef": "ba-z"}, - sha256s_by_version = {"fizz": "ba-z"}, + hashes_by_version = {"fizz": "ba-z"}, whls = {"deadb33f": "ba-z"}, ), "bar": struct( index_url = "https://main.com/bar/", sdists = {"deadbeef": "bar"}, - sha256s_by_version = {"fizz": "bar"}, + hashes_by_version = {"fizz": "bar"}, whls = {"deadb33f": "bar"}, ), "foo": struct( index_url = "https://extra.com/foo/", sdists = {"deadbeef": "foo"}, - sha256s_by_version = {"fizz": "foo"}, + hashes_by_version = {"fizz": "foo"}, whls = {"deadb33f": "foo"}, ), }) diff --git a/tests/pypi/urllib/urllib_tests.bzl b/tests/pypi/urllib/urllib_tests.bzl index 40c48dc854..1a175dc120 100644 --- a/tests/pypi/urllib/urllib_tests.bzl +++ b/tests/pypi/urllib/urllib_tests.bzl @@ -8,7 +8,8 @@ _tests = [] def _test_absolute_url(env): # Already absolute for already_absolute in [ - "file://foo", + "file:///foo", + "file:///c:/foo", "https://foo.com", "http://foo.com", ]: @@ -25,7 +26,13 @@ def _test_absolute_url(env): env.expect.that_str(urllib.absolute_url("https://example.com/relative/", "../relative/file.whl")).equals("https://example.com/relative/file.whl") # Relative URL for files - env.expect.that_str(urllib.absolute_url("file://{PYPI_BAZEL_WORKSPACE_ROOT}", "vendor/distro/file.whl")).equals("file://{PYPI_BAZEL_WORKSPACE_ROOT}/vendor/distro/file.whl") + env.expect.that_str(urllib.absolute_url("file://${PYPI_BAZEL_WORKSPACE_ROOT}", "vendor/distro/file.whl")).equals("file://${PYPI_BAZEL_WORKSPACE_ROOT}/vendor/distro/file.whl") + env.expect.that_str(urllib.absolute_url( + "file://${PYPI_BAZEL_WORKSPACE_ROOT}", + "vendor/distro/file.whl", + envsubst = ["PYPI_BAZEL_WORKSPACE_ROOT"], + getenv = {"PYPI_BAZEL_WORKSPACE_ROOT": "/some/dir"}.get, + )).equals("file:///some/dir/vendor/distro/file.whl") _tests.append(_test_absolute_url) @@ -36,6 +43,7 @@ def _test_strip_empty_path_segments(env): env.expect.that_str(urllib.strip_empty_path_segments("scheme://with///multiple//empty/segments")).equals("scheme://with/multiple/empty/segments") env.expect.that_str(urllib.strip_empty_path_segments("scheme://with//trailing/slash/")).equals("scheme://with/trailing/slash/") env.expect.that_str(urllib.strip_empty_path_segments("scheme://with/trailing/slashes///")).equals("scheme://with/trailing/slashes/") + env.expect.that_str(urllib.strip_empty_path_segments("file:///home/user//foo")).equals("file:///home/user/foo") _tests.append(_test_strip_empty_path_segments) diff --git a/tests/pypi/whl_extract/BUILD.bazel b/tests/pypi/whl_extract/BUILD.bazel new file mode 100644 index 0000000000..3983477f2b --- /dev/null +++ b/tests/pypi/whl_extract/BUILD.bazel @@ -0,0 +1,11 @@ +load("@rules_shell//shell:sh_test.bzl", "sh_test") +load(":whl_extract_tests.bzl", "whl_extract_test_suite") + +whl_extract_test_suite(name = "whl_extract_tests") + +sh_test( + name = "wheel_record_rewriter_test", + srcs = ["wheel_record_rewriter_test.sh"], + args = ["$(location //python/private/pypi:wheel_record_rewriter)"], + data = ["//python/private/pypi:wheel_record_rewriter"], +) diff --git a/tests/pypi/whl_extract/wheel_record_rewriter_test.sh b/tests/pypi/whl_extract/wheel_record_rewriter_test.sh new file mode 100755 index 0000000000..c5880520ad --- /dev/null +++ b/tests/pypi/whl_extract/wheel_record_rewriter_test.sh @@ -0,0 +1,91 @@ +#!/bin/sh +set -eu + +REWRITER="$1" +TMP_DIR="$(mktemp -d)" +trap 'rm -rf "$TMP_DIR"' EXIT + +run_rewriter() { + case "$REWRITER" in + *.ps1) + in_file="$1" + out_file="$2" + platform_type="$3" + data_dir="$4" + if command -v cygpath >/dev/null 2>&1; then + in_file="$(cygpath -w "$in_file")" + out_file="$(cygpath -w "$out_file")" + fi + powershell.exe -ExecutionPolicy Bypass -NoProfile -File "$REWRITER" "$in_file" "$out_file" "$platform_type" "$data_dir" + ;; + *) + "$REWRITER" "$@" + ;; + esac +} + +INPUT="$TMP_DIR/input_RECORD" +cat <<'EOF' > "$INPUT" +foo-1.0.data/purelib/pkg/__init__.py,sha256=abc,100 +foo-1.0.data/purelib/pkg/module.py,sha256=def,200 +foo-1.0.data/platlib/pkg/_ext.so,sha256=ghi,300 +foo-1.0.data/data/pkg/data.txt,sha256=111,10 +foo-1.0.data/headers/pkg/header.h,sha256=222,20 +foo-1.0.data/scripts/my_script.sh,sha256=333,30 +"foo-1.0.data/purelib/pkg/my file.py",sha256=abc,100 +"foo-1.0.data/scripts/my tool",sha256=def,200 +"foo-1.0.data/headers/my header.h",sha256=ghi,300 +"foo-1.0.data/data/my data.txt",sha256=jkl,400 +foo-1.0.data/custom_dir/custom.txt,sha256=xyz,123 +top_level/__init__.py,sha256=aaa,50 +foo-1.0.dist-info/METADATA,sha256=bbb,60 +foo-1.0.dist-info/RECORD,, +EOF + +# Test Unix rewrite +UNIX_OUT="$TMP_DIR/unix_RECORD" +run_rewriter "$INPUT" "$UNIX_OUT" "unix" "foo-1.0.data" + +EXPECTED_UNIX="$TMP_DIR/expected_unix" +cat <<'EOF' > "$EXPECTED_UNIX" +pkg/__init__.py,sha256=abc,100 +pkg/module.py,sha256=def,200 +pkg/_ext.so,sha256=ghi,300 +../../../pkg/data.txt,sha256=111,10 +../../../include/pkg/header.h,sha256=222,20 +../../../bin/my_script.sh,sha256=333,30 +"pkg/my file.py",sha256=abc,100 +"../../../bin/my tool",sha256=def,200 +"../../../include/my header.h",sha256=ghi,300 +"../../../my data.txt",sha256=jkl,400 +foo-1.0.data/custom_dir/custom.txt,sha256=xyz,123 +top_level/__init__.py,sha256=aaa,50 +foo-1.0.dist-info/METADATA,sha256=bbb,60 +foo-1.0.dist-info/RECORD,, +EOF + +diff -u --strip-trailing-cr "$EXPECTED_UNIX" "$UNIX_OUT" + +# Test Windows rewrite +WIN_OUT="$TMP_DIR/win_RECORD" +run_rewriter "$INPUT" "$WIN_OUT" "windows" "foo-1.0.data" + +EXPECTED_WIN="$TMP_DIR/expected_win" +cat <<'EOF' > "$EXPECTED_WIN" +pkg/__init__.py,sha256=abc,100 +pkg/module.py,sha256=def,200 +pkg/_ext.so,sha256=ghi,300 +../../pkg/data.txt,sha256=111,10 +../../Include/pkg/header.h,sha256=222,20 +../../Scripts/my_script.sh,sha256=333,30 +"pkg/my file.py",sha256=abc,100 +"../../Scripts/my tool",sha256=def,200 +"../../Include/my header.h",sha256=ghi,300 +"../../my data.txt",sha256=jkl,400 +foo-1.0.data/custom_dir/custom.txt,sha256=xyz,123 +top_level/__init__.py,sha256=aaa,50 +foo-1.0.dist-info/METADATA,sha256=bbb,60 +foo-1.0.dist-info/RECORD,, +EOF + +diff -u --strip-trailing-cr "$EXPECTED_WIN" "$WIN_OUT" diff --git a/tests/pypi/whl_extract/whl_extract_tests.bzl b/tests/pypi/whl_extract/whl_extract_tests.bzl new file mode 100644 index 0000000000..f7e1e6d2cf --- /dev/null +++ b/tests/pypi/whl_extract/whl_extract_tests.bzl @@ -0,0 +1,119 @@ +"""Tests for whl_extract and gen_wheel_record.""" + +load("@rules_testing//lib:analysis_test.bzl", "analysis_test") +load("@rules_testing//lib:test_suite.bzl", "test_suite") +load("@rules_testing//lib:util.bzl", rt_util = "util") +load( + "//python/private/pypi:gen_wheel_record.bzl", # buildifier: disable=bzl-visibility + "gen_wheel_record", +) +load( + "//tests/support/platforms:platforms.bzl", # buildifier: disable=bzl-visibility + "platform_targets", +) + +_tests = [] + +def _test_gen_wheel_record(name): + rt_util.helper_target( + native.genrule, + name = name + "_src", + outs = [name + "_orig/alpha-1.0.dist-info/RECORD"], + cmd = "echo 'alpha-1.0.data/scripts/foo.sh' > $@", + ) + rt_util.helper_target( + gen_wheel_record, + name = name + "_subject", + srcs = [":" + name + "_src"], + ) + analysis_test( + name = name, + target = name + "_subject", + impl = _test_gen_wheel_record_impl, + ) + +_tests.append(_test_gen_wheel_record) + +def _test_gen_wheel_record_impl(env, target): + files = target[DefaultInfo].files.to_list() + env.expect.that_collection(files).has_size(1) + env.expect.that_str(files[0].short_path).contains( + "site-packages/alpha-1.0.dist-info/RECORD", + ) + +def _test_gen_wheel_record_windows(name): + rt_util.helper_target( + native.genrule, + name = name + "_src", + outs = [name + "_orig/beta-1.0.dist-info/RECORD"], + cmd = "echo 'beta-1.0.data/scripts/foo.sh' > $@", + ) + rt_util.helper_target( + gen_wheel_record, + name = name + "_subject", + srcs = [":" + name + "_src"], + ) + analysis_test( + name = name, + target = name + "_subject", + config_settings = { + "//command_line_option:platforms": [ + platform_targets.WINDOWS_X86_64, + ], + }, + impl = _test_gen_wheel_record_windows_impl, + ) + +_tests.append(_test_gen_wheel_record_windows) + +def _test_gen_wheel_record_windows_impl(env, target): + files = target[DefaultInfo].files.to_list() + env.expect.that_collection(files).has_size(1) + env.expect.that_str(files[0].short_path).contains( + "site-packages/beta-1.0.dist-info/RECORD", + ) + +def _test_gen_wheel_record_multiple_srcs(name): + rt_util.helper_target( + native.genrule, + name = name + "_src1", + outs = [name + "_orig1/gamma-1.0.dist-info/RECORD"], + cmd = "echo 'gamma-1.0.data/scripts/foo.sh' > $@", + ) + rt_util.helper_target( + native.genrule, + name = name + "_src2", + outs = [name + "_orig2/delta-2.0.dist-info/RECORD"], + cmd = "echo 'delta-2.0.data/scripts/bar.sh' > $@", + ) + rt_util.helper_target( + gen_wheel_record, + name = name + "_subject", + srcs = [":" + name + "_src1", ":" + name + "_src2"], + ) + analysis_test( + name = name, + target = name + "_subject", + impl = _test_gen_wheel_record_multiple_srcs_impl, + ) + +_tests.append(_test_gen_wheel_record_multiple_srcs) + +def _test_gen_wheel_record_multiple_srcs_impl(env, target): + files = target[DefaultInfo].files.to_list() + env.expect.that_collection(files).has_size(2) + paths = [f.short_path for f in files] + env.expect.that_bool( + any(["site-packages/gamma-1.0.dist-info/RECORD" in p for p in paths]), + ).equals(True) + env.expect.that_bool( + any(["site-packages/delta-2.0.dist-info/RECORD" in p for p in paths]), + ).equals(True) + +def whl_extract_test_suite(name): + """Create the test suite. + + Args: + name: the name of the test suite + """ + test_suite(name = name, tests = _tests) diff --git a/tests/pypi/whl_library_targets/whl_library_targets_tests.bzl b/tests/pypi/whl_library_targets/whl_library_targets_tests.bzl index 91db15f296..3fe1b99768 100644 --- a/tests/pypi/whl_library_targets/whl_library_targets_tests.bzl +++ b/tests/pypi/whl_library_targets/whl_library_targets_tests.bzl @@ -17,8 +17,8 @@ load("@rules_testing//lib:test_suite.bzl", "test_suite") load( "//python/private/pypi:whl_library_targets.bzl", - "whl_library_targets", - "whl_library_targets_from_requires", + "whl_library_deps_targets", + "whl_library_srcs", ) # buildifier: disable=bzl-visibility load("//tests/support/mocks:mocks.bzl", "mocks") @@ -30,19 +30,19 @@ def _test_filegroups(env): def glob(include, *, exclude = [], allow_empty): _ = exclude # @unused env.expect.that_bool(allow_empty).equals(True) - if include == ["rewrite-bin/*"] or include == ["bin/*"]: + if include in [["rewrite-bin/*"], ["bin/*"], ["rewrite-record/*/RECORD"]]: return [] return include - whl_library_targets( + whl_library_srcs( name = "", - dep_template = "", native = struct( filegroup = lambda **kwargs: calls.append(kwargs), glob = glob, ), rules = struct( venv_rewrite_shebang = lambda **kwargs: None, + gen_wheel_record = lambda **kwargs: None, ), ) @@ -63,81 +63,19 @@ def _test_filegroups(env): "visibility": ["//visibility:public"], }, { - "name": "whl", + "name": "whl_file", "srcs": [""], - "data": [], "visibility": ["//visibility:public"], }, ]) # buildifier: @unsorted-dict-items _tests.append(_test_filegroups) -def _test_platforms(env): - calls = [] - - whl_library_targets( - name = "", - dep_template = None, - dependencies_by_platform = { - "@//python/config_settings:is_python_3.9": ["py39_dep"], - "@platforms//cpu:aarch64": ["arm_dep"], - "@platforms//os:windows": ["win_dep"], - "cp310.11_linux_ppc64le": ["full_version_dep"], - "cp310_linux_ppc64le": ["py310_linux_ppc64le_dep"], - "linux_x86_64": ["linux_intel_dep"], - }, - filegroups = {}, - native = struct( - config_setting = lambda **kwargs: calls.append(kwargs), - glob = lambda *args, **kwargs: [], - ), - rules = struct( - venv_rewrite_shebang = lambda **kwargs: None, - ), - ) - - env.expect.that_collection(calls).contains_exactly([ - { - "name": "is_python_3.10.11_linux_ppc64le", - "visibility": ["//visibility:private"], - "constraint_values": [ - "@platforms//cpu:ppc64le", - "@platforms//os:linux", - ], - "flag_values": { - Label("//python/config_settings:python_version"): "3.10.11", - }, - }, - { - "name": "is_python_3.10_linux_ppc64le", - "visibility": ["//visibility:private"], - "constraint_values": [ - "@platforms//cpu:ppc64le", - "@platforms//os:linux", - ], - "flag_values": { - Label("//python/config_settings:python_version"): "3.10", - }, - }, - { - "name": "is_linux_x86_64", - "visibility": ["//visibility:private"], - "constraint_values": [ - "@platforms//cpu:x86_64", - "@platforms//os:linux", - ], - }, - ]) # buildifier: @unsorted-dict-items - -_tests.append(_test_platforms) - def _test_copy(env): calls = [] - whl_library_targets( + whl_library_srcs( name = "", - dep_template = None, - dependencies_by_platform = {}, filegroups = {}, copy_files = {"file_src": "file_dest"}, copy_executables = {"exec_src": "exec_dest"}, @@ -147,6 +85,7 @@ def _test_copy(env): rules = struct( copy_file = lambda **kwargs: calls.append(kwargs), venv_rewrite_shebang = lambda **kwargs: None, + gen_wheel_record = lambda **kwargs: None, ), ) @@ -168,7 +107,7 @@ def _test_copy(env): _tests.append(_test_copy) -def _test_whl_and_library_deps_from_requires(env): +def _test_whl_library_deps_targets(env): filegroup_calls = [] py_library_calls = [] env_marker_setting_calls = [] @@ -181,10 +120,9 @@ def _test_whl_and_library_deps_from_requires(env): m_glob.results.append(["site-packages/foo/DATA.txt"]) # data m_glob.results.append(["site-packages/foo/PYI.pyi"]) # pyi - whl_library_targets_from_requires( + whl_library_deps_targets( name = "foo-0-py3-none-any.whl", metadata_name = "Foo", - metadata_version = "0", dep_template = "@pypi//{name}:{target}", requires_dist = [ "foo", # this self-edge will be ignored @@ -193,11 +131,13 @@ def _test_whl_and_library_deps_from_requires(env): "booo", # this is effectively excluded due to the list below ], include = ["foo", "bar", "bar_baz"], - data_exclude = [], # Overrides for testing - filegroups = {}, + repo = None, + aliases = None, + extras = [], native = struct( filegroup = lambda **kwargs: filegroup_calls.append(kwargs), + alias = lambda **kwargs: None, config_setting = lambda **_: None, glob = m_glob.glob, ), @@ -212,7 +152,9 @@ def _test_whl_and_library_deps_from_requires(env): env.expect.that_collection(filegroup_calls).contains_exactly([ { "name": "whl", - "srcs": ["foo-0-py3-none-any.whl"], + # NOTE @aignas 2026-07-25: depending on the brackets position one may get different + # results in the expectation. + "srcs": ["whl_file"], "data": ["@pypi//bar:whl"] + select({ ":is_include_bar_baz_true": ["@pypi//bar_baz:whl"], "//conditions:default": [], @@ -228,56 +170,16 @@ def _test_whl_and_library_deps_from_requires(env): env.expect.that_dict(py_library_call).contains_exactly({ "name": "pkg", - "srcs": ["site-packages/foo/SRCS.py"] + select({ - Label("//python/config_settings:_is_venvs_site_packages_yes"): [], - "//conditions:default": ["_create_inits_target"], - }), - "pyi_srcs": ["site-packages/foo/PYI.pyi"], - "data": ["site-packages/foo/DATA.txt", "data"], - "imports": ["site-packages"], - "deps": ["@pypi//bar:pkg"] + select({ + "srcs": ["srcs"], + "deps": ["srcs", "@pypi//bar:pkg"] + select({ ":is_include_bar_baz_true": ["@pypi//bar_baz:pkg"], "//conditions:default": [], }), - "tags": ["pypi_name=Foo", "pypi_version=0"], + "tags": [], "visibility": ["//visibility:public"], - "experimental_venvs_site_packages": Label("//python/config_settings:venvs_site_packages"), - "namespace_package_files": [] + select({ - Label("//python/config_settings:_is_venvs_site_packages_yes"): [], - "//conditions:default": ["_create_inits_target"], - }), }) # buildifier: @unsorted-dict-items - env.expect.that_collection(m_glob.calls).contains_exactly([ - # bin call - mocks.glob_call( - ["bin/*"], - allow_empty = True, - ), - # rewrite-bin call - mocks.glob_call( - ["rewrite-bin/*"], - allow_empty = True, - ), - # srcs call - mocks.glob_call( - ["site-packages/**/*.py"], - exclude = [], - allow_empty = True, - ), - # data call - mocks.glob_call( - ["site-packages/**/*"], - exclude = [ - "**/*.py", - "**/*.pyc", - "**/*.pyc.*", - ], - allow_empty = True, - ), - # pyi call - mocks.glob_call(["site-packages/**/*.pyi"], allow_empty = True), - ]) + env.expect.that_collection(m_glob.calls).contains_exactly([]) env.expect.that_collection(env_marker_setting_calls).contains_exactly([ { @@ -287,207 +189,68 @@ def _test_whl_and_library_deps_from_requires(env): }, ]) # buildifier: @unsorted-dict-items -_tests.append(_test_whl_and_library_deps_from_requires) +_tests.append(_test_whl_library_deps_targets) -def _test_whl_and_library_deps(env): +def _test_whl_library_deps_targets_no_deps(env): + alias_calls = [] filegroup_calls = [] py_library_calls = [] - m_glob = mocks.glob() - m_glob.results.append([]) # bin - m_glob.results.append([]) # rewrite-bin - m_glob.results.append(["site-packages/foo/SRCS.py"]) - m_glob.results.append(["site-packages/foo/DATA.txt"]) - m_glob.results.append(["site-packages/foo/PYI.pyi"]) + env_marker_setting_calls = [] - whl_library_targets( - name = "foo.whl", - dep_template = "@pypi_{name}//:{target}", - dependencies = ["foo", "bar-baz"], - dependencies_by_platform = { - "@//python/config_settings:is_python_3.9": ["py39_dep"], - "@platforms//cpu:aarch64": ["arm_dep"], - "@platforms//os:windows": ["win_dep"], - "cp310_linux_ppc64le": ["py310_linux_ppc64le_dep"], - "cp39_anyos_aarch64": ["py39_arm_dep"], - "cp39_linux_anyarch": ["py39_linux_dep"], - "linux_x86_64": ["linux_intel_dep"], - }, - data_exclude = [], - tags = ["tag1", "tag2"], - # Overrides for testing - filegroups = {}, + whl_library_deps_targets( + name = "foo-0-py3-none-any.whl", + metadata_name = "Foo", + dep_template = "@pypi//{name}:{target}", + requires_dist = [], + group_name = "qux", + repo = None, + aliases = {}, + extras = [], native = struct( filegroup = lambda **kwargs: filegroup_calls.append(kwargs), + alias = lambda **kwargs: alias_calls.append(kwargs), config_setting = lambda **_: None, - glob = m_glob.glob, + glob = lambda **_: [], ), rules = struct( py_library = lambda **kwargs: py_library_calls.append(kwargs), - create_inits = lambda **kwargs: ["_create_inits_target"], - venv_rewrite_shebang = lambda **kwargs: None, + env_marker_setting = lambda **kwargs: env_marker_setting_calls.append(kwargs), ), ) - env.expect.that_collection(filegroup_calls).contains_exactly([ + # If the package is in a group but has no deps, then the public labels should be aliases + # to the srcs targets and no other targets should be created. + env.expect.that_collection(alias_calls).contains_exactly([ + { + "name": "pkg", + "actual": "srcs", + "visibility": ["//visibility:public"], + }, { "name": "whl", - "srcs": ["foo.whl"], - "data": [ - "@pypi_bar_baz//:whl", - "@pypi_foo//:whl", - ] + select( - { - Label("//python/config_settings:is_python_3.9"): ["@pypi_py39_dep//:whl"], - "@platforms//cpu:aarch64": ["@pypi_arm_dep//:whl"], - "@platforms//os:windows": ["@pypi_win_dep//:whl"], - ":is_python_3.10_linux_ppc64le": ["@pypi_py310_linux_ppc64le_dep//:whl"], - ":is_python_3.9_anyos_aarch64": ["@pypi_py39_arm_dep//:whl"], - ":is_python_3.9_linux_anyarch": ["@pypi_py39_linux_dep//:whl"], - ":is_linux_x86_64": ["@pypi_linux_intel_dep//:whl"], - "//conditions:default": [], - }, - ), + "actual": "whl_file", "visibility": ["//visibility:public"], }, ]) # buildifier: @unsorted-dict-items - env.expect.that_collection(py_library_calls).has_size(1) - if len(py_library_calls) != 1: - return - env.expect.that_dict(py_library_calls[0]).contains_exactly({ - "name": "pkg", - "srcs": ["site-packages/foo/SRCS.py"] + select({ - Label("//python/config_settings:_is_venvs_site_packages_yes"): [], - "//conditions:default": ["_create_inits_target"], - }), - "pyi_srcs": ["site-packages/foo/PYI.pyi"], - "data": ["site-packages/foo/DATA.txt", "data"], - "imports": ["site-packages"], - "deps": [ - "@pypi_bar_baz//:pkg", - "@pypi_foo//:pkg", - ] + select( - { - Label("//python/config_settings:is_python_3.9"): ["@pypi_py39_dep//:pkg"], - "@platforms//cpu:aarch64": ["@pypi_arm_dep//:pkg"], - "@platforms//os:windows": ["@pypi_win_dep//:pkg"], - ":is_python_3.10_linux_ppc64le": ["@pypi_py310_linux_ppc64le_dep//:pkg"], - ":is_python_3.9_anyos_aarch64": ["@pypi_py39_arm_dep//:pkg"], - ":is_python_3.9_linux_anyarch": ["@pypi_py39_linux_dep//:pkg"], - ":is_linux_x86_64": ["@pypi_linux_intel_dep//:pkg"], - "//conditions:default": [], - }, - ), - "tags": ["tag1", "tag2"], - "visibility": ["//visibility:public"], - "experimental_venvs_site_packages": Label("//python/config_settings:venvs_site_packages"), - "namespace_package_files": [] + select({ - Label("//python/config_settings:_is_venvs_site_packages_yes"): [], - "//conditions:default": ["_create_inits_target"], - }), - }) # buildifier: @unsorted-dict-items - -_tests.append(_test_whl_and_library_deps) + env.expect.that_collection(filegroup_calls).contains_exactly([]) + env.expect.that_collection(py_library_calls).contains_exactly([]) + env.expect.that_collection(env_marker_setting_calls).contains_exactly([]) -def _test_group(env): - alias_calls = [] - py_library_calls = [] - - m_glob = mocks.glob() - m_glob.results.append([]) # bin - m_glob.results.append([]) # rewrite-bin - m_glob.results.append(["site-packages/foo/srcs.py"]) - m_glob.results.append(["site-packages/foo/data.txt"]) - m_glob.results.append(["site-packages/foo/pyi.pyi"]) - - whl_library_targets( - name = "foo.whl", - dep_template = "@pypi_{name}//:{target}", - dependencies = ["foo", "bar-baz", "qux"], - dependencies_by_platform = { - "linux_x86_64": ["box", "box-amd64"], - "windows_x86_64": ["fox"], - "@platforms//os:linux": ["box"], # buildifier: disable=unsorted-dict-items to check that we sort inside the test - }, - tags = [], - data_exclude = [], - group_name = "qux", - group_deps = ["foo", "fox", "qux"], - # Overrides for testing - filegroups = {}, - native = struct( - config_setting = lambda **_: None, - glob = m_glob.glob, - alias = lambda **kwargs: alias_calls.append(kwargs), - ), - rules = struct( - py_library = lambda **kwargs: py_library_calls.append(kwargs), - create_inits = lambda **kwargs: ["_create_inits_target"], - venv_rewrite_shebang = lambda **kwargs: None, - ), - ) - - env.expect.that_collection(alias_calls).contains_exactly([ - {"name": "pkg", "actual": "@pypi__config//_groups:qux_pkg", "visibility": ["//visibility:public"]}, - {"name": "whl", "actual": "@pypi__config//_groups:qux_whl", "visibility": ["//visibility:public"]}, - ]) # buildifier: @unsorted-dict-items - - env.expect.that_collection(py_library_calls).has_size(1) - if len(py_library_calls) != 1: - return - - py_library_call = py_library_calls[0] - env.expect.where(case = "verify py library call").that_dict( - py_library_call, - ).contains_exactly({ - "name": "_pkg", - "srcs": ["site-packages/foo/srcs.py"] + select({ - Label("//python/config_settings:_is_venvs_site_packages_yes"): [], - "//conditions:default": ["_create_inits_target"], - }), - "pyi_srcs": ["site-packages/foo/pyi.pyi"], - "data": ["site-packages/foo/data.txt", "data"], - "imports": ["site-packages"], - "deps": ["@pypi_bar_baz//:pkg"] + select({ - "@platforms//os:linux": ["@pypi_box//:pkg"], - ":is_linux_x86_64": ["@pypi_box//:pkg", "@pypi_box_amd64//:pkg"], - "//conditions:default": [], - }), - "tags": [], - "visibility": ["@pypi__config//_groups:__pkg__"], - "experimental_venvs_site_packages": Label("//python/config_settings:venvs_site_packages"), - "namespace_package_files": [] + select({ - Label("//python/config_settings:_is_venvs_site_packages_yes"): [], - "//conditions:default": ["_create_inits_target"], - }), - }) # buildifier: @unsorted-dict-items - - env.expect.that_collection(m_glob.calls, expr = "glob calls").contains_exactly([ - mocks.glob_call(["bin/*"], allow_empty = True), - mocks.glob_call(["rewrite-bin/*"], allow_empty = True), - mocks.glob_call(["site-packages/**/*.py"], exclude = [], allow_empty = True), - mocks.glob_call(["site-packages/**/*"], exclude = [ - "**/*.py", - "**/*.pyc", - "**/*.pyc.*", - ], allow_empty = True), - mocks.glob_call(["site-packages/**/*.pyi"], allow_empty = True), - ]) - -_tests.append(_test_group) +_tests.append(_test_whl_library_deps_targets_no_deps) def _test_sdist_excludes_record(env): py_library_calls = [] m_glob = mocks.glob() m_glob.results.append([]) # bin m_glob.results.append([]) # rewrite-bin + m_glob.results.append([]) # rewrite-record m_glob.results.append([]) # srcs m_glob.results.append([]) # data m_glob.results.append([]) # pyi - whl_library_targets( + whl_library_srcs( name = "foo.whl", - dep_template = "@pypi_{name}//:{target}", sdist_filename = "foo.tar.gz", filegroups = {}, native = struct( @@ -499,6 +262,7 @@ def _test_sdist_excludes_record(env): py_library = lambda **kwargs: py_library_calls.append(kwargs), create_inits = lambda **kwargs: [], venv_rewrite_shebang = lambda **kwargs: None, + gen_wheel_record = lambda **kwargs: None, ), ) @@ -517,6 +281,63 @@ def _test_sdist_excludes_record(env): _tests.append(_test_sdist_excludes_record) +def _test_exclude_bazel_files(env): + # Regression test: the `extracted_whl_files` glob must always exclude the + # Bazel repo files, even when the wheel is not built from an sdist. + for sdist_filename in [None, "foo.tar.gz"]: + m_glob = mocks.glob() + m_glob.results.append([]) # bin + m_glob.results.append([]) # rewrite-bin + m_glob.results.append([]) # rewrite-record + m_glob.results.append([]) # extracted_whl_files + m_glob.results.append([]) # dist_info + m_glob.results.append([]) # data + + whl_library_srcs( + name = "foo.whl", + sdist_filename = sdist_filename, + native = struct( + filegroup = lambda **_: None, + glob = m_glob.glob, + ), + rules = struct( + venv_rewrite_shebang = lambda **kwargs: None, + gen_wheel_record = lambda **kwargs: None, + ), + ) + + expected_exclude = [ + "BUILD", + "BUILD.bazel", + "REPO.bazel", + "WORKSPACE", + "WORKSPACE.bzlmod", + "WORKSPACE.bazel", + ] + if sdist_filename: + expected_exclude.append(sdist_filename) + + env.expect.that_collection(m_glob.calls).contains_exactly([ + mocks.glob_call(["bin/*"], allow_empty = True), + mocks.glob_call(["rewrite-bin/*"], allow_empty = True), + mocks.glob_call(["rewrite-record/*/RECORD"], allow_empty = True), + mocks.glob_call( + include = ["**"], + exclude = expected_exclude, + allow_empty = True, + ), + mocks.glob_call( + include = ["site-packages/*.dist-info/**"], + allow_empty = True, + ), + mocks.glob_call( + include = ["data/**", "bin/**", "include/**"], + allow_empty = True, + ), + ]) + +_tests.append(_test_exclude_bazel_files) + def whl_library_targets_test_suite(name): """create the test suite. diff --git a/tests/pypi/whl_repo_name/whl_repo_name_tests.bzl b/tests/pypi/whl_repo_name/whl_repo_name_tests.bzl index 35e6bcdf9f..968bdc3d2a 100644 --- a/tests/pypi/whl_repo_name/whl_repo_name_tests.bzl +++ b/tests/pypi/whl_repo_name/whl_repo_name_tests.bzl @@ -25,6 +25,18 @@ def _test_simple(env): _tests.append(_test_simple) +def _test_simple_canonical_digest(env): + got = whl_repo_name("foo-1.2.3-py3-none-any.whl", "sha256:deadbeef") + env.expect.that_str(got).equals("foo_py3_none_any_deadbeef") + +_tests.append(_test_simple_canonical_digest) + +def _test_simple_canonical_digest_other_algo(env): + got = whl_repo_name("foo-1.2.3-py3-none-any.whl", "sha512:deadbeef000deadbeef") + env.expect.that_str(got).equals("foo_py3_none_any_deadbeef") + +_tests.append(_test_simple_canonical_digest_other_algo) + def _test_simple_no_sha(env): got = whl_repo_name("foo-1.2.3-py3-none-any.whl", "") env.expect.that_str(got).equals("foo_1_2_3_py3_none_any") diff --git a/tests/pytest_test/BUILD.bazel b/tests/pytest_test/BUILD.bazel new file mode 100644 index 0000000000..b15094a615 --- /dev/null +++ b/tests/pytest_test/BUILD.bazel @@ -0,0 +1,22 @@ +load("//tests/support:support.bzl", "SUPPORTS_BZLMOD") +load("//tests/support/pytest_test:pytest_test.bzl", "pytest_test") + +pytest_test( + name = "pytest_script_venv_test", + srcs = [ + "basic_test.py", + ], + config_settings = { + "@rules_python//python/config_settings:bootstrap_impl": "script", + "@rules_python//python/config_settings:venvs_site_packages": "yes", + }, + target_compatible_with = SUPPORTS_BZLMOD, +) + +pytest_test( + name = "pytest_default_test", + srcs = [ + "basic_test.py", + ], + target_compatible_with = SUPPORTS_BZLMOD, +) diff --git a/tests/pytest_test/basic_test.py b/tests/pytest_test/basic_test.py new file mode 100644 index 0000000000..30e2c41ab9 --- /dev/null +++ b/tests/pytest_test/basic_test.py @@ -0,0 +1,2 @@ +def test_foo(): + assert True diff --git a/tests/python/BUILD.bazel b/tests/python/BUILD.bazel index 2553536b63..887fe969b5 100644 --- a/tests/python/BUILD.bazel +++ b/tests/python/BUILD.bazel @@ -12,6 +12,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -load(":python_tests.bzl", "python_test_suite") +load(":python_tests.bzl", "register_python_tests") -python_test_suite(name = "python_tests") +register_python_tests(name = "python_tests") diff --git a/tests/python/python_tests.bzl b/tests/python/python_tests.bzl index 60e7311c00..cbef5637fd 100644 --- a/tests/python/python_tests.bzl +++ b/tests/python/python_tests.bzl @@ -16,118 +16,26 @@ load("@pythons_hub//:versions.bzl", "MINOR_MAPPING") load("@rules_testing//lib:test_suite.bzl", "test_suite") +load("//python/private:bzlmod_enabled.bzl", "BZLMOD_ENABLED") # buildifier: disable=bzl-visibility load("//python/private:python.bzl", "parse_modules") # buildifier: disable=bzl-visibility load("//python/private:repo_utils.bzl", "repo_utils") # buildifier: disable=bzl-visibility -load("//tests/support/mocks:mocks.bzl", "mocks") +load("//tests/support/mocks:python_ext.bzl", "python_ext") _tests = [] -def _mod(*, name, defaults = [], toolchain = [], override = [], single_version_override = [], single_version_platform_override = [], is_root = False): - return mocks.module( - name, - is_root = is_root, - defaults = defaults, - toolchain = toolchain, - override = override, - single_version_override = single_version_override, - single_version_platform_override = single_version_platform_override, - ) - -def _defaults(python_version = None, python_version_env = None, python_version_file = None): - return struct( - python_version = python_version, - python_version_env = python_version_env, - python_version_file = python_version_file, - ) - -def _toolchain(python_version, *, is_default = False, **kwargs): - return struct( - is_default = is_default, - python_version = python_version, - **kwargs - ) - -def _override( - auth_patterns = {}, - available_python_versions = [], - base_url = "", - minor_mapping = {}, - netrc = "", - register_all_versions = False, - add_target_settings = []): - return struct( - auth_patterns = auth_patterns, - available_python_versions = available_python_versions, - base_url = base_url, - minor_mapping = minor_mapping, - netrc = netrc, - register_all_versions = register_all_versions, - add_target_settings = add_target_settings, - ) - def _rules_python_module(is_root = False): """A mock of what the real rules_python MODULE.bazel looks like.""" - return _mod( + return python_ext.module( name = "rules_python", - defaults = [_defaults(python_version = "3.11")], - toolchain = [_toolchain("3.11")], + defaults = [python_ext.defaults(python_version = "3.11")], + toolchain = [python_ext.toolchain(python_version = "3.11")], is_root = is_root, ) -def _single_version_override( - python_version = "", - sha256 = {}, - urls = [], - patch_strip = 0, - patches = [], - strip_prefix = "python", - distutils_content = "", - distutils = None): - if not python_version: - fail("missing mandatory args: python_version ({})".format(python_version)) - - return struct( - python_version = python_version, - sha256 = sha256, - urls = urls, - patch_strip = patch_strip, - patches = patches, - strip_prefix = strip_prefix, - distutils_content = distutils_content, - distutils = distutils, - ) - -def _single_version_platform_override( - coverage_tool = None, - patch_strip = 0, - patches = [], - platform = "", - python_version = "", - sha256 = "", - strip_prefix = "python", - urls = []): - if not platform or not python_version: - fail("missing mandatory args: platform ({}) and python_version ({})".format(platform, python_version)) - - return struct( - sha256 = sha256, - urls = urls, - strip_prefix = strip_prefix, - platform = platform, - coverage_tool = coverage_tool, - python_version = python_version, - patch_strip = patch_strip, - patches = patches, - target_compatible_with = [], - target_settings = [], - os_name = "", - arch = "", - ) - def _test_default_from_rules_python_when_rules_python_is_root(env): """Verify that rules_python (as root module) default is applied.""" py = parse_modules( - module_ctx = mocks.mctx( + module_ctx = python_ext.mctx( _rules_python_module(is_root = True), ), logger = repo_utils.logger(verbosity_level = 0, name = "python"), @@ -139,7 +47,7 @@ def _test_default_from_rules_python_when_rules_python_is_root(env): env.expect.that_dict(py.config.minor_mapping).contains_exactly(MINOR_MAPPING) env.expect.that_collection(py.config.kwargs).has_size(0) env.expect.that_collection(py.config.default.keys()).contains_exactly([ - "base_url", + "base_urls", "tool_versions", "platforms", ]) @@ -157,7 +65,7 @@ _tests.append(_test_default_from_rules_python_when_rules_python_is_root) def _test_default_from_rules_python_when_rules_python_is_not_root(env): """Verify that rules_python default applies when rules_python is not the root module.""" py = parse_modules( - module_ctx = mocks.mctx( + module_ctx = python_ext.mctx( _rules_python_module(), ), logger = repo_utils.logger(verbosity_level = 0, name = "python"), @@ -176,9 +84,13 @@ _tests.append(_test_default_from_rules_python_when_rules_python_is_not_root) def _test_default_with_patch_version(env): py = parse_modules( - module_ctx = mocks.mctx( + module_ctx = python_ext.mctx( modules = [ - _mod(name = "alpha", toolchain = [_toolchain("3.11.2")], is_root = True), + python_ext.module( + name = "alpha", + is_root = True, + toolchain = [python_ext.toolchain(python_version = "3.11.2")], + ), _rules_python_module(is_root = False), ], ), @@ -198,19 +110,22 @@ _tests.append(_test_default_with_patch_version) def _test_toolchain_ordering(env): py = parse_modules( - module_ctx = mocks.mctx( - _mod( + module_ctx = python_ext.mctx( + python_ext.module( name = "my_module", + is_root = True, toolchain = [ - _toolchain("3.10"), - _toolchain("3.10.15"), - _toolchain(MINOR_MAPPING["3.10"]), - _toolchain("3.10.13"), - _toolchain("3.11.1"), - _toolchain("3.11.10"), - _toolchain(MINOR_MAPPING["3.11"], is_default = True), + python_ext.toolchain(python_version = "3.10"), + python_ext.toolchain(python_version = "3.10.15"), + python_ext.toolchain(python_version = MINOR_MAPPING["3.10"]), + python_ext.toolchain(python_version = "3.10.13"), + python_ext.toolchain(python_version = "3.11.1"), + python_ext.toolchain(python_version = "3.11.10"), + python_ext.toolchain( + python_version = MINOR_MAPPING["3.11"], + is_default = True, + ), ], - is_root = True, ), _rules_python_module(), ), @@ -245,12 +160,16 @@ _tests.append(_test_toolchain_ordering) def _test_default_from_defaults(env): py = parse_modules( - module_ctx = mocks.mctx( - _mod( + module_ctx = python_ext.mctx( + python_ext.module( name = "my_root_module", - defaults = [_defaults(python_version = "3.11")], - toolchain = [_toolchain("3.10"), _toolchain("3.11"), _toolchain("3.12")], + defaults = [python_ext.defaults(python_version = "3.11")], is_root = True, + toolchain = [ + python_ext.toolchain(python_version = "3.10"), + python_ext.toolchain(python_version = "3.11"), + python_ext.toolchain(python_version = "3.12"), + ], ), ), logger = repo_utils.logger(verbosity_level = 0, name = "python"), @@ -272,12 +191,21 @@ _tests.append(_test_default_from_defaults) def _test_default_from_defaults_env(env): py = parse_modules( - module_ctx = mocks.mctx( - _mod( + module_ctx = python_ext.mctx( + python_ext.module( name = "my_root_module", - defaults = [_defaults(python_version = "3.11", python_version_env = "PYENV_VERSION")], - toolchain = [_toolchain("3.10"), _toolchain("3.11"), _toolchain("3.12")], + defaults = [ + python_ext.defaults( + python_version = "3.11", + python_version_env = "PYENV_VERSION", + ), + ], is_root = True, + toolchain = [ + python_ext.toolchain(python_version = "3.10"), + python_ext.toolchain(python_version = "3.11"), + python_ext.toolchain(python_version = "3.12"), + ], ), environ = {"PYENV_VERSION": "3.12"}, ), @@ -300,12 +228,20 @@ _tests.append(_test_default_from_defaults_env) def _test_default_from_defaults_file(env): py = parse_modules( - module_ctx = mocks.mctx( - _mod( + module_ctx = python_ext.mctx( + python_ext.module( name = "my_root_module", - defaults = [_defaults(python_version_file = "@@//:.python-version")], - toolchain = [_toolchain("3.10"), _toolchain("3.11"), _toolchain("3.12")], + defaults = [ + python_ext.defaults( + python_version_file = "@@//:.python-version", + ), + ], is_root = True, + toolchain = [ + python_ext.toolchain(python_version = "3.10"), + python_ext.toolchain(python_version = "3.11"), + python_ext.toolchain(python_version = "3.12"), + ], ), mock_files = {"@@//:.python-version": "3.12\n"}, ), @@ -328,11 +264,11 @@ _tests.append(_test_default_from_defaults_file) def _test_default_from_single_toolchain(env): py = parse_modules( - module_ctx = mocks.mctx( - _mod( + module_ctx = python_ext.mctx( + python_ext.module( name = "my_root_module", - toolchain = [_toolchain("3.12")], is_root = True, + toolchain = [python_ext.toolchain(python_version = "3.12")], ), _rules_python_module(), ), @@ -344,15 +280,15 @@ _tests.append(_test_default_from_single_toolchain) def _test_defaults_overrides_single_toolchain(env): py = parse_modules( - module_ctx = mocks.mctx( - _mod( + module_ctx = python_ext.mctx( + python_ext.module( name = "my_root_module", defaults = [ # This relies on rules_python registering 3.11 - _defaults(python_version = "3.11"), + python_ext.defaults(python_version = "3.11"), ], - toolchain = [_toolchain("3.12")], is_root = True, + toolchain = [python_ext.toolchain(python_version = "3.12")], ), _rules_python_module(), ), @@ -364,15 +300,18 @@ _tests.append(_test_defaults_overrides_single_toolchain) def _test_defaults_overrides_toolchains_setting_is_default(env): py = parse_modules( - module_ctx = mocks.mctx( - _mod( + module_ctx = python_ext.mctx( + python_ext.module( name = "my_root_module", - defaults = [_defaults(python_version = "3.13")], + defaults = [python_ext.defaults(python_version = "3.13")], + is_root = True, toolchain = [ - _toolchain("3.13"), - _toolchain("3.12", is_default = True), + python_ext.toolchain(python_version = "3.13"), + python_ext.toolchain( + python_version = "3.12", + is_default = True, + ), ], - is_root = True, ), _rules_python_module(), ), @@ -384,10 +323,23 @@ _tests.append(_test_defaults_overrides_toolchains_setting_is_default) def _test_first_occurance_of_the_toolchain_wins(env): py = parse_modules( - module_ctx = mocks.mctx( + module_ctx = python_ext.mctx( modules = [ - _mod(name = "my_module", is_root = True, toolchain = [_toolchain("3.12")]), - _mod(name = "some_module", toolchain = [_toolchain("3.12", configure_coverage_tool = True)]), + python_ext.module( + name = "my_module", + is_root = True, + toolchain = [python_ext.toolchain(python_version = "3.12")], + ), + python_ext.module( + name = "some_module", + is_root = False, + toolchain = [ + python_ext.toolchain( + python_version = "3.12", + configure_coverage_tool = True, + ), + ], + ), _rules_python_module(), ], environ = { @@ -427,17 +379,17 @@ _tests.append(_test_first_occurance_of_the_toolchain_wins) def _test_auth_overrides(env): py = parse_modules( - module_ctx = mocks.mctx( - _mod( + module_ctx = python_ext.mctx( + python_ext.module( name = "my_module", - toolchain = [_toolchain("3.12")], + is_root = True, override = [ - _override( - netrc = "/my/netrc", + python_ext.override( auth_patterns = {"foo": "bar"}, + netrc = "/my/netrc", ), ], - is_root = True, + toolchain = [python_ext.toolchain(python_version = "3.12")], ), _rules_python_module(), ), @@ -469,18 +421,18 @@ _tests.append(_test_auth_overrides) def _test_add_target_settings(env): py = parse_modules( - module_ctx = mocks.mctx( - _mod( + module_ctx = python_ext.mctx( + python_ext.module( name = "my_module", - toolchain = [_toolchain("3.12")], + is_root = True, override = [ - _override( + python_ext.override( add_target_settings = [ "@@//my:custom_setting", ], ), ], - is_root = True, + toolchain = [python_ext.toolchain(python_version = "3.12")], ), _rules_python_module(), ), @@ -495,46 +447,51 @@ _tests.append(_test_add_target_settings) def _test_add_new_version(env): py = parse_modules( - module_ctx = mocks.mctx( - _mod( + module_ctx = python_ext.mctx( + python_ext.module( name = "my_module", is_root = True, - toolchain = [_toolchain("3.13")], + override = [ + python_ext.override( + available_python_versions = [ + "3.12.4", + "3.13.0", + "3.13.1", + "3.13.99", + ], + base_urls = [], + minor_mapping = { + "3.13": "3.13.99", + }, + ), + ], single_version_override = [ - _single_version_override( + python_ext.single_version_override( + distutils = None, + distutils_content = "", + patch_strip = 0, + patches = [], python_version = "3.13.0", sha256 = { "aarch64-unknown-linux-gnu": "deadbeef", }, - urls = ["example.org"], - patch_strip = 0, - patches = [], strip_prefix = "prefix", - distutils_content = "", - distutils = None, + urls = ["example.org"], ), ], single_version_platform_override = [ - _single_version_platform_override( - sha256 = "deadb00f", - urls = ["something.org", "else.org"], - strip_prefix = "python", - platform = "aarch64-unknown-linux-gnu", + python_ext.single_version_platform_override( coverage_tool = "specific_cov_tool", - python_version = "3.13.99", patch_strip = 2, patches = ["specific-patch.txt"], + platform = "aarch64-unknown-linux-gnu", + python_version = "3.13.99", + sha256 = "deadb00f", + strip_prefix = "python", + urls = ["something.org", "else.org"], ), ], - override = [ - _override( - base_url = "", - available_python_versions = ["3.12.4", "3.13.0", "3.13.1", "3.13.99"], - minor_mapping = { - "3.13": "3.13.99", - }, - ), - ], + toolchain = [python_ext.toolchain(python_version = "3.13")], ), ), logger = repo_utils.logger(verbosity_level = 0, name = "python"), @@ -576,13 +533,24 @@ _tests.append(_test_add_new_version) def _test_register_all_versions(env): py = parse_modules( - module_ctx = mocks.mctx( - _mod( + module_ctx = python_ext.mctx( + python_ext.module( name = "my_module", is_root = True, - toolchain = [_toolchain("3.13")], + override = [ + python_ext.override( + available_python_versions = [ + "3.12.4", + "3.13.0", + "3.13.1", + "3.13.99", + ], + base_urls = [], + register_all_versions = True, + ), + ], single_version_override = [ - _single_version_override( + python_ext.single_version_override( python_version = "3.13.0", sha256 = { "aarch64-unknown-linux-gnu": "deadbeef", @@ -591,20 +559,14 @@ def _test_register_all_versions(env): ), ], single_version_platform_override = [ - _single_version_platform_override( - sha256 = "deadb00f", - urls = ["something.org"], + python_ext.single_version_platform_override( platform = "aarch64-unknown-linux-gnu", python_version = "3.13.99", + sha256 = "deadb00f", + urls = ["something.org"], ), ], - override = [ - _override( - base_url = "", - available_python_versions = ["3.12.4", "3.13.0", "3.13.1", "3.13.99"], - register_all_versions = True, - ), - ], + toolchain = [python_ext.toolchain(python_version = "3.13")], ), ), logger = repo_utils.logger(verbosity_level = 0, name = "python"), @@ -642,17 +604,26 @@ _tests.append(_test_register_all_versions) def _test_ignore_unsupported_versions(env): py = parse_modules( - module_ctx = mocks.mctx( - _mod( + module_ctx = python_ext.mctx( + python_ext.module( name = "my_module", is_root = True, - toolchain = [ - _toolchain("3.11"), - _toolchain("3.12"), - _toolchain("3.13", is_default = True), + override = [ + python_ext.override( + available_python_versions = [ + "3.12.4", + "3.13.0", + "3.13.1", + ], + base_urls = [], + minor_mapping = { + "3.12": "3.12.4", + "3.13": "3.13.1", + }, + ), ], single_version_override = [ - _single_version_override( + python_ext.single_version_override( python_version = "3.13.0", sha256 = { "aarch64-unknown-linux-gnu": "deadbeef", @@ -661,21 +632,19 @@ def _test_ignore_unsupported_versions(env): ), ], single_version_platform_override = [ - _single_version_platform_override( - sha256 = "deadb00f", - urls = ["something.org"], + python_ext.single_version_platform_override( platform = "aarch64-unknown-linux-gnu", python_version = "3.13.99", + sha256 = "deadb00f", + urls = ["something.org"], ), ], - override = [ - _override( - base_url = "", - available_python_versions = ["3.12.4", "3.13.0", "3.13.1"], - minor_mapping = { - "3.12": "3.12.4", - "3.13": "3.13.1", - }, + toolchain = [ + python_ext.toolchain(python_version = "3.11"), + python_ext.toolchain(python_version = "3.12"), + python_ext.toolchain( + python_version = "3.13", + is_default = True, ), ], ), @@ -713,47 +682,47 @@ _tests.append(_test_ignore_unsupported_versions) def _test_add_patches(env): py = parse_modules( - module_ctx = mocks.mctx( - _mod( + module_ctx = python_ext.mctx( + python_ext.module( name = "my_module", is_root = True, - toolchain = [_toolchain("3.13")], + override = [ + python_ext.override( + available_python_versions = ["3.13.0"], + base_urls = [], + minor_mapping = { + "3.13": "3.13.0", + }, + ), + ], single_version_override = [ - _single_version_override( + python_ext.single_version_override( + distutils = None, + distutils_content = "", + patch_strip = 1, + patches = ["common.txt"], python_version = "3.13.0", sha256 = { "aarch64-apple-darwin": "deadbeef", "aarch64-unknown-linux-gnu": "deadbeef", }, - urls = ["example.org"], - patch_strip = 1, - patches = ["common.txt"], strip_prefix = "prefix", - distutils_content = "", - distutils = None, + urls = ["example.org"], ), ], single_version_platform_override = [ - _single_version_platform_override( - sha256 = "deadb00f", - urls = ["something.org", "else.org"], - strip_prefix = "python", - platform = "aarch64-unknown-linux-gnu", + python_ext.single_version_platform_override( coverage_tool = "specific_cov_tool", - python_version = "3.13.0", patch_strip = 2, patches = ["specific-patch.txt"], + platform = "aarch64-unknown-linux-gnu", + python_version = "3.13.0", + sha256 = "deadb00f", + strip_prefix = "python", + urls = ["something.org", "else.org"], ), ], - override = [ - _override( - base_url = "", - available_python_versions = ["3.13.0"], - minor_mapping = { - "3.13": "3.13.0", - }, - ), - ], + toolchain = [python_ext.toolchain(python_version = "3.13")], ), ), logger = repo_utils.logger(verbosity_level = 0, name = "python"), @@ -792,15 +761,15 @@ _tests.append(_test_add_patches) def _test_fail_two_overrides(env): errors = [] parse_modules( - module_ctx = mocks.mctx( - _mod( + module_ctx = python_ext.mctx( + python_ext.module( name = "my_module", is_root = True, - toolchain = [_toolchain("3.13")], override = [ - _override(base_url = "foo"), - _override(base_url = "bar"), + python_ext.override(base_urls = ["foo"]), + python_ext.override(base_urls = ["bar"]), ], + toolchain = [python_ext.toolchain(python_version = "3.13")], ), ), _fail = errors.append, @@ -816,20 +785,26 @@ def _test_single_version_override_errors(env): for test in [ struct( overrides = [ - _single_version_override(python_version = "3.12.4", distutils_content = "foo"), - _single_version_override(python_version = "3.12.4", distutils_content = "foo"), + python_ext.single_version_override( + distutils_content = "foo", + python_version = "3.12.4", + ), + python_ext.single_version_override( + distutils_content = "foo", + python_version = "3.12.4", + ), ], want_error = "Only a single 'python.single_version_override' can be present for '3.12.4'", ), ]: errors = [] parse_modules( - module_ctx = mocks.mctx( - _mod( + module_ctx = python_ext.mctx( + python_ext.module( name = "my_module", is_root = True, - toolchain = [_toolchain("3.13")], single_version_override = test.overrides, + toolchain = [python_ext.toolchain(python_version = "3.13")], ), ), _fail = errors.append, @@ -843,32 +818,46 @@ def _test_single_version_platform_override_errors(env): for test in [ struct( overrides = [ - _single_version_platform_override(python_version = "3.12.4", platform = "foo", coverage_tool = "foo"), - _single_version_platform_override(python_version = "3.12.4", platform = "foo", coverage_tool = "foo"), + python_ext.single_version_platform_override( + coverage_tool = "foo", + platform = "foo", + python_version = "3.12.4", + ), + python_ext.single_version_platform_override( + coverage_tool = "foo", + platform = "foo", + python_version = "3.12.4", + ), ], want_error = "Only a single 'python.single_version_platform_override' can be present for '(\"3.12.4\", \"foo\")'", ), struct( overrides = [ - _single_version_platform_override(python_version = "3.12", platform = "foo"), + python_ext.single_version_platform_override( + platform = "foo", + python_version = "3.12", + ), ], want_error = "The 'python_version' attribute needs to specify the full version in at least 'X.Y.Z' format, got: '3.12'", ), struct( overrides = [ - _single_version_platform_override(python_version = "foo", platform = "foo"), + python_ext.single_version_platform_override( + platform = "foo", + python_version = "foo", + ), ], want_error = "Failed to parse PEP 440 version identifier 'foo'. Parse error at 'foo'", ), ]: errors = [] parse_modules( - module_ctx = mocks.mctx( - _mod( + module_ctx = python_ext.mctx( + python_ext.module( name = "my_module", - toolchain = [_toolchain("3.13")], - single_version_platform_override = test.overrides, is_root = True, + single_version_platform_override = test.overrides, + toolchain = [python_ext.toolchain(python_version = "3.13")], ), ), _fail = lambda *a: errors.append(" ".join(a)), @@ -889,3 +878,17 @@ def python_test_suite(name): name: the name of the test suite """ test_suite(name = name, basic_tests = _tests) + +def register_python_tests(name): + """Registers the python tests if Bzlmod is enabled, otherwise defines an empty test_suite. + + Args: + name: The name of the test target. + """ + if BZLMOD_ENABLED: + python_test_suite(name = name) + else: + native.test_suite( + name = name, + tests = [], + ) diff --git a/tests/python_bzlmod_ext/BUILD.bazel b/tests/python_bzlmod_ext/BUILD.bazel new file mode 100644 index 0000000000..e266c01b9c --- /dev/null +++ b/tests/python_bzlmod_ext/BUILD.bazel @@ -0,0 +1,7 @@ +load(":test_helpers.bzl", "register_python_bzlmod_ext_tests") + +register_python_bzlmod_ext_tests( + name = "python_bzlmod_ext_tests", + parse_runtime_manifest_name = "parse_runtime_manifest_tests", + runtime_manifests_name = "runtime_manifests_tests", +) diff --git a/tests/python_bzlmod_ext/parse_runtime_manifest_tests.bzl b/tests/python_bzlmod_ext/parse_runtime_manifest_tests.bzl new file mode 100644 index 0000000000..4493576147 --- /dev/null +++ b/tests/python_bzlmod_ext/parse_runtime_manifest_tests.bzl @@ -0,0 +1,183 @@ +"""Tests for manifest parsing Starlark functions.""" + +load("@bazel_skylib//lib:structs.bzl", "structs") +load("@rules_testing//lib:analysis_test.bzl", "analysis_test") +load("@rules_testing//lib:test_suite.bzl", "test_suite") +load("@rules_testing//lib:util.bzl", rt_util = "util") +load("//python/private:pbs_manifest.bzl", "parse_filename", "parse_runtime_manifest") # buildifier: disable=bzl-visibility + +_tests = [] + +def _test_parse_filename_baseline(name): + """Sets up the baseline filename parsing test. + + Args: + name: The name of the test. + """ + rt_util.helper_target( + native.filegroup, + name = name + "_subject", + ) + analysis_test( + name = name, + target = name + "_subject", + impl = _test_parse_filename_baseline_impl, + ) + +def _test_parse_filename_baseline_impl(env, target): + _ = target # @unused + + # 1. Baseline + parsed1 = parse_filename("cpython-3.11.15+20260414-x86_64-unknown-linux-gnu-install_only.tar.gz") + env.expect.that_dict(parsed1).contains_exactly({ + "arch": "x86_64", + "archive_flavor": "install_only", + "build_flavor": "", + "build_version": "20260414", + "freethreaded": False, + "libc": "gnu", + "location": "cpython-3.11.15+20260414-x86_64-unknown-linux-gnu-install_only.tar.gz", + "microarch": "", + "os": "linux", + "python_version": "3.11.15", + "vendor": "unknown", + }) + + # 2. Microarch + parsed2 = parse_filename("cpython-3.10.20+20260414-x86_64_v2-unknown-linux-musl-lto-full.tar.zst") + env.expect.that_dict(parsed2).contains_exactly({ + "arch": "x86_64", + "archive_flavor": "full", + "build_flavor": "lto", + "build_version": "20260414", + "freethreaded": False, + "libc": "musl", + "location": "cpython-3.10.20+20260414-x86_64_v2-unknown-linux-musl-lto-full.tar.zst", + "microarch": "v2", + "os": "linux", + "python_version": "3.10.20", + "vendor": "unknown", + }) + + # 3. Freethreaded + parsed3 = parse_filename("cpython-3.13.13+20260414-aarch64-apple-darwin-freethreaded+pgo+lto-full.tar.zst") + env.expect.that_dict(parsed3).contains_exactly({ + "arch": "aarch64", + "archive_flavor": "full", + "build_flavor": "pgo+lto", + "build_version": "20260414", + "freethreaded": True, + "libc": "", + "location": "cpython-3.13.13+20260414-aarch64-apple-darwin-freethreaded+pgo+lto-full.tar.zst", + "microarch": "", + "os": "darwin", + "python_version": "3.13.13", + "vendor": "apple", + }) + + # 4. Invalid + parsed4 = parse_filename("invalid-filename.tar.gz") + env.expect.that_bool(parsed4 == None).equals(True) + + # 5. Full URL (should return the original URL as location) + parsed5 = parse_filename("https://github.com/astral-sh/python-build-standalone/releases/download/20260414/cpython-3.11.15+20260414-x86_64-unknown-linux-gnu-install_only.tar.gz") + env.expect.that_dict(parsed5).contains_exactly({ + "arch": "x86_64", + "archive_flavor": "install_only", + "build_flavor": "", + "build_version": "20260414", + "freethreaded": False, + "libc": "gnu", + "location": "https://github.com/astral-sh/python-build-standalone/releases/download/20260414/cpython-3.11.15+20260414-x86_64-unknown-linux-gnu-install_only.tar.gz", + "microarch": "", + "os": "linux", + "python_version": "3.11.15", + "vendor": "unknown", + }) + +_tests.append(_test_parse_filename_baseline) + +def _test_parse_runtime_manifest(name): + """Sets up the manifest file parsing test. + + Args: + name: The name of the test. + """ + rt_util.helper_target( + native.filegroup, + name = name + "_subject", + ) + analysis_test( + name = name, + target = name + "_subject", + impl = _test_parse_runtime_manifest_impl, + ) + +def _test_parse_runtime_manifest_impl(env, target): + _ = target # @unused + content = """ +8b14030dd3af9ea7f7c51b4c90feb04afd8a8f45435727e67b875270bd08f3bc cpython-3.11.15+20260414-x86_64-unknown-linux-gnu-install_only.tar.gz +a57ffd435652092d16b30e783f9826c55e9c64b0f0a72cbae0a9f39e663137fb cpython-3.11.15+20260414-aarch64-apple-darwin-install_only.tar.gz +ce18fdfd47c66830a40ea9b9e314a14b1636bbfd684501bc5ca1fc6d55a7933f https://example.com/cpython-3.10.20+20260414-x86_64_v2-unknown-linux-musl-lto-full.tar.zst +1111111111111111111111111111111111111111111111111111111111111111 cpython-3.13.13+20260414-aarch64-apple-darwin-freethreaded+pgo+lto-full.tar.zst +""" + parsed = parse_runtime_manifest(content) + env.expect.that_collection(parsed).has_size(4) + + env.expect.that_dict(structs.to_dict(parsed[0])).contains_exactly({ + "arch": "x86_64", + "archive_flavor": "install_only", + "build_flavor": "", + "build_version": "20260414", + "freethreaded": False, + "libc": "gnu", + "location": "cpython-3.11.15+20260414-x86_64-unknown-linux-gnu-install_only.tar.gz", + "microarch": "", + "os": "linux", + "python_version": "3.11.15", + "sha256": "8b14030dd3af9ea7f7c51b4c90feb04afd8a8f45435727e67b875270bd08f3bc", + "vendor": "unknown", + }) + + env.expect.that_dict(structs.to_dict(parsed[2])).contains_exactly({ + "arch": "x86_64", + "archive_flavor": "full", + "build_flavor": "lto", + "build_version": "20260414", + "freethreaded": False, + "libc": "musl", + "location": "https://example.com/cpython-3.10.20+20260414-x86_64_v2-unknown-linux-musl-lto-full.tar.zst", + "microarch": "v2", + "os": "linux", + "python_version": "3.10.20", + "sha256": "ce18fdfd47c66830a40ea9b9e314a14b1636bbfd684501bc5ca1fc6d55a7933f", + "vendor": "unknown", + }) + + env.expect.that_dict(structs.to_dict(parsed[3])).contains_exactly({ + "arch": "aarch64", + "archive_flavor": "full", + "build_flavor": "pgo+lto", + "build_version": "20260414", + "freethreaded": True, + "libc": "", + "location": "cpython-3.13.13+20260414-aarch64-apple-darwin-freethreaded+pgo+lto-full.tar.zst", + "microarch": "", + "os": "darwin", + "python_version": "3.13.13", + "sha256": "1111111111111111111111111111111111111111111111111111111111111111", + "vendor": "apple", + }) + +_tests.append(_test_parse_runtime_manifest) + +def parse_runtime_manifest_test_suite(name): + """Defines the test suite for manifest parsing. + + Args: + name: The name of the test suite. + """ + test_suite( + name = name, + tests = _tests, + ) diff --git a/tests/python_bzlmod_ext/runtime_manifests_tests.bzl b/tests/python_bzlmod_ext/runtime_manifests_tests.bzl new file mode 100644 index 0000000000..6de0ff93c2 --- /dev/null +++ b/tests/python_bzlmod_ext/runtime_manifests_tests.bzl @@ -0,0 +1,162 @@ +"""Starlark unit tests for dynamic toolchain registration via manifests.""" + +load("@rules_testing//lib:analysis_test.bzl", "analysis_test") +load("@rules_testing//lib:test_suite.bzl", "test_suite") +load("@rules_testing//lib:util.bzl", rt_util = "util") +load("//python/private:python.bzl", "parse_modules") # buildifier: disable=bzl-visibility +load("//python/private:repo_utils.bzl", "repo_utils") # buildifier: disable=bzl-visibility +load("//tests/support/mocks:python_ext.bzl", "python_ext") # buildifier: disable=bzl-visibility + +_tests = [] + +_mock_logger = repo_utils.logger( + name = "mock", + verbosity_level = "ERROR", +) + +def _test_dynamic_manifest_toolchains(name): + rt_util.helper_target( + native.filegroup, + name = name + "_subject", + ) + analysis_test( + name = name, + target = name + "_subject", + impl = _test_dynamic_manifest_toolchains_impl, + ) + +def _test_dynamic_manifest_toolchains_impl(env, target): + _ = target # @unused + + # Construct Bzlmod mock module locally inside the test execution block. + # We test using virtual patch version "3.11.99" (not present in TOOL_VERSIONS) + # so that the populated config contains ONLY our dynamically parsed manifest keys + # without any pre-populated multi-platform templates, allowing exact dictionary match! + root_module = python_ext.module( + name = "runtime_manifests", + override = [ + python_ext.override( + add_runtime_manifest_urls = [ + "https://github.com/astral-sh/python-build-standalone/releases/download/20260414/SHA256SUMS", + ], + runtime_manifest_sha = "ce18fdfd47c66830a40ea9b9e314a14b1636bbfd684501bc5ca1fc6d55a7933f", + register_all_versions = True, + ), + ], + defaults = [ + python_ext.defaults( + python_version = "3.11.99", + ), + ], + ) + + # Pre-populate mock_files directly to bypass download output struct key mismatch in mock read lookups. + mock_mctx = python_ext.mctx( + modules = [root_module], + mock_files = { + "runtime_manifest": """ +01e607cf764b97d4d5d6f69fd1ff3d8a9a162513dde5c39e98260fce40fe220a cpython-3.11.99+20260414-x86_64-unknown-linux-gnu-pgo+lto-full.tar.zst +8b14030dd3af9ea7f7c51b4c90feb04afd8a8f45435727e67b875270bd08f3bc cpython-3.11.99+20260414-x86_64-unknown-linux-gnu-install_only.tar.gz +""", + }, + ) + + res = parse_modules( + module_ctx = mock_mctx, + logger = _mock_logger, + ) + + tool_versions = res.config.default["tool_versions"] + env.expect.that_bool("3.11.99" in tool_versions).equals(True) + + version_info = tool_versions["3.11.99"] + + # Assert on the entire dictionary at once! + env.expect.that_dict(version_info).contains_exactly({ + "sha256": { + "x86_64-unknown-linux-gnu": "8b14030dd3af9ea7f7c51b4c90feb04afd8a8f45435727e67b875270bd08f3bc", + }, + "strip_prefix": { + "x86_64-unknown-linux-gnu": "python", + }, + "url": { + "x86_64-unknown-linux-gnu": [ + "https://github.com/astral-sh/python-build-standalone/releases/download/20260414/cpython-3.11.99+20260414-x86_64-unknown-linux-gnu-install_only.tar.gz", + ], + }, + }) + +_tests.append(_test_dynamic_manifest_toolchains) + +def _test_dynamic_manifest_files(name): + rt_util.helper_target( + native.filegroup, + name = name + "_subject", + ) + analysis_test( + name = name, + target = name + "_subject", + impl = _test_dynamic_manifest_files_impl, + ) + +def _test_dynamic_manifest_files_impl(env, target): + _ = target # @unused + + root_module = python_ext.module( + name = "runtime_manifests", + override = [ + python_ext.override( + add_runtime_manifest_files = [ + Label("//:SHA256SUMS"), + ], + base_urls = ["https://example.com/dl"], + register_all_versions = True, + ), + ], + defaults = [ + python_ext.defaults( + python_version = "3.12.99", + ), + ], + ) + + mock_mctx = python_ext.mctx( + modules = [root_module], + mock_files = { + str(Label("//:SHA256SUMS")): """ +01e607cf764b97d4d5d6f69fd1ff3d8a9a162513dde5c39e98260fce40fe220a cpython-3.12.99+20260414-x86_64-unknown-linux-gnu-pgo+lto-full.tar.zst +""", + }, + ) + + res = parse_modules( + module_ctx = mock_mctx, + logger = _mock_logger, + ) + + tool_versions = res.config.default["tool_versions"] + env.expect.that_bool("3.12.99" in tool_versions).equals(True) + + version_info = tool_versions["3.12.99"] + + env.expect.that_dict(version_info).contains_exactly({ + "sha256": { + "x86_64-unknown-linux-gnu": "01e607cf764b97d4d5d6f69fd1ff3d8a9a162513dde5c39e98260fce40fe220a", + }, + "strip_prefix": { + "x86_64-unknown-linux-gnu": "python/install", + }, + "url": { + "x86_64-unknown-linux-gnu": [ + "https://example.com/dl/cpython-3.12.99+20260414-x86_64-unknown-linux-gnu-pgo+lto-full.tar.zst", + ], + }, + }) + +_tests.append(_test_dynamic_manifest_files) + +def runtime_manifests_test_suite(name): + test_suite( + name = name, + tests = _tests, + ) diff --git a/tests/python_bzlmod_ext/test_helpers.bzl b/tests/python_bzlmod_ext/test_helpers.bzl new file mode 100644 index 0000000000..78ad57c110 --- /dev/null +++ b/tests/python_bzlmod_ext/test_helpers.bzl @@ -0,0 +1,48 @@ +# Copyright 2026 The Bazel Authors. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Helpers to conditionally register tests depending on Bzlmod enablement.""" + +load("//python/private:bzlmod_enabled.bzl", "BZLMOD_ENABLED") # buildifier: disable=bzl-visibility +load(":parse_runtime_manifest_tests.bzl", "parse_runtime_manifest_test_suite") +load(":runtime_manifests_tests.bzl", "runtime_manifests_test_suite") + +def register_python_bzlmod_ext_tests(name, parse_runtime_manifest_name, runtime_manifests_name): + """Registers the Bzlmod extension tests if Bzlmod is enabled, otherwise defines empty test_suites. + + Args: + name: The name of the master test_suite target. + parse_runtime_manifest_name: The name of the parse_runtime_manifest test target. + runtime_manifests_name: The name of the runtime_manifests test target. + """ + if BZLMOD_ENABLED: + parse_runtime_manifest_test_suite(name = parse_runtime_manifest_name) + runtime_manifests_test_suite(name = runtime_manifests_name) + else: + native.test_suite( + name = parse_runtime_manifest_name, + tests = [], + ) + native.test_suite( + name = runtime_manifests_name, + tests = [], + ) + + native.test_suite( + name = name, + tests = [ + parse_runtime_manifest_name, + runtime_manifests_name, + ], + ) 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/repo_utils/repo_utils_test.bzl b/tests/repo_utils/repo_utils_test.bzl index ce9e48b5a6..56d125724d 100644 --- a/tests/repo_utils/repo_utils_test.bzl +++ b/tests/repo_utils/repo_utils_test.bzl @@ -2,10 +2,21 @@ load("@rules_testing//lib:test_suite.bzl", "test_suite") load("//python/private:repo_utils.bzl", "repo_utils") # buildifier: disable=bzl-visibility -load("//tests/support:mocks.bzl", "mocks") +load("//tests/support/mocks:mocks.bzl", "mocks") _tests = [] +def _make_rctx(*, os_name, mock_extracts = None, mock_files = None): + return mocks.rctx( + attr = { + "name": "unit", + "_rule_name": "test_rule", + }, + mock_files = mock_files, + mock_extracts = mock_extracts, + os_name = os_name, + ) + def _test_get_platforms_os_name(env): mock_mrctx = mocks.rctx(os_name = "Mac OS X") got = repo_utils.get_platforms_os_name(mock_mrctx) @@ -50,6 +61,68 @@ def _test_is_relative_to(env): _tests.append(_test_is_relative_to) +def _test_extract_calls_chmod_when_enabled(env): + mock_rctx = _make_rctx( + os_name = "linux", + mock_extracts = {"test.whl": {"f1": "c1"}}, + ) + + repo_utils.extract( + mock_rctx, + archive = mock_rctx.path("test.whl"), + output = "out", + supports_whl_extraction = True, + extract_needs_chmod = True, + ) + + env.expect.that_bool(len(mock_rctx.execute_calls) > 0).equals(True) + env.expect.that_str(mock_rctx.execute_calls[0][0]).equals("chmod") + +_tests.append(_test_extract_calls_chmod_when_enabled) + +def _test_extract_skips_chmod_when_disabled(env): + mock_rctx = _make_rctx( + os_name = "linux", + mock_extracts = {"test.whl": {"f1": "c1"}}, + ) + + repo_utils.extract( + mock_rctx, + archive = mock_rctx.path("test.whl"), + output = "out", + supports_whl_extraction = True, + extract_needs_chmod = False, + ) + + env.expect.that_collection(mock_rctx.execute_calls).contains_exactly([]) + +_tests.append(_test_extract_skips_chmod_when_disabled) + +def _test_maybe_fix_permissions_calls_chmod_on_linux(env): + mock_rctx = _make_rctx(os_name = "linux") + + repo_utils.maybe_fix_permissions( + mock_rctx, + whl_path = mock_rctx.path("test.whl"), + ) + + env.expect.that_bool(len(mock_rctx.execute_calls) > 0).equals(True) + env.expect.that_str(mock_rctx.execute_calls[0][0]).equals("chmod") + +_tests.append(_test_maybe_fix_permissions_calls_chmod_on_linux) + +def _test_maybe_fix_permissions_skips_on_windows(env): + mock_rctx = _make_rctx(os_name = "windows") + + repo_utils.maybe_fix_permissions( + mock_rctx, + whl_path = mock_rctx.path("test.whl"), + ) + + env.expect.that_collection(mock_rctx.execute_calls).contains_exactly([]) + +_tests.append(_test_maybe_fix_permissions_skips_on_windows) + def repo_utils_test_suite(name): """Create the test suite. 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 47c964631f..78e2554aa3 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 @@ -28,10 +30,10 @@ 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 + assert r is not None # type assert + self.assertRaises(ValueError, lambda: r.Rlocation(None)) # 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)) # pyrefly: ignore[bad-argument-type] self.assertRaisesRegex( ValueError, "is not normalized", lambda: r.Rlocation("../foo") ) @@ -67,11 +69,11 @@ def testRlocationArgumentValidation(self) -> None: def testRlocationWithData(self) -> None: r = runfiles.Create() - assert r is not None # mypy doesn't understand the unittest api. + assert r is not None # type assert 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) @@ -84,7 +86,7 @@ def testCreatesManifestBasedRunfiles(self) -> None: "TEST_SRCDIR": "always ignored", } ) - assert r is not None # mypy doesn't understand the unittest api. + assert r is not None # type assert self.assertEqual(r.Rlocation("a/b"), "c/d") self.assertIsNone(r.Rlocation("foo")) @@ -96,7 +98,7 @@ def testManifestBasedRunfilesEnvVars(self) -> None: "TEST_SRCDIR": "always ignored", } ) - assert r is not None # mypy doesn't understand the unittest api. + assert r is not None # type assert self.assertDictEqual( r.EnvVars(), { @@ -113,7 +115,7 @@ def testManifestBasedRunfilesEnvVars(self) -> None: "TEST_SRCDIR": "always ignored", } ) - assert r is not None # mypy doesn't understand the unittest api. + assert r is not None # type assert self.assertDictEqual( r.EnvVars(), { @@ -134,7 +136,7 @@ def testManifestBasedRunfilesEnvVars(self) -> None: "TEST_SRCDIR": "always ignored", } ) - assert r is not None # mypy doesn't understand the unittest api. + assert r is not None # type assert self.assertDictEqual( r.EnvVars(), { @@ -151,7 +153,7 @@ def testCreatesDirectoryBasedRunfiles(self) -> None: "TEST_SRCDIR": "always ignored", } ) - assert r is not None # mypy doesn't understand the unittest api. + assert r is not None # type assert self.assertEqual(r.Rlocation("a/b"), "runfiles/dir/a/b") self.assertEqual(r.Rlocation("foo"), "runfiles/dir/foo") @@ -162,7 +164,7 @@ def testDirectoryBasedRunfilesEnvVars(self) -> None: "TEST_SRCDIR": "always ignored", } ) - assert r is not None # mypy doesn't understand the unittest api. + assert r is not None # type assert self.assertDictEqual( r.EnvVars(), { @@ -195,6 +197,62 @@ def testFailsToCreateAnyRunfilesBecauseEnvvarsAreNotDefined(self) -> None: self.assertIsNone(runfiles.Create({"TEST_SRCDIR": "always ignored"})) self.assertIsNone(runfiles.Create({"FOO": "bar"})) + def testCreatesManifestBasedRunfilesWithCreateOrRaise(self) -> None: + with _MockFile(contents=["a/b c/d"]) as mf: + r = runfiles.CreateOrRaise( + { + "RUNFILES_MANIFEST_FILE": mf.Path(), + "RUNFILES_DIR": "ignored when RUNFILES_MANIFEST_FILE has a value", + "TEST_SRCDIR": "always ignored", + } + ) + self.assertEqual(r.Rlocation("a/b"), "c/d") + self.assertIsNone(r.Rlocation("foo")) + + r_class = runfiles.Runfiles.CreateOrRaise( + { + "RUNFILES_MANIFEST_FILE": mf.Path(), + } + ) + self.assertEqual(r_class.Rlocation("a/b"), "c/d") + + def testCreatesDirectoryBasedRunfilesWithCreateOrRaise(self) -> None: + r = runfiles.CreateOrRaise( + { + "RUNFILES_DIR": "runfiles/dir", + "TEST_SRCDIR": "always ignored", + } + ) + self.assertEqual(r.Rlocation("a/b"), "runfiles/dir/a/b") + self.assertEqual(r.Rlocation("foo"), "runfiles/dir/foo") + + r_class = runfiles.Runfiles.CreateOrRaise( + { + "RUNFILES_DIR": "runfiles/dir", + } + ) + self.assertEqual(r_class.Rlocation("a/b"), "runfiles/dir/a/b") + + def testFailsToCreateManifestBasedBecauseManifestDoesNotExistWithCreateOrRaise( + self, + ) -> None: + def _Run(): + runfiles.CreateOrRaise({"RUNFILES_MANIFEST_FILE": "non-existing path"}) + + self.assertRaisesRegex(IOError, "non-existing path", _Run) + + def testFailsToCreateAnyRunfilesWithCreateOrRaise(self) -> None: + with self.assertRaises(RuntimeError): + runfiles.CreateOrRaise({"TEST_SRCDIR": "always ignored"}) + with self.assertRaises(RuntimeError): + runfiles.CreateOrRaise({"FOO": "bar"}) + with self.assertRaises(RuntimeError): + runfiles.CreateOrRaise({}) + with self.assertRaises(RuntimeError): + runfiles.Runfiles.CreateOrRaise({"TEST_SRCDIR": "always ignored"}) + with self.assertRaises(RuntimeError): + runfiles.Runfiles.CreateOrRaise({}) + def testManifestBasedRlocation(self) -> None: with _MockFile( contents=[ @@ -705,7 +763,7 @@ def testCurrentRepository(self) -> None: else: expected = "rules_python" r = runfiles.Create() - assert r is not None # mypy doesn't understand the unittest api. + assert r is not None # type assert self.assertEqual(r.CurrentRepository(), expected) @staticmethod @@ -715,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") @@ -739,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/BUILD.bazel b/tests/runtime_env_toolchain/BUILD.bazel index f1bda251f9..42bd9cfc7e 100644 --- a/tests/runtime_env_toolchain/BUILD.bazel +++ b/tests/runtime_env_toolchain/BUILD.bazel @@ -44,6 +44,7 @@ py_reconfig_test( py_reconfig_test( name = "bootstrap_script_test", srcs = ["toolchain_runs_test.py"], + args = ["'argument with spaces'"], bootstrap_impl = "script", data = [ "//tests/support:current_build_settings", diff --git a/tests/runtime_env_toolchain/toolchain_runs_test.py b/tests/runtime_env_toolchain/toolchain_runs_test.py index 13b5775ff0..50c6878c13 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( @@ -26,6 +25,7 @@ def test_ran(self): ) if settings["bootstrap_impl"] == "script": + self.assertEqual(sys.argv[1:], ["argument with spaces"]) # Verify we're running in a venv self.assertNotEqual(sys.prefix, sys.base_prefix) # .venv/ occurs for a build-time venv. @@ -35,4 +35,4 @@ def test_ran(self): if __name__ == "__main__": - unittest.main() + unittest.main(argv=sys.argv[:1]) diff --git a/tests/support/cc_toolchains/BUILD.bazel b/tests/support/cc_toolchains/BUILD.bazel index 1c1a714626..a74a8016fc 100644 --- a/tests/support/cc_toolchains/BUILD.bazel +++ b/tests/support/cc_toolchains/BUILD.bazel @@ -57,7 +57,9 @@ py_cc_toolchain( headers = ":py_headers", headers_abi3 = ":py_headers_abi3", libs = ":fake_libs", + platform_machine = "x86_64", python_version = "3.999", + sys_platform = "linux", tags = PREVENT_IMPLICIT_BUILDING_TAGS, ) diff --git a/tests/support/explicit_init_py_test.bzl b/tests/support/explicit_init_py_test.bzl new file mode 100644 index 0000000000..abc32a9441 --- /dev/null +++ b/tests/support/explicit_init_py_test.bzl @@ -0,0 +1,50 @@ +"""Parameterized analysis test for __init__.py generation behavior.""" + +load("@rules_testing//lib:analysis_test.bzl", "analysis_test") +load("@rules_testing//lib:truth.bzl", "matching") +load("@rules_testing//lib:util.bzl", rt_util = "util") +load("//python:py_binary.bzl", "py_binary") +load("//python/private:bzlmod_enabled.bzl", "BZLMOD_ENABLED") # buildifier: disable=bzl-visibility + +def _explicit_init_py_test_impl(env, target): + empty_filenames = target[DefaultInfo].default_runfiles.empty_filenames.to_list() + collection = env.expect.that_collection( + empty_filenames, + container_name = "empty_filenames", + ) + if env.ctx.attr.expect_generated_init: + collection.contains_predicate(matching.str_endswith("__init__.py")) + else: + collection.not_contains_predicate(matching.str_endswith("__init__.py")) + +def explicit_init_py_test(*, name, main, expect_generated_init, legacy_create_init = -1, **kwargs): + """Test that verifies whether __init__.py is generated for a py_binary. + + Args: + name: Test name. + main: Source file for the py_binary subject. + expect_generated_init: Whether __init__.py generation is expected. + legacy_create_init: Value for the legacy_create_init attribute (-1, 0, or 1). + **kwargs: Additional args forwarded to the test rule (e.g. tags). + """ + if not BZLMOD_ENABLED: + native.test_suite(name = name, tests = []) + return + + subject_name = name + "_subject" + rt_util.helper_target( + py_binary, + name = subject_name, + srcs = [main], + main = main, + legacy_create_init = legacy_create_init, + ) + analysis_test( + name = name, + target = subject_name, + impl = _explicit_init_py_test_impl, + attrs = { + "expect_generated_init": attr.bool(mandatory = True), + }, + attr_values = dict(kwargs, expect_generated_init = expect_generated_init), + ) diff --git a/tests/support/mocks.bzl b/tests/support/mocks.bzl deleted file mode 100644 index 2a4ccd0fc4..0000000000 --- a/tests/support/mocks.bzl +++ /dev/null @@ -1,31 +0,0 @@ -"""Mocks for testing.""" - -def _rctx(os_name = "linux", os_arch = "x86_64", environ = None, **kwargs): - """Creates a mock of repository_ctx or module_ctx. - - Args: - os_name: The OS name to mock (e.g., "linux", "Mac OS X", "windows"). - os_arch: The OS architecture to mock (e.g., "x86_64", "aarch64"). - environ: A dictionary representing the environment variables. - **kwargs: Additional attributes to add to the mock struct. - - Returns: - A struct mocking repository_ctx. - """ - if environ == None: - environ = {} - - attrs = { - "getenv": environ.get, - "os": struct( - name = os_name, - arch = os_arch, - ), - } - attrs.update(kwargs) - - return struct(**attrs) - -mocks = struct( - rctx = _rctx, -) diff --git a/tests/support/mocks/mocks.bzl b/tests/support/mocks/mocks.bzl index 48f8f95830..6c79b1f4ff 100644 --- a/tests/support/mocks/mocks.bzl +++ b/tests/support/mocks/mocks.bzl @@ -105,9 +105,12 @@ def _module_new(name, *, is_root = False, **tags): def _mctx_read(self, x, watch = None): _ = watch # @unused path_str = x._path if hasattr(x, "_path") else str(x) - if path_str not in self.mock_files: - fail("File not found in mock_files: " + path_str) - return self.mock_files[path_str] + if path_str in self.mock_files: + return self.mock_files[path_str] + for k, v in self.mock_files.items(): + if k in path_str or k in path_str.replace(":", "/"): + return v + fail("File not found in mock_files: " + path_str) def _mctx_path(self, x): return _path_new(str(x), self.mock_files) @@ -182,7 +185,7 @@ def _mctx_download( return struct(success = True, wait = lambda: struct(success = True)) return struct(success = False, wait = lambda: struct(success = False)) -def _mctx_report_progress(self, message): +def _mrctx_report_progress(self, message): self.report_progress_calls.append(message) return None @@ -256,7 +259,7 @@ def _mctx_new( path = lambda *a, **k: _mctx_path(self, *a, **k), read = lambda *a, **k: _mctx_read(self, *a, **k), download = lambda *a, **k: _mctx_download(self, *a, **k), - report_progress = lambda *a, **k: _mctx_report_progress(self, *a, **k), + report_progress = lambda *a, **k: _mrctx_report_progress(self, *a, **k), add_module = lambda **k: _mctx_add_module(self, **k), ) return self @@ -436,6 +439,7 @@ def _rctx_execute( environment, custom_reporter, ) # @unused + self.execute_calls.append(arguments) return struct(return_code = 0, stdout = "", stderr = "") def _rctx_symlink(self, target, link_name): @@ -482,7 +486,10 @@ def _rctx_new( mock_which = mock_which, mock_downloads = mock_downloads, mock_extracts = mock_extracts, + execute_calls = [], + report_progress_calls = [], attr = struct(**attr), + name = attr.get("name", "mock"), os = struct( name = os_name, arch = arch_name, @@ -491,10 +498,12 @@ def _rctx_new( path = lambda *a, **k: _rctx_path(self, *a, **k), read = lambda *a, **k: _rctx_read(self, *a, **k), file = lambda *a, **k: _rctx_file(self, *a, **k), + getenv = environ.get, template = lambda *a, **k: _rctx_template(self, *a, **k), which = lambda *a, **k: _rctx_which(self, *a, **k), download = lambda *a, **k: _rctx_download(self, *a, **k), extract = lambda *a, **k: _rctx_extract(self, *a, **k), + delete = lambda x: True, download_and_extract = lambda *a, **k: _rctx_download_and_extract( self, *a, @@ -502,6 +511,7 @@ def _rctx_new( ), execute = lambda *a, **k: _rctx_execute(self, *a, **k), symlink = lambda *a, **k: _rctx_symlink(self, *a, **k), + report_progress = lambda *a, **k: _mrctx_report_progress(self, *a, **k), ) return self diff --git a/tests/support/mocks/python_ext.bzl b/tests/support/mocks/python_ext.bzl index f20a6c7263..dc3ac41f8d 100644 --- a/tests/support/mocks/python_ext.bzl +++ b/tests/support/mocks/python_ext.bzl @@ -26,10 +26,11 @@ def _module(name = "rules_python", is_root = True, **tags): def _override(**kwargs): """Creates a mock python.override tag with default values.""" attrs = { + "add_runtime_manifest_files": [], "add_runtime_manifest_urls": [], "add_target_settings": [], "available_python_versions": [], - "base_url": "https://github.com/astral-sh/python-build-standalone/releases/download", + "base_urls": ["https://github.com/astral-sh/python-build-standalone/releases/download"], "ignore_root_user_error": True, "minor_mapping": {}, "register_all_versions": False, @@ -92,8 +93,45 @@ def _toolchain(**kwargs): attrs.update(kwargs) return mocks.tag(**attrs) +_DEFAULT_RUNTIMES_MANIFEST = """ +87275619c2706affa4d1090d2ca3dad354b6d69f8b85dbfafe38785870751b9a 20251031/cpython-3.9.25+20251031-x86_64-unknown-linux-gnu-install_only.tar.gz +6112d46355857680b81849764a6cf9f38cc4cd0d1cf29d432bc12fe5aeedf9d0 20260414/cpython-3.10.20+20260414-x86_64-unknown-linux-gnu-install_only.tar.gz +1111111111111111111111111111111111111111111111111111111111111111 20241016/cpython-3.10.15+20241016-x86_64-unknown-linux-gnu-install_only.tar.gz +2222222222222222222222222222222222222222222222222222222222222222 20240224/cpython-3.10.13+20240224-x86_64-unknown-linux-gnu-install_only.tar.gz +0000000000000000000000000000000000000000000000000000000000000000 20260414/cpython-3.11.15+20260414-x86_64-unknown-linux-gnu-install_only.tar.gz +3333333333333333333333333333333333333333333333333333333333333333 20241016/cpython-3.11.10+20241016-x86_64-unknown-linux-gnu-install_only.tar.gz +4444444444444444444444444444444444444444444444444444444444444444 20230116/cpython-3.11.2+20230116-x86_64-unknown-linux-gnu-install_only.tar.gz +5555555555555555555555555555555555555555555555555555555555555555 20230116/cpython-3.11.1+20230116-x86_64-unknown-linux-gnu-install_only.tar.gz +6666666666666666666666666666666666666666666666666666666666666666 20260414/cpython-3.12.13+20260414-x86_64-unknown-linux-gnu-install_only.tar.gz +7777777777777777777777777777777777777777777777777777777777777777 20240726/cpython-3.12.4+20240726-x86_64-unknown-linux-gnu-install_only.tar.gz +8888888888888888888888888888888888888888888888888888888888888888 20260414/cpython-3.13.13+20260414-x86_64-unknown-linux-gnu-install_only.tar.gz +9999999999999999999999999999999999999999999999999999999999999999 20241016/cpython-3.13.0+20241016-x86_64-unknown-linux-gnu-install_only.tar.gz +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 20260414/cpython-3.14.4+20260414-x86_64-unknown-linux-gnu-install_only.tar.gz +bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 20251031/cpython-3.14.0+20251031-x86_64-unknown-linux-gnu-install_only.tar.gz +cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc 20260414/cpython-3.15.0a8+20260414-x86_64-unknown-linux-gnu-install_only.tar.gz +dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd 20241205/cpython-3.13.1+20241205-x86_64-unknown-linux-gnu-install_only.tar.gz +""" + +def _mctx(*args, **kwargs): + """Creates a mock module_ctx pre-populated with the default runtimes manifest. + + Args: + *args: Positional arguments to pass to mocks.mctx. + **kwargs: Keyword arguments to pass to mocks.mctx. + + Returns: + A mock module_ctx struct. + """ + mock_files = { + "python/private/runtimes_manifest.txt": _DEFAULT_RUNTIMES_MANIFEST, + } + mock_files.update(kwargs.pop("mock_files", {})) + kwargs["mock_files"] = mock_files + return mocks.mctx(*args, **kwargs) + python_ext = struct( defaults = _defaults, + mctx = _mctx, module = _module, override = _override, single_version_override = _single_version_override, diff --git a/tests/support/py_cc_toolchain_info_subject.bzl b/tests/support/py_cc_toolchain_info_subject.bzl index 3820e04e90..d6aa258a64 100644 --- a/tests/support/py_cc_toolchain_info_subject.bzl +++ b/tests/support/py_cc_toolchain_info_subject.bzl @@ -18,15 +18,26 @@ load("@rules_testing//lib:truth.bzl", "subjects") def _py_cc_toolchain_info_subject_new(info, *, meta): # buildifier: disable=uninitialized public = struct( + abi_flags = lambda *a, **k: _py_cc_toolchain_info_subject_abi_flags(self, *a, **k), headers = lambda *a, **k: _py_cc_toolchain_info_subject_headers(self, *a, **k), headers_abi3 = lambda *a, **k: _py_cc_toolchain_info_subject_headers_abi3(self, *a, **k), libs = lambda *a, **k: _py_cc_toolchain_info_subject_libs(self, *a, **k), + platform_machine = lambda *a, **k: _py_cc_toolchain_info_subject_platform_machine(self, *a, **k), + platform_tag = lambda *a, **k: _py_cc_toolchain_info_subject_platform_tag(self, *a, **k), python_version = lambda *a, **k: _py_cc_toolchain_info_subject_python_version(self, *a, **k), + soabi = lambda *a, **k: _py_cc_toolchain_info_subject_soabi(self, *a, **k), + sys_platform = lambda *a, **k: _py_cc_toolchain_info_subject_sys_platform(self, *a, **k), actual = info, ) self = struct(actual = info, meta = meta) return public +def _py_cc_toolchain_info_subject_abi_flags(self): + return subjects.str( + self.actual.abi_flags, + meta = self.meta.derive("abi_flags()"), + ) + def _py_cc_toolchain_info_subject_headers(self): return subjects.struct( self.actual.headers, @@ -54,12 +65,36 @@ def _py_cc_toolchain_info_subject_libs(self): ), ) +def _py_cc_toolchain_info_subject_platform_machine(self): + return subjects.str( + self.actual.platform_machine, + meta = self.meta.derive("platform_machine()"), + ) + +def _py_cc_toolchain_info_subject_platform_tag(self): + return subjects.str( + self.actual.platform_tag, + meta = self.meta.derive("platform_tag()"), + ) + def _py_cc_toolchain_info_subject_python_version(self): return subjects.str( self.actual.python_version, meta = self.meta.derive("python_version()"), ) +def _py_cc_toolchain_info_subject_soabi(self): + return subjects.str( + self.actual.soabi, + meta = self.meta.derive("soabi()"), + ) + +def _py_cc_toolchain_info_subject_sys_platform(self): + return subjects.str( + self.actual.sys_platform, + meta = self.meta.derive("sys_platform()"), + ) + # Disable this to aid doc generation # buildifier: disable=name-conventions PyCcToolchainInfoSubject = struct( diff --git a/tests/support/py_runtime_info_subject.bzl b/tests/support/py_runtime_info_subject.bzl index 541d4d9e18..0c6c672743 100644 --- a/tests/support/py_runtime_info_subject.bzl +++ b/tests/support/py_runtime_info_subject.bzl @@ -37,6 +37,9 @@ def py_runtime_info_subject(info, *, meta): coverage_tool = lambda *a, **k: _py_runtime_info_subject_coverage_tool(self, *a, **k), files = lambda *a, **k: _py_runtime_info_subject_files(self, *a, **k), interpreter = lambda *a, **k: _py_runtime_info_subject_interpreter(self, *a, **k), + interpreter_files_to_run = lambda *a, **k: ( + _py_runtime_info_subject_interpreter_files_to_run(self, *a, **k) + ), interpreter_path = lambda *a, **k: _py_runtime_info_subject_interpreter_path(self, *a, **k), interpreter_version_info = lambda *a, **k: _py_runtime_info_subject_interpreter_version_info(self, *a, **k), python_version = lambda *a, **k: _py_runtime_info_subject_python_version(self, *a, **k), @@ -84,6 +87,15 @@ def _py_runtime_info_subject_interpreter(self): meta = self.meta.derive("interpreter()"), ) +def _py_runtime_info_subject_interpreter_files_to_run(self): + return subjects.struct( + self.actual.interpreter_files_to_run, + attrs = dict( + executable = subjects.file, + ), + meta = self.meta.derive("interpreter_files_to_run()"), + ) + def _py_runtime_info_subject_interpreter_path(self): return subjects.str( self.actual.interpreter_path, diff --git a/tests/support/pyrefly/BUILD.bazel b/tests/support/pyrefly/BUILD.bazel new file mode 100644 index 0000000000..447af06f8f --- /dev/null +++ b/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/tests/support/pyrefly/pyrefly.bzl b/tests/support/pyrefly/pyrefly.bzl new file mode 100644 index 0000000000..ac0d7ea331 --- /dev/null +++ b/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/support/pytest_test/BUILD.bazel b/tests/support/pytest_test/BUILD.bazel new file mode 100644 index 0000000000..4e6f6dd168 --- /dev/null +++ b/tests/support/pytest_test/BUILD.bazel @@ -0,0 +1,29 @@ +load("@bazel_skylib//:bzl_library.bzl", "bzl_library") + +package(default_visibility = ["//:__subpackages__"]) + +filegroup( + name = "bootstrap_template", + srcs = ["pytest_bootstrap_template.py"], +) + +# keep +bzl_library( + name = "pytest_test", + srcs = ["pytest_test.bzl"], + deps = [ + "//python:py_test", + ], +) + +# These aliases are used to avoid duplicate targets in the deps list +alias( + name = "default_pytest", + actual = "@pypi//pytest", +) + +# These aliases are used to avoid duplicate targets in the deps list +alias( + name = "default_pytest_bazel", + actual = "@pypi//pytest_bazel", +) diff --git a/tests/support/pytest_test/pytest_bootstrap_template.py b/tests/support/pytest_test/pytest_bootstrap_template.py new file mode 100644 index 0000000000..2587353306 --- /dev/null +++ b/tests/support/pytest_test/pytest_bootstrap_template.py @@ -0,0 +1,8 @@ +import sys + +import pytest_bazel # pyrefly: ignore[missing-import] + +TEST_FILES = """%TEST_FILES%""".splitlines() + +args = sys.argv[1:] + TEST_FILES +sys.exit(pytest_bazel.main(args)) diff --git a/tests/support/pytest_test/pytest_test.bzl b/tests/support/pytest_test/pytest_test.bzl new file mode 100644 index 0000000000..893b22e5ce --- /dev/null +++ b/tests/support/pytest_test/pytest_test.bzl @@ -0,0 +1,74 @@ +"""pytest_test rule implementation.""" + +load("//python:py_test.bzl", "py_test") + +_DEFAULT_PYTEST = Label("//tests/support/pytest_test:default_pytest") +_DEFAULT_PYTEST_BAZEL = Label("//tests/support/pytest_test:default_pytest_bazel") + +def pytest_test( + *, + name, + srcs, + pytest = None, + pytest_bazel = None, + **kwargs): + """Run pytest tests. + + Args: + name: A unique name for this target. + srcs: List of source files (test files). These are the files that + pytest will run as tests. + pytest: The pytest target to use. Defaults to @pypi//pytest. + pytest_bazel: The pytest-bazel target to use. Defaults to + @pypi//pytest_bazel. + **kwargs: Additional arguments passed to py_test. Note that `main` is + not a supported argument. + """ + if pytest == None: + pytest = _DEFAULT_PYTEST + if pytest_bazel == None: + pytest_bazel = _DEFAULT_PYTEST_BAZEL + + bootstrap_target = name + "_bootstrap" + main_file = name + "_boot.py" + _write_pytest_bootstrap( + name = bootstrap_target, + srcs = srcs, + output_name = main_file, + ) + + py_test( + name = name, + main = main_file, + srcs = [bootstrap_target] + srcs, + deps = kwargs.pop("deps", []) + [ + pytest, + pytest_bazel, + ], + **kwargs + ) + +def _write_pytest_bootstrap_impl(ctx): + output = ctx.actions.declare_file(ctx.attr.output_name) + test_files = "\n".join([f.short_path for f in ctx.files.srcs]) + + ctx.actions.expand_template( + output = output, + template = ctx.file._bootstrap_template, + substitutions = { + "%TEST_FILES%": test_files, + }, + ) + return [DefaultInfo(files = depset([output]))] + +_write_pytest_bootstrap = rule( + implementation = _write_pytest_bootstrap_impl, + attrs = { + "output_name": attr.string(mandatory = True), + "srcs": attr.label_list(allow_files = True), + "_bootstrap_template": attr.label( + default = "//tests/support/pytest_test:bootstrap_template", + allow_single_file = True, + ), + }, +) diff --git a/tests/toolchains/BUILD.bazel b/tests/toolchains/BUILD.bazel index f32ab6f056..b336a06269 100644 --- a/tests/toolchains/BUILD.bazel +++ b/tests/toolchains/BUILD.bazel @@ -13,6 +13,7 @@ # limitations under the License. load("@bazel_skylib//rules:build_test.bzl", "build_test") +load("@bazel_skylib//rules:diff_test.bzl", "diff_test") load("//python/private:bzlmod_enabled.bzl", "BZLMOD_ENABLED") # buildifier: disable=bzl-visibility load("//tests/support:py_reconfig.bzl", "py_reconfig_test") load(":defs.bzl", "define_toolchain_tests") @@ -38,3 +39,18 @@ build_test( "@python_3_11//:python_headers", ], ) + +# Verify that runtimes_manifest_workspace.bzl exactly matches runtimes_manifest.txt +genrule( + name = "gen_expected_runtimes_manifest_workspace", + srcs = ["//python/private:runtimes_manifest.txt"], + outs = ["expected_runtimes_manifest_workspace.bzl"], + cmd = "$(execpath //python/private:sync_runtimes_manifest_workspace) $(location //python/private:runtimes_manifest.txt) $@", + tools = ["//python/private:sync_runtimes_manifest_workspace"], +) + +diff_test( + name = "runtimes_manifest_workspace_sync_test", + file1 = "//python/private:runtimes_manifest_workspace.bzl", + file2 = ":expected_runtimes_manifest_workspace.bzl", +) 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/toolchains/transitions/transitions_tests.bzl b/tests/toolchains/transitions/transitions_tests.bzl index 0cd79b373b..81ce1e68cc 100644 --- a/tests/toolchains/transitions/transitions_tests.bzl +++ b/tests/toolchains/transitions/transitions_tests.bzl @@ -14,11 +14,10 @@ "" -load("@pythons_hub//:versions.bzl", "DEFAULT_PYTHON_VERSION", "MINOR_MAPPING") +load("@pythons_hub//:versions.bzl", "DEFAULT_PYTHON_VERSION", "MINOR_MAPPING", "PYTHON_VERSIONS") load("@rules_testing//lib:analysis_test.bzl", "analysis_test") load("@rules_testing//lib:test_suite.bzl", "test_suite") load("@rules_testing//lib:util.bzl", rt_util = "util") -load("//python:versions.bzl", "TOOL_VERSIONS") load("//python/private:bzlmod_enabled.bzl", "BZLMOD_ENABLED") # buildifier: disable=bzl-visibility load("//python/private:common_labels.bzl", "labels") # buildifier: disable=bzl-visibility load("//python/private:full_version.bzl", "full_version") # buildifier: disable=bzl-visibility @@ -142,7 +141,7 @@ def _test_full_version(name): name = name, tests = { v.replace(".", "_"): (v, v) - for v in TOOL_VERSIONS + for v in PYTHON_VERSIONS }, ) diff --git a/tests/tools/private/release/BUILD.bazel b/tests/tools/private/release/BUILD.bazel index 9f3bc0542a..dc02394960 100644 --- a/tests/tools/private/release/BUILD.bazel +++ b/tests/tools/private/release/BUILD.bazel @@ -1,10 +1,214 @@ -load("@rules_python//python:defs.bzl", "py_test") +load("//python:py_library.bzl", "py_library") +load("//tests/support:support.bzl", "NOT_WINDOWS") +load("//tests/support/pytest_test:pytest_test.bzl", "pytest_test") -py_test( +py_library( + name = "conftest", + testonly = True, + srcs = ["conftest.py"], + target_compatible_with = NOT_WINDOWS, + deps = [ + ":release_test_helper", + "//tools/private/release:release_lib", + "@pypi//pytest_mock", + ], +) + +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", + ], +) + +pytest_test( + name = "add_backports_test", + srcs = ["add_backports_test.py"], + target_compatible_with = NOT_WINDOWS, + deps = [ + ":conftest", + ":release_test_helper", + "//tools/private/release:release_lib", + ], +) + +pytest_test( + name = "changelog_news_test", + srcs = ["changelog_news_test.py"], + target_compatible_with = NOT_WINDOWS, + deps = [ + ":conftest", + ":release_test_helper", + "//tools/private/release:release_lib", + ], +) + +pytest_test( + name = "complete_prepare_test", + srcs = ["complete_prepare_test.py"], + target_compatible_with = NOT_WINDOWS, + deps = [ + ":conftest", + ":release_test_helper", + "//tools/private/release:release_lib", + ], +) + +pytest_test( + name = "complete_sync_changelog_test", + srcs = ["complete_sync_changelog_test.py"], + target_compatible_with = NOT_WINDOWS, + deps = [ + ":conftest", + ":release_test_helper", + "//tools/private/release:release_lib", + ], +) + +pytest_test( + name = "create_release_branch_test", + srcs = ["create_release_branch_test.py"], + target_compatible_with = NOT_WINDOWS, + deps = [ + ":conftest", + ":release_test_helper", + "//tools/private/release:release_lib", + ], +) + +pytest_test( + name = "git_test", + srcs = ["git_test.py"], + target_compatible_with = NOT_WINDOWS, + deps = [ + ":conftest", + ":release_test_helper", + "//tools/private/release:release_lib", + ], +) + +pytest_test( + name = "on_pr_merged_test", + srcs = ["on_pr_merged_test.py"], + target_compatible_with = NOT_WINDOWS, + deps = [ + ":conftest", + ":release_test_helper", + "//tools/private/release:release_lib", + ], +) + +pytest_test( + name = "promote_test", + srcs = ["promote_test.py"], + target_compatible_with = NOT_WINDOWS, + deps = [ + ":conftest", + ":release_test_helper", + "//tools/private/release:release_lib", + ], +) + +pytest_test( + name = "release_issue_test", + srcs = ["release_issue_test.py"], + target_compatible_with = NOT_WINDOWS, + deps = [ + ":conftest", + ":release_test_helper", + "//tools/private/release:release_lib", + ], +) + +pytest_test( name = "release_test", srcs = ["release_test.py"], + target_compatible_with = NOT_WINDOWS, + deps = [ + ":conftest", + ":release_test_helper", + "//tools/private/release:release_lib", + ], +) + +pytest_test( + name = "backport_create_releases_test", + srcs = ["backport_create_releases_test.py"], + target_compatible_with = NOT_WINDOWS, + deps = [ + ":conftest", + ":release_test_helper", + "//tools/private/release:release_lib", + ], +) + +pytest_test( + name = "prepare_test", + srcs = ["prepare_test.py"], + target_compatible_with = NOT_WINDOWS, + deps = [ + ":conftest", + ":release_test_helper", + "//tools/private/release:release_lib", + ], +) + +pytest_test( + name = "gh_test", + srcs = ["gh_test.py"], + target_compatible_with = NOT_WINDOWS, + deps = [ + ":conftest", + ":release_test_helper", + "//tools/private/release:release_lib", + ], +) + +pytest_test( + name = "backport_prepare_test", + srcs = ["backport_prepare_test.py"], + target_compatible_with = NOT_WINDOWS, + deps = [ + ":conftest", + ":release_test_helper", + "//tools/private/release:release_lib", + ], +) + +pytest_test( + name = "process_backports_test", + srcs = ["process_backports_test.py"], + target_compatible_with = NOT_WINDOWS, + deps = [ + ":conftest", + ":release_test_helper", + "//tools/private/release:release_lib", + ], +) + +pytest_test( + name = "create_rc_test", + srcs = ["create_rc_test.py"], + target_compatible_with = NOT_WINDOWS, + deps = [ + ":conftest", + ":release_test_helper", + "//tools/private/release:release_lib", + ], +) + +pytest_test( + name = "utils_test", + srcs = ["utils_test.py"], + target_compatible_with = NOT_WINDOWS, deps = [ - "//tools/private/release", - "@dev_pip//packaging", + ":conftest", + ":release_test_helper", + "//tools/private/release:release_lib", ], ) diff --git a/tests/tools/private/release/add_backports_test.py b/tests/tools/private/release/add_backports_test.py new file mode 100644 index 0000000000..c1c7b5cc0a --- /dev/null +++ b/tests/tools/private/release/add_backports_test.py @@ -0,0 +1,101 @@ +import argparse + +from tools.private.release.add_backports import AddBackports + +pytest_plugins = ["tests.tools.private.release.release_test_helper"] + + +def test_add_backports_explicit_issue(mock_gh): + args = argparse.Namespace(issue=123, prs=["124", "125"]) + mock_gh.issues[123] = { + "title": "Release 2.1.0", + "body": """ +## Checklist +- [ ] Prepare Release +- [ ] Create Release branch +- [ ] Tag Final + +## Backports +""", + "labels": ["type: release"], + "number": 123, + "url": "https://github.com/bazel-contrib/rules_python/issues/123", + } + result = AddBackports(args, mock_gh).run() + + assert result == 0 + updated_body = mock_gh.get_issue_body(123) + assert "- [ ] #124" in updated_body + assert "- [ ] #125" in updated_body + assert "- [ ] Tag RC0" in updated_body + assert "- [ ] Sync Changelog #124" in updated_body + assert "- [ ] Sync Changelog #125" in updated_body + + +def test_add_backports_auto_discover_success(mock_gh): + args = argparse.Namespace(issue=None, prs=["124"]) + issue_num = mock_gh.create_issue( + title="Release 2.1.0", + body=""" +## Checklist +- [ ] Prepare Release +- [ ] Create Release branch +- [ ] Tag Final + +## Backports +""", + labels=["type: release"], + ) + result = AddBackports(args, mock_gh).run() + + assert result == 0 + updated_body = mock_gh.get_issue_body(issue_num) + assert "- [ ] #124" in updated_body + + +def test_add_backports_auto_discover_no_issues(mock_gh): + args = argparse.Namespace(issue=None, prs=["124"]) + + result = AddBackports(args, mock_gh).run() + + assert result == 1 + + +def test_add_backports_auto_discover_multiple_issues(mock_gh): + args = argparse.Namespace(issue=None, prs=["124"]) + mock_gh.create_issue( + title="Release 2.1.0", body="## Backports\n", labels=["type: release"] + ) + mock_gh.create_issue( + title="Release 2.2.0", body="## Backports\n", labels=["type: release"] + ) + + result = AddBackports(args, mock_gh).run() + + assert result == 1 + + +def test_add_backports_no_auto_add_rc_if_pending(mock_gh): + args = argparse.Namespace(issue=123, prs=["124"]) + mock_gh.issues[123] = { + "title": "Release 2.1.0", + "body": """ +## Checklist +- [ ] Prepare Release +- [ ] Create Release branch +- [ ] Tag RC0 +- [ ] Tag Final + +## Backports +""", + "labels": ["type: release"], + "number": 123, + "url": "https://github.com/bazel-contrib/rules_python/issues/123", + } + result = AddBackports(args, mock_gh).run() + + assert result == 0 + updated_body = mock_gh.get_issue_body(123) + assert "Tag RC1" not in updated_body + assert "- [ ] Tag RC0" in updated_body + assert "- [ ] Sync Changelog #124" in updated_body diff --git a/tests/tools/private/release/backport_create_releases_test.py b/tests/tools/private/release/backport_create_releases_test.py new file mode 100644 index 0000000000..81dc7e00ce --- /dev/null +++ b/tests/tools/private/release/backport_create_releases_test.py @@ -0,0 +1,174 @@ +import argparse + +from tools.private.release.backport_create_releases import BackportCreateReleases + +# Register pytest fixtures (such as release_tool_env) from release_test_helper +pytest_plugins = ["tests.tools.private.release.release_test_helper"] + + +def test_create_releases_all_success(release_tool_env, mock_gh): + # Arrange + args = argparse.Namespace(issue=123, dry_run=False) + gh = mock_gh + + # Setup backport issue in mock GH + backport_body = """* PR: #456 +* From version: 1.7 +* To version: 1.9 + +## Tasks + +- [x] Verify apply 1.7 | status=success +- [x] Verify apply 1.8 | status=success +- [x] Verify apply 1.9 | status=success +- [ ] Track Release 1.7.2 +- [ ] Track Release 1.8.1 +- [ ] Track Release 1.9.0""" + + gh.issues[123] = { + "title": "Backport: #456", + "body": backport_body, + "labels": ["type:backport-pr"], + "number": 123, + "url": "https://github.com/.../issues/123", + } + + # Act + result = BackportCreateReleases(args, gh).run() + + # Assert + assert result == 0 + + # Verify release issues created (IDs 1001, 1002, 1003) + assert 1001 in gh.issues + assert 1002 in gh.issues + assert 1003 in gh.issues + + # 1.7.2 (patch) should not have Tag RC0 + assert gh.issues[1001]["title"] == "Release 1.7.2" + assert "Tag RC0" not in gh.issues[1001]["body"] + assert "## Backports\n- [ ] #456" in gh.issues[1001]["body"] + + # 1.9.0 (minor) should have Tag RC0 + assert gh.issues[1003]["title"] == "Release 1.9.0" + assert "Tag RC0" in gh.issues[1003]["body"] + + # Verify backport issue updated + expected_updated_backport_body = """* PR: #456 +* From version: 1.7 +* To version: 1.9 + +## Tasks + +- [x] Verify apply 1.7 | status=success +- [x] Verify apply 1.8 | status=success +- [x] Verify apply 1.9 | status=success +- [x] Track Release 1.7.2 | status=success release_issue=#1001 +- [x] Track Release 1.8.1 | status=success release_issue=#1002 +- [x] Track Release 1.9.0 | status=success release_issue=#1003""" + + assert gh.issues[123]["body"] == expected_updated_backport_body + + +def test_create_releases_dependency_blocking(release_tool_env, mock_gh): + # Arrange + args = argparse.Namespace(issue=123, dry_run=False) + gh = mock_gh + + # 1.8 failed, 1.7 and 1.9 succeeded + backport_body = """* PR: #456 +* From version: 1.7 +* To version: 1.9 + +## Tasks + +- [x] Verify apply 1.7 | status=success +- [ ] Verify apply 1.8 | status=failed-conflict +- [x] Verify apply 1.9 | status=success +- [ ] Track Release 1.7.2 +- [ ] Track Release 1.8.1 +- [ ] Track Release 1.9.0""" + + gh.issues[123] = { + "title": "Backport: #456", + "body": backport_body, + "labels": ["type:backport-pr"], + "number": 123, + "url": "https://github.com/.../issues/123", + } + + # Act + result = BackportCreateReleases(args, gh).run() + + # Assert + assert result == 0 + + # Only 1.9.0 (ID 1001) should be created. 1.7.2 and 1.8.1 are blocked by 1.8 failure. + assert len(gh.issues) == 2 # Backport issue + 1 release issue + assert 1001 in gh.issues + assert gh.issues[1001]["title"] == "Release 1.9.0" + + # Verify backport issue updated with block statuses + expected_updated_backport_body = """* PR: #456 +* From version: 1.7 +* To version: 1.9 + +## Tasks + +- [x] Verify apply 1.7 | status=success +- [ ] Verify apply 1.8 | status=failed-conflict +- [x] Verify apply 1.9 | status=success +- [ ] Track Release 1.7.2 | status=error-later-release-did-not-apply +- [ ] Track Release 1.8.1 | status=error-later-release-did-not-apply +- [x] Track Release 1.9.0 | status=success release_issue=#1001""" + + assert gh.issues[123]["body"] == expected_updated_backport_body + + +def test_create_releases_already_exists(release_tool_env, mock_gh): + # Arrange + args = argparse.Namespace(issue=123, dry_run=False) + gh = mock_gh + + backport_body = """* PR: #456 +* From version: 1.7 +* To version: 1.7 + +## Tasks + +- [x] Verify apply 1.7 | status=success +- [ ] Track Release 1.7.2""" + + gh.issues[123] = { + "title": "Backport: #456", + "body": backport_body, + "labels": ["type:backport-pr"], + "number": 123, + "url": "https://github.com/.../issues/123", + } + + # Pre-create the release issue to simulate it already existing + gh.create_issue( + title="Release 1.7.2", + body="existing body\n## Backports\n", + labels=["type: release"], + ) # Will get ID 1001 + + # Act + result = BackportCreateReleases(args, gh).run() + + # Assert + assert result == 0 + assert len(gh.issues) == 2 + + # Should link existing issue 1001 + expected_updated_backport_body = """* PR: #456 +* From version: 1.7 +* To version: 1.7 + +## Tasks + +- [x] Verify apply 1.7 | status=success +- [x] Track Release 1.7.2 | status=success release_issue=#1001""" + + assert gh.issues[123]["body"] == expected_updated_backport_body diff --git a/tests/tools/private/release/backport_prepare_test.py b/tests/tools/private/release/backport_prepare_test.py new file mode 100644 index 0000000000..ddef4885b9 --- /dev/null +++ b/tests/tools/private/release/backport_prepare_test.py @@ -0,0 +1,238 @@ +import argparse +from unittest.mock import ANY, call + +from tools.private.release.backport_prepare import BackportPrepare +from tools.private.release.gh import BACKPORT_LABEL + +pytest_plugins = ["tests.tools.private.release.release_test_helper"] + + +def test_prepare_from_issue_success(mocker, mock_git, mock_gh): + # Arrange + args = argparse.Namespace( + issue=123, + pr=None, + from_minor=None, + to_minor=None, + remote="my-remote", + dry_run=False, + ) + + # Setup backport issue in mock GH + backport_body = "* PR: #456\n* From version: 1.7\n* To version: 1.9\n" + mock_gh.issues[123] = { + "title": "Backport: #456", + "body": backport_body, + "labels": ["type: backport-pr"], + "number": 123, + "url": "https://github.com/.../issues/123", + } + + # Setup PR info in mock GH + mock_gh.prs[456] = { + "state": "MERGED", + "mergeCommit": {"oid": "pr_merge_sha_12345"}, + } + + # Mock remote branches + mock_git.get_remote_branches.return_value = [ + "main", + "release/1.6", + "release/1.7", + "release/1.8", + "release/1.9", + "release/2.0", + ] + mock_git.get_current_branch.return_value = "work-branch" + + mock_news = mocker.patch("tools.private.release.backport_prepare.changelog_news") + mock_det = mocker.patch( + "tools.private.release.backport_prepare.determine_next_version" + ) + mock_det.side_effect = ["1.7.2", "1.8.1", "1.9.0"] + + # Act + result = BackportPrepare(args, mock_git, mock_gh).run() + + # Assert + assert result == 0 + mock_git.fetch.assert_called_once_with("my-remote", tags=True, force=True) + + # Verify checkouts and cherry-picks + mock_git.checkout.assert_has_calls( + [ + call("release/1.7", track_remote="my-remote"), + call("release/1.8", track_remote="my-remote"), + call("release/1.9", track_remote="my-remote"), + call("work-branch"), # Restored branch + ] + ) + + mock_git.cherry_pick.assert_has_calls( + [ + call("pr_merge_sha_12345"), + call("pr_merge_sha_12345"), + call("pr_merge_sha_12345"), + ] + ) + + # Verify changelog updates + mock_news.update_changelog.assert_has_calls( + [ + call("1.7.2", ANY), + call("1.8.1", ANY), + call("1.9.0", ANY), + ] + ) + + # Verify issue body update + expected_body = ( + "* PR: #456\n" + "* From version: 1.7\n" + "* To version: 1.9\n" + "\n" + "## Tasks\n" + "\n" + "- [x] Verify apply 1.7 | status=success\n" + "- [x] Verify apply 1.8 | status=success\n" + "- [x] Verify apply 1.9 | status=success\n" + "- [ ] Track Release 1.7.2\n" + "- [ ] Track Release 1.8.1\n" + "- [ ] Track Release 1.9.0" + ) + assert mock_gh.issues[123]["body"] == expected_body + + +def test_prepare_manual_success(mocker, mock_git, mock_gh): + # Arrange + args = argparse.Namespace( + issue=None, + pr="#456", + from_minor="1.7", + to_minor="1.8", + remote="my-remote", + dry_run=False, + ) + mock_gh.prs[456] = { + "state": "MERGED", + "mergeCommit": {"oid": "pr_merge_sha_12345"}, + } + mock_git.get_remote_branches.return_value = [ + "release/1.7", + "release/1.8", + ] + mock_git.get_current_branch.return_value = "work-branch" + + mocker.patch("tools.private.release.backport_prepare.changelog_news") + mock_det = mocker.patch( + "tools.private.release.backport_prepare.determine_next_version" + ) + mock_det.side_effect = ["1.7.2", "1.8.1"] + + # Act + result = BackportPrepare(args, mock_git, mock_gh).run() + + # Assert + assert result == 0 + assert 1001 in mock_gh.issues + issue = mock_gh.issues[1001] + assert issue["title"] == "Backport: #456" + assert issue["labels"] == [BACKPORT_LABEL] + body = issue["body"] + assert "- [x] Verify apply 1.7 | status=success" in body + assert "- [x] Verify apply 1.8 | status=success" in body + assert "- [ ] Track Release 1.7.2" in body + assert "- [ ] Track Release 1.8.1" in body + + +def test_prepare_manual_with_patch_versions(mocker, mock_git, mock_gh): + # Arrange + args = argparse.Namespace( + issue=None, + pr="#456", + from_minor="1.7.0", + to_minor="1.8.0", + remote="my-remote", + dry_run=False, + ) + mock_gh.prs[456] = { + "state": "MERGED", + "mergeCommit": {"oid": "pr_merge_sha_12345"}, + } + mock_git.get_remote_branches.return_value = [ + "release/1.7", + "release/1.8", + ] + mock_git.get_current_branch.return_value = "work-branch" + + mocker.patch("tools.private.release.backport_prepare.changelog_news") + mock_det = mocker.patch( + "tools.private.release.backport_prepare.determine_next_version" + ) + mock_det.side_effect = ["1.7.2", "1.8.1"] + + # Act + result = BackportPrepare(args, mock_git, mock_gh).run() + + # Assert + assert result == 0 + assert 1001 in mock_gh.issues + body = mock_gh.issues[1001]["body"] + assert "- [x] Verify apply 1.7 | status=success" in body + assert "- [x] Verify apply 1.8 | status=success" in body + + +def test_prepare_verify_failed(mocker, mock_git, mock_gh): + # Arrange + args = argparse.Namespace( + issue=123, + pr=None, + from_minor=None, + to_minor=None, + remote="my-remote", + dry_run=False, + ) + issue_body = "* PR: #456\n* From version: 1.7\n* To version: 1.8\n" + mock_gh.issues[123] = { + "title": "Backport: #456", + "body": issue_body, + "labels": ["type: backport-pr"], + "number": 123, + "url": "https://github.com/.../issues/123", + } + mock_gh.prs[456] = { + "state": "MERGED", + "mergeCommit": {"oid": "pr_merge_sha_12345"}, + } + mock_git.get_remote_branches.return_value = [ + "release/1.7", + "release/1.8", + ] + mock_git.get_current_branch.return_value = "work-branch" + + mock_news = mocker.patch("tools.private.release.backport_prepare.changelog_news") + mock_det = mocker.patch( + "tools.private.release.backport_prepare.determine_next_version" + ) + mock_det.side_effect = ["1.7.2", "1.8.1"] + mock_git.cherry_pick.side_effect = [Exception("Conflict"), None] + mock_news.update_changelog.side_effect = [Exception("Changelog error")] + + # Act + result = BackportPrepare(args, mock_git, mock_gh).run() + + # Assert + assert result == 0 + expected_body = ( + "* PR: #456\n" + "* From version: 1.7\n" + "* To version: 1.8\n" + "\n" + "## Tasks\n" + "\n" + "- [ ] Verify apply 1.7 | status=failed-conflict\n" + "- [ ] Verify apply 1.8 | status=failed-changelog\n" + "- [ ] Track Release 1.7.2\n" + "- [ ] Track Release 1.8.1" + ) + assert mock_gh.issues[123]["body"] == expected_body diff --git a/tests/tools/private/release/changelog_news_test.py b/tests/tools/private/release/changelog_news_test.py new file mode 100644 index 0000000000..dd2bd6df79 --- /dev/null +++ b/tests/tools/private/release/changelog_news_test.py @@ -0,0 +1,555 @@ +import pathlib + +import pytest + +from tools.private.release import changelog_news + + +def test_update_changelog_with_news(tmp_path): + # Arrange + changelog = """# Changelog + +{#unreleased} +## Unreleased + +[unreleased]: https://github.com/bazel-contrib/rules_python/releases/tag/unreleased + +Unreleased changes are tracked as individual files in the [news/](./news) +directory, or view the [latest generated +changelog](https://rules-python.readthedocs.io/en/latest/changelog.html). + +{#v2-0-2} +## [2.0.2] - 2026-05-14 + +[2.0.2]: https://github.com/bazel-contrib/rules_python/releases/tag/2.0.2 + +{#v2-0-2-added} +### Added +* (toolchains) Some older change. +""" + changelog_path = tmp_path / "CHANGELOG.md" + changelog_path.write_text(changelog) + + news_dir = tmp_path / "news" + news_dir.mkdir() + + # Create news files + (news_dir / "123.fixed.md").write_text("Fixed a bug in the compiler") + # Test that it handles prefixing "* " if not present + (news_dir / "456.added.md").write_text("* Added a new feature for Python 3.13") + # Empty file should be ignored + (news_dir / "789.changed.md").write_text("") + # Invalid name should be ignored + (news_dir / "invalid_name.md").write_text("Should be ignored") + + # Act + changelog_news.update_changelog( + "3.0.0", + "2026-06-16", + changelog_path=changelog_path, + news_dir=news_dir, + ) + + # Assert + # 1. News files matching the pattern should be deleted (even empty ones) + assert not (news_dir / "123.fixed.md").exists() + assert not (news_dir / "456.added.md").exists() + assert not (news_dir / "789.changed.md").exists() + # Invalid name does not match pattern -> NOT deleted + assert (news_dir / "invalid_name.md").exists() + + new_content = changelog_path.read_text() + + # 2. A fresh active Unreleased section should be present + assert "{#unreleased}" in new_content + assert "## Unreleased" in new_content + assert ( + "Unreleased changes are tracked as individual files in the [news/](./news)\n" + "directory, or view the [latest generated\n" + "changelog](https://rules-python.readthedocs.io/en/latest/changelog.html)." + in new_content + ) + + # 3. The new release section should be present + assert "{#v3-0-0}" in new_content + assert "## [3.0.0] - 2026-06-16" in new_content + assert ( + "[3.0.0]: https://github.com/bazel-contrib/rules_python/releases/tag/3.0.0" + in new_content + ) + + # 4. Correct categories and content + assert "{#v3-0-0-fixed}\n### Fixed\n* Fixed a bug in the compiler" in new_content + assert ( + "{#v3-0-0-added}\n### Added\n* Added a new feature for Python 3.13" + in new_content + ) + + # 5. Omitted categories should NOT be present in the new release + assert "{#v3-0-0-removed}" not in new_content + assert "{#v3-0-0-changed}" not in new_content + + # 6. Old release should still be there + assert "{#v2-0-2}" in new_content + assert "## [2.0.2] - 2026-05-14" in new_content + + +def test_update_changelog_sorting(tmp_path): + # Arrange + changelog = """# Changelog + +{#unreleased} +## Unreleased + +[unreleased]: https://github.com/bazel-contrib/rules_python/releases/tag/unreleased + +Unreleased changes are tracked as individual files in the [news/](./news) +directory, or view the [latest generated +changelog](https://rules-python.readthedocs.io/en/latest/changelog.html). + +{#v2-0-2} +## [2.0.2] - 2026-05-14 + +[2.0.2]: https://github.com/bazel-contrib/rules_python/releases/tag/2.0.2 + +{#v2-0-2-added} +### Added +* (toolchains) Some older change. +""" + changelog_path = tmp_path / "CHANGELOG.md" + changelog_path.write_text(changelog) + + news_dir = tmp_path / "news" + news_dir.mkdir() + + # Create news files with different sub-categories and some without + (news_dir / "1.fixed.md").write_text("* (zebra) Zebra fix") + (news_dir / "2.fixed.md").write_text("* (apple) Apple fix") + (news_dir / "3.fixed.md").write_text("No subcategory B") + (news_dir / "4.fixed.md").write_text("* (apple) Another apple fix") + (news_dir / "5.fixed.md").write_text("No subcategory A") + + # Act + changelog_news.update_changelog( + "3.0.0", + "2026-06-16", + changelog_path=changelog_path, + news_dir=news_dir, + ) + + # Assert + new_content = changelog_path.read_text() + + expected_fixed_section = ( + "### Fixed\n" + "* No subcategory A\n" + "* No subcategory B\n" + "* (apple) Another apple fix\n" + "* (apple) Apple fix\n" + "* (zebra) Zebra fix\n" + ) + + assert expected_fixed_section in new_content + + +pytest_plugins = ["tests.tools.private.release.release_test_helper"] + + +def test_update_changelog_read_failure(mocker, tmp_path): + # Arrange + original_read_text = pathlib.Path.read_text + + mock_read_text = mocker.patch.object(pathlib.Path, "read_text", autospec=True) + + def side_effect(path_self, *args, **kwargs): + if "bad_file.fixed.md" in str(path_self): + raise IOError("Simulated read error") + return original_read_text(path_self, *args, **kwargs) + + mock_read_text.side_effect = side_effect + + changelog = """# Changelog + +{#unreleased} +## Unreleased + +[unreleased]: https://github.com/bazel-contrib/rules_python/releases/tag/unreleased + +Unreleased changes are tracked as individual files in the [news/](./news) +directory, or view the [latest generated +changelog](https://rules-python.readthedocs.io/en/latest/changelog.html). + +{#v2-0-2} +## [2.0.2] - 2026-05-14 + +[2.0.2]: https://github.com/bazel-contrib/rules_python/releases/tag/2.0.2 + +{#v2-0-2-added} +### Added +* (toolchains) Some older change. +""" + changelog_path = tmp_path / "CHANGELOG.md" + changelog_path.write_text(changelog) + + news_dir = tmp_path / "news" + news_dir.mkdir() + + # Create the bad file (must exist so it is found by iterdir) + bad_file = news_dir / "bad_file.fixed.md" + bad_file.write_text("some content that won't be read") + + # Create a good file too + good_file = news_dir / "good_file.fixed.md" + good_file.write_text("* (sub) Good fix") + + # Act & Assert + with pytest.raises(IOError): + changelog_news.update_changelog( + "3.0.0", + "2026-06-16", + changelog_path=changelog_path, + news_dir=news_dir, + ) + + # Both files should still exist (no deletion on failure!) + assert bad_file.exists() + assert good_file.exists() + + # Changelog should not be modified + new_content = changelog_path.read_text() + assert changelog == new_content + + +def test_update_changelog_merge_existing(tmp_path): + # Arrange + changelog = """# Changelog + +{#unreleased} +## Unreleased + +[unreleased]: https://github.com/bazel-contrib/rules_python/releases/tag/unreleased + +Unreleased changes are tracked as individual files in the [news/](./news) +directory, or view the [latest generated +changelog](https://rules-python.readthedocs.io/en/latest/changelog.html). + +{#v2-0-3} +## [2.0.3] - 2026-06-15 + +[2.0.3]: https://github.com/bazel-contrib/rules_python/releases/tag/2.0.3 + +{#v2-0-3-fixed} +### Fixed +* (pypi) Old fix + multi-line detail + * nested bullet item +* (pypi) Z old fix +""" + changelog_path = tmp_path / "CHANGELOG.md" + changelog_path.write_text(changelog) + + news_dir = tmp_path / "news" + news_dir.mkdir() + + # Create news files to merge + (news_dir / "1.fixed.md").write_text("(pypi) New fix") + (news_dir / "2.added.md").write_text("(toolchains) New feature") + + # Act + changelog_news.update_changelog( + "2.0.3", + "2026-06-15", + changelog_path=changelog_path, + news_dir=news_dir, + ) + + # Assert + assert not (news_dir / "1.fixed.md").exists() + assert not (news_dir / "2.added.md").exists() + + new_content = changelog_path.read_text() + + expected_fixed_section = ( + "### Fixed\n" + "* (pypi) New fix\n" + "* (pypi) Old fix\n" + " multi-line detail\n" + " * nested bullet item\n" + "* (pypi) Z old fix\n" + ) + assert expected_fixed_section in new_content + + expected_added_section = "### Added\n* (toolchains) New feature\n" + assert expected_added_section in new_content + assert "Unreleased changes are tracked as individual files" in new_content + + +def test_update_changelog_does_not_leak(tmp_path): + # Arrange + changelog = """# Changelog + +{#unreleased} +## Unreleased + +[unreleased]: https://github.com/bazel-contrib/rules_python/releases/tag/unreleased + +Unreleased changes are tracked as individual files in the [news/](./news) +directory, or view the [latest generated +changelog](https://rules-python.readthedocs.io/en/latest/changelog.html). + +{#v2-0-2} +## [2.0.2] - 2026-05-14 + +[2.0.2]: https://github.com/bazel-contrib/rules_python/releases/tag/2.0.2 + +This release body mentions the word unreleased and {#unreleased} anchor to test leaks. +""" + changelog_path = tmp_path / "CHANGELOG.md" + changelog_path.write_text(changelog) + + news_dir = tmp_path / "news" + news_dir.mkdir() + (news_dir / "1.fixed.md").write_text("Some fix") + + # Act + changelog_news.update_changelog( + "3.0.0", + "2026-06-16", + changelog_path=changelog_path, + news_dir=news_dir, + ) + + # Assert + new_content = changelog_path.read_text() + assert ( + "This release body mentions the word unreleased and {#unreleased} anchor to test leaks." + in new_content + ) + + +def test_update_changelog_empty_news(tmp_path): + # Arrange + changelog = """# Changelog + +{#unreleased} +## Unreleased + +[unreleased]: https://github.com/bazel-contrib/rules_python/releases/tag/unreleased + +Unreleased changes are tracked as individual files in the [news/](./news) +directory, or view the [latest generated +changelog](https://rules-python.readthedocs.io/en/latest/changelog.html). + +{#v2-0-2} +## [2.0.2] - 2026-05-14 + +[2.0.2]: https://github.com/bazel-contrib/rules_python/releases/tag/2.0.2 + +{#v2-0-2-added} +### Added +* (toolchains) Some older change. +""" + changelog_path = tmp_path / "CHANGELOG.md" + changelog_path.write_text(changelog) + + news_dir = tmp_path / "news" + news_dir.mkdir() + + # Act + changelog_news.update_changelog( + "3.0.0", + "2026-06-16", + changelog_path=changelog_path, + news_dir=news_dir, + ) + + # Assert + new_content = changelog_path.read_text() + + assert "{#v3-0-0}" in new_content + assert "## [3.0.0] - 2026-06-16" in new_content + assert ( + "[3.0.0]: https://github.com/bazel-contrib/rules_python/releases/tag/3.0.0" + in new_content + ) + assert "No notable changes." in new_content + assert "{#v3-0-0-fixed}" not in new_content + assert "{#v3-0-0-added}" not in new_content + + +def test_update_changelog_selective_news_files(tmp_path): + # Arrange + changelog = """# Changelog + +{#unreleased} +## Unreleased + +[unreleased]: https://github.com/bazel-contrib/rules_python/releases/tag/unreleased + +{#v2-0-2} +## [2.0.2] - 2026-05-14 + +[2.0.2]: https://github.com/bazel-contrib/rules_python/releases/tag/2.0.2 +""" + changelog_path = tmp_path / "CHANGELOG.md" + changelog_path.write_text(changelog) + + news_dir = tmp_path / "news" + news_dir.mkdir() + + # Create news files + (news_dir / "123.fixed.md").write_text("Fix A") + (news_dir / "456.fixed.md").write_text("Fix B") + + # Act: Only process 123.fixed.md + changelog_news.update_changelog( + "2.0.3", + "2026-06-16", + changelog_path=changelog_path, + news_dir=news_dir, + news_files=[news_dir / "123.fixed.md"], + ) + + # Assert + assert not (news_dir / "123.fixed.md").exists() + assert (news_dir / "456.fixed.md").exists() + + new_content = changelog_path.read_text() + assert "Fix A" in new_content + assert "Fix B" not in new_content + + +def test_update_changelog_insertion_point(tmp_path): + # Arrange + changelog = """# Changelog + +{#unreleased} +## Unreleased + +[unreleased]: https://github.com/bazel-contrib/rules_python/releases/tag/unreleased + +{#v2-2-0} +## [2.2.0] - 2026-06-30 + +[2.2.0]: https://github.com/bazel-contrib/rules_python/releases/tag/2.2.0 + +{#v2-0-0} +## [2.0.0] - 2026-04-09 + +[2.0.0]: https://github.com/bazel-contrib/rules_python/releases/tag/2.0.0 +""" + changelog_path = tmp_path / "CHANGELOG.md" + changelog_path.write_text(changelog) + + news_dir = tmp_path / "news" + news_dir.mkdir() + (news_dir / "123.fixed.md").write_text("Fix in 2.1.0") + + # Act: Insert 2.1.0 + changelog_news.update_changelog( + "2.1.0", + "2026-06-17", + changelog_path=changelog_path, + news_dir=news_dir, + ) + + # Assert + new_content = changelog_path.read_text() + + idx_2_2_0 = new_content.index("{#v2-2-0}") + idx_2_1_0 = new_content.index("{#v2-1-0}") + idx_2_0_0 = new_content.index("{#v2-0-0}") + + assert idx_2_2_0 < idx_2_1_0 < idx_2_0_0 + assert "Fix in 2.1.0" in new_content + + +def test_update_changelog_insertion_point_too_small(tmp_path): + # Arrange + changelog = """# Changelog + +{#unreleased} +## Unreleased + +[unreleased]: https://github.com/bazel-contrib/rules_python/releases/tag/unreleased + +{#v2-0-0} +## [2.0.0] - 2026-04-09 + +[2.0.0]: https://github.com/bazel-contrib/rules_python/releases/tag/2.0.0 +""" + changelog_path = tmp_path / "CHANGELOG.md" + changelog_path.write_text(changelog) + + news_dir = tmp_path / "news" + news_dir.mkdir() + (news_dir / "123.fixed.md").write_text("Fix in 1.0.0") + + # Act & Assert + with pytest.raises( + ValueError, match="Could not find a version in CHANGELOG.md smaller than 1.0.0" + ): + changelog_news.update_changelog( + "1.0.0", + "2026-01-01", + changelog_path=changelog_path, + news_dir=news_dir, + ) + + +def test_format_entry(): + # Multi-line without leading bullet + raw = "(foo) bla bla\nblabl bla\nblabla" + expected = "* (foo) bla bla\n blabl bla\n blabla" + assert changelog_news._format_entry(raw) == expected + + # Multi-line with leading bullet + raw_with_bullet = "* (foo) bla bla\nblabl bla\nblabla" + assert changelog_news._format_entry(raw_with_bullet) == expected + + # Multi-line already indented + raw_indented = "* (foo) bla bla\n blabl bla\n blabla" + assert changelog_news._format_entry(raw_indented) == expected + + # Empty string + assert changelog_news._format_entry("") == "" + + +def test_update_changelog_multiline_indentation(tmp_path): + # Arrange + changelog = """# Changelog + +{#unreleased} +## Unreleased + +[unreleased]: https://github.com/bazel-contrib/rules_python/releases/tag/unreleased + +{#v2-0-0} +## [2.0.0] - 2026-04-09 + +[2.0.0]: https://github.com/bazel-contrib/rules_python/releases/tag/2.0.0 +""" + changelog_path = tmp_path / "CHANGELOG.md" + changelog_path.write_text(changelog) + + news_dir = tmp_path / "news" + news_dir.mkdir() + (news_dir / "1.fixed.md").write_text("(foo) bla bla\nblabl bla\nblabla") + (news_dir / "2.added.md").write_text("* (bar) feature one\nsecond line of feature") + + # Act + changelog_news.update_changelog( + "2.1.0", + "2026-06-17", + changelog_path=changelog_path, + news_dir=news_dir, + ) + + # Assert + new_content = changelog_path.read_text() + + expected_fixed = "### Fixed\n* (foo) bla bla\n blabl bla\n blabla" + expected_added = "### Added\n* (bar) feature one\n second line of feature" + + assert expected_fixed in new_content + assert expected_added in new_content diff --git a/tests/tools/private/release/complete_prepare_test.py b/tests/tools/private/release/complete_prepare_test.py new file mode 100644 index 0000000000..b710c0d0c9 --- /dev/null +++ b/tests/tools/private/release/complete_prepare_test.py @@ -0,0 +1,138 @@ +import argparse + +from tools.private.release.complete_prepare import CompletePrepare + +pytest_plugins = ["tests.tools.private.release.release_test_helper"] + + +def test_complete_prepare_with_pr_success(mock_gh): + args = argparse.Namespace(pr=456, issue=None) + mock_gh.prs[456] = { + "state": "MERGED", + "body": "Prepare release for v2.0.0\n\nWork towards #123", + "mergeCommit": {"oid": "abcdef1234567890"}, + } + issue_body = """ +## Checklist +- [ ] Prepare Release | status=pending pr=#456 +- [ ] Create Release branch +- [ ] Tag Final +""" + mock_gh.issues[123] = { + "title": "Release 2.0.0", + "body": issue_body, + "labels": ["type: release"], + "number": 123, + "url": "https://github.com/bazel-contrib/rules_python/issues/123", + } + + result = CompletePrepare(args, mock_gh).run() + + assert result == 0 + updated_body = mock_gh.get_issue_body(123) + assert ( + "- [x] Prepare Release | status=done pr=#456 commit= abcdef12" in updated_body + ) + + +def test_complete_prepare_writes_github_output(release_tool_env, mock_gh): + args = argparse.Namespace(pr=456, issue=None) + mock_gh.prs[456] = { + "state": "MERGED", + "body": "Prepare release for v2.0.0\n\nWork towards #123", + "mergeCommit": {"oid": "abcdef1234567890"}, + } + issue_body = """ +## Checklist +- [ ] Prepare Release | status=pending pr=#456 +- [ ] Create Release branch +- [ ] Tag Final +""" + mock_gh.issues[123] = { + "title": "Release 2.0.0", + "body": issue_body, + "labels": ["type: release"], + "number": 123, + "url": "https://github.com/bazel-contrib/rules_python/issues/123", + } + + result = CompletePrepare(args, mock_gh).run() + assert result == 0 + assert release_tool_env.github_output_file.exists() + assert ( + release_tool_env.github_output_file.read_text(encoding="utf-8") == "issue=123\n" + ) + + +def test_complete_prepare_with_issue_success(mock_gh): + args = argparse.Namespace(pr=None, issue=123) + mock_gh.prs[456] = { + "state": "MERGED", + "body": "Prepare release for v2.0.0", + "mergeCommit": {"oid": "1234567890abcdef"}, + } + issue_body = """ +## Checklist +- [ ] Prepare Release | status=pending pr=#456 +- [ ] Create Release branch +- [ ] Tag Final +""" + mock_gh.issues[123] = { + "title": "Release 2.0.0", + "body": issue_body, + "labels": ["type: release"], + "number": 123, + "url": "https://github.com/bazel-contrib/rules_python/issues/123", + } + + result = CompletePrepare(args, mock_gh).run() + + assert result == 0 + updated_body = mock_gh.get_issue_body(123) + assert ( + "- [x] Prepare Release | status=done pr=#456 commit= 12345678" in updated_body + ) + + +def test_complete_prepare_no_args(mock_gh): + args = argparse.Namespace(pr=None, issue=None) + result = CompletePrepare(args, mock_gh).run() + assert result == 1 + + +def test_complete_prepare_not_merged(mock_gh): + args = argparse.Namespace(pr=456, issue=123) + mock_gh.prs[456] = { + "state": "OPEN", + "body": "Prepare release for v2.0.0", + } + issue_body = """ +## Checklist +- [ ] Prepare Release | status=pending pr=#456 +""" + mock_gh.issues[123] = { + "title": "Release 2.0.0", + "body": issue_body, + "labels": ["type: release"], + "number": 123, + } + + result = CompletePrepare(args, mock_gh).run() + assert result == 1 + + +def test_complete_prepare_issue_missing_pr_task(mock_gh): + args = argparse.Namespace(pr=None, issue=123) + issue_body = """ +## Checklist +- [ ] Create Release branch +""" + mock_gh.issues[123] = { + "title": "Release 2.0.0", + "body": issue_body, + "labels": ["type: release"], + "number": 123, + } + + result = CompletePrepare(args, mock_gh).run() + assert result == 1 diff --git a/tests/tools/private/release/complete_sync_changelog_test.py b/tests/tools/private/release/complete_sync_changelog_test.py new file mode 100644 index 0000000000..d0eb148b57 --- /dev/null +++ b/tests/tools/private/release/complete_sync_changelog_test.py @@ -0,0 +1,105 @@ +import argparse + +from tools.private.release.complete_sync_changelog import CompleteSyncChangelog + +pytest_plugins = ["tests.tools.private.release.release_test_helper"] + + +def test_complete_sync_changelog_success(mock_gh): + args = argparse.Namespace(pr=999) + mock_gh.prs[999] = { + "state": "MERGED", + "body": "Updates CHANGELOG.md\n\nRelease-Tracking-Issue: #123", + "mergeCommit": {"oid": "abcdef1234567890"}, + } + issue_body = """ +## Checklist +- [ ] Prepare Release +- [ ] Create Release branch +- [ ] Sync Changelog #124 | status=pending pr=#999 +- [ ] Sync Changelog #125 | status=pending pr=#999 +- [ ] Sync Changelog #126 | status=pending pr=#888 +- [ ] Tag Final + +## Backports +""" + mock_gh.issues[123] = { + "title": "Release 2.1.0", + "body": issue_body, + "labels": ["type: release"], + "number": 123, + "url": "https://github.com/bazel-contrib/rules_python/issues/123", + } + + result = CompleteSyncChangelog(args, mock_gh).run() + + assert result == 0 + updated_body = mock_gh.get_issue_body(123) + + # Check that only tasks pointing to #999 were marked checked=True and status=done + assert ( + "- [x] Sync Changelog #124 | status=done pr=#999 commit= abcdef12" + in updated_body + ) + assert ( + "- [x] Sync Changelog #125 | status=done pr=#999 commit= abcdef12" + in updated_body + ) + # Task pointing to #888 should remain unchanged + assert "- [ ] Sync Changelog #126 | status=pending pr=#888" in updated_body + + +def test_complete_sync_changelog_not_merged(mock_gh): + args = argparse.Namespace(pr=999) + mock_gh.prs[999] = { + "state": "OPEN", + "body": "Updates CHANGELOG.md\n\nRelease-Tracking-Issue: #123", + } + + result = CompleteSyncChangelog(args, mock_gh).run() + + assert result == 1 + + +def test_complete_sync_changelog_missing_tracking_issue_link(mock_gh): + args = argparse.Namespace(pr=999) + mock_gh.prs[999] = { + "state": "MERGED", + "body": "Updates CHANGELOG.md without tracking issue link", + "mergeCommit": {"oid": "abcdef1234567890"}, + } + + result = CompleteSyncChangelog(args, mock_gh).run() + + assert result == 1 + + +def test_complete_sync_changelog_no_matching_tasks(mock_gh): + args = argparse.Namespace(pr=999) + mock_gh.prs[999] = { + "state": "MERGED", + "body": "Updates CHANGELOG.md\n\nRelease-Tracking-Issue: #123", + "mergeCommit": {"oid": "abcdef1234567890"}, + } + # Checklist has no tasks pointing to #999 + issue_body = """ +## Checklist +- [ ] Prepare Release +- [ ] Create Release branch +- [ ] Sync Changelog #124 | status=pending pr=#888 +- [ ] Tag Final + +## Backports +""" + mock_gh.issues[123] = { + "title": "Release 2.1.0", + "body": issue_body, + "labels": ["type: release"], + "number": 123, + "url": "https://github.com/bazel-contrib/rules_python/issues/123", + } + + result = CompleteSyncChangelog(args, mock_gh).run() + + assert result == 0 + assert mock_gh.get_issue_body(123) == issue_body diff --git a/tests/tools/private/release/conftest.py b/tests/tools/private/release/conftest.py new file mode 100644 index 0000000000..f8798c11c7 --- /dev/null +++ b/tests/tools/private/release/conftest.py @@ -0,0 +1,50 @@ +import dataclasses +from unittest.mock import MagicMock + +import pytest + +pytest_plugins = ["tests.tools.private.release.release_test_helper"] + + +@dataclasses.dataclass +class AutoPatchCmdHelpers: + """Dataclass holding mocked command helpers.""" + + run_cmd: MagicMock + run_git: MagicMock + run_gh: MagicMock + + +@pytest.fixture(name="mock_run_cmd") +def fixture_mock_run_cmd(mocker): + """Fixture to patch shell.run_cmd and its imports in git and gh modules.""" + mock = mocker.patch("tools.private.release.shell.run_cmd") + mocker.patch("tools.private.release.git.run_cmd", mock) + mocker.patch("tools.private.release.gh.run_cmd", mock) + return mock + + +@pytest.fixture(name="mock_run_git") +def fixture_mock_run_git(mocker): + """Fixture to patch Git._run_git.""" + return mocker.patch("tools.private.release.git.Git._run_git") + + +@pytest.fixture(name="mock_run_gh") +def fixture_mock_run_gh(mocker): + """Fixture to patch GitHub._run_gh.""" + return mocker.patch("tools.private.release.gh.GitHub._run_gh") + + +@pytest.fixture(name="auto_patch_cmd_helpers", autouse=True) +def fixture_auto_patch_cmd_helpers(mock_run_cmd, mock_run_git, mock_run_gh): + """Automatically patches run_cmd, Git, and GitHub CLI helpers. + + This prevents tests from executing command line tools that could have + side-effects. + """ + return AutoPatchCmdHelpers( + run_cmd=mock_run_cmd, + run_git=mock_run_git, + run_gh=mock_run_gh, + ) diff --git a/tests/tools/private/release/create_rc_test.py b/tests/tools/private/release/create_rc_test.py new file mode 100644 index 0000000000..6aee746dbb --- /dev/null +++ b/tests/tools/private/release/create_rc_test.py @@ -0,0 +1,420 @@ +import argparse +import os +import pathlib +import tempfile +from unittest.mock import call + +from tools.private.release.create_rc import CreateRc + +pytest_plugins = ["tests.tools.private.release.release_test_helper"] + + +def test_create_rc_success_first_rc(mocker, mock_git, mock_gh): + # Arrange + args = argparse.Namespace( + issue=123, remote="my-remote", triggering_comment=None, dry_run=False + ) + mock_gh.issues[123] = { + "title": "Release 2.0.0", + "body": """ +## Checklist +- [x] Prepare Release | status=done pr=#122 commit=abcdef12 +- [x] Create Release branch | status=done branch=release/2.0 commit=abcdef12 +- [ ] Tag RC0 | status=pending +""", + "labels": ["type: release"], + } + mock_git.get_remote_tags.return_value = [] + mock_git.get_commit_sha.return_value = "1234567890" + + # Act + with tempfile.TemporaryDirectory() as tmpdir: + github_output_file = pathlib.Path(tmpdir) / "github_output" + mocker.patch.dict(os.environ, {"GITHUB_OUTPUT": str(github_output_file)}) + result = CreateRc(args, mock_git, mock_gh).run() + + # Assert + assert result == 0 + assert github_output_file.exists() + assert github_output_file.read_text() == "tag_name=2.0.0-rc0\n" + + mock_git.fetch.assert_has_calls( + [call("my-remote"), call("my-remote", tags=True, force=True)] + ) + mock_git.checkout.assert_not_called() + mock_git.tag.assert_called_once_with("2.0.0-rc0", "my-remote/release/2.0") + mock_git.push.assert_called_once_with("my-remote", "2.0.0-rc0") + mock_git.get_commit_sha.assert_called_once_with("my-remote/release/2.0") + + updated_body = mock_gh.get_issue_body(123) + assert "tag=2.0.0-rc0" in updated_body + assert "commit= 12345678" in updated_body + + assert 123 in mock_gh.issue_comments + comment_text = mock_gh.issue_comments[123][0] + assert "**New Release Candidate Tagged!** 🐍🌿" in comment_text + assert ( + "tagged on branch [`release/2.0`](https://github.com/bazel-contrib/rules_python/tree/release/2.0)" + in comment_text + ) + assert ( + "- [Github Release 2.0.0-rc0](https://github.com/bazel-contrib/rules_python/releases/tag/2.0.0-rc0)" + in comment_text + ) + assert ( + "- [BCR Entry 2.0.0-rc0](https://registry.bazel.build/modules/rules_python/2.0.0-rc0)" + in comment_text + ) + assert ( + "- [BCR PRs](https://github.com/bazelbuild/bazel-central-registry/pulls?q=is%3Apr+rules_python+2.0.0-rc0)" + in comment_text + ) + assert ( + "- [Release workflow status](https://github.com/bazel-contrib/rules_python/actions/workflows/release_create_rc.yaml)" + in comment_text + ) + + +def test_create_rc_success_with_run_id(mocker, mock_git, mock_gh): + # Arrange + args = argparse.Namespace( + issue=123, remote="my-remote", triggering_comment=None, dry_run=False + ) + mock_gh.issues[123] = { + "title": "Release 2.0.0", + "body": """ +## Checklist +- [x] Prepare Release | status=done pr=#122 commit=abcdef12 +- [x] Create Release branch | status=done branch=release/2.0 commit=abcdef12 +- [ ] Tag RC0 | status=pending +""", + "labels": ["type: release"], + } + mock_git.get_remote_tags.return_value = [] + mock_git.get_commit_sha.return_value = "1234567890" + + # Act + mocker.patch.dict(os.environ, {"GITHUB_RUN_ID": "987654321"}) + result = CreateRc(args, mock_git, mock_gh).run() + + # Assert + assert result == 0 + assert 123 in mock_gh.issue_comments + comment_text = mock_gh.issue_comments[123][0] + assert ( + "- [Release workflow status](https://github.com/bazel-contrib/rules_python/actions/runs/987654321)" + in comment_text + ) + assert ( + "tagged on branch [`release/2.0`](https://github.com/bazel-contrib/rules_python/tree/release/2.0)" + in comment_text + ) + + +def test_create_rc_success_next_rc(mock_git, mock_gh): + # Arrange + args = argparse.Namespace( + issue=123, remote="my-remote", triggering_comment=None, dry_run=False + ) + mock_gh.issues[123] = { + "title": "Release 2.0.0", + "body": """ +## Checklist +- [x] Prepare Release | status=done pr=#122 commit=abcdef12 +- [x] Create Release branch | status=done branch=release/2.0 commit=abcdef12 +- [x] Tag RC0 | status=done tag=2.0.0-rc0 commit=abcdef12 +- [ ] Tag RC1 | status=pending +""", + "labels": ["type: release"], + } + mock_git.get_remote_tags.return_value = ["2.0.0-rc0"] + mock_git.get_commit_sha.return_value = "1234567890" + + # Act + result = CreateRc(args, mock_git, mock_gh).run() + + # Assert + assert result == 0 + mock_git.fetch.assert_has_calls( + [call("my-remote"), call("my-remote", tags=True, force=True)] + ) + mock_git.checkout.assert_not_called() + mock_git.tag.assert_called_once_with("2.0.0-rc1", "my-remote/release/2.0") + mock_git.push.assert_called_once_with("my-remote", "2.0.0-rc1") + mock_git.get_commit_sha.assert_called_once_with("my-remote/release/2.0") + + updated_body = mock_gh.get_issue_body(123) + assert "tag=2.0.0-rc1" in updated_body + + assert 123 in mock_gh.issue_comments + comment_text = mock_gh.issue_comments[123][0] + assert "**New Release Candidate Tagged!** 🐍🌿" in comment_text + + +def test_create_rc_gating_on_backports(mock_git, mock_gh): + # Arrange + args = argparse.Namespace( + issue=123, remote="my-remote", triggering_comment=None, dry_run=False + ) + mock_gh.issues[123] = { + "title": "Release 2.0.0", + "body": """ +## Checklist +- [x] Prepare Release | status=done pr=#122 commit=abcdef12 +- [x] Create Release branch | status=done branch=release/2.0 commit=abcdef12 +- [ ] Tag RC0 | status=pending + +## Backports +- [ ] #124 | status=pending +""", + "labels": ["type: release"], + } + # Act + result = CreateRc(args, mock_git, mock_gh).run() + + # Assert + assert result == 1 + mock_git.tag.assert_not_called() + mock_git.push.assert_not_called() + + +def test_create_rc_not_blocked_by_ignored_backports(mock_git, mock_gh): + # Arrange + args = argparse.Namespace( + issue=123, remote="my-remote", triggering_comment=None, dry_run=False + ) + mock_gh.issues[123] = { + "title": "Release 2.0.0", + "body": """ +## Checklist +- [x] Prepare Release | status=done pr=#122 commit=abcdef12 +- [x] Create Release branch | status=done branch=release/2.0 commit=abcdef12 +- [ ] Tag RC0 | status=pending + +## Backports +- [ ] #124 | status=ignore +""", + "labels": ["type: release"], + } + mock_git.get_remote_tags.return_value = [] + mock_git.get_commit_sha.return_value = "1234567890" + + # Act + result = CreateRc(args, mock_git, mock_gh).run() + + # Assert + assert result == 0 + mock_git.tag.assert_called_once_with("2.0.0-rc0", "my-remote/release/2.0") + mock_git.push.assert_called_once_with("my-remote", "2.0.0-rc0") + + +def test_create_rc_with_finished_backports(mock_git, mock_gh): + # Arrange + args = argparse.Namespace( + issue=123, remote="my-remote", triggering_comment=None, dry_run=False + ) + mock_gh.issues[123] = { + "title": "Release 2.0.0", + "body": """ +## Checklist +- [x] Prepare Release | status=done pr=#122 commit=abcdef12 +- [x] Create Release branch | status=done branch=release/2.0 commit=abcdef12 +- [ ] Tag RC0 | status=pending + +## Backports +- [x] #124 | status=done rc=rc0 commit=abcdef12 +""", + "labels": ["type: release"], + } + mock_git.get_remote_tags.return_value = [] + mock_git.get_commit_sha.return_value = "1234567890" + + # Act + result = CreateRc(args, mock_git, mock_gh).run() + + # Assert + assert result == 0 + mock_git.tag.assert_called_once_with("2.0.0-rc0", "my-remote/release/2.0") + mock_git.push.assert_called_once_with("my-remote", "2.0.0-rc0") + + +def test_create_rc_auto_add_task(mock_git, mock_gh): + # Arrange + args = argparse.Namespace( + issue=123, remote="my-remote", triggering_comment=None, dry_run=False + ) + mock_gh.issues[123] = { + "title": "Release 2.0.0", + "body": """ +## Checklist +- [x] Prepare Release | status=done pr=#122 commit=abcdef12 +- [x] Create Release branch | status=done branch=release/2.0 commit=abcdef12 +- [x] Tag RC0 | status=done tag=2.0.0-rc0 commit=abcdef12 +- [ ] Tag Final +""", + "labels": ["type: release"], + } + mock_git.get_remote_tags.return_value = ["2.0.0-rc0"] + mock_git.get_commit_sha.return_value = "1234567890" + + # Act + result = CreateRc(args, mock_git, mock_gh).run() + + # Assert + assert result == 0 + mock_git.tag.assert_called_once_with("2.0.0-rc1", "my-remote/release/2.0") + mock_git.push.assert_called_once_with("my-remote", "2.0.0-rc1") + + updated_body = mock_gh.get_issue_body(123) + assert "- [x] Tag RC1 | status=done tag=2.0.0-rc1 commit= 12345678" in updated_body + + +def test_create_rc_calls_process_backports(mocker, mock_git, mock_gh): + # Arrange + mock_pb_class = mocker.patch("tools.private.release.create_rc.ProcessBackports") + mock_pb = mock_pb_class.return_value + mock_pb.run.return_value = 0 + + args = argparse.Namespace( + issue=123, remote="my-remote", triggering_comment=None, dry_run=False + ) + mock_gh.issues[123] = { + "title": "Release 2.0.0", + "body": """ +## Checklist +- [x] Prepare Release | status=done pr=#122 commit=abcdef12 +- [x] Create Release branch | status=done branch=release/2.0 commit=abcdef12 +- [ ] Tag RC0 | status=pending +""", + "labels": ["type: release"], + } + mock_git.get_remote_tags.return_value = [] + mock_git.get_commit_sha.return_value = "1234567890" + + # Act + result = CreateRc(args, mock_git, mock_gh).run() + + # Assert + assert result == 0 + mock_pb_class.assert_called_once() + called_args = mock_pb_class.call_args[0][0] + assert called_args.issue == 123 + assert called_args.remote == "my-remote" + assert not called_args.dry_run + assert called_args.add is None + assert called_args.triggering_comment is None + mock_pb.run.assert_called_once() + + +def test_create_rc_aborts_on_process_backports_failure(mocker, mock_git, mock_gh): + # Arrange + mock_pb_class = mocker.patch("tools.private.release.create_rc.ProcessBackports") + mock_pb = mock_pb_class.return_value + mock_pb.run.return_value = 1 + + args = argparse.Namespace( + issue=123, remote="my-remote", triggering_comment=None, dry_run=False + ) + + # Act + result = CreateRc(args, mock_git, mock_gh).run() + + # Assert + assert result == 1 + mock_pb_class.assert_called_once() + mock_pb.run.assert_called_once() + mock_git.tag.assert_not_called() + + +def test_create_rc_failure_reacts_to_comment(mocker, mock_git, mock_gh): + # Arrange + mock_pb_class = mocker.patch("tools.private.release.create_rc.ProcessBackports") + mock_pb = mock_pb_class.return_value + mock_pb.run.return_value = 1 # Simulate failure + + args = argparse.Namespace( + issue=123, remote="my-remote", triggering_comment=456, dry_run=False + ) + + # Act + result = CreateRc(args, mock_git, mock_gh).run() + + # Assert + assert result == 1 + assert mock_gh.reactions.get(456) == ["-1"] + + +def test_create_rc_failure_no_comment_no_reaction(mocker, mock_git, mock_gh): + # Arrange + mock_pb_class = mocker.patch("tools.private.release.create_rc.ProcessBackports") + mock_pb = mock_pb_class.return_value + mock_pb.run.return_value = 1 # Simulate failure + + args = argparse.Namespace( + issue=123, remote="my-remote", triggering_comment=None, dry_run=False + ) + + # Act + result = CreateRc(args, mock_git, mock_gh).run() + + # Assert + assert result == 1 + assert 456 not in mock_gh.reactions + + +def test_create_rc_success_with_comment_no_reaction(mocker, mock_git, mock_gh): + # Arrange + mock_pb_class = mocker.patch("tools.private.release.create_rc.ProcessBackports") + mock_pb = mock_pb_class.return_value + mock_pb.run.return_value = 0 + + args = argparse.Namespace( + issue=123, remote="my-remote", triggering_comment=456, dry_run=False + ) + mock_gh.issues[123] = { + "title": "Release 2.0.0", + "body": """ +## Checklist +- [x] Prepare Release | status=done pr=#122 commit=abcdef12 +- [x] Create Release branch | status=done branch=release/2.0 commit=abcdef12 +- [ ] Tag RC0 | status=pending +""", + "labels": ["type: release"], + } + mock_git.get_remote_tags.return_value = [] + mock_git.get_commit_sha.return_value = "1234567890" + + # Act + result = CreateRc(args, mock_git, mock_gh).run() + + # Assert + assert result == 0 + assert 456 not in mock_gh.reactions + + +def test_create_rc_precondition_failure_reacts_to_comment(mocker, mock_git, mock_gh): + # Arrange + mock_pb_class = mocker.patch("tools.private.release.create_rc.ProcessBackports") + mock_pb = mock_pb_class.return_value + mock_pb.run.return_value = 0 # Backports succeed + + args = argparse.Namespace( + issue=123, remote="my-remote", triggering_comment=456, dry_run=False + ) + mock_gh.issues[123] = { + "title": "Release 2.0.0", + "body": """ +## Checklist +- [ ] Prepare Release | status=pending +- [ ] Create Release branch | status=pending +- [ ] Tag RC0 | status=pending +""", + "labels": ["type: release"], + } + + # Act + result = CreateRc(args, mock_git, mock_gh).run() + + # Assert + assert result == 1 + assert mock_gh.reactions.get(456) == ["-1"] diff --git a/tests/tools/private/release/create_release_branch_test.py b/tests/tools/private/release/create_release_branch_test.py new file mode 100644 index 0000000000..332ffec851 --- /dev/null +++ b/tests/tools/private/release/create_release_branch_test.py @@ -0,0 +1,157 @@ +import argparse + +from tools.private.release.create_release_branch import CreateReleaseBranch + +pytest_plugins = ["tests.tools.private.release.release_test_helper"] + + +def test_create_release_branch_success(mock_git, mock_gh): + # Arrange + args = argparse.Namespace(issue=123, remote="my-remote") + mock_gh.issues[123] = { + "title": "Release 2.0.0", + "body": """ +## Checklist +- [x] Prepare Release | status=done pr=#122 commit=abcdef12 +- [ ] Create Release branch | status=pending +""", + "labels": ["type: release"], + } + mock_git.branch_exists.return_value = False + mock_git.remote_branch_exists.return_value = False + + # Act + result = CreateReleaseBranch(args, mock_git, mock_gh).run() + + # Assert + assert result == 0 + mock_git.fetch.assert_called_once_with("my-remote") + mock_git.checkout.assert_not_called() + mock_git.push.assert_called_once_with( + "my-remote", "abcdef12:refs/heads/release/2.0" + ) + + updated_body = mock_gh.get_issue_body(123) + assert ( + "branch_url=https://github.com/bazel-contrib/rules_python/tree/release/2.0" + in updated_body + ) + assert "commit= abcdef12" in updated_body + + +def test_create_release_branch_prepare_not_done(mock_git, mock_gh): + # Arrange + args = argparse.Namespace(issue=123, remote="my-remote") + mock_gh.issues[123] = { + "title": "Release 2.0.0", + "body": """ +## Checklist +- [ ] Prepare Release | status=pending +- [ ] Create Release branch | status=pending +""", + "labels": ["type: release"], + } + # Act + result = CreateReleaseBranch(args, mock_git, mock_gh).run() + + # Assert + assert result == 1 + mock_git.fetch.assert_not_called() + mock_git.push.assert_not_called() + + +def test_create_release_branch_already_checked(mock_git, mock_gh): + # Arrange + args = argparse.Namespace(issue=123, remote="my-remote") + mock_gh.issues[123] = { + "title": "Release 2.0.0", + "body": """ +## Checklist +- [x] Prepare Release | status=done pr=#122 commit=abcdef12 +- [x] Create Release branch | status=done branch=release/2.0 commit=abcdef12 +""", + "labels": ["type: release"], + } + # Act + result = CreateReleaseBranch(args, mock_git, mock_gh).run() + + # Assert + assert result == 0 + mock_git.fetch.assert_not_called() + mock_git.push.assert_not_called() + + +def test_create_release_branch_already_exists_same_commit(mock_git, mock_gh): + # Arrange + args = argparse.Namespace(issue=123, remote="my-remote") + mock_gh.issues[123] = { + "title": "Release 2.0.0", + "body": """ +## Checklist +- [x] Prepare Release | status=done pr=#122 commit=abcdef12 +- [ ] Create Release branch | status=pending +""", + "labels": ["type: release"], + } + mock_git.remote_branch_exists.return_value = True + mock_git.get_commit_sha.return_value = "abcdef12" + + # Act + result = CreateReleaseBranch(args, mock_git, mock_gh).run() + + # Assert + assert result == 0 + mock_git.fetch.assert_called_once_with("my-remote") + mock_git.push.assert_not_called() + + +def test_create_release_branch_already_exists_fast_forward(mock_git, mock_gh): + # Arrange + args = argparse.Namespace(issue=123, remote="my-remote") + mock_gh.issues[123] = { + "title": "Release 2.0.0", + "body": """ +## Checklist +- [x] Prepare Release | status=done pr=#122 commit=abcdef12 +- [ ] Create Release branch | status=pending +""", + "labels": ["type: release"], + } + mock_git.remote_branch_exists.return_value = True + mock_git.get_commit_sha.return_value = "oldcommit" + mock_git.is_ancestor.return_value = True + + # Act + result = CreateReleaseBranch(args, mock_git, mock_gh).run() + + # Assert + assert result == 0 + mock_git.fetch.assert_called_once_with("my-remote") + mock_git.push.assert_called_once_with( + "my-remote", "abcdef12:refs/heads/release/2.0" + ) + + +def test_create_release_branch_already_exists_non_ff(mock_git, mock_gh): + # Arrange + args = argparse.Namespace(issue=123, remote="my-remote") + mock_gh.issues[123] = { + "title": "Release 2.0.0", + "body": """ +## Checklist +- [x] Prepare Release | status=done pr=#122 commit=abcdef12 +- [ ] Create Release branch | status=pending +""", + "labels": ["type: release"], + } + mock_git.remote_branch_exists.return_value = True + mock_git.get_commit_sha.return_value = "othercommit" + mock_git.is_ancestor.return_value = False + + # Act + result = CreateReleaseBranch(args, mock_git, mock_gh).run() + + # Assert + assert result == 1 + mock_git.fetch.assert_called_once_with("my-remote") + mock_git.push.assert_not_called() diff --git a/tests/tools/private/release/gh_test.py b/tests/tools/private/release/gh_test.py new file mode 100644 index 0000000000..7e19b2232c --- /dev/null +++ b/tests/tools/private/release/gh_test.py @@ -0,0 +1,96 @@ +import pytest + +from tools.private.release import shell +from tools.private.release.gh import GitHub +from tools.private.release.git import Git + +pytest_plugins = ["tests.tools.private.release.release_test_helper"] + + +@pytest.fixture(name="gh") +def fixture_gh(): + return GitHub("my-owner/my-repo") + + +def test_resolve_pr_number_digit(mocker, gh): + mock_run_cmd = mocker.patch("tools.private.release.gh.run_cmd") + # 124 and #125 should resolve immediately without running command + assert gh.resolve_pr_number("124") == 124 + assert gh.resolve_pr_number("#125") == 125 + mock_run_cmd.assert_not_called() + + +def test_resolve_pr_number_url_simple(mocker, gh): + mock_run_cmd = mocker.patch("tools.private.release.gh.run_cmd") + url = "https://github.com/my-owner/my-repo/pull/126" + # Should resolve via regex without calling gh + result = gh.resolve_pr_number(url) + assert result == 126 + mock_run_cmd.assert_not_called() + + +def test_resolve_pr_number_url_with_subpath(mocker, gh): + mock_run_cmd = mocker.patch("tools.private.release.gh.run_cmd") + url = "https://github.com/my-owner/my-repo/pull/126/files" + # Should resolve via regex without calling gh + result = gh.resolve_pr_number(url) + assert result == 126 + mock_run_cmd.assert_not_called() + + +def test_resolve_pr_number_url_with_query(mocker, gh): + mock_run_cmd = mocker.patch("tools.private.release.gh.run_cmd") + url = "https://github.com/my-owner/my-repo/pull/126/files?w=1" + # Should resolve via regex without calling gh + result = gh.resolve_pr_number(url) + assert result == 126 + mock_run_cmd.assert_not_called() + + +def test_resolve_pr_number_url_other_repo(mocker, gh): + mock_run_cmd = mocker.patch("tools.private.release.gh.run_cmd") + # URL for a different repo should fail immediately without calling gh + url = "https://github.com/other-owner/other-repo/pull/126" + with pytest.raises(ValueError, match="URL is not for the configured repository"): + gh.resolve_pr_number(url) + mock_run_cmd.assert_not_called() + + +def test_resolve_pr_number_invalid(mocker, gh): + mock_run_cmd = mocker.patch("tools.private.release.gh.run_cmd") + with pytest.raises(ValueError, match="Could not resolve PR reference"): + gh.resolve_pr_number("invalid-ref") + mock_run_cmd.assert_not_called() + + +def test_auto_patched_helpers_prevent_real_execution(auto_patch_cmd_helpers): + # Calling run_cmd directly hits the mock + shell.run_cmd("echo", "test") + auto_patch_cmd_helpers.run_cmd.assert_called_with("echo", "test") + + # Git._run_git hits the mock + git = Git(".") + git._run_git("status") + auto_patch_cmd_helpers.run_git.assert_called_with("status") + + # GitHub._run_gh hits the mock + gh_obj = GitHub("foo/bar") + gh_obj._run_gh("issue", "list") + auto_patch_cmd_helpers.run_gh.assert_called_with("issue", "list") + + +def test_update_issue_body(gh, auto_patch_cmd_helpers): + captured_body = {} + + def mock_run(*args, **kwargs): + for arg in args: + if isinstance(arg, str) and arg.startswith("--body-file="): + path = arg.split("=", 1)[1] + with open(path, encoding="utf-8") as f: + captured_body["content"] = f.read() + return None + + auto_patch_cmd_helpers.run_gh.side_effect = mock_run + gh.update_issue_body(123, "new body content") + auto_patch_cmd_helpers.run_gh.assert_called_once() + assert captured_body["content"] == "new body content" diff --git a/tests/tools/private/release/git_test.py b/tests/tools/private/release/git_test.py new file mode 100644 index 0000000000..e39787f187 --- /dev/null +++ b/tests/tools/private/release/git_test.py @@ -0,0 +1,154 @@ +import subprocess + +import pytest + +from tools.private.release.git import Git + +pytest_plugins = ["tests.tools.private.release.release_test_helper"] + + +@pytest.fixture(name="git_obj") +def fixture_git_obj(mocker): + git = Git(".") + git.mock_run_git = mocker.patch.object(git, "_run_git") # pyrefly: ignore[missing-attribute] + return git + + +def test_checkout_simple(git_obj): + git_obj.checkout("my-branch") + git_obj.mock_run_git.assert_called_once_with( + "checkout", "my-branch", capture_output=False + ) + + +def test_checkout_track_remote_new_branch(mocker, git_obj): + mock_branch_exists = mocker.patch( + "tools.private.release.git.Git.branch_exists", return_value=False + ) + + git_obj.checkout("my-branch", track_remote="origin") + + mock_branch_exists.assert_called_once_with("my-branch") + git_obj.mock_run_git.assert_called_once_with( + "checkout", "--track", "origin/my-branch", capture_output=False + ) + + +def test_checkout_track_remote_existing_branch(mocker, git_obj): + mock_branch_exists = mocker.patch( + "tools.private.release.git.Git.branch_exists", return_value=True + ) + mock_reset_hard = mocker.patch("tools.private.release.git.Git.reset_hard") + + git_obj.checkout("my-branch", track_remote="origin") + + mock_branch_exists.assert_called_once_with("my-branch") + git_obj.mock_run_git.assert_called_once_with( + "checkout", "my-branch", capture_output=False + ) + mock_reset_hard.assert_called_once_with(reset_to="origin/my-branch") + + +def test_fetch_default(git_obj): + git_obj.fetch() + git_obj.mock_run_git.assert_called_once_with( + "fetch", "origin", capture_output=False + ) + + +def test_fetch_custom_remote(git_obj): + git_obj.fetch("upstream") + git_obj.mock_run_git.assert_called_once_with( + "fetch", "upstream", capture_output=False + ) + + +def test_fetch_with_refspec(git_obj): + git_obj.fetch("origin", refspec="my-branch") + git_obj.mock_run_git.assert_called_once_with( + "fetch", "origin", "my-branch", capture_output=False + ) + + +def test_fetch_with_tags_and_force(git_obj): + git_obj.fetch("origin", tags=True, force=True) + git_obj.mock_run_git.assert_called_once_with( + "fetch", "origin", "--tags", "--force", capture_output=False + ) + + +def test_fetch_all_options(git_obj): + git_obj.fetch("origin", refspec="my-branch", tags=True, force=True) + git_obj.mock_run_git.assert_called_once_with( + "fetch", "origin", "my-branch", "--tags", "--force", capture_output=False + ) + + +def test_get_modified_files(git_obj): + git_obj.mock_run_git.return_value = "file1.txt\nfile2.py\n\n" + files = git_obj.get_modified_files("HEAD") + git_obj.mock_run_git.assert_called_once_with( + "show", "--name-only", "--format=", "HEAD" + ) + assert files == ["file1.txt", "file2.py"] + + +def test_get_modified_files_empty(git_obj): + git_obj.mock_run_git.return_value = "" + files = git_obj.get_modified_files("HEAD") + assert files == [] + + +def test_diff_has_changes(git_obj): + git_obj.mock_run_git.return_value = "some diff output" + output = git_obj.diff() + git_obj.mock_run_git.assert_called_once_with("diff") + assert output == "some diff output" + + +def test_diff_empty(git_obj): + git_obj.mock_run_git.return_value = "" + output = git_obj.diff() + git_obj.mock_run_git.assert_called_once_with("diff") + assert output == "" + + +def test_apply(git_obj): + git_obj.apply("patch.patch") + git_obj.mock_run_git.assert_called_once_with( + "apply", "patch.patch", capture_output=False + ) + + +def test_apply_check_clean(git_obj): + git_obj.mock_run_git.return_value = "" + result = git_obj.apply_check("patch.patch") + git_obj.mock_run_git.assert_called_once_with( + "apply", "--check", "patch.patch", capture_output=False + ) + assert result is True + + +def test_apply_check_conflict(git_obj): + git_obj.mock_run_git.side_effect = subprocess.CalledProcessError( + 1, ["git", "apply", "--check", "patch.patch"] + ) + result = git_obj.apply_check("patch.patch") + git_obj.mock_run_git.assert_called_once_with( + "apply", "--check", "patch.patch", capture_output=False + ) + assert result is False + + +def test_reset_hard_default(git_obj): + git_obj.reset_hard() + git_obj.mock_run_git.assert_called_once_with( + "reset", "--hard", "HEAD", capture_output=False + ) + + +def test_reset_hard_custom(git_obj): + git_obj.reset_hard(reset_to="my-commit") + git_obj.mock_run_git.assert_called_once_with( + "reset", "--hard", "my-commit", capture_output=False + ) diff --git a/tests/tools/private/release/on_pr_merged_test.py b/tests/tools/private/release/on_pr_merged_test.py new file mode 100644 index 0000000000..df3698d1dd --- /dev/null +++ b/tests/tools/private/release/on_pr_merged_test.py @@ -0,0 +1,92 @@ +import argparse +from unittest.mock import MagicMock + +from tools.private.release.on_pr_merged import OnPrMerged + +pytest_plugins = ["tests.tools.private.release.release_test_helper"] + + +def test_on_pr_merged_no_comment(mocker, mock_git, mock_gh): + args = argparse.Namespace(pr=124, remote="origin", dry_run=True) + mock_gh.pr_comments = { + 124: [ + {"body": "Some comment"}, + {"body": "Another comment /backport_wrong"}, + ] + } + + mock_pb = mocker.patch("tools.private.release.on_pr_merged.ProcessBackports") + result = OnPrMerged(args, mock_git, mock_gh).run() + + assert result == 1 + mock_pb.assert_not_called() + + +def test_on_pr_merged_has_comment_no_active_release(mocker, mock_git, mock_gh): + args = argparse.Namespace(pr=124, remote="origin", dry_run=True) + mock_gh.pr_comments = {124: [{"body": "/backport"}]} + + mock_pb = mocker.patch("tools.private.release.on_pr_merged.ProcessBackports") + result = OnPrMerged(args, mock_git, mock_gh).run() + + assert result == 1 + mock_pb.assert_not_called() + + +def test_on_pr_merged_has_comment_not_in_backports(mocker, mock_git, mock_gh): + args = argparse.Namespace(pr=124, remote="origin", dry_run=True) + mock_gh.pr_comments = {124: [{"body": " /backport "}]} + mock_gh.create_issue( + title="Release 2.1.0", + body=""" +## Checklist +- [ ] Prepare Release + +## Backports +- [ ] #125 | status=pending +""", + labels=["type: release"], + ) + + mock_pb = mocker.patch("tools.private.release.on_pr_merged.ProcessBackports") + result = OnPrMerged(args, mock_git, mock_gh).run() + + assert result == 1 + mock_pb.assert_not_called() + + +def test_on_pr_merged_success(mocker, mock_git, mock_gh): + args = argparse.Namespace(pr=124, remote="origin", dry_run=True) + mock_gh.pr_comments = {124: [{"body": "/backport"}]} + issue_num = mock_gh.create_issue( + title="Release 2.1.0", + body=""" +## Checklist +- [ ] Prepare Release + +## Backports +- [ ] #124 | status=pending +""", + labels=["type: release"], + ) + + mock_pb_class = mocker.patch("tools.private.release.on_pr_merged.ProcessBackports") + mock_pb_instance = MagicMock() + mock_pb_instance.run.return_value = 0 + mock_pb_class.return_value = mock_pb_instance + + result = OnPrMerged(args, mock_git, mock_gh).run() + + assert result == 0 + + # Verify ProcessBackports was instantiated with correct args + mock_pb_class.assert_called_once() + called_args = mock_pb_class.call_args[0][0] + assert called_args.issue == issue_num + assert called_args.remote == "origin" + assert called_args.dry_run is True + assert called_args.add is None + assert called_args.triggering_comment is None + + # Verify ProcessBackports.run() was called + mock_pb_instance.run.assert_called_once() diff --git a/tests/tools/private/release/prepare_test.py b/tests/tools/private/release/prepare_test.py new file mode 100644 index 0000000000..64275df96f --- /dev/null +++ b/tests/tools/private/release/prepare_test.py @@ -0,0 +1,211 @@ +import argparse + +from tools.private.release.gh import RELEASE_PREPARED_LABEL +from tools.private.release.prepare import Prepare + +pytest_plugins = ["tests.tools.private.release.release_test_helper"] + + +def test_prepare_success_existing_issue(mocker, release_tool_env, mock_git, mock_gh): + mocker.patch("tools.private.release.prepare.replace_version_next") + mocker.patch("tools.private.release.prepare.changelog_news") + + # Arrange + args = argparse.Namespace(version="2.0.0", issue=None, dry_run=False) + mock_gh.create_issue( + title="Release 2.0.0", + body="- [ ] Prepare Release", + labels=["type: release"], + ) # Assigns issue 1001 + mock_git.status.side_effect = ["", "M foo"] + mock_git.branch_exists.return_value = False + + # Act + result = Prepare(args, mock_git, mock_gh).run() + + # Assert + assert result == 0 + assert 1002 in mock_gh.prs + assert mock_gh.prs[1002]["title"] == "Prepare release v2.0.0" + assert mock_gh.prs[1002]["body"] == "Work towards #1001" + assert mock_gh.prs[1002]["labels"] == [RELEASE_PREPARED_LABEL] + mock_git.add_modified_and_deleted.assert_called_once() + + +def test_prepare_success_create_issue(mocker, release_tool_env, mock_git, mock_gh): + mocker.patch("tools.private.release.prepare.replace_version_next") + mocker.patch("tools.private.release.prepare.changelog_news") + + # Arrange: release_tool_env sets up template_file automatically + args = argparse.Namespace(version="2.0.0", issue=None, dry_run=False) + mock_git.status.side_effect = ["", "M foo"] + mock_git.branch_exists.return_value = False + + # Act + result = Prepare(args, mock_git, mock_gh).run() + + # Assert + assert result == 0 + assert 1001 in mock_gh.issues + assert mock_gh.issues[1001]["title"] == "Release 2.0.0" + assert 1002 in mock_gh.prs + assert mock_gh.prs[1002]["title"] == "Prepare release v2.0.0" + assert mock_gh.prs[1002]["body"] == "Work towards #1001" + assert mock_gh.prs[1002]["labels"] == [RELEASE_PREPARED_LABEL] + mock_git.add_modified_and_deleted.assert_called_once() + + +def test_prepare_ambiguous_issue(mocker, release_tool_env, mock_git, mock_gh): + mocker.patch("tools.private.release.prepare.replace_version_next") + mocker.patch("tools.private.release.prepare.changelog_news") + + # Arrange + args = argparse.Namespace(version="2.0.0", issue=None, dry_run=False) + mock_gh.create_issue( + title="Release 2.0.0", body="issue 1", labels=["type: release"] + ) + mock_gh.create_issue( + title="Release 2.0.0", body="issue 2", labels=["type: release"] + ) + mock_git.status.side_effect = ["", "M foo"] + mock_git.branch_exists.return_value = False + + # Act + result = Prepare(args, mock_git, mock_gh).run() + + # Assert + assert result == 1 + mock_git.add_modified_and_deleted.assert_not_called() + + +def test_prepare_dry_run(mocker, release_tool_env, mock_git, mock_gh): + mocker.patch("tools.private.release.prepare.replace_version_next") + mocker.patch("tools.private.release.prepare.changelog_news") + + # Arrange + args = argparse.Namespace(version="2.0.0", issue=None, dry_run=True) + mock_gh.create_issue(title="Release 2.0.0", body="body", labels=["type: release"]) + mock_git.status.side_effect = [""] + + # Act + result = Prepare(args, mock_git, mock_gh).run() + + # Assert + assert result == 0 + mock_git.checkout.assert_not_called() + mock_git.commit.assert_not_called() + mock_git.push.assert_not_called() + mock_git.fetch.assert_called_once() + mock_git.add_modified_and_deleted.assert_not_called() + + +def test_prepare_use_associated_pr_from_tracking_issue( + mocker, release_tool_env, mock_git, mock_gh +): + mocker.patch("tools.private.release.prepare.replace_version_next") + mocker.patch("tools.private.release.prepare.changelog_news") + + # Arrange + args = argparse.Namespace(version="2.0.0", issue=None, dry_run=False) + mock_gh.create_issue( + title="Release 2.0.0", + body="- [ ] Prepare Release | status=pending pr=#456", + labels=["type: release"], + ) + mock_git.status.side_effect = ["", ""] + mock_git.branch_exists.return_value = True + + # Act + result = Prepare(args, mock_git, mock_gh).run() + + # Assert + assert result == 0 + mock_git.checkout.assert_called_once_with("prepare-2.0.0") + mock_git.commit.assert_not_called() + mock_git.push.assert_called_once_with( + "origin", "prepare-2.0.0", set_upstream=True, force=True + ) + updated_body = mock_gh.get_issue_body(1001) + assert "pr=#456" in updated_body + + +def test_prepare_create_pr_when_none_associated( + mocker, release_tool_env, mock_git, mock_gh +): + mocker.patch("tools.private.release.prepare.replace_version_next") + mocker.patch("tools.private.release.prepare.changelog_news") + + # Arrange + args = argparse.Namespace(version="2.0.0", issue=None, dry_run=False) + mock_gh.create_issue( + title="Release 2.0.0", + body="- [ ] Prepare Release", + labels=["type: release"], + ) + mock_git.status.side_effect = ["", ""] + mock_git.branch_exists.return_value = True + + # Act + result = Prepare(args, mock_git, mock_gh).run() + + # Assert + assert result == 0 + mock_git.checkout.assert_called_once_with("prepare-2.0.0") + mock_git.commit.assert_not_called() + mock_git.push.assert_called_once_with( + "origin", "prepare-2.0.0", set_upstream=True, force=True + ) + updated_body = mock_gh.get_issue_body(1001) + assert "pr=#1002" in updated_body + assert mock_gh.prs[1002]["labels"] == [RELEASE_PREPARED_LABEL] + + +def test_prepare_reuse_existing_pr(mocker, release_tool_env, mock_git, mock_gh): + mocker.patch("tools.private.release.prepare.replace_version_next") + mocker.patch("tools.private.release.prepare.changelog_news") + + # Arrange + args = argparse.Namespace(version="2.0.0", issue=None, dry_run=False) + mock_gh.create_issue( + title="Release 2.0.0", + body="- [ ] Prepare Release", + labels=["type: release"], + ) + mock_gh.prs[456] = { + "number": 456, + "head": "prepare-2.0.0", + "state": "OPEN", + "url": "https://github.com/foo/bar/pull/456", + } + mock_git.status.side_effect = ["", ""] + mock_git.branch_exists.return_value = True + + # Act + result = Prepare(args, mock_git, mock_gh).run() + + # Assert + assert result == 0 + mock_git.checkout.assert_called_once_with("prepare-2.0.0") + mock_git.commit.assert_not_called() + mock_git.push.assert_called_once_with( + "origin", "prepare-2.0.0", set_upstream=True, force=True + ) + updated_body = mock_gh.get_issue_body(1001) + assert "pr=#456" in updated_body + + +def test_prepare_dry_run_no_issue(mocker, release_tool_env, mock_git, mock_gh): + mocker.patch("tools.private.release.prepare.replace_version_next") + mocker.patch("tools.private.release.prepare.changelog_news") + + # Arrange + args = argparse.Namespace(version="2.0.0", issue=None, dry_run=True) + mock_git.status.side_effect = [""] + + # Act + result = Prepare(args, mock_git, mock_gh).run() + + # Assert + assert result == 0 + mock_git.checkout.assert_not_called() + mock_git.add_modified_and_deleted.assert_not_called() diff --git a/tests/tools/private/release/process_backports_test.py b/tests/tools/private/release/process_backports_test.py new file mode 100644 index 0000000000..5c37e63a97 --- /dev/null +++ b/tests/tools/private/release/process_backports_test.py @@ -0,0 +1,493 @@ +import argparse +import datetime +from unittest.mock import ANY, call + +from tools.private.release.process_backports import ProcessBackports + +pytest_plugins = ["tests.tools.private.release.release_test_helper"] + + +def test_process_backports_no_pending(mock_git, mock_gh): + args = argparse.Namespace( + issue=123, remote="origin", dry_run=False, add=None, triggering_comment=None + ) + mock_gh.issues[123] = { + "title": "Release 2.0.0", + "body": "No backports here", + "labels": ["type: release"], + } + + result = ProcessBackports(args, mock_git, mock_gh).run() + + assert result == 0 + mock_git.fetch.assert_not_called() + + +def test_process_backports_success(mocker, mock_git, mock_gh): + mock_changelog = mocker.patch( + "tools.private.release.process_backports.changelog_news" + ) + mock_replace = mocker.patch( + "tools.private.release.process_backports.replace_version_next" + ) + mock_datetime = mocker.patch("tools.private.release.process_backports.datetime") + mock_datetime.date.today.return_value = datetime.date(2026, 7, 1) + + args = argparse.Namespace( + issue=123, remote="origin", dry_run=False, add=None, triggering_comment=None + ) + mock_gh.issues[123] = { + "title": "Release 2.0.0", + "body": """ +## Checklist +- [ ] Prepare Release +- [ ] Create Release branch +- [ ] Sync Changelog #124 +- [ ] Tag Final + +## Backports +- [ ] #124 | status=pending +""", + "labels": ["type: release"], + } + mock_gh.prs[124] = { + "state": "MERGED", + "mergeCommit": {"oid": "abcdef12"}, + } + mock_git.get_remote_tags.return_value = [] + mock_git.sort_commits_chronologically.return_value = ["abcdef12"] + mock_git.get_commit_sha.side_effect = ["12345678", "12345678", "main_sha"] + mock_git.get_commit_message.return_value = 'Cherry-pick "fix bug"' + mock_git.get_modified_files.return_value = ["news/124.fixed.md"] + mock_git.diff.return_value = "version diff for 124" + mock_git.apply_check.return_value = True + + result = ProcessBackports(args, mock_git, mock_gh).run() + + assert result == 0 + mock_git.fetch.assert_has_calls( + [ + call("origin", tags=True, force=True), + call("origin"), + call("origin", refspec="main"), + ] + ) + mock_git.checkout.assert_has_calls( + [ + call("release/2.0", track_remote="origin"), + call("main", track_remote="origin"), + call("prepare-2.0.0-backports-6affdae", create_branch=True), + call("release/2.0"), + ] + ) + mock_git.cherry_pick.assert_called_once_with("abcdef12") + mock_git.diff.assert_called_once() + mock_git.apply_check.assert_called_once_with(ANY) + mock_git.apply.assert_called_once_with(ANY) + mock_changelog.update_changelog.assert_has_calls( + [ + call("2.0.0", "2026-07-01"), + call( + "2.0.0", + "2026-07-01", + news_files=["news/124.fixed.md"], + delete_news=True, + ), + ] + ) + assert mock_git.add_modified_and_deleted.call_count == 2 + mock_replace.assert_called_once_with("2.0.0") + mock_git.commit.assert_has_calls( + [ + call('Cherry-pick "fix bug"\n\nWork towards #123', amend=True), + call("chore(release): sync changelog for v2.0.0 backports"), + ] + ) + + updated_body = mock_gh.get_issue_body(123) + assert "- [x] #124 | status=done rc=rc0 commit= 12345678" in updated_body + assert "- [ ] Sync Changelog #124 | status=pending pr=#1001" in updated_body + + +def test_process_backports_sync_branch_exists(mocker, mock_git, mock_gh): + mocker.patch("tools.private.release.process_backports.changelog_news") + mocker.patch("tools.private.release.process_backports.replace_version_next") + mock_datetime = mocker.patch("tools.private.release.process_backports.datetime") + mock_datetime.date.today.return_value = datetime.date(2026, 7, 1) + + args = argparse.Namespace( + issue=123, remote="origin", dry_run=False, add=None, triggering_comment=None + ) + mock_gh.issues[123] = { + "title": "Release 2.0.0", + "body": """ +## Checklist +- [ ] Prepare Release +- [ ] Create Release branch +- [ ] Sync Changelog #124 +- [ ] Tag Final + +## Backports +- [ ] #124 | status=pending +""", + "labels": ["type: release"], + } + mock_gh.prs[124] = { + "state": "MERGED", + "mergeCommit": {"oid": "abcdef12"}, + } + mock_git.get_remote_tags.return_value = [] + mock_git.sort_commits_chronologically.return_value = ["abcdef12"] + mock_git.get_commit_sha.side_effect = ["12345678", "12345678", "main_sha"] + mock_git.get_commit_message.return_value = 'Cherry-pick "fix bug"' + mock_git.get_modified_files.return_value = ["news/124.fixed.md"] + mock_git.diff.return_value = "version diff for 124" + mock_git.apply_check.return_value = True + mock_git.branch_exists.return_value = True + + result = ProcessBackports(args, mock_git, mock_gh).run() + + assert result == 0 + mock_git.checkout.assert_has_calls( + [ + call("release/2.0", track_remote="origin"), + call("main", track_remote="origin"), + call("prepare-2.0.0-backports-6affdae"), + call("release/2.0"), + ] + ) + mock_git.reset_hard.assert_has_calls([call(reset_to="main")]) + + +def test_process_backports_dry_run(mocker, mock_git, mock_gh): + mocker.patch("tools.private.release.process_backports.changelog_news") + mocker.patch("tools.private.release.process_backports.replace_version_next") + mock_datetime = mocker.patch("tools.private.release.process_backports.datetime") + mock_datetime.date.today.return_value = datetime.date(2026, 7, 1) + + args = argparse.Namespace( + issue=123, remote="origin", dry_run=True, add=None, triggering_comment=None + ) + mock_gh.issues[123] = { + "title": "Release 2.0.0", + "body": """ +## Checklist +- [ ] Prepare Release +- [ ] Create Release branch +- [ ] Sync Changelog #124 +- [ ] Tag Final + +## Backports +- [ ] #124 | status=pending +""", + "labels": ["type: release"], + } + mock_gh.prs[124] = { + "state": "MERGED", + "mergeCommit": {"oid": "abcdef12"}, + } + mock_git.get_remote_tags.return_value = [] + mock_git.sort_commits_chronologically.return_value = ["abcdef12"] + mock_git.get_commit_sha.side_effect = ["12345678", "main_sha"] + mock_git.get_commit_message.return_value = 'Cherry-pick "fix bug"' + mock_git.get_modified_files.return_value = ["news/124.fixed.md"] + mock_git.diff.return_value = "version diff for 124" + mock_git.apply_check.return_value = True + + result = ProcessBackports(args, mock_git, mock_gh).run() + + assert result == 0 + mock_git.apply.assert_not_called() + mock_git.push.assert_not_called() + + +def test_process_backports_ignored_and_failed_states(mock_git, mock_gh): + args = argparse.Namespace( + issue=123, remote="origin", dry_run=False, add=None, triggering_comment=None + ) + mock_gh.issues[123] = { + "title": "Release 2.0.0", + "body": """ +## Checklist +- [ ] Prepare Release +- [ ] Create Release branch + +## Backports +- [ ] #124 | status=pending +- [ ] #125 | status=pending +- [ ] #126 | status=pending +""", + "labels": ["type: release"], + } + mock_git.get_remote_tags.return_value = [] + + mock_gh.prs[124] = {"state": "OPEN"} + mock_gh.prs[125] = {"state": "OPEN", "isDraft": True} + mock_gh.prs[126] = {"state": "CLOSED"} + + result = ProcessBackports(args, mock_git, mock_gh).run() + + assert result == 1 + updated_body = mock_gh.get_issue_body(123) + assert "- [ ] #126 | status=error-closed-pr" in updated_body + mock_git.checkout.assert_not_called() + mock_git.cherry_pick.assert_not_called() + + +def test_process_backports_ignored_error_status(mock_git, mock_gh): + args = argparse.Namespace( + issue=123, remote="origin", dry_run=False, add=None, triggering_comment=None + ) + mock_gh.issues[123] = { + "title": "Release 2.0.0", + "body": """ +## Checklist +- [ ] Prepare Release +- [ ] Create Release branch + +## Backports +- [ ] #124 | status=error-merge-conflict +- [ ] #125 | status=error-some-other-error +""", + "labels": ["type: release"], + } + mock_git.get_remote_tags.return_value = [] + + result = ProcessBackports(args, mock_git, mock_gh).run() + + assert result == 0 + mock_git.checkout.assert_not_called() + + +def test_process_backports_cherry_pick_failed(mocker, mock_git, mock_gh): + mock_datetime = mocker.patch("tools.private.release.process_backports.datetime") + mock_datetime.date.today.return_value = datetime.date(2026, 7, 1) + args = argparse.Namespace( + issue=123, remote="origin", dry_run=False, add=None, triggering_comment=None + ) + mock_gh.issues[123] = { + "title": "Release 2.0.0", + "body": """ +## Checklist +- [ ] Prepare Release +- [ ] Create Release branch + +## Backports +- [ ] #124 | status=pending +""", + "labels": ["type: release"], + } + mock_gh.prs[124] = { + "state": "MERGED", + "mergeCommit": {"oid": "abcdef12"}, + } + mock_git.get_remote_tags.return_value = [] + mock_git.sort_commits_chronologically.return_value = ["abcdef12"] + mock_git.cherry_pick.side_effect = Exception("Cherry-pick conflict") + + result = ProcessBackports(args, mock_git, mock_gh).run() + + assert result == 1 + mock_git.checkout.assert_called_once_with("release/2.0", track_remote="origin") + mock_git.cherry_pick.assert_called_once_with("abcdef12") + mock_git.cherry_pick_abort.assert_called_once() + + updated_body = mock_gh.get_issue_body(123) + assert "- [ ] #124 | status=error-merge-conflict" in updated_body + mock_git.commit.assert_not_called() + mock_git.push.assert_not_called() + + +def test_process_backports_add_backports_and_auto_add_rc_task( + mocker, mock_git, mock_gh +): + mocker.patch("tools.private.release.process_backports.changelog_news") + mocker.patch("tools.private.release.process_backports.replace_version_next") + mock_datetime = mocker.patch("tools.private.release.process_backports.datetime") + mock_datetime.date.today.return_value = datetime.date(2026, 7, 1) + args = argparse.Namespace( + issue=123, + remote="origin", + dry_run=False, + add=["https://github.com/bazel-contrib/rules_python/pull/124"], + triggering_comment=None, + ) + mock_gh.issues[123] = { + "title": "Release 2.0.0", + "body": """ +## Checklist +- [x] Prepare Release | status=done pr=#122 commit=abcdef12 +- [x] Create Release branch | status=done branch=release/2.0 commit=abcdef12 +- [x] Tag RC0 | status=done tag=2.0.0-rc0 commit=abcdef12 +- [ ] Tag Final + +## Backports +""", + "labels": ["type: release"], + } + mock_gh.prs[124] = { + "state": "MERGED", + "mergeCommit": {"oid": "abcdef12"}, + } + mock_git.get_remote_tags.return_value = ["2.0.0-rc0"] + mock_git.get_commit_sha.return_value = "12345678" + mock_git.get_commit_message.return_value = 'Cherry-pick "fix bug"' + mock_git.sort_commits_chronologically.return_value = ["abcdef12"] + mock_git.diff.return_value = "version diff" + mock_git.apply_check.return_value = True + + result = ProcessBackports(args, mock_git, mock_gh).run() + + assert result == 0 + updated_body = mock_gh.get_issue_body(123) + assert "- [x] #124 | status=done rc=rc1 commit= 12345678" in updated_body + assert "- [ ] Sync Changelog #124 | status=pending pr=#1001" in updated_body + + +def test_process_backports_add_backports_marks_invalid(mocker, mock_git, mock_gh): + mocker.patch("tools.private.release.process_backports.changelog_news") + mocker.patch("tools.private.release.process_backports.replace_version_next") + mock_datetime = mocker.patch("tools.private.release.process_backports.datetime") + mock_datetime.date.today.return_value = datetime.date(2026, 7, 1) + args = argparse.Namespace( + issue=123, + remote="origin", + dry_run=False, + add=["124", "invalid", "125"], + triggering_comment=None, + ) + mock_gh.issues[123] = { + "title": "Release 2.0.0", + "body": """ +## Checklist +- [x] Prepare Release | status=done pr=#122 commit=abcdef12 +- [x] Create Release branch | status=done branch=release/2.0 commit=abcdef12 +- [x] Tag RC0 | status=done tag=2.0.0-rc0 commit=abcdef12 +- [ ] Tag Final + +## Backports +""", + "labels": ["type: release"], + } + mock_gh.prs[124] = { + "state": "MERGED", + "mergeCommit": {"oid": "sha_124"}, + } + mock_gh.prs[125] = { + "state": "MERGED", + "mergeCommit": {"oid": "sha_125"}, + } + mock_git.get_remote_tags.return_value = ["2.0.0-rc0"] + mock_git.get_commit_sha.return_value = "1234567890" + mock_git.get_commit_message.return_value = 'Cherry-pick "fix bug"' + mock_git.sort_commits_chronologically.return_value = ["sha_124", "sha_125"] + mock_git.diff.return_value = "version diff" + mock_git.apply_check.return_value = True + + result = ProcessBackports(args, mock_git, mock_gh).run() + + assert result == 0 + updated_body = mock_gh.get_issue_body(123) + assert "- [ ] invalid | status=error-invalid-pr" in updated_body + assert "- [ ] Sync Changelog #124 | status=pending pr=#1001" in updated_body + assert "- [ ] Sync Changelog #125 | status=pending pr=#1001" in updated_body + + +def test_process_backports_version_sync_failure(mocker, mock_git, mock_gh): + mock_changelog = mocker.patch( + "tools.private.release.process_backports.changelog_news" + ) + mock_replace = mocker.patch( + "tools.private.release.process_backports.replace_version_next" + ) + mock_datetime = mocker.patch("tools.private.release.process_backports.datetime") + mock_datetime.date.today.return_value = datetime.date(2026, 7, 1) + args = argparse.Namespace( + issue=123, remote="origin", dry_run=False, add=None, triggering_comment=None + ) + mock_gh.issues[123] = { + "title": "Release 2.0.0", + "body": """ +## Checklist +- [ ] Prepare Release +- [ ] Create Release branch +- [ ] Sync Changelog #124 +- [ ] Sync Changelog #125 +- [ ] Tag Final + +## Backports +- [ ] #124 | status=pending +- [ ] #125 | status=pending +""", + "labels": ["type: release"], + } + mock_gh.prs[124] = { + "state": "MERGED", + "mergeCommit": {"oid": "sha_124"}, + } + mock_gh.prs[125] = { + "state": "MERGED", + "mergeCommit": {"oid": "sha_125"}, + } + mock_git.get_remote_tags.return_value = [] + mock_git.sort_commits_chronologically.return_value = ["sha_124", "sha_125"] + mock_git.get_commit_sha.side_effect = [ + "12345678", + "sha_124_amended", + "sha_125_amended", + "main_sha", + ] + mock_git.get_commit_message.return_value = 'Cherry-pick "fix bug"' + mock_git.get_modified_files.side_effect = [ + ["news/124.fixed.md"], + ["news/125.fixed.md"], + ] + mock_git.diff.return_value = "diff content" + mock_git.apply_check.side_effect = [False, True] + + result = ProcessBackports(args, mock_git, mock_gh).run() + + assert result == 0 + mock_git.checkout.assert_has_calls( + [ + call("release/2.0", track_remote="origin"), + call("main", track_remote="origin"), + call("prepare-2.0.0-backports-b552a96", create_branch=True), + call("release/2.0"), + ] + ) + mock_git.cherry_pick.assert_has_calls( + [ + call("sha_124"), + call("sha_125"), + ] + ) + assert mock_git.diff.call_count == 2 + assert mock_git.apply_check.call_count == 2 + mock_git.apply.assert_called_once_with(ANY) + + mock_changelog.update_changelog.assert_has_calls( + [ + call("2.0.0", "2026-07-01"), + call("2.0.0", "2026-07-01"), + call( + "2.0.0", + "2026-07-01", + news_files=["news/124.fixed.md", "news/125.fixed.md"], + delete_news=True, + ), + ] + ) + assert mock_git.add_modified_and_deleted.call_count == 3 + assert mock_replace.call_count == 2 + + assert 1001 in mock_gh.prs + pr = mock_gh.prs[1001] + assert ( + "Warning: These PRs failed to update their version markers:\n- #124" + in pr["body"] + ) + + updated_body = mock_gh.get_issue_body(123) + assert "- [ ] Sync Changelog #124 | status=pending pr=#1001" in updated_body + assert "- [ ] Sync Changelog #125 | status=pending pr=#1001" in updated_body diff --git a/tests/tools/private/release/promote_test.py b/tests/tools/private/release/promote_test.py new file mode 100644 index 0000000000..b7fe9c9848 --- /dev/null +++ b/tests/tools/private/release/promote_test.py @@ -0,0 +1,281 @@ +import argparse +from pathlib import Path +from unittest.mock import call, patch + +from tools.private.release.promote import Promote + +pytest_plugins = ["tests.tools.private.release.release_test_helper"] + + +def test_promote_rc_success(mock_git, mock_gh): + # Arrange + issue_num = mock_gh.create_issue( + title="Release 2.0.0", + body="- [ ] Tag Final", + labels=["type: release"], + ) + args = argparse.Namespace( + version="2.0.0", issue=issue_num, dry_run=False, remote="my-remote" + ) + mock_git.get_remote_tags.return_value = ["2.0.0-rc0", "2.0.0-rc1"] + mock_git.get_commit_sha.return_value = "abcdef123456" + mock_git.tag_exists.return_value = False + + # Act + result = Promote(args, mock_git, mock_gh).run() + + # Assert + assert result == 0 + mock_git.fetch.assert_has_calls( + [ + call("my-remote", tags=True, force=True), + call("my-remote", refspec="release/2.0"), + ] + ) + mock_git.get_commit_sha.assert_has_calls( + [call("2.0.0-rc1"), call("my-remote/release/2.0")] + ) + mock_git.checkout.assert_not_called() + mock_git.tag_exists.assert_called_once_with("2.0.0") + mock_git.tag.assert_called_once_with("2.0.0", "abcdef123456") + mock_git.push.assert_called_once_with("my-remote", "2.0.0") + + # Verify issue update + expected_updated_body = "- [x] Tag Final | status=done tag=2.0.0 commit= abcdef12" + assert mock_gh.get_issue_body(issue_num) == expected_updated_body + + expected_comment = ( + "**New Release Tagged!** 🐍🌿\n\n" + "Version **2.0.0** has been successfully generated and tagged on branch [`release/2.0`](https://github.com/bazel-contrib/rules_python/tree/release/2.0).\n\n" + "- [Github Release 2.0.0](https://github.com/bazel-contrib/rules_python/releases/tag/2.0.0)\n" + "- [BCR Entry 2.0.0](https://registry.bazel.build/modules/rules_python/2.0.0)\n" + "- [BCR PRs](https://github.com/bazelbuild/bazel-central-registry/pulls?q=is%3Apr%20%28%22bazel-contrib/rules_python%22%20in%3Atitle%29%20%28%22%402.0.0%22%20in%3Atitle%29)\n" + "- [Release workflow status](https://github.com/bazel-contrib/rules_python/actions/workflows/release_promote.yaml)" + ) + assert mock_gh.issue_comments[issue_num] == [expected_comment] + + +def test_promote_rc_writes_github_output(tmp_path, monkeypatch, mock_git, mock_gh): + # Arrange + github_output_path = str(tmp_path / "github_output") + monkeypatch.setenv("GITHUB_OUTPUT", github_output_path) + issue_num = mock_gh.create_issue( + title="Release 2.0.0", + body="- [ ] Tag Final", + labels=["type: release"], + ) + args = argparse.Namespace( + version="2.0.0", issue=issue_num, dry_run=False, remote="my-remote" + ) + mock_git.get_remote_tags.return_value = ["2.0.0-rc0", "2.0.0-rc1"] + mock_git.get_commit_sha.return_value = "abcdef123456" + mock_git.tag_exists.return_value = False + + # Act + result = Promote(args, mock_git, mock_gh).run() + + # Assert + assert result == 0 + assert Path(github_output_path).exists() + content = Path(github_output_path).read_text(encoding="utf-8") + assert content == "version=2.0.0\n" + + +def test_promote_rc_resolve_issue_success(mock_git, mock_gh): + # Arrange + args = argparse.Namespace( + version="2.0.0", issue=None, dry_run=False, remote="my-remote" + ) + issue_num = mock_gh.create_issue( + title="Release 2.0.0", + body="- [ ] Tag Final", + labels=["type: release"], + ) + mock_git.get_remote_tags.return_value = ["2.0.0-rc1"] + mock_git.tag_exists.return_value = False + mock_git.get_commit_sha.return_value = "abcdef123456" + + # Act + result = Promote(args, mock_git, mock_gh).run() + + # Assert + assert result == 0 + mock_git.fetch.assert_has_calls( + [ + call("my-remote", tags=True, force=True), + call("my-remote", refspec="release/2.0"), + ] + ) + mock_git.get_commit_sha.assert_has_calls( + [call("2.0.0-rc1"), call("my-remote/release/2.0")] + ) + mock_git.checkout.assert_not_called() + mock_git.tag.assert_called_once_with("2.0.0", "abcdef123456") + mock_git.push.assert_called_once_with("my-remote", "2.0.0") + + expected_updated_body = "- [x] Tag Final | status=done tag=2.0.0 commit= abcdef12" + assert mock_gh.get_issue_body(issue_num) == expected_updated_body + + +def test_promote_patch_success(mock_git, mock_gh): + # Arrange + issue_num = mock_gh.create_issue( + title="Release 2.0.1", + body="- [ ] Tag Final", + labels=["type: release"], + ) + args = argparse.Namespace( + version="2.0.1", issue=issue_num, dry_run=False, remote="my-remote" + ) + mock_git.tag_exists.return_value = False + mock_git.get_commit_sha.return_value = "12345678" + + # Act + result = Promote(args, mock_git, mock_gh).run() + + # Assert + assert result == 0 + mock_git.fetch.assert_has_calls( + [ + call("my-remote", tags=True, force=True), + call("my-remote", refspec="release/2.0"), + ] + ) + mock_git.get_current_branch.assert_not_called() + mock_git.get_tags.assert_not_called() + mock_git.get_remote_tags.assert_not_called() + + mock_git.checkout.assert_not_called() + mock_git.get_commit_sha.assert_called_once_with("my-remote/release/2.0") + mock_git.tag.assert_called_once_with("2.0.1", "12345678") + mock_git.push.assert_called_once_with("my-remote", "2.0.1") + + expected_updated_body = "- [x] Tag Final | status=done tag=2.0.1 commit= 12345678" + assert mock_gh.get_issue_body(issue_num) == expected_updated_body + + +@patch("builtins.print") +def test_promote_rc_dry_run_success(mock_print, mock_git, mock_gh): + # Arrange + issue_num = mock_gh.create_issue( + title="Release 2.0.0", + body="- [ ] Tag Final", + labels=["type: release"], + ) + args = argparse.Namespace( + version="2.0.0", issue=issue_num, dry_run=True, remote="my-remote" + ) + mock_git.get_remote_tags.return_value = ["2.0.0-rc0", "2.0.0-rc1"] + mock_git.get_commit_sha.return_value = "abcdef123456" + mock_git.tag_exists.return_value = False + + # Act + result = Promote(args, mock_git, mock_gh).run() + + # Assert + assert result == 0 + mock_git.fetch.assert_has_calls( + [ + call("my-remote", tags=True, force=True), + call("my-remote", refspec="release/2.0"), + ] + ) + mock_git.get_commit_sha.assert_has_calls( + [call("2.0.0-rc1"), call("my-remote/release/2.0")] + ) + mock_git.tag_exists.assert_called_once_with("2.0.0") + + # Core dry-run assertions: NO modifications + mock_git.tag.assert_not_called() + mock_git.push.assert_not_called() + + mock_print.assert_has_calls( + [ + call(f"Verifying tracking issue #{issue_num} format..."), + call("Fetching remote branch my-remote/release/2.0..."), + call( + "[DRY RUN] Pre-conditions passed successfully for promoting" + " 2.0.0-rc1 to 2.0.0." + ), + call("[DRY RUN] Would tag commit abcdef12 as 2.0.0"), + call("[DRY RUN] Would push tag 2.0.0 to my-remote"), + call(f"[DRY RUN] Would update tracking issue #{issue_num} checklist"), + call(f"[DRY RUN] Would post comment to tracking issue #{issue_num}"), + ] + ) + + +def test_promote_rc_tag_already_exists(mock_git, mock_gh): + # Arrange + args = argparse.Namespace( + version="2.0.0", issue=123, dry_run=False, remote="my-remote" + ) + mock_git.get_remote_tags.return_value = ["2.0.0-rc1"] + mock_git.tag_exists.return_value = True + + # Act + result = Promote(args, mock_git, mock_gh).run() + + # Assert + assert result == 1 + mock_git.checkout.assert_not_called() + mock_git.tag.assert_not_called() + mock_git.push.assert_not_called() + + +def test_promote_rc_issue_not_found(mock_git, mock_gh): + # Arrange + args = argparse.Namespace( + version="2.0.0", issue=None, dry_run=False, remote="my-remote" + ) + mock_git.get_remote_tags.return_value = ["2.0.0-rc1"] + mock_git.tag_exists.return_value = False + + # Act + result = Promote(args, mock_git, mock_gh).run() + + # Assert + assert result == 1 + mock_git.checkout.assert_not_called() + mock_git.tag.assert_not_called() + mock_git.push.assert_not_called() + + +def test_promote_rc_issue_malformed(mock_git, mock_gh): + # Arrange + issue_num = mock_gh.create_issue( + title="Release 2.0.0", + body="malformed body", + labels=["type: release"], + ) + args = argparse.Namespace( + version="2.0.0", issue=issue_num, dry_run=False, remote="my-remote" + ) + mock_git.get_remote_tags.return_value = ["2.0.0-rc1"] + mock_git.tag_exists.return_value = False + mock_git.get_commit_sha.return_value = "abcdef123456" + + # Act + result = Promote(args, mock_git, mock_gh).run() + + # Assert + assert result == 1 + mock_git.checkout.assert_not_called() + mock_git.tag.assert_not_called() + mock_git.push.assert_not_called() + + +def test_promote_rc_no_rc_found(mock_git, mock_gh): + # Arrange + args = argparse.Namespace( + version="2.0.0", issue=123, dry_run=False, remote="my-remote" + ) + mock_git.get_remote_tags.return_value = [] + + # Act + result = Promote(args, mock_git, mock_gh).run() + + # Assert + assert result == 1 + mock_git.checkout.assert_not_called() + mock_git.tag.assert_not_called() diff --git a/tests/tools/private/release/release_issue_test.py b/tests/tools/private/release/release_issue_test.py new file mode 100644 index 0000000000..869a303ebf --- /dev/null +++ b/tests/tools/private/release/release_issue_test.py @@ -0,0 +1,157 @@ +from tools.private.release.release_issue import ( + add_backports_to_body, + add_sync_changelog_task_to_body, + format_metadata_line, + parse_checklist_state, + parse_metadata_line, +) + + +def test_parse_metadata_line_spaces(): + # Test with spaces around '=' + line = "- [ ] Tag Final | tag = 2.0.0 commit = abcdef12" + expected = { + "checked": False, + "name": "Tag Final", + "metadata": {"tag": "2.0.0", "commit": "abcdef12"}, + "original_line": line, + } + assert parse_metadata_line(line) == expected + + # Test with spaces after '=' + line = "- [ ] Tag Final | tag= 2.0.0 commit= abcdef12" + expected = { + "checked": False, + "name": "Tag Final", + "metadata": {"tag": "2.0.0", "commit": "abcdef12"}, + "original_line": line, + } + assert parse_metadata_line(line) == expected + + # Test with standard format (no spaces) + line = "- [ ] Tag Final | tag=2.0.0 commit=abcdef12" + expected = { + "checked": False, + "name": "Tag Final", + "metadata": {"tag": "2.0.0", "commit": "abcdef12"}, + "original_line": line, + } + assert parse_metadata_line(line) == expected + + # Test with no metadata + line = "- [ ] Tag Final" + expected = { + "checked": False, + "name": "Tag Final", + "metadata": {}, + "original_line": line, + } + assert parse_metadata_line(line) == expected + + +def test_format_metadata_line(): + # Test with commit metadata (should have space) + metadata = {"status": "done", "tag": "2.0.0", "commit": "abcdef12"} + expected = "- [x] Tag Final | status=done tag=2.0.0 commit= abcdef12" + assert format_metadata_line(True, "Tag Final", metadata) == expected + + # Test with other metadata (should not have space) + metadata = {"status": "done", "pr": "#122"} + expected = "- [x] Prepare Release | status=done pr=#122" + assert format_metadata_line(True, "Prepare Release", metadata) == expected + + # Test with no metadata + expected = "- [ ] Tag Final" + assert format_metadata_line(False, "Tag Final", {}) == expected + + +def test_add_backports_to_body(): + body = """ +## Checklist +- [ ] Prepare Release +- [ ] Create Release branch +- [ ] Tag Final + +## Backports +- [ ] #123 | status=done +""" + items = [ + {"ref": "124"}, + {"ref": "#124"}, + {"ref": "125"}, + {"ref": "#123"}, + ] + updated_body = add_backports_to_body(body, items) + expected_body = """ +## Checklist +- [ ] Prepare Release +- [ ] Create Release branch +- [ ] Tag Final + +## Backports +- [ ] #123 | status=done +- [ ] #124 +- [ ] #125 +""" + assert updated_body.strip() == expected_body.strip() + + +def test_add_sync_changelog_task_to_body(): + body = """ +## Checklist +- [ ] Prepare Release +- [ ] Create Release branch +- [ ] Tag Final +""" + # Insert first task (should go before Tag Final) + body = add_sync_changelog_task_to_body(body, 124) + expected = """ +## Checklist +- [ ] Prepare Release +- [ ] Create Release branch +- [ ] Sync Changelog #124 +- [ ] Tag Final +""" + assert body.strip() == expected.strip() + + # Insert second task (should go after the last Sync Changelog task) + body = add_sync_changelog_task_to_body(body, 125) + expected = """ +## Checklist +- [ ] Prepare Release +- [ ] Create Release branch +- [ ] Sync Changelog #124 +- [ ] Sync Changelog #125 +- [ ] Tag Final +""" + assert body.strip() == expected.strip() + + # Insert duplicate (should be ignored) + body = add_sync_changelog_task_to_body(body, 124) + assert body.strip() == expected.strip() + + +def test_parse_checklist_state_with_sync_changelogs(): + body = """ +## Checklist +- [x] Prepare Release | status=done pr=#122 commit=abcdef12 +- [x] Create Release branch | status=done branch=release/2.0 +- [ ] Sync Changelog #124 | status=pending pr=#125 +- [ ] Sync Changelog #126 +- [ ] Tag Final +""" + state = parse_checklist_state(body) + assert 124 in state["sync_changelogs"] + assert 126 in state["sync_changelogs"] + + task_124 = state["sync_changelogs"][124] + assert task_124.name == "Sync Changelog #124" + assert not task_124.checked + assert task_124.status == "pending" + assert task_124.pr == "#125" + + task_126 = state["sync_changelogs"][126] + assert task_126.name == "Sync Changelog #126" + assert not task_126.checked + assert task_126.status is None + assert task_126.pr is None diff --git a/tests/tools/private/release/release_test.py b/tests/tools/private/release/release_test.py index 9f4f3b249b..651af24c2f 100644 --- a/tests/tools/private/release/release_test.py +++ b/tests/tools/private/release/release_test.py @@ -1,270 +1,19 @@ -import os -import pathlib -import shutil -import tempfile -import unittest -from unittest.mock import patch +import pytest from tools.private.release import release as releaser -_UNRELEASED_TEMPLATE = """ - -""" - - -class ReleaserTest(unittest.TestCase): - def setUp(self): - self.tmpdir = pathlib.Path(tempfile.mkdtemp()) - self.original_cwd = os.getcwd() - self.addCleanup(shutil.rmtree, self.tmpdir) - - os.chdir(self.tmpdir) - # NOTE: On windows, this must be done before files are deleted. - self.addCleanup(os.chdir, self.original_cwd) - - def test_update_changelog(self): - changelog = f""" -# Changelog - -{_UNRELEASED_TEMPLATE} - -{{#v0-0-0}} -## Unreleased - -[0.0.0]: https://github.com/bazel-contrib/rules_python/releases/tag/0.0.0 - -{{#v0-0-0-changed}} -### Changed -* Nothing changed - -{{#v0-0-0-fixed}} -### Fixed -* Nothing fixed - -{{#v0-0-0-added}} -### Added -* Nothing added - -{{#v0-0-0-removed}} -### Removed -* Nothing removed. -""" - changelog_path = self.tmpdir / "CHANGELOG.md" - changelog_path.write_text(changelog) - - # Act - releaser.update_changelog( - "1.23.4", - "2025-01-01", - changelog_path=changelog_path, - ) - - # Assert - new_content = changelog_path.read_text() - - self.assertIn( - _UNRELEASED_TEMPLATE, new_content, msg=f"ACTUAL:\n\n{new_content}\n\n" - ) - self.assertIn("## [1.23.4] - 2025-01-01", new_content) - self.assertIn( - "[1.23.4]: https://github.com/bazel-contrib/rules_python/releases/tag/1.23.4", - new_content, - ) - self.assertIn("{#v1-23-4}", new_content) - self.assertIn("{#v1-23-4-changed}", new_content) - self.assertIn("{#v1-23-4-fixed}", new_content) - self.assertIn("{#v1-23-4-added}", new_content) - self.assertIn("{#v1-23-4-removed}", new_content) - - def test_replace_version_next(self): - # Arrange - mock_file_content = """ -:::{versionadded} VERSION_NEXT_FEATURE -blabla -::: - -:::{versionchanged} VERSION_NEXT_PATCH -blabla -::: -""" - (self.tmpdir / "mock_file.bzl").write_text(mock_file_content) - - releaser.replace_version_next("0.28.0") - - new_content = (self.tmpdir / "mock_file.bzl").read_text() - - self.assertIn(":::{versionadded} 0.28.0", new_content) - self.assertIn(":::{versionadded} 0.28.0", new_content) - self.assertNotIn("VERSION_NEXT_FEATURE", new_content) - self.assertNotIn("VERSION_NEXT_PATCH", new_content) - - def test_replace_version_next_excludes_bazel_dirs(self): - # Arrange - mock_file_content = """ -:::{versionadded} VERSION_NEXT_FEATURE -blabla -::: -""" - bazel_dir = self.tmpdir / "bazel-rules_python" - bazel_dir.mkdir() - (bazel_dir / "mock_file.bzl").write_text(mock_file_content) - - tools_dir = self.tmpdir / "tools" / "private" / "release" - tools_dir.mkdir(parents=True) - (tools_dir / "mock_file.bzl").write_text(mock_file_content) - - tests_dir = self.tmpdir / "tests" / "tools" / "private" / "release" - tests_dir.mkdir(parents=True) - (tests_dir / "mock_file.bzl").write_text(mock_file_content) - - version = "0.28.0" - - # Act - releaser.replace_version_next(version) - - # Assert - new_content = (bazel_dir / "mock_file.bzl").read_text() - self.assertIn("VERSION_NEXT_FEATURE", new_content) - - new_content = (tools_dir / "mock_file.bzl").read_text() - self.assertIn("VERSION_NEXT_FEATURE", new_content) - - new_content = (tests_dir / "mock_file.bzl").read_text() - self.assertIn("VERSION_NEXT_FEATURE", new_content) - - def test_valid_version(self): - # These should not raise an exception - releaser.create_parser().parse_args(["0.28.0"]) - releaser.create_parser().parse_args(["1.0.0"]) - releaser.create_parser().parse_args(["1.2.3rc4"]) - - def test_invalid_version(self): - with self.assertRaises(SystemExit): - releaser.create_parser().parse_args(["0.28"]) - with self.assertRaises(SystemExit): - releaser.create_parser().parse_args(["a.b.c"]) - - -class GetLatestVersionTest(unittest.TestCase): - @patch("tools.private.release.release._get_git_tags") - def test_get_latest_version_success(self, mock_get_tags): - mock_get_tags.return_value = ["0.1.0", "1.0.0", "0.2.0"] - self.assertEqual(releaser.get_latest_version(), "1.0.0") - - @patch("tools.private.release.release._get_git_tags") - def test_get_latest_version_rc_is_latest(self, mock_get_tags): - mock_get_tags.return_value = ["0.1.0", "1.0.0", "1.1.0rc0"] - with self.assertRaisesRegex( - ValueError, "The latest version is a pre-release version: 1.1.0rc0" - ): - releaser.get_latest_version() - - @patch("tools.private.release.release._get_git_tags") - def test_get_latest_version_no_tags(self, mock_get_tags): - mock_get_tags.return_value = [] - with self.assertRaisesRegex( - RuntimeError, "No git tags found matching X.Y.Z or X.Y.ZrcN format." - ): - releaser.get_latest_version() - - @patch("tools.private.release.release._get_git_tags") - def test_get_latest_version_no_matching_tags(self, mock_get_tags): - mock_get_tags.return_value = ["v1.0", "latest"] - with self.assertRaisesRegex( - RuntimeError, "No git tags found matching X.Y.Z or X.Y.ZrcN format." - ): - releaser.get_latest_version() - - @patch("tools.private.release.release._get_git_tags") - def test_get_latest_version_only_rc_tags(self, mock_get_tags): - mock_get_tags.return_value = ["1.0.0rc0", "1.1.0rc0"] - with self.assertRaisesRegex( - ValueError, "The latest version is a pre-release version: 1.1.0rc0" - ): - releaser.get_latest_version() - - -class DetermineNextVersionTest(unittest.TestCase): - def setUp(self): - self.tmpdir = pathlib.Path(tempfile.mkdtemp()) - self.original_cwd = os.getcwd() - self.addCleanup(shutil.rmtree, self.tmpdir) - - os.chdir(self.tmpdir) - # NOTE: On windows, this must be done before files are deleted. - self.addCleanup(os.chdir, self.original_cwd) - - self.mock_get_latest_version = patch( - "tools.private.release.release.get_latest_version" - ).start() - self.addCleanup(patch.stopall) - - def test_no_markers(self): - (self.tmpdir / "mock_file.bzl").write_text("no markers here") - self.mock_get_latest_version.return_value = "1.2.3" - - next_version = releaser.determine_next_version() - - self.assertEqual(next_version, "1.2.4") - - def test_only_patch(self): - (self.tmpdir / "mock_file.bzl").write_text( - ":::{versionchanged} VERSION_NEXT_PATCH" - ) - self.mock_get_latest_version.return_value = "1.2.3" - - next_version = releaser.determine_next_version() - - self.assertEqual(next_version, "1.2.4") - - def test_only_feature(self): - (self.tmpdir / "mock_file.bzl").write_text( - ":::{versionadded} VERSION_NEXT_FEATURE" - ) - self.mock_get_latest_version.return_value = "1.2.3" - - next_version = releaser.determine_next_version() - - self.assertEqual(next_version, "1.3.0") - - def test_both_markers(self): - (self.tmpdir / "mock_file_patch.bzl").write_text( - ":::{versionchanged} VERSION_NEXT_PATCH" - ) - (self.tmpdir / "mock_file_feature.bzl").write_text( - ":::{versionadded} VERSION_NEXT_FEATURE" - ) - self.mock_get_latest_version.return_value = "1.2.3" - - next_version = releaser.determine_next_version() - - self.assertEqual(next_version, "1.3.0") - - -if __name__ == "__main__": - unittest.main() +def test_invalid_version(): + with pytest.raises(SystemExit): + releaser.create_parser().parse_args(["prepare", "0.28"]) + with pytest.raises(SystemExit): + releaser.create_parser().parse_args(["prepare", "a.b.c"]) diff --git a/tests/tools/private/release/release_test_helper.py b/tests/tools/private/release/release_test_helper.py new file mode 100644 index 0000000000..0b1c4e0351 --- /dev/null +++ b/tests/tools/private/release/release_test_helper.py @@ -0,0 +1,62 @@ +import dataclasses +from pathlib import Path +from unittest.mock import MagicMock, patch + +import pytest + +from tools.private.release.mock_gh import MockGitHub + + +@dataclasses.dataclass +class ReleaseToolEnv: + """Environment setup for testing release tools. + + Attributes: + git_root: The root path of the temporary Git repository workspace. + github_output_file: Path to the mocked GITHUB_OUTPUT file. + """ + + git_root: Path + github_output_file: Path + + +DEFAULT_RELEASE_TEMPLATE_CONTENT = ( + "template content\n" + "- [ ] Prepare Release\n" + "- [ ] Tag RC0\n" + "- [ ] Tag Final\n" + "\n" + "## Backports\n" +) + + +@pytest.fixture(name="mock_git") +def fixture_mock_git(): + mock_git_inst = MagicMock() + mock_git_inst.get_current_branch.return_value = None + mock_git_inst.get_tags.return_value = [] + mock_git_inst.get_remote_tags.return_value = [] + mock_git_inst.status.return_value = "" + mock_git_inst.branch_exists.return_value = False + mock_git_inst.tag_exists.return_value = False + + with patch("tools.private.release.utils.Git", return_value=mock_git_inst): + yield mock_git_inst + + +@pytest.fixture(name="mock_gh") +def fixture_mock_gh(): + return MockGitHub() + + +@pytest.fixture(name="release_tool_env") +def fixture_release_tool_env(tmp_path, monkeypatch): + """Fixture providing a temp cwd with release template set up.""" + monkeypatch.chdir(tmp_path) + template_dir = tmp_path / ".github" / "ISSUE_TEMPLATE" + template_dir.mkdir(parents=True, exist_ok=True) + template_file = template_dir / "release_tracking_template.md" + template_file.write_text(DEFAULT_RELEASE_TEMPLATE_CONTENT, encoding="utf-8") + github_output_file = tmp_path / "github_output" + monkeypatch.setenv("GITHUB_OUTPUT", str(github_output_file)) + yield ReleaseToolEnv(git_root=tmp_path, github_output_file=github_output_file) diff --git a/tests/tools/private/release/utils_test.py b/tests/tools/private/release/utils_test.py new file mode 100644 index 0000000000..d39b5e108e --- /dev/null +++ b/tests/tools/private/release/utils_test.py @@ -0,0 +1,313 @@ +import pytest + +from tools.private.release import utils + +pytest_plugins = ["tests.tools.private.release.release_test_helper"] + + +def test_get_latest_version_success(mocker): + mocker.patch( + "tools.private.release.git.Git.get_tags", + return_value=["0.1.0", "1.0.0", "0.2.0"], + ) + assert utils.get_latest_version() == "1.0.0" + + +def test_get_latest_version_rc_is_latest(mocker): + mocker.patch( + "tools.private.release.git.Git.get_tags", + return_value=["0.1.0", "1.0.0", "1.1.0rc0"], + ) + with pytest.raises( + ValueError, match="The latest version is a pre-release version: 1.1.0rc0" + ): + utils.get_latest_version() + + +def test_get_latest_version_no_tags(mocker): + mocker.patch("tools.private.release.git.Git.get_tags", return_value=[]) + with pytest.raises( + RuntimeError, match="No git tags found matching X.Y.Z or X.Y.ZrcN format." + ): + utils.get_latest_version() + + +def test_get_latest_version_no_matching_tags(mocker): + mocker.patch( + "tools.private.release.git.Git.get_tags", return_value=["v1.0", "latest"] + ) + with pytest.raises( + RuntimeError, match="No git tags found matching X.Y.Z or X.Y.ZrcN format." + ): + utils.get_latest_version() + + +def test_get_latest_version_only_rc_tags(mocker): + mocker.patch( + "tools.private.release.git.Git.get_tags", return_value=["1.0.0rc0", "1.1.0rc0"] + ) + with pytest.raises( + ValueError, match="The latest version is a pre-release version: 1.1.0rc0" + ): + utils.get_latest_version() + + +def test_get_latest_rc_tag_no_tags(mocker): + mocker.patch("tools.private.release.git.Git.get_tags", return_value=[]) + assert utils.get_latest_rc_tag("2.0.0") is None + + +def test_get_latest_rc_tag_no_matching_tags(mocker): + mocker.patch( + "tools.private.release.git.Git.get_tags", + return_value=[ + "1.0.0", + "2.0.0", + "v2.0.0-rc0", + "2.1.0-rc0", + ], + ) + assert utils.get_latest_rc_tag("2.0.0") is None + + +def test_get_latest_rc_tag_success(mocker): + mocker.patch( + "tools.private.release.git.Git.get_tags", + return_value=[ + "2.0.0-rc0", + "2.0.0-rc2", + "2.0.0-rc1", + "2.1.0-rc0", + ], + ) + assert utils.get_latest_rc_tag("2.0.0") == "2.0.0-rc2" + + +def test_get_latest_rc_tag_ignores_v_prefix(mocker): + mocker.patch( + "tools.private.release.git.Git.get_tags", + return_value=["v2.0.0-rc0", "2.0.0-rc1"], + ) + assert utils.get_latest_rc_tag("2.0.0") == "2.0.0-rc1" + + +def test_get_latest_rc_tag_remote_success(mocker): + mock_get_remote_tags = mocker.patch( + "tools.private.release.git.Git.get_remote_tags", + return_value=[ + "2.0.0-rc0", + "2.0.0-rc2", + "2.0.0-rc1", + "2.1.0-rc0", + ], + ) + assert utils.get_latest_rc_tag("2.0.0", remote="origin") == "2.0.0-rc2" + mock_get_remote_tags.assert_called_once_with("origin") + + +def test_determine_next_version_no_markers(mocker, release_tool_env): + mocker.patch( + "tools.private.release.git.Git.get_current_branch", return_value="main" + ) + mocker.patch("tools.private.release.utils.get_latest_version", return_value="1.2.3") + (release_tool_env.git_root / "mock_file.bzl").write_text("no markers here") + + next_version = utils.determine_next_version() + + assert next_version == "1.2.4" + + +def test_determine_next_version_only_patch(mocker, release_tool_env): + mocker.patch( + "tools.private.release.git.Git.get_current_branch", return_value="main" + ) + mocker.patch("tools.private.release.utils.get_latest_version", return_value="1.2.3") + (release_tool_env.git_root / "mock_file.bzl").write_text( + ":::{versionchanged} VERSION_NEXT_PATCH" + ) + + next_version = utils.determine_next_version() + + assert next_version == "1.2.4" + + +def test_determine_next_version_only_feature(mocker, release_tool_env): + mocker.patch( + "tools.private.release.git.Git.get_current_branch", return_value="main" + ) + mocker.patch("tools.private.release.utils.get_latest_version", return_value="1.2.3") + (release_tool_env.git_root / "mock_file.bzl").write_text( + ":::{versionadded} VERSION_NEXT_FEATURE" + ) + + next_version = utils.determine_next_version() + + assert next_version == "1.3.0" + + +def test_determine_next_version_both_markers(mocker, release_tool_env): + mocker.patch( + "tools.private.release.git.Git.get_current_branch", return_value="main" + ) + mocker.patch("tools.private.release.utils.get_latest_version", return_value="1.2.3") + (release_tool_env.git_root / "mock_file_patch.bzl").write_text( + ":::{versionchanged} VERSION_NEXT_PATCH" + ) + (release_tool_env.git_root / "mock_file_feature.bzl").write_text( + ":::{versionadded} VERSION_NEXT_FEATURE" + ) + + next_version = utils.determine_next_version() + + assert next_version == "1.3.0" + + +def test_determine_next_version_on_release_branch_with_existing_tags(mocker): + mocker.patch( + "tools.private.release.git.Git.get_current_branch", return_value="release/0.37" + ) + mocker.patch( + "tools.private.release.git.Git.get_tags", + return_value=["0.37.0", "0.37.1", "0.36.0"], + ) + + next_version = utils.determine_next_version() + + assert next_version == "0.37.2" + + +def test_determine_next_version_on_release_branch_no_tags(mocker): + mocker.patch( + "tools.private.release.git.Git.get_current_branch", return_value="release/0.38" + ) + mocker.patch( + "tools.private.release.git.Git.get_tags", return_value=["0.37.0"] + ) # No 0.38.x tags + + next_version = utils.determine_next_version() + + assert next_version == "0.38.0" + + +def test_determine_next_version_on_release_branch_with_active_rc(mocker): + mocker.patch( + "tools.private.release.git.Git.get_current_branch", return_value="release/0.37" + ) + # 0.37.0-rc0 and rc1 exist, but no stable 0.37.0 yet + mocker.patch( + "tools.private.release.git.Git.get_tags", + return_value=["0.37.0-rc0", "0.37.0-rc1", "0.36.0"], + ) + + next_version = utils.determine_next_version() + + # Should target 0.37.0, not 0.37.1 + assert next_version == "0.37.0" + + +def test_determine_next_version_on_release_branch_with_stable_and_active_patch_rc( + mocker, +): + mocker.patch( + "tools.private.release.git.Git.get_current_branch", return_value="release/0.37" + ) + # 0.37.0 stable exists, and 0.37.1-rc0 exists (but no stable 0.37.1 yet) + mocker.patch( + "tools.private.release.git.Git.get_tags", + return_value=["0.37.0", "0.37.1-rc0", "0.36.0"], + ) + + next_version = utils.determine_next_version() + + # Should target 0.37.1, not 0.37.2 + assert next_version == "0.37.1" + + +def test_determine_next_version_on_main_branch_fallback(mocker, release_tool_env): + mocker.patch( + "tools.private.release.git.Git.get_current_branch", return_value="main" + ) + mocker.patch("tools.private.release.utils.get_latest_version", return_value="1.2.3") + (release_tool_env.git_root / "mock_file.bzl").write_text("no markers here") + + next_version = utils.determine_next_version() + + assert next_version == "1.2.4" + + +def test_replace_version_next(release_tool_env): + # Arrange + mock_file_content = """ +:::{versionadded} VERSION_NEXT_FEATURE +blabla +::: + +:::{versionchanged} VERSION_NEXT_PATCH +blabla +::: +""" + (release_tool_env.git_root / "mock_file.bzl").write_text(mock_file_content) + + utils.replace_version_next("0.28.0") + + new_content = (release_tool_env.git_root / "mock_file.bzl").read_text() + + assert ":::{versionadded} 0.28.0" in new_content + assert "VERSION_NEXT_FEATURE" not in new_content + assert "VERSION_NEXT_PATCH" not in new_content + + +def test_replace_version_next_excludes_bazel_dirs(release_tool_env): + # Arrange + mock_file_content = """ +:::{versionadded} VERSION_NEXT_FEATURE +blabla +::: +""" + agents_dir = release_tool_env.git_root / ".agents" + agents_dir.mkdir() + (agents_dir / "mock_file.md").write_text(mock_file_content) + + bazel_dir = release_tool_env.git_root / "bazel-rules_python" + bazel_dir.mkdir() + (bazel_dir / "mock_file.bzl").write_text(mock_file_content) + + tools_dir = release_tool_env.git_root / "tools" / "private" / "release" + tools_dir.mkdir(parents=True) + (tools_dir / "mock_file.bzl").write_text(mock_file_content) + + tests_dir = release_tool_env.git_root / "tests" / "tools" / "private" / "release" + tests_dir.mkdir(parents=True) + (tests_dir / "mock_file.bzl").write_text(mock_file_content) + + version = "0.28.0" + + # Act + utils.replace_version_next(version) + + # Assert + new_content = (agents_dir / "mock_file.md").read_text() + assert "VERSION_NEXT_FEATURE" in new_content + + new_content = (bazel_dir / "mock_file.bzl").read_text() + assert "VERSION_NEXT_FEATURE" in new_content + + new_content = (tools_dir / "mock_file.bzl").read_text() + assert "VERSION_NEXT_FEATURE" in new_content + + new_content = (tests_dir / "mock_file.bzl").read_text() + assert "VERSION_NEXT_FEATURE" in new_content + + +def test_determine_next_version_ignores_agents_markers(mocker, release_tool_env): + mocker.patch( + "tools.private.release.git.Git.get_current_branch", return_value="main" + ) + mocker.patch("tools.private.release.utils.get_latest_version", return_value="1.2.3") + agents_dir = release_tool_env.git_root / ".agents" + agents_dir.mkdir() + (agents_dir / "mock_file.md").write_text(":::{versionadded} VERSION_NEXT_FEATURE") + + next_version = utils.determine_next_version() + + assert next_version == "1.2.4" diff --git a/tests/tools/zipapp/BUILD.bazel b/tests/tools/zipapp/BUILD.bazel index b71e9b2589..97c8096ce9 100644 --- a/tests/tools/zipapp/BUILD.bazel +++ b/tests/tools/zipapp/BUILD.bazel @@ -1,19 +1,23 @@ -load("//python:py_test.bzl", "py_test") +load("//tests/support:support.bzl", "SUPPORTS_BZLMOD") +load("//tests/support/pytest_test:pytest_test.bzl", "pytest_test") -py_test( +pytest_test( name = "zipper_test", srcs = ["zipper_test.py"], + target_compatible_with = SUPPORTS_BZLMOD, deps = ["//tools/private/zipapp:zipper_lib"], ) -py_test( +pytest_test( name = "exe_zip_maker_test", srcs = ["exe_zip_maker_test.py"], + target_compatible_with = SUPPORTS_BZLMOD, deps = ["//tools/private/zipapp:exe_zip_maker_lib"], ) -py_test( +pytest_test( name = "zip_main_maker_test", srcs = ["zip_main_maker_test.py"], + target_compatible_with = SUPPORTS_BZLMOD, deps = ["//tools/private/zipapp:zip_main_maker_lib"], ) diff --git a/tests/tools/zipapp/exe_zip_maker_test.py b/tests/tools/zipapp/exe_zip_maker_test.py index 73c509bdbe..97df258e92 100644 --- a/tests/tools/zipapp/exe_zip_maker_test.py +++ b/tests/tools/zipapp/exe_zip_maker_test.py @@ -1,71 +1,46 @@ import hashlib -import pathlib -import shutil import stat -import tempfile -import unittest from tools.private.zipapp import exe_zip_maker -class ExeZipMakerTest(unittest.TestCase): - def setUp(self): - self.test_dir = pathlib.Path(tempfile.mkdtemp()) - self.preamble_path = self.test_dir / "preamble.txt" - self.zip_path = self.test_dir / "data.zip" - self.output_path = self.test_dir / "output.exe" +def test_create_exe_zip(tmp_path): + preamble_path = tmp_path / "preamble.txt" + zip_path = tmp_path / "data.zip" + output_path = tmp_path / "output.exe" - def tearDown(self): - shutil.rmtree(self.test_dir) + # Create dummy zip file + zip_content = b"PK\x03\x04dummyzipcontent" + zip_path.write_bytes(zip_content) - def assertStartsWith(self, actual, expected): - if not actual.startswith(expected): - self.fail(f"{actual!r} does not start with {expected!r}") + # Calculate expected hash + expected_hash = hashlib.sha256(zip_content).hexdigest().encode("utf-8") - def test_create_exe_zip(self): - # Create dummy zip file - zip_content = b"PK\x03\x04dummyzipcontent" - self.zip_path.write_bytes(zip_content) + # Create preamble with placeholder + preamble_text = b"#!/bin/bash\nEXPECTED_HASH='%ZIP_HASH%'\n# ... logic ...\n" + preamble_path.write_bytes(preamble_text) - # Calculate expected hash - expected_hash = hashlib.sha256(zip_content).hexdigest().encode("utf-8") + # Call create_exe_zip directly + exe_zip_maker.create_exe_zip(str(preamble_path), str(zip_path), str(output_path)) - # Create preamble with placeholder - preamble_text = b"#!/bin/bash\nEXPECTED_HASH='%ZIP_HASH%'\n# ... logic ...\n" - self.preamble_path.write_bytes(preamble_text) + # Verify output exists + assert output_path.exists(), f"Output path '{output_path}' should exist" - # Call create_exe_zip directly - exe_zip_maker.create_exe_zip( - str(self.preamble_path), str(self.zip_path), str(self.output_path) - ) + # Verify executable bit + st = output_path.stat() + assert st.st_mode & stat.S_IEXEC, ( + f"Output path '{output_path}' should be executable" + ) - # Verify output exists - self.assertTrue( - self.output_path.exists(), - msg=f"Output path '{self.output_path}' should exist", - ) + # Verify content + content = output_path.read_bytes() - # Verify executable bit - st = self.output_path.stat() - self.assertTrue( - st.st_mode & stat.S_IEXEC, - msg=f"Output path '{self.output_path}' should be executable", - ) + # Split content back into preamble and zip + # We know the preamble text length after substitution. + expected_preamble = preamble_text.replace(b"%ZIP_HASH%", expected_hash) - # Verify content - content = self.output_path.read_bytes() - - # Split content back into preamble and zip - # We know the preamble text length after substitution. - expected_preamble = preamble_text.replace(b"%ZIP_HASH%", expected_hash) - - self.assertStartsWith(content, expected_preamble) - self.assertTrue( - content.endswith(zip_content), - msg="Output content should end with the zip content", - ) - self.assertEqual(len(content), len(expected_preamble) + len(zip_content)) - - -if __name__ == "__main__": - unittest.main() + assert content.startswith(expected_preamble) + assert content.endswith(zip_content), ( + "Output content should end with the zip content" + ) + assert len(content) == len(expected_preamble) + len(zip_content) diff --git a/tests/tools/zipapp/zip_main_maker_test.py b/tests/tools/zipapp/zip_main_maker_test.py index dd8e8e8029..5c7f57c590 100644 --- a/tests/tools/zipapp/zip_main_maker_test.py +++ b/tests/tools/zipapp/zip_main_maker_test.py @@ -1,101 +1,90 @@ import hashlib import os -import tempfile -import unittest -from unittest import mock from tools.private.zipapp import zip_main_maker -class ZipMainMakerTest(unittest.TestCase): - def setUp(self): - self.temp_dir = tempfile.TemporaryDirectory() - self.addCleanup(self.temp_dir.cleanup) - - def test_creates_zip_main(self): - template_path = os.path.join(self.temp_dir.name, "template.py") - with open(template_path, "w", encoding="utf-8") as f: - f.write("hash=%APP_HASH%\nfoo=%FOO%\n") - - output_path = os.path.join(self.temp_dir.name, "output.py") - - file1_path = os.path.join(self.temp_dir.name, "file1.txt") - with open(file1_path, "wb") as f: - f.write(b"content1") - - file2_path = os.path.join(self.temp_dir.name, "file2.txt") - with open(file2_path, "wb") as f: - f.write(b"content2") - - # Add a symlink to test symlink hashing - symlink_path = os.path.join(self.temp_dir.name, "symlink.txt") - os.symlink(file1_path, symlink_path) - - manifest_path = os.path.join(self.temp_dir.name, "manifest.txt") - with open(manifest_path, "w", encoding="utf-8") as f: - f.write(f"rf-file|0|file1.txt|{file1_path}\n") - f.write(f"rf-file|0|file2.txt|{file2_path}\n") - f.write(f"rf-symlink|1|symlink.txt|{symlink_path}\n") - f.write("rf-empty|empty_file.txt\n") - - argv = [ - "zip_main_maker.py", - "--template", - template_path, - "--output", - output_path, - "--substitution", - "%FOO%=bar", - "--hash_files_manifest", - manifest_path, - ] - - with mock.patch("sys.argv", argv): - zip_main_maker.main() - - # Calculate expected hash - h = hashlib.sha256() - line1 = f"rf-file|0|file1.txt|{file1_path}" - line2 = f"rf-file|0|file2.txt|{file2_path}" - line3 = f"rf-symlink|1|symlink.txt|{symlink_path}" - line4 = "rf-empty|empty_file.txt" - - # Sort lines like the program does - lines = sorted([line1, line2, line3, line4]) - for line in lines: - parts = line.split("|") - if len(parts) > 1: - _, rest = line.split("|", 1) - h.update(rest.encode("utf-8")) - else: - h.update(line.encode("utf-8")) - - type_ = parts[0] - if type_ == "rf-empty": +def test_creates_zip_main(tmp_path, monkeypatch): + temp_dir = str(tmp_path) + template_path = os.path.join(temp_dir, "template.py") + with open(template_path, "w", encoding="utf-8") as f: + f.write("hash=%APP_HASH%\nfoo=%FOO%\n") + + output_path = os.path.join(temp_dir, "output.py") + + file1_path = os.path.join(temp_dir, "file1.txt") + with open(file1_path, "wb") as f: + f.write(b"content1") + + file2_path = os.path.join(temp_dir, "file2.txt") + with open(file2_path, "wb") as f: + f.write(b"content2") + + # Add a symlink to test symlink hashing + symlink_path = os.path.join(temp_dir, "symlink.txt") + os.symlink(file1_path, symlink_path) + + manifest_path = os.path.join(temp_dir, "manifest.txt") + with open(manifest_path, "w", encoding="utf-8") as f: + f.write(f"rf-file|0|file1.txt|{file1_path}\n") + f.write(f"rf-file|0|file2.txt|{file2_path}\n") + f.write(f"rf-symlink|1|symlink.txt|{symlink_path}\n") + f.write("rf-empty|empty_file.txt\n") + + argv = [ + "zip_main_maker.py", + "--template", + template_path, + "--output", + output_path, + "--substitution", + "%FOO%=bar", + "--hash_files_manifest", + manifest_path, + ] + + monkeypatch.setattr("sys.argv", argv) + zip_main_maker.main() + + # Calculate expected hash + h = hashlib.sha256() + line1 = f"rf-file|0|file1.txt|{file1_path}" + line2 = f"rf-file|0|file2.txt|{file2_path}" + line3 = f"rf-symlink|1|symlink.txt|{symlink_path}" + line4 = "rf-empty|empty_file.txt" + + # Sort lines like the program does + lines = sorted([line1, line2, line3, line4]) + for line in lines: + parts = line.split("|") + if len(parts) > 1: + _, rest = line.split("|", 1) + h.update(rest.encode("utf-8")) + else: + h.update(line.encode("utf-8")) + + type_ = parts[0] + if type_ == "rf-empty": + continue + if len(parts) >= 4: + is_symlink_str = parts[1] + path = parts[-1] + if not path: continue - if len(parts) >= 4: - is_symlink_str = parts[1] - path = parts[-1] - if not path: - continue - if is_symlink_str == "-1": - is_symlink = not os.path.exists(path) - else: - is_symlink = is_symlink_str == "1" - - if is_symlink: - h.update(os.readlink(path).encode("utf-8")) - else: - with open(path, "rb") as f: - h.update(f.read()) - - expected_hash = h.hexdigest() + if is_symlink_str == "-1": + is_symlink = not os.path.exists(path) + else: + is_symlink = is_symlink_str == "1" - with open(output_path, "r", encoding="utf-8") as f: - content = f.read() + if is_symlink: + h.update(os.readlink(path).encode("utf-8")) + else: + with open(path, "rb") as f: + h.update(f.read()) - self.assertEqual(content, f"hash={expected_hash}\nfoo=bar\n") + expected_hash = h.hexdigest() + with open(output_path, "r", encoding="utf-8") as f: + content = f.read() -if __name__ == "__main__": - unittest.main() + assert content == f"hash={expected_hash}\nfoo=bar\n" diff --git a/tests/tools/zipapp/zipper_test.py b/tests/tools/zipapp/zipper_test.py index ac70917c30..bbd85b6536 100644 --- a/tests/tools/zipapp/zipper_test.py +++ b/tests/tools/zipapp/zipper_test.py @@ -1,8 +1,5 @@ import os -import pathlib import shutil -import tempfile -import unittest import zipfile from tools.private.zipapp import zipper @@ -12,333 +9,294 @@ def symlink_target_path(p): return p.replace("/", os.sep) -class ZipperTest(unittest.TestCase): - def setUp(self): - self.test_dir = pathlib.Path(tempfile.mkdtemp()) - self.manifest_path = self.test_dir / "manifest.txt" - self.output_zip = self.test_dir / "output.zip" - - def tearDown(self): - shutil.rmtree(self.test_dir) - - def _create_zip(self, **kwargs): - defaults = { - "manifest_path": self.manifest_path, - "output_zip": self.output_zip, - "compress_level": 0, - "workspace_name": "my_ws", - "legacy_external_runfiles": False, - "runfiles_dir": "runfiles", - # We need to generate paths for the platform we're running on. - "platform_pathsep": os.sep, +def is_symlink(zip_info): + # Check upper 4 bits of external_attr for S_IFLNK + # S_IFLNK is 0o120000 = 0xA000 + attr = zip_info.external_attr >> 16 + return (attr & 0xF000) == 0xA000 + + +def assert_zip_file_content(zf, path, content=None, is_symlink_file=False, target=None): + info = zf.getinfo(path) + if is_symlink_file: + assert is_symlink(info), f"{path} should be a symlink but is not" + assert zf.read(path).decode() == target + else: + assert not is_symlink(info), f"{path} should NOT be a symlink but is" + assert zf.read(path).decode() == content + + +def create_zip(manifest_path, output_zip, **kwargs): + defaults = { + "manifest_path": manifest_path, + "output_zip": output_zip, + "compress_level": 0, + "workspace_name": "my_ws", + "legacy_external_runfiles": False, + "runfiles_dir": "runfiles", + "platform_pathsep": os.sep, + } + defaults.update(kwargs) + zipper.create_zip(**defaults) + + +def extract_zip(zip_path, extract_dir): + # Manually extract to preserve symlinks + with zipfile.ZipFile(zip_path, "r") as zf: + for info in zf.infolist(): + extract_path = extract_dir / info.filename + extract_path.parent.mkdir(parents=True, exist_ok=True) + if is_symlink(info): + target = zf.read(info).decode() + os.symlink(target, extract_path) + else: + with zf.open(info) as src, open(extract_path, "wb") as dst: + shutil.copyfileobj(src, dst) + + +def test_create_zip_with_files_and_symlinks(tmp_path): + manifest_path = tmp_path / "manifest.txt" + output_zip = tmp_path / "output.zip" + + file1_path = tmp_path / "file1.txt" + file1_path.write_text("content1") + + link_target_path = "target.txt" + symlink_path = tmp_path / "symlink_source" + symlink_path.symlink_to(link_target_path) + + manifest_content = [ + f"regular|0|file1.txt|{file1_path}", + f"rf-file|0|foo/bar.txt|{file1_path}", + f"rf-symlink|1|link1|{symlink_path}", + f"rf-root-symlink|0|root_file|{file1_path}", + "rf-empty|empty_file", + ] + manifest_path.write_text("\n".join(manifest_content)) + + create_zip(manifest_path, output_zip) + + assert output_zip.exists() + + with zipfile.ZipFile(output_zip, "r") as zf: + assert set(zf.namelist()) == { + "file1.txt", + "runfiles/my_ws/foo/bar.txt", + "runfiles/my_ws/link1", + "runfiles/root_file", + "runfiles/my_ws/empty_file", } - defaults.update(kwargs) - zipper.create_zip(**defaults) - - def assertZipFileContent( - self, zf, path, content=None, is_symlink=False, target=None - ): - info = zf.getinfo(path) - if is_symlink: - self.assertTrue( - self.is_symlink(info), - f"{path} should be a symlink but is not", - ) - self.assertEqual(zf.read(path).decode(), target) - else: - self.assertFalse( - self.is_symlink(info), - f"{path} should NOT be a symlink but is", - ) - self.assertEqual(zf.read(path).decode(), content) - - def test_create_zip_with_files_and_symlinks(self): - file1_path = self.test_dir / "file1.txt" - file1_path.write_text("content1") - - link_target_path = "target.txt" # Relative target - symlink_path = self.test_dir / "symlink_source" - symlink_path.symlink_to(link_target_path) - - manifest_content = [ - f"regular|0|file1.txt|{file1_path}", - f"rf-file|0|foo/bar.txt|{file1_path}", - f"rf-symlink|1|link1|{symlink_path}", # Should read target 'target.txt' - f"rf-root-symlink|0|root_file|{file1_path}", - "rf-empty|empty_file", - ] - self.manifest_path.write_text("\n".join(manifest_content)) - - self._create_zip() - - self.assertTrue(self.output_zip.exists()) - - with zipfile.ZipFile(self.output_zip, "r") as zf: - self.assertEqual( - set(zf.namelist()), - { - "file1.txt", - "runfiles/my_ws/foo/bar.txt", - "runfiles/my_ws/link1", - "runfiles/root_file", - "runfiles/my_ws/empty_file", - }, - ) - - self.assertZipFileContent(zf, "file1.txt", content="content1") - self.assertZipFileContent( - zf, "runfiles/my_ws/foo/bar.txt", content="content1" - ) - self.assertZipFileContent( - zf, "runfiles/my_ws/link1", is_symlink=True, target="target.txt" - ) - self.assertZipFileContent(zf, "runfiles/root_file", content="content1") - self.assertZipFileContent(zf, "runfiles/my_ws/empty_file", content="") - - def test_create_zip_with_direct_symlink(self): - # Test the 'symlink' manifest entry type - manifest_content = [ - "symlink|path/to/link|target/path", - ] - self.manifest_path.write_text("\n".join(manifest_content)) - - self._create_zip() - - with zipfile.ZipFile(self.output_zip, "r") as zf: - self.assertEqual(zf.namelist(), ["runfiles/path/to/link"]) - self.assertZipFileContent( - zf, - "runfiles/path/to/link", - is_symlink=True, - target=symlink_target_path("../../target/path"), - ) - - def test_pathsep_normalization(self): - # Test that pathsep="\\" normalizes paths - file1_path = self.test_dir / "file1.txt" - file1_path.write_text("content1") - - manifest_content = [ - f"regular|0|dir/file.txt|{file1_path}", - "symlink|link/path|target/path", - ] - self.manifest_path.write_text("\n".join(manifest_content)) - - # Use backslash as platform_pathsep - self._create_zip(platform_pathsep="\\") - - with zipfile.ZipFile(self.output_zip, "r") as zf: - # zipfile.namelist() always returns with forward slashes - # But the content of the symlink should be normalized if it was passed through path_norm - self.assertEqual( - set(zf.namelist()), - {"dir/file.txt", "runfiles/link/path"}, - ) - # The target of the symlink should have backslashes - self.assertZipFileContent( - zf, - "runfiles/link/path", - is_symlink=True, - target="..\\target\\path", - ) - - def test_symlink_precedence(self): - # Test that 'symlink' entries take precedence over others for the same path - file1_path = self.test_dir / "file1.txt" - file1_path.write_text("content1") - - manifest_content = [ - # Same zip path: runfiles/my_ws/path/to/file - f"rf-file|0|path/to/file|{file1_path}", - "symlink|my_ws/path/to/file|symlink/target", - ] - self.manifest_path.write_text("\n".join(manifest_content)) - - self._create_zip() - - with zipfile.ZipFile(self.output_zip, "r") as zf: - self.assertEqual(zf.namelist(), ["runfiles/my_ws/path/to/file"]) - # It should be the symlink, not the file - self.assertZipFileContent( - zf, - "runfiles/my_ws/path/to/file", - is_symlink=True, - target=symlink_target_path("../../../symlink/target"), - ) - - def test_timestamps_are_deterministic(self): - # Create a content file with a specific recent timestamp - file1_path = self.test_dir / "file1.txt" - file1_path.write_text("content1") - - # Set mtime to something recent (e.g. now) - os.utime(file1_path, None) - - manifest_content = [ - f"regular|0|file1.txt|{file1_path}", - ] - self.manifest_path.write_text("\n".join(manifest_content)) + assert_zip_file_content(zf, "file1.txt", content="content1") + assert_zip_file_content(zf, "runfiles/my_ws/foo/bar.txt", content="content1") + assert_zip_file_content( + zf, "runfiles/my_ws/link1", is_symlink_file=True, target="target.txt" + ) + assert_zip_file_content(zf, "runfiles/root_file", content="content1") + assert_zip_file_content(zf, "runfiles/my_ws/empty_file", content="") - self._create_zip() - with zipfile.ZipFile(self.output_zip, "r") as zf: - info = zf.getinfo("file1.txt") - # DOS epoch is 1980-01-01 00:00:00 - expected_date_time = (1980, 1, 1, 0, 0, 0) - self.assertEqual(info.date_time, expected_date_time) +def test_create_zip_with_direct_symlink(tmp_path): + manifest_path = tmp_path / "manifest.txt" + output_zip = tmp_path / "output.zip" - def test_runfiles_mapping_with_cross_repo_paths(self): - # Create content file - file1_path = self.test_dir / "file1.txt" - file1_path.write_text("content1") + manifest_content = ["symlink|path/to/link|target/path"] + manifest_path.write_text("\n".join(manifest_content)) - manifest_content = [ - f"rf-file|0|../other_repo/foo.txt|{file1_path}", - "rf-empty|../other_repo/empty_file", - ] + create_zip(manifest_path, output_zip) - self.manifest_path.write_text("\n".join(manifest_content)) - - self._create_zip(workspace_name="my_ws") - - with zipfile.ZipFile(self.output_zip, "r") as zf: - self.assertEqual( - set(zf.namelist()), - { - "runfiles/other_repo/foo.txt", - "runfiles/other_repo/empty_file", - }, - ) - self.assertZipFileContent( - zf, "runfiles/other_repo/foo.txt", content="content1" - ) - self.assertZipFileContent(zf, "runfiles/other_repo/empty_file", content="") - - def test_runfiles_mapping_with_legacy_external_paths(self): - file1_path = self.test_dir / "file1.txt" - file1_path.write_text("content1") - - manifest_content = [ - f"rf-file|0|external/other_repo/foo.txt|{file1_path}", - "rf-empty|external/other_repo/empty_file", - ] + with zipfile.ZipFile(output_zip, "r") as zf: + assert zf.namelist() == ["runfiles/path/to/link"] + assert_zip_file_content( + zf, + "runfiles/path/to/link", + is_symlink_file=True, + target=symlink_target_path("../../target/path"), + ) - self.manifest_path.write_text("\n".join(manifest_content)) - - self._create_zip(workspace_name="my_ws", legacy_external_runfiles=True) - - with zipfile.ZipFile(self.output_zip, "r") as zf: - self.assertEqual( - set(zf.namelist()), - { - "runfiles/other_repo/foo.txt", - "runfiles/other_repo/empty_file", - }, - ) - self.assertZipFileContent( - zf, "runfiles/other_repo/foo.txt", content="content1" - ) - self.assertZipFileContent(zf, "runfiles/other_repo/empty_file", content="") - - def test_output_deterministic(self): - # Create files - file1 = self.test_dir / "file1" - file1.write_text("1") - file2 = self.test_dir / "file2" - file2.write_text("2") - file3 = self.test_dir / "file3" - file3.write_text("3") - - # Manifest entries mixed up - # We want the final order to be: - # 1. a/regular (regular) - # 2. runfiles/a_root_link (rf-root-symlink) - # 3. runfiles/my_ws/b_rf_file (rf-file) - # 4. runfiles/my_ws/c_rf_link (rf-symlink) - # 5. runfiles/my_ws/d_rf_empty (rf-empty) - # 6. z/regular (regular) - - manifest_content = [ - f"regular|0|z/regular|{file1}", - f"rf-file|0|b_rf_file|{file2}", # -> runfiles/my_ws/b_rf_file - f"rf-root-symlink|0|a_root_link|{file3}", # -> runfiles/a_root_link - f"regular|0|a/regular|{file3}", - "rf-empty|d_rf_empty", # -> runfiles/my_ws/d_rf_empty - f"rf-symlink|0|c_rf_link|{file3}", # -> runfiles/my_ws/c_rf_link - ] - self.manifest_path.write_text("\n".join(manifest_content)) - - self._create_zip(workspace_name="my_ws") - - with zipfile.ZipFile(self.output_zip, "r") as zf: - self.assertEqual( - zf.namelist(), - [ - "a/regular", - "runfiles/a_root_link", - "runfiles/my_ws/b_rf_file", - "runfiles/my_ws/c_rf_link", - "runfiles/my_ws/d_rf_empty", - "z/regular", - ], - ) - - def _extract_zip(self, zip_path, extract_dir): - # Manually extract to preserve symlinks - with zipfile.ZipFile(zip_path, "r") as zf: - for info in zf.infolist(): - extract_path = extract_dir / info.filename - extract_path.parent.mkdir(parents=True, exist_ok=True) - if self.is_symlink(info): - target = zf.read(info).decode() - # On Windows, relative symlinks must use backslashes to be readable - os.symlink(target, extract_path) - else: - with zf.open(info) as src, open(extract_path, "wb") as dst: - shutil.copyfileobj(src, dst) - - def test_symlink_extraction(self): - # Test that 'symlink' entries extract correctly as relative symlinks - # Create a file that the symlink will point to - target_file = self.test_dir / "target_file.txt" - target_file.write_text("target content") - - manifest_content = [ - f"rf-file|0|target/path|{target_file}", - "symlink|my_ws/path/to/link|my_ws/target/path", - f"rf-file|0|same_dir_target|{target_file}", - "symlink|my_ws/same_dir_link|my_ws/same_dir_target", - ] - self.manifest_path.write_text("\n".join(manifest_content)) +def test_pathsep_normalization(tmp_path): + manifest_path = tmp_path / "manifest.txt" + output_zip = tmp_path / "output.zip" - self._create_zip(workspace_name="my_ws") + file1_path = tmp_path / "file1.txt" + file1_path.write_text("content1") - extract_dir = self.test_dir / "extract" - extract_dir.mkdir() + manifest_content = [ + f"regular|0|dir/file.txt|{file1_path}", + "symlink|link/path|target/path", + ] + manifest_path.write_text("\n".join(manifest_content)) - self._extract_zip(self.output_zip, extract_dir) + create_zip(manifest_path, output_zip, platform_pathsep="\\") - link_path = extract_dir / "runfiles/my_ws/path/to/link" - self.assertTrue(link_path.is_symlink(), f"{link_path} should be a symlink") - self.assertEqual( - os.readlink(link_path), "../../target/path".replace("/", os.path.sep) + with zipfile.ZipFile(output_zip, "r") as zf: + assert set(zf.namelist()) == {"dir/file.txt", "runfiles/link/path"} + assert_zip_file_content( + zf, + "runfiles/link/path", + is_symlink_file=True, + target="..\\target\\path", ) - self.assertEqual(link_path.read_text(), "target content") - link2_path = extract_dir / "runfiles/my_ws/same_dir_link" - self.assertTrue(link2_path.is_symlink(), f"{link2_path} should be a symlink") - # Relative path from runfiles/my_ws/ to runfiles/my_ws/same_dir_target is just same_dir_target - self.assertEqual(os.readlink(link2_path), "same_dir_target") - self.assertEqual(link2_path.read_text(), "target content") - def is_symlink(self, zip_info): - # Check upper 4 bits of external_attr for S_IFLNK - # S_IFLNK is 0o120000 = 0xA000 - attr = zip_info.external_attr >> 16 - return (attr & 0xF000) == 0xA000 +def test_symlink_precedence(tmp_path): + manifest_path = tmp_path / "manifest.txt" + output_zip = tmp_path / "output.zip" + + file1_path = tmp_path / "file1.txt" + file1_path.write_text("content1") + + manifest_content = [ + f"rf-file|0|path/to/file|{file1_path}", + "symlink|my_ws/path/to/file|symlink/target", + ] + manifest_path.write_text("\n".join(manifest_content)) + + create_zip(manifest_path, output_zip) + + with zipfile.ZipFile(output_zip, "r") as zf: + assert zf.namelist() == ["runfiles/my_ws/path/to/file"] + assert_zip_file_content( + zf, + "runfiles/my_ws/path/to/file", + is_symlink_file=True, + target=symlink_target_path("../../../symlink/target"), + ) + + +def test_timestamps_are_deterministic(tmp_path): + manifest_path = tmp_path / "manifest.txt" + output_zip = tmp_path / "output.zip" + + file1_path = tmp_path / "file1.txt" + file1_path.write_text("content1") + os.utime(file1_path, None) + + manifest_content = [f"regular|0|file1.txt|{file1_path}"] + manifest_path.write_text("\n".join(manifest_content)) + + create_zip(manifest_path, output_zip) + + with zipfile.ZipFile(output_zip, "r") as zf: + info = zf.getinfo("file1.txt") + expected_date_time = (1980, 1, 1, 0, 0, 0) + assert info.date_time == expected_date_time + + +def test_runfiles_mapping_with_cross_repo_paths(tmp_path): + manifest_path = tmp_path / "manifest.txt" + output_zip = tmp_path / "output.zip" + + file1_path = tmp_path / "file1.txt" + file1_path.write_text("content1") + + manifest_content = [ + f"rf-file|0|../other_repo/foo.txt|{file1_path}", + "rf-empty|../other_repo/empty_file", + ] + manifest_path.write_text("\n".join(manifest_content)) + + create_zip(manifest_path, output_zip, workspace_name="my_ws") + + with zipfile.ZipFile(output_zip, "r") as zf: + assert set(zf.namelist()) == { + "runfiles/other_repo/foo.txt", + "runfiles/other_repo/empty_file", + } + assert_zip_file_content(zf, "runfiles/other_repo/foo.txt", content="content1") + assert_zip_file_content(zf, "runfiles/other_repo/empty_file", content="") + + +def test_runfiles_mapping_with_legacy_external_paths(tmp_path): + manifest_path = tmp_path / "manifest.txt" + output_zip = tmp_path / "output.zip" + + file1_path = tmp_path / "file1.txt" + file1_path.write_text("content1") + + manifest_content = [ + f"rf-file|0|external/other_repo/foo.txt|{file1_path}", + "rf-empty|external/other_repo/empty_file", + ] + manifest_path.write_text("\n".join(manifest_content)) + + create_zip( + manifest_path, output_zip, workspace_name="my_ws", legacy_external_runfiles=True + ) + + with zipfile.ZipFile(output_zip, "r") as zf: + assert set(zf.namelist()) == { + "runfiles/other_repo/foo.txt", + "runfiles/other_repo/empty_file", + } + assert_zip_file_content(zf, "runfiles/other_repo/foo.txt", content="content1") + assert_zip_file_content(zf, "runfiles/other_repo/empty_file", content="") + + +def test_output_deterministic(tmp_path): + manifest_path = tmp_path / "manifest.txt" + output_zip = tmp_path / "output.zip" + + file1 = tmp_path / "file1" + file1.write_text("1") + file2 = tmp_path / "file2" + file2.write_text("2") + file3 = tmp_path / "file3" + file3.write_text("3") + + manifest_content = [ + f"regular|0|z/regular|{file1}", + f"rf-file|0|b_rf_file|{file2}", + f"rf-root-symlink|0|a_root_link|{file3}", + f"regular|0|a/regular|{file3}", + "rf-empty|d_rf_empty", + f"rf-symlink|0|c_rf_link|{file3}", + ] + + manifest_path.write_text("\n".join(manifest_content)) + + create_zip(manifest_path, output_zip, workspace_name="my_ws") + + with zipfile.ZipFile(output_zip, "r") as zf: + assert zf.namelist() == [ + "a/regular", + "runfiles/a_root_link", + "runfiles/my_ws/b_rf_file", + "runfiles/my_ws/c_rf_link", + "runfiles/my_ws/d_rf_empty", + "z/regular", + ] + + +def test_symlink_extraction(tmp_path): + manifest_path = tmp_path / "manifest.txt" + output_zip = tmp_path / "output.zip" + + target_file = tmp_path / "target_file.txt" + target_file.write_text("target content") + + manifest_content = [ + f"rf-file|0|target/path|{target_file}", + "symlink|my_ws/path/to/link|my_ws/target/path", + f"rf-file|0|same_dir_target|{target_file}", + "symlink|my_ws/same_dir_link|my_ws/same_dir_target", + ] + manifest_path.write_text("\n".join(manifest_content)) + + create_zip(manifest_path, output_zip, workspace_name="my_ws") + + extract_dir = tmp_path / "extract" + extract_dir.mkdir() + + extract_zip(output_zip, extract_dir) + link_path = extract_dir / "runfiles/my_ws/path/to/link" + assert link_path.is_symlink(), f"{link_path} should be a symlink" + assert os.readlink(link_path) == "../../target/path".replace("/", os.path.sep) + assert link_path.read_text() == "target content" -if __name__ == "__main__": - unittest.main() + link2_path = extract_dir / "runfiles/my_ws/same_dir_link" + assert link2_path.is_symlink(), f"{link2_path} should be a symlink" + assert os.readlink(link2_path) == "same_dir_target" + assert link2_path.read_text() == "target content" diff --git a/tests/uv/lock/BUILD.bazel b/tests/uv/lock/BUILD.bazel index 6b6902da44..60d680bde8 100644 --- a/tests/uv/lock/BUILD.bazel +++ b/tests/uv/lock/BUILD.bazel @@ -1,5 +1,15 @@ load(":lock_tests.bzl", "lock_test_suite") +load(":uv_lock_to_requirements_tests.bzl", "uv_lock_to_requirements_test_suite") + +exports_files( + glob(["testdata/*"]), + visibility = ["//tests:__subpackages__"], +) lock_test_suite( name = "lock_tests", ) + +uv_lock_to_requirements_test_suite( + name = "uv_lock_to_requirements_tests", +) diff --git a/tests/uv/lock/lock_run_test.py b/tests/uv/lock/lock_run_test.py index f64cbdccec..2de9147d99 100644 --- a/tests/uv/lock/lock_run_test.py +++ b/tests/uv/lock/lock_run_test.py @@ -1,3 +1,4 @@ +import os import subprocess import tempfile import unittest @@ -6,18 +7,143 @@ from python import runfiles rfiles = runfiles.Create() +assert rfiles is not None, "Failed to create runfiles" def _relative_rpath(path: str) -> Path: - p = (Path("_main") / "tests" / "uv" / "lock" / path).as_posix() - rpath = rfiles.Rlocation(p) - if not rpath: - raise ValueError(f"Could not find file: {p}") + """Find file in runfiles, handling Windows .bat/.exe wrappers.""" + # On Windows, try executable extensions first to avoid matching symlink + # entries in the runfiles manifest that point to non-executable files + # (e.g. a Python source file instead of the .exe launcher). + exts = (".exe", ".bat", "") if os.name == "nt" else ("", ".exe", ".bat") + for ext in exts: + p = (Path("_main") / "tests" / "uv" / "lock" / (path + ext)).as_posix() + rpath = rfiles.Rlocation(p) + if rpath: + rp = Path(rpath) + if rp.exists(): + return rp - return Path(rpath) + # Fallback: look in runfiles directory directly (handles .bat wrappers on + # Windows where Rlocation may return a runfiles link that doesn't exist) + runfiles_dir = os.environ.get("RUNFILES_DIR") + if runfiles_dir: + exts = (".exe", ".bat", "") if os.name == "nt" else ("", ".bat", ".exe") + for ext in exts: + rp = Path(runfiles_dir, "_main", "tests", "uv", "lock", path + ext) + if rp.exists(): + return rp + + raise ValueError(f"Could not find file in runfiles: {path}") + + +def _run_binary(path: Path, **kwargs): + """Run a binary, handling Windows .bat files.""" + if os.name == "nt": + return subprocess.run( + ["cmd.exe", "/c", str(path)], + **kwargs, + ) + return subprocess.run(path, **kwargs) class LockTests(unittest.TestCase): + def _subprocess_env(self, workspace_dir: Path) -> dict[str, str]: + env = { + "BUILD_WORKSPACE_DIRECTORY": str(workspace_dir), + } + # Inherit specific env vars needed for finding runfiles on Windows + for key in ( + "PATH", + "RUNFILES_DIR", + "RUNFILES_MANIFEST_FILE", + "SYSTEMROOT", + "PATHEXT", + ): + if key in os.environ: + env[key] = os.environ[key] + return env + + def test_requirements_run_script_for_new_file(self): + """Verify the requirements_new_file.run script has expected args.""" + run_script_path = _relative_rpath("requirements_new_file.run") + content = run_script_path.read_text() + + if os.name == "nt": + self.assertIn("@echo off", content) + else: + self.assertIn("#!/usr/bin/env bash", content) + self.assertIn("BUILD_WORKSPACE_DIRECTORY", content) + self.assertIn("--no-progress", content) + self.assertIn("--quiet", content) + self.assertIn("does_not_exist.txt", content) + + def test_uv_lock_run_script(self): + """Verify the uv_lock_test.run script has expected args.""" + run_script_path = _relative_rpath("uv_lock_test.run") + content = run_script_path.read_text() + + if os.name == "nt": + self.assertIn("@echo off", content) + else: + self.assertIn("#!/usr/bin/env bash", content) + self.assertIn("--no-progress", content) + self.assertIn("--quiet", content) + + def test_run_script_has_no_output_file_arg(self): + """Verify the uv lock .run script does NOT have --output-file (uv lock doesn't use it).""" + run_script_path = _relative_rpath("uv_lock_test.run") + content = run_script_path.read_text() + + self.assertNotIn("--output-file", content) + + def test_debug_requirements_diff(self): + """Temporary test to print diff of generated vs expected requirements on Windows.""" + expected_path = _relative_rpath("testdata/requirements.txt") + try: + # Pass "requirements.out" directly. We need to handle the case where + # _relative_rpath might append extensions on Windows. + # Actually, _relative_rpath has: + # exts = (".exe", ".bat", "") if os.name == "nt" else ... + # So on Windows it will try .exe, .bat, then empty. + # If "requirements.out" exists, the empty extension will match it. + generated_path = _relative_rpath("requirements.out") + except ValueError as e: + print(f"Could not find requirements.out: {e}") + # Dump runfiles directory to help debug if needed + runfiles_dir = os.environ.get("RUNFILES_DIR") + if runfiles_dir: + print(f"RUNFILES_DIR: {runfiles_dir}") + for root, _, files in os.walk(runfiles_dir): + for f in files: + if "requirements" in f: + print(os.path.join(root, f)) + raise + + expected = expected_path.read_text() + generated = generated_path.read_text() + + if expected != generated: + import difflib + + diff = list( + difflib.unified_diff( + expected.splitlines(keepends=True), + generated.splitlines(keepends=True), + fromfile="expected (testdata/requirements.txt)", + tofile="generated (requirements.out)", + ) + ) + print("\n=== DIFF START ===") + print("".join(diff)) + print("=== DIFF END ===\n") + + # Also print representation to see line endings + print(f"Expected line endings: {repr(expected[:100])}") + print(f"Generated line endings: {repr(generated[:100])}") + + self.assertEqual(expected, generated, "Files differ! See diff above.") + def test_requirements_updating_for_the_first_time(self): # Given copier_path = _relative_rpath("requirements_new_file.update") @@ -30,19 +156,18 @@ def test_requirements_updating_for_the_first_time(self): self.assertFalse( want_path.exists(), "The path should not exist after the test" ) - output = subprocess.run( + output = _run_binary( copier_path, capture_output=True, - env={ - "BUILD_WORKSPACE_DIRECTORY": f"{workspace_dir}", - }, + env=self._subprocess_env(workspace_dir), ) # Then self.assertEqual(0, output.returncode, output.stderr) + stdout = output.stdout.decode("utf-8").replace("\\", "/") self.assertIn( "cp /tests/uv/lock/requirements_new_file", - output.stdout.decode("utf-8"), + stdout, ) self.assertTrue(want_path.exists(), "The path should exist after the test") self.assertNotEqual(want_path.read_text(), "") @@ -50,8 +175,6 @@ def test_requirements_updating_for_the_first_time(self): def test_requirements_updating(self): # Given copier_path = _relative_rpath("requirements.update") - existing_file = _relative_rpath("testdata/requirements.txt") - want_text = existing_file.read_text() # When with tempfile.TemporaryDirectory() as dir: @@ -65,25 +188,22 @@ def test_requirements_updating(self): / "requirements.txt" ) want_path.parent.mkdir(parents=True) - want_path.write_text( - want_text + "\n\n" - ) # Write something else to see that it is restored - output = subprocess.run( + output = _run_binary( copier_path, capture_output=True, - env={ - "BUILD_WORKSPACE_DIRECTORY": f"{workspace_dir}", - }, + env=self._subprocess_env(workspace_dir), ) # Then self.assertEqual(0, output.returncode) + stdout = output.stdout.decode("utf-8").replace("\\", "/") self.assertIn( "cp /tests/uv/lock/requirements", - output.stdout.decode("utf-8"), + stdout, ) - self.assertEqual(want_path.read_text(), want_text) + self.assertTrue(want_path.exists(), "The path should exist after the test") + self.assertNotEqual(want_path.read_text(), "") def test_requirements_run_on_the_first_time(self): # Given @@ -98,14 +218,12 @@ def test_requirements_run_on_the_first_time(self): want_path.parent.mkdir(parents=True) self.assertFalse( - want_path.exists(), "The path should not exist after the test" + want_path.exists(), "The path should not exist before the test" ) - output = subprocess.run( + output = _run_binary( copier_path, capture_output=True, - env={ - "BUILD_WORKSPACE_DIRECTORY": f"{workspace_dir}", - }, + env=self._subprocess_env(workspace_dir), ) # Then @@ -113,16 +231,33 @@ def test_requirements_run_on_the_first_time(self): self.assertTrue(want_path.exists(), "The path should exist after the test") got_contents = want_path.read_text() self.assertNotEqual(got_contents, "") - self.assertIn( - got_contents, - output.stdout.decode("utf-8"), - ) + # NOTE: stdout is typically empty because uv runs with --quiet --no-progress + + def test_requirements_run_script_has_expected_args(self): + """Verify the .run script template has expected args embedded.""" + run_script_path = _relative_rpath("requirements.run") + content = run_script_path.read_text() + + if os.name == "nt": + self.assertIn("@echo off", content) + self.assertIn("%*", content) + else: + self.assertIn("#!/usr/bin/env bash", content) + self.assertIn('"$@"', content) + self.assertIn("BUILD_WORKSPACE_DIRECTORY", content) + self.assertIn("--custom-compile-command", content) + self.assertIn("--generate-hashes", content) + self.assertIn("--no-strip-extras", content) + self.assertIn("--no-python-downloads", content) + self.assertIn("--no-cache", content) + self.assertIn("--no-progress", content) + self.assertIn("--quiet", content) + self.assertIn("--output-file", content) + self.assertIn("requirements.txt", content) def test_requirements_run(self): # Given copier_path = _relative_rpath("requirements.run") - existing_file = _relative_rpath("testdata/requirements.txt") - want_text = existing_file.read_text() # When with tempfile.TemporaryDirectory() as dir: @@ -135,18 +270,12 @@ def test_requirements_run(self): / "testdata" / "requirements.txt" ) - want_path.parent.mkdir(parents=True) - want_path.write_text( - want_text + "\n\n" - ) # Write something else to see that it is restored - output = subprocess.run( + output = _run_binary( copier_path, capture_output=True, - env={ - "BUILD_WORKSPACE_DIRECTORY": f"{workspace_dir}", - }, + env=self._subprocess_env(workspace_dir), ) # Then @@ -154,10 +283,7 @@ def test_requirements_run(self): self.assertTrue(want_path.exists(), "The path should exist after the test") got_contents = want_path.read_text() self.assertNotEqual(got_contents, "") - self.assertIn( - got_contents, - output.stdout.decode("utf-8"), - ) + # NOTE: stdout is typically empty because uv runs with --quiet --no-progress if __name__ == "__main__": diff --git a/tests/uv/lock/lock_tests.bzl b/tests/uv/lock/lock_tests.bzl index 1eb5b1d903..e3e035717f 100644 --- a/tests/uv/lock/lock_tests.bzl +++ b/tests/uv/lock/lock_tests.bzl @@ -14,6 +14,7 @@ "" +load("@bazel_skylib//rules:diff_test.bzl", "diff_test") load("@bazel_skylib//rules:native_binary.bzl", "native_test") load("//python/uv:lock.bzl", "lock") load("//tests/support:py_reconfig.bzl", "py_reconfig_test") @@ -65,6 +66,8 @@ def lock_test_suite(name): "requirements.update", "requirements.run", "testdata/requirements.txt", + "uv_lock_test.run", + ":requirements", ], main = "lock_run_test.py", tags = [ @@ -77,19 +80,26 @@ def lock_test_suite(name): # `--index-url`. "no-remote-exec", ], - # FIXME @aignas 2025-03-19: It seems that currently: - # 1. The Windows runners are not compatible with the `uv` Windows binaries. - # 2. The Python launcher is having trouble launching scripts from within the Python test. - target_compatible_with = select({ - "@platforms//os:windows": ["@platforms//:incompatible"], - "//conditions:default": [], - }), ) - # document and check that this actually works - native_test( + # Document and check that the action output matches the in-source file. + diff_test( name = "requirements_test", - src = ":requirements.update", + timeout = "short", + file1 = ":requirements", + file2 = "testdata/requirements.txt", + ) + + lock( + name = "uv_lock_test", + srcs = ["testdata/pyproject.toml"], + out = "testdata/uv_lock_expected.lock", + tags = ["no-remote-exec"], + ) + + native_test( + name = "uv_lock_test_check", + src = ":uv_lock_test.update", target_compatible_with = select({ "@platforms//os:windows": ["@platforms//:incompatible"], "//conditions:default": [], @@ -100,6 +110,8 @@ def lock_test_suite(name): name = name, tests = [ ":requirements_test", + "//tests/uv/lock/pyproject_toml:requirements_test", ":requirements_run_tests", + ":uv_lock_test_check", ], ) diff --git a/tests/uv/lock/pyproject_toml/BUILD.bazel b/tests/uv/lock/pyproject_toml/BUILD.bazel new file mode 100644 index 0000000000..d73d206293 --- /dev/null +++ b/tests/uv/lock/pyproject_toml/BUILD.bazel @@ -0,0 +1,31 @@ +load("@bazel_skylib//rules:diff_test.bzl", "diff_test") +load("//python/uv:lock.bzl", "lock") + +# This test verifies that the `lock` rule automatically passes `--project` to `uv pip compile` based +# on the package directory, so that `[tool.uv]` settings from a `pyproject.toml` in the same +# directory are applied. It will exclude a particular dependency from the lock-file, so it will be +# easy to see if we have any issues. +lock( + name = "requirements", + srcs = ["pyproject.toml"], + out = "requirements.txt", + build_constraints = [ + "//tests/uv/lock:testdata/build_constraints.txt", + "//tests/uv/lock:testdata/build_constraints2.txt", + ], + constraints = [ + "//tests/uv/lock:testdata/constraints.txt", + "//tests/uv/lock:testdata/constraints2.txt", + ], + # It seems that the CI remote executors for the RBE do not have network + # connectivity due to current CI setup. + tags = ["no-remote-exec"], +) + +diff_test( + name = "requirements_test", + timeout = "short", + file1 = ":requirements", + file2 = "requirements.txt", + visibility = ["//tests/uv/lock:__pkg__"], +) diff --git a/tests/uv/lock/pyproject_toml/pyproject.toml b/tests/uv/lock/pyproject_toml/pyproject.toml new file mode 100644 index 0000000000..06b96309da --- /dev/null +++ b/tests/uv/lock/pyproject_toml/pyproject.toml @@ -0,0 +1,8 @@ +[project] +name = "test" +version = "0.0.0" +dependencies = ["requests"] + +[tool.uv] +no-build-isolation = true +exclude-dependencies = ["charset-normalizer"] diff --git a/tests/uv/lock/pyproject_toml/requirements.txt b/tests/uv/lock/pyproject_toml/requirements.txt new file mode 100644 index 0000000000..2f4330e1ae --- /dev/null +++ b/tests/uv/lock/pyproject_toml/requirements.txt @@ -0,0 +1,26 @@ +# This file was autogenerated by uv via the following command: +# bazel run //tests/uv/lock/pyproject_toml:requirements.update +certifi==2025.1.31 \ + --hash=sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651 \ + --hash=sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe + # via + # -c tests/uv/lock/testdata/constraints.txt + # requests +idna==3.10 \ + --hash=sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9 \ + --hash=sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3 + # via + # -c tests/uv/lock/testdata/constraints.txt + # requests +requests==2.32.3 \ + --hash=sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760 \ + --hash=sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6 + # via + # -c tests/uv/lock/testdata/constraints.txt + # test (tests/uv/lock/pyproject_toml/pyproject.toml) +urllib3==2.3.0 \ + --hash=sha256:1cee9ad369867bfdbbb48b7dd50374c0967a0bb7710050facf0dd6911440e3df \ + --hash=sha256:f8c5449b3cf0861679ce7e0503c7b44b5ec981bec0d1d3795a07f1ba96f0204d + # via + # -c tests/uv/lock/testdata/constraints.txt + # requests diff --git a/tests/uv/lock/testdata/constraints.txt b/tests/uv/lock/testdata/constraints.txt index 18ade2c5b9..1d4f29173d 100644 --- a/tests/uv/lock/testdata/constraints.txt +++ b/tests/uv/lock/testdata/constraints.txt @@ -1 +1,5 @@ charset-normalizer==3.4.0 +certifi==2025.1.31 +requests==2.32.3 +idna==3.10 +urllib3==2.3.0 diff --git a/tests/uv/lock/testdata/pyproject.toml b/tests/uv/lock/testdata/pyproject.toml new file mode 100644 index 0000000000..d72efcf7c0 --- /dev/null +++ b/tests/uv/lock/testdata/pyproject.toml @@ -0,0 +1,5 @@ +[project] +name = "test-project" +version = "0.0.1" +dependencies = ["requests"] +requires-python = ">=3.9" diff --git a/tests/uv/lock/testdata/requirements.txt b/tests/uv/lock/testdata/requirements.txt index d02844636d..8b718fb145 100644 --- a/tests/uv/lock/testdata/requirements.txt +++ b/tests/uv/lock/testdata/requirements.txt @@ -3,7 +3,9 @@ certifi==2025.1.31 \ --hash=sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651 \ --hash=sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe - # via requests + # via + # -c tests/uv/lock/testdata/constraints.txt + # requests charset-normalizer==3.4.0 \ --hash=sha256:0099d79bdfcf5c1f0c2c72f91516702ebf8b0b8ddd8905f97a8aecf49712c621 \ --hash=sha256:0713f3adb9d03d49d365b70b84775d0a0d18e4ab08d12bc46baa6132ba78aaf6 \ @@ -117,12 +119,18 @@ charset-normalizer==3.4.0 \ idna==3.10 \ --hash=sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9 \ --hash=sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3 - # via requests + # via + # -c tests/uv/lock/testdata/constraints.txt + # requests requests==2.32.3 \ --hash=sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760 \ --hash=sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6 - # via -r tests/uv/lock/testdata/requirements.in + # via + # -c tests/uv/lock/testdata/constraints.txt + # -r tests/uv/lock/testdata/requirements.in urllib3==2.3.0 \ --hash=sha256:1cee9ad369867bfdbbb48b7dd50374c0967a0bb7710050facf0dd6911440e3df \ --hash=sha256:f8c5449b3cf0861679ce7e0503c7b44b5ec981bec0d1d3795a07f1ba96f0204d - # via requests + # via + # -c tests/uv/lock/testdata/constraints.txt + # requests diff --git a/tests/uv/lock/testdata/uv_lock_expected.lock b/tests/uv/lock/testdata/uv_lock_expected.lock new file mode 100644 index 0000000000..fc027674ef --- /dev/null +++ b/tests/uv/lock/testdata/uv_lock_expected.lock @@ -0,0 +1,218 @@ +version = 1 +revision = 3 +requires-python = ">=3.9" +resolution-markers = [ + "python_full_version >= '3.10'", + "python_full_version < '3.10'", +] + +[[package]] +name = "certifi" +version = "2026.4.22" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/25/ee/6caf7a40c36a1220410afe15a1cc64993a1f864871f698c0f93acb72842a/certifi-2026.4.22.tar.gz", hash = "sha256:8d455352a37b71bf76a79caa83a3d6c25afee4a385d632127b6afb3963f1c580", size = 137077, upload-time = "2026-04-22T11:26:11.191Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/30/7cd8fdcdfbc5b869528b079bfb76dcdf6056b1a2097a662e5e8c04f42965/certifi-2026.4.22-py3-none-any.whl", hash = "sha256:3cb2210c8f88ba2318d29b0388d1023c8492ff72ecdde4ebdaddbb13a31b1c4a", size = 135707, upload-time = "2026-04-22T11:26:09.372Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/a1/67fe25fac3c7642725500a3f6cfe5821ad557c3abb11c9d20d12c7008d3e/charset_normalizer-3.4.7.tar.gz", hash = "sha256:ae89db9e5f98a11a4bf50407d4363e7b09b31e55bc117b4f7d80aab97ba009e5", size = 144271, upload-time = "2026-04-02T09:28:39.342Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/26/08/0f303cb0b529e456bb116f2d50565a482694fbb94340bf56d44677e7ed03/charset_normalizer-3.4.7-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cdd68a1fb318e290a2077696b7eb7a21a49163c455979c639bf5a5dcdc46617d", size = 315182, upload-time = "2026-04-02T09:25:40.673Z" }, + { url = "https://files.pythonhosted.org/packages/24/47/b192933e94b546f1b1fe4df9cc1f84fcdbf2359f8d1081d46dd029b50207/charset_normalizer-3.4.7-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e17b8d5d6a8c47c85e68ca8379def1303fd360c3e22093a807cd34a71cd082b8", size = 209329, upload-time = "2026-04-02T09:25:42.354Z" }, + { url = "https://files.pythonhosted.org/packages/c2/b4/01fa81c5ca6141024d89a8fc15968002b71da7f825dd14113207113fabbd/charset_normalizer-3.4.7-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:511ef87c8aec0783e08ac18565a16d435372bc1ac25a91e6ac7f5ef2b0bff790", size = 231230, upload-time = "2026-04-02T09:25:44.281Z" }, + { url = "https://files.pythonhosted.org/packages/20/f7/7b991776844dfa058017e600e6e55ff01984a063290ca5622c0b63162f68/charset_normalizer-3.4.7-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:007d05ec7321d12a40227aae9e2bc6dca73f3cb21058999a1df9e193555a9dcc", size = 225890, upload-time = "2026-04-02T09:25:45.475Z" }, + { url = "https://files.pythonhosted.org/packages/20/e7/bed0024a0f4ab0c8a9c64d4445f39b30c99bd1acd228291959e3de664247/charset_normalizer-3.4.7-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cf29836da5119f3c8a8a70667b0ef5fdca3bb12f80fd06487cfa575b3909b393", size = 216930, upload-time = "2026-04-02T09:25:46.58Z" }, + { url = "https://files.pythonhosted.org/packages/e2/ab/b18f0ab31cdd7b3ddb8bb76c4a414aeb8160c9810fdf1bc62f269a539d87/charset_normalizer-3.4.7-cp310-cp310-manylinux_2_31_armv7l.whl", hash = "sha256:12d8baf840cc7889b37c7c770f478adea7adce3dcb3944d02ec87508e2dcf153", size = 202109, upload-time = "2026-04-02T09:25:48.031Z" }, + { url = "https://files.pythonhosted.org/packages/82/e5/7e9440768a06dfb3075936490cb82dbf0ee20a133bf0dd8551fa096914ec/charset_normalizer-3.4.7-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d560742f3c0d62afaccf9f41fe485ed69bd7661a241f86a3ef0f0fb8b1a397af", size = 214684, upload-time = "2026-04-02T09:25:49.245Z" }, + { url = "https://files.pythonhosted.org/packages/71/94/8c61d8da9f062fdf457c80acfa25060ec22bf1d34bbeaca4350f13bcfd07/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b14b2d9dac08e28bb8046a1a0434b1750eb221c8f5b87a68f4fa11a6f97b5e34", size = 212785, upload-time = "2026-04-02T09:25:50.671Z" }, + { url = "https://files.pythonhosted.org/packages/66/cd/6e9889c648e72c0ab2e5967528bb83508f354d706637bc7097190c874e13/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:bc17a677b21b3502a21f66a8cc64f5bfad4df8a0b8434d661666f8ce90ac3af1", size = 203055, upload-time = "2026-04-02T09:25:51.802Z" }, + { url = "https://files.pythonhosted.org/packages/92/2e/7a951d6a08aefb7eb8e1b54cdfb580b1365afdd9dd484dc4bee9e5d8f258/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:750e02e074872a3fad7f233b47734166440af3cdea0add3e95163110816d6752", size = 232502, upload-time = "2026-04-02T09:25:53.388Z" }, + { url = "https://files.pythonhosted.org/packages/58/d5/abcf2d83bf8e0a1286df55cd0dc1d49af0da4282aa77e986df343e7de124/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:4e5163c14bffd570ef2affbfdd77bba66383890797df43dc8b4cc7d6f500bf53", size = 214295, upload-time = "2026-04-02T09:25:54.765Z" }, + { url = "https://files.pythonhosted.org/packages/47/3a/7d4cd7ed54be99973a0dc176032cba5cb1f258082c31fa6df35cff46acfc/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:6ed74185b2db44f41ef35fd1617c5888e59792da9bbc9190d6c7300617182616", size = 227145, upload-time = "2026-04-02T09:25:55.904Z" }, + { url = "https://files.pythonhosted.org/packages/1d/98/3a45bf8247889cf28262ebd3d0872edff11565b2a1e3064ccb132db3fbb0/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:94e1885b270625a9a828c9793b4d52a64445299baa1fea5a173bf1d3dd9a1a5a", size = 218884, upload-time = "2026-04-02T09:25:57.074Z" }, + { url = "https://files.pythonhosted.org/packages/ad/80/2e8b7f8915ed5c9ef13aa828d82738e33888c485b65ebf744d615040c7ea/charset_normalizer-3.4.7-cp310-cp310-win32.whl", hash = "sha256:6785f414ae0f3c733c437e0f3929197934f526d19dfaa75e18fdb4f94c6fb374", size = 148343, upload-time = "2026-04-02T09:25:58.199Z" }, + { url = "https://files.pythonhosted.org/packages/35/1b/3b8c8c77184af465ee9ad88b5aea46ea6b2e1f7b9dc9502891e37af21e30/charset_normalizer-3.4.7-cp310-cp310-win_amd64.whl", hash = "sha256:6696b7688f54f5af4462118f0bfa7c1621eeb87154f77fa04b9295ce7a8f2943", size = 159174, upload-time = "2026-04-02T09:25:59.322Z" }, + { url = "https://files.pythonhosted.org/packages/be/c1/feb40dca40dbb21e0a908801782d9288c64fc8d8e562c2098e9994c8c21b/charset_normalizer-3.4.7-cp310-cp310-win_arm64.whl", hash = "sha256:66671f93accb62ed07da56613636f3641f1a12c13046ce91ffc923721f23c008", size = 147805, upload-time = "2026-04-02T09:26:00.756Z" }, + { url = "https://files.pythonhosted.org/packages/c2/d7/b5b7020a0565c2e9fa8c09f4b5fa6232feb326b8c20081ccded47ea368fd/charset_normalizer-3.4.7-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7641bb8895e77f921102f72833904dcd9901df5d6d72a2ab8f31d04b7e51e4e7", size = 309705, upload-time = "2026-04-02T09:26:02.191Z" }, + { url = "https://files.pythonhosted.org/packages/5a/53/58c29116c340e5456724ecd2fff4196d236b98f3da97b404bc5e51ac3493/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:202389074300232baeb53ae2569a60901f7efadd4245cf3a3bf0617d60b439d7", size = 206419, upload-time = "2026-04-02T09:26:03.583Z" }, + { url = "https://files.pythonhosted.org/packages/b2/02/e8146dc6591a37a00e5144c63f29fb7c97a734ea8a111190783c0e60ab63/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:30b8d1d8c52a48c2c5690e152c169b673487a2a58de1ec7393196753063fcd5e", size = 227901, upload-time = "2026-04-02T09:26:04.738Z" }, + { url = "https://files.pythonhosted.org/packages/fb/73/77486c4cd58f1267bf17db420e930c9afa1b3be3fe8c8b8ebbebc9624359/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:532bc9bf33a68613fd7d65e4b1c71a6a38d7d42604ecf239c77392e9b4e8998c", size = 222742, upload-time = "2026-04-02T09:26:06.36Z" }, + { url = "https://files.pythonhosted.org/packages/a1/fa/f74eb381a7d94ded44739e9d94de18dc5edc9c17fb8c11f0a6890696c0a9/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2fe249cb4651fd12605b7288b24751d8bfd46d35f12a20b1ba33dea122e690df", size = 214061, upload-time = "2026-04-02T09:26:08.347Z" }, + { url = "https://files.pythonhosted.org/packages/dc/92/42bd3cefcf7687253fb86694b45f37b733c97f59af3724f356fa92b8c344/charset_normalizer-3.4.7-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:65bcd23054beab4d166035cabbc868a09c1a49d1efe458fe8e4361215df40265", size = 199239, upload-time = "2026-04-02T09:26:09.823Z" }, + { url = "https://files.pythonhosted.org/packages/4c/3d/069e7184e2aa3b3cddc700e3dd267413dc259854adc3380421c805c6a17d/charset_normalizer-3.4.7-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:08e721811161356f97b4059a9ba7bafb23ea5ee2255402c42881c214e173c6b4", size = 210173, upload-time = "2026-04-02T09:26:10.953Z" }, + { url = "https://files.pythonhosted.org/packages/62/51/9d56feb5f2e7074c46f93e0ebdbe61f0848ee246e2f0d89f8e20b89ebb8f/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e060d01aec0a910bdccb8be71faf34e7799ce36950f8294c8bf612cba65a2c9e", size = 209841, upload-time = "2026-04-02T09:26:12.142Z" }, + { url = "https://files.pythonhosted.org/packages/d2/59/893d8f99cc4c837dda1fe2f1139079703deb9f321aabcb032355de13b6c7/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:38c0109396c4cfc574d502df99742a45c72c08eff0a36158b6f04000043dbf38", size = 200304, upload-time = "2026-04-02T09:26:13.711Z" }, + { url = "https://files.pythonhosted.org/packages/7d/1d/ee6f3be3464247578d1ed5c46de545ccc3d3ff933695395c402c21fa6b77/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:1c2a768fdd44ee4a9339a9b0b130049139b8ce3c01d2ce09f67f5a68048d477c", size = 229455, upload-time = "2026-04-02T09:26:14.941Z" }, + { url = "https://files.pythonhosted.org/packages/54/bb/8fb0a946296ea96a488928bdce8ef99023998c48e4713af533e9bb98ef07/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:1a87ca9d5df6fe460483d9a5bbf2b18f620cbed41b432e2bddb686228282d10b", size = 210036, upload-time = "2026-04-02T09:26:16.478Z" }, + { url = "https://files.pythonhosted.org/packages/9a/bc/015b2387f913749f82afd4fcba07846d05b6d784dd16123cb66860e0237d/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:d635aab80466bc95771bb78d5370e74d36d1fe31467b6b29b8b57b2a3cd7d22c", size = 224739, upload-time = "2026-04-02T09:26:17.751Z" }, + { url = "https://files.pythonhosted.org/packages/17/ab/63133691f56baae417493cba6b7c641571a2130eb7bceba6773367ab9ec5/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ae196f021b5e7c78e918242d217db021ed2a6ace2bc6ae94c0fc596221c7f58d", size = 216277, upload-time = "2026-04-02T09:26:18.981Z" }, + { url = "https://files.pythonhosted.org/packages/06/6d/3be70e827977f20db77c12a97e6a9f973631a45b8d186c084527e53e77a4/charset_normalizer-3.4.7-cp311-cp311-win32.whl", hash = "sha256:adb2597b428735679446b46c8badf467b4ca5f5056aae4d51a19f9570301b1ad", size = 147819, upload-time = "2026-04-02T09:26:20.295Z" }, + { url = "https://files.pythonhosted.org/packages/20/d9/5f67790f06b735d7c7637171bbfd89882ad67201891b7275e51116ed8207/charset_normalizer-3.4.7-cp311-cp311-win_amd64.whl", hash = "sha256:8e385e4267ab76874ae30db04c627faaaf0b509e1ccc11a95b3fc3e83f855c00", size = 159281, upload-time = "2026-04-02T09:26:21.74Z" }, + { url = "https://files.pythonhosted.org/packages/ca/83/6413f36c5a34afead88ce6f66684d943d91f233d76dd083798f9602b75ae/charset_normalizer-3.4.7-cp311-cp311-win_arm64.whl", hash = "sha256:d4a48e5b3c2a489fae013b7589308a40146ee081f6f509e047e0e096084ceca1", size = 147843, upload-time = "2026-04-02T09:26:22.901Z" }, + { url = "https://files.pythonhosted.org/packages/0c/eb/4fc8d0a7110eb5fc9cc161723a34a8a6c200ce3b4fbf681bc86feee22308/charset_normalizer-3.4.7-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:eca9705049ad3c7345d574e3510665cb2cf844c2f2dcfe675332677f081cbd46", size = 311328, upload-time = "2026-04-02T09:26:24.331Z" }, + { url = "https://files.pythonhosted.org/packages/f8/e3/0fadc706008ac9d7b9b5be6dc767c05f9d3e5df51744ce4cc9605de7b9f4/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6178f72c5508bfc5fd446a5905e698c6212932f25bcdd4b47a757a50605a90e2", size = 208061, upload-time = "2026-04-02T09:26:25.568Z" }, + { url = "https://files.pythonhosted.org/packages/42/f0/3dd1045c47f4a4604df85ec18ad093912ae1344ac706993aff91d38773a2/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e1421b502d83040e6d7fb2fb18dff63957f720da3d77b2fbd3187ceb63755d7b", size = 229031, upload-time = "2026-04-02T09:26:26.865Z" }, + { url = "https://files.pythonhosted.org/packages/dc/67/675a46eb016118a2fbde5a277a5d15f4f69d5f3f5f338e5ee2f8948fcf43/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:edac0f1ab77644605be2cbba52e6b7f630731fc42b34cb0f634be1a6eface56a", size = 225239, upload-time = "2026-04-02T09:26:28.044Z" }, + { url = "https://files.pythonhosted.org/packages/4b/f8/d0118a2f5f23b02cd166fa385c60f9b0d4f9194f574e2b31cef350ad7223/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5649fd1c7bade02f320a462fdefd0b4bd3ce036065836d4f42e0de958038e116", size = 216589, upload-time = "2026-04-02T09:26:29.239Z" }, + { url = "https://files.pythonhosted.org/packages/b1/f1/6d2b0b261b6c4ceef0fcb0d17a01cc5bc53586c2d4796fa04b5c540bc13d/charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:203104ed3e428044fd943bc4bf45fa73c0730391f9621e37fe39ecf477b128cb", size = 202733, upload-time = "2026-04-02T09:26:30.5Z" }, + { url = "https://files.pythonhosted.org/packages/6f/c0/7b1f943f7e87cc3db9626ba17807d042c38645f0a1d4415c7a14afb5591f/charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:298930cec56029e05497a76988377cbd7457ba864beeea92ad7e844fe74cd1f1", size = 212652, upload-time = "2026-04-02T09:26:31.709Z" }, + { url = "https://files.pythonhosted.org/packages/38/dd/5a9ab159fe45c6e72079398f277b7d2b523e7f716acc489726115a910097/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:708838739abf24b2ceb208d0e22403dd018faeef86ddac04319a62ae884c4f15", size = 211229, upload-time = "2026-04-02T09:26:33.282Z" }, + { url = "https://files.pythonhosted.org/packages/d5/ff/531a1cad5ca855d1c1a8b69cb71abfd6d85c0291580146fda7c82857caa1/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:0f7eb884681e3938906ed0434f20c63046eacd0111c4ba96f27b76084cd679f5", size = 203552, upload-time = "2026-04-02T09:26:34.845Z" }, + { url = "https://files.pythonhosted.org/packages/c1/4c/a5fb52d528a8ca41f7598cb619409ece30a169fbdf9cdce592e53b46c3a6/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4dc1e73c36828f982bfe79fadf5919923f8a6f4df2860804db9a98c48824ce8d", size = 230806, upload-time = "2026-04-02T09:26:36.152Z" }, + { url = "https://files.pythonhosted.org/packages/59/7a/071feed8124111a32b316b33ae4de83d36923039ef8cf48120266844285b/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:aed52fea0513bac0ccde438c188c8a471c4e0f457c2dd20cdbf6ea7a450046c7", size = 212316, upload-time = "2026-04-02T09:26:37.672Z" }, + { url = "https://files.pythonhosted.org/packages/fd/35/f7dba3994312d7ba508e041eaac39a36b120f32d4c8662b8814dab876431/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:fea24543955a6a729c45a73fe90e08c743f0b3334bbf3201e6c4bc1b0c7fa464", size = 227274, upload-time = "2026-04-02T09:26:38.93Z" }, + { url = "https://files.pythonhosted.org/packages/8a/2d/a572df5c9204ab7688ec1edc895a73ebded3b023bb07364710b05dd1c9be/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:bb6d88045545b26da47aa879dd4a89a71d1dce0f0e549b1abcb31dfe4a8eac49", size = 218468, upload-time = "2026-04-02T09:26:40.17Z" }, + { url = "https://files.pythonhosted.org/packages/86/eb/890922a8b03a568ca2f336c36585a4713c55d4d67bf0f0c78924be6315ca/charset_normalizer-3.4.7-cp312-cp312-win32.whl", hash = "sha256:2257141f39fe65a3fdf38aeccae4b953e5f3b3324f4ff0daf9f15b8518666a2c", size = 148460, upload-time = "2026-04-02T09:26:41.416Z" }, + { url = "https://files.pythonhosted.org/packages/35/d9/0e7dffa06c5ab081f75b1b786f0aefc88365825dfcd0ac544bdb7b2b6853/charset_normalizer-3.4.7-cp312-cp312-win_amd64.whl", hash = "sha256:5ed6ab538499c8644b8a3e18debabcd7ce684f3fa91cf867521a7a0279cab2d6", size = 159330, upload-time = "2026-04-02T09:26:42.554Z" }, + { url = "https://files.pythonhosted.org/packages/9e/5d/481bcc2a7c88ea6b0878c299547843b2521ccbc40980cb406267088bc701/charset_normalizer-3.4.7-cp312-cp312-win_arm64.whl", hash = "sha256:56be790f86bfb2c98fb742ce566dfb4816e5a83384616ab59c49e0604d49c51d", size = 147828, upload-time = "2026-04-02T09:26:44.075Z" }, + { url = "https://files.pythonhosted.org/packages/c1/3b/66777e39d3ae1ddc77ee606be4ec6d8cbd4c801f65e5a1b6f2b11b8346dd/charset_normalizer-3.4.7-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:f496c9c3cc02230093d8330875c4c3cdfc3b73612a5fd921c65d39cbcef08063", size = 309627, upload-time = "2026-04-02T09:26:45.198Z" }, + { url = "https://files.pythonhosted.org/packages/2e/4e/b7f84e617b4854ade48a1b7915c8ccfadeba444d2a18c291f696e37f0d3b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ea948db76d31190bf08bd371623927ee1339d5f2a0b4b1b4a4439a65298703c", size = 207008, upload-time = "2026-04-02T09:26:46.824Z" }, + { url = "https://files.pythonhosted.org/packages/c4/bb/ec73c0257c9e11b268f018f068f5d00aa0ef8c8b09f7753ebd5f2880e248/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a277ab8928b9f299723bc1a2dabb1265911b1a76341f90a510368ca44ad9ab66", size = 228303, upload-time = "2026-04-02T09:26:48.397Z" }, + { url = "https://files.pythonhosted.org/packages/85/fb/32d1f5033484494619f701e719429c69b766bfc4dbc61aa9e9c8c166528b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3bec022aec2c514d9cf199522a802bd007cd588ab17ab2525f20f9c34d067c18", size = 224282, upload-time = "2026-04-02T09:26:49.684Z" }, + { url = "https://files.pythonhosted.org/packages/fa/07/330e3a0dda4c404d6da83b327270906e9654a24f6c546dc886a0eb0ffb23/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e044c39e41b92c845bc815e5ae4230804e8e7bc29e399b0437d64222d92809dd", size = 215595, upload-time = "2026-04-02T09:26:50.915Z" }, + { url = "https://files.pythonhosted.org/packages/e3/7c/fc890655786e423f02556e0216d4b8c6bcb6bdfa890160dc66bf52dee468/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:f495a1652cf3fbab2eb0639776dad966c2fb874d79d87ca07f9d5f059b8bd215", size = 201986, upload-time = "2026-04-02T09:26:52.197Z" }, + { url = "https://files.pythonhosted.org/packages/d8/97/bfb18b3db2aed3b90cf54dc292ad79fdd5ad65c4eae454099475cbeadd0d/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e712b419df8ba5e42b226c510472b37bd57b38e897d3eca5e8cfd410a29fa859", size = 211711, upload-time = "2026-04-02T09:26:53.49Z" }, + { url = "https://files.pythonhosted.org/packages/6f/a5/a581c13798546a7fd557c82614a5c65a13df2157e9ad6373166d2a3e645d/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7804338df6fcc08105c7745f1502ba68d900f45fd770d5bdd5288ddccb8a42d8", size = 210036, upload-time = "2026-04-02T09:26:54.975Z" }, + { url = "https://files.pythonhosted.org/packages/8c/bf/b3ab5bcb478e4193d517644b0fb2bf5497fbceeaa7a1bc0f4d5b50953861/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:481551899c856c704d58119b5025793fa6730adda3571971af568f66d2424bb5", size = 202998, upload-time = "2026-04-02T09:26:56.303Z" }, + { url = "https://files.pythonhosted.org/packages/e7/4e/23efd79b65d314fa320ec6017b4b5834d5c12a58ba4610aa353af2e2f577/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f59099f9b66f0d7145115e6f80dd8b1d847176df89b234a5a6b3f00437aa0832", size = 230056, upload-time = "2026-04-02T09:26:57.554Z" }, + { url = "https://files.pythonhosted.org/packages/b9/9f/1e1941bc3f0e01df116e68dc37a55c4d249df5e6fa77f008841aef68264f/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:f59ad4c0e8f6bba240a9bb85504faa1ab438237199d4cce5f622761507b8f6a6", size = 211537, upload-time = "2026-04-02T09:26:58.843Z" }, + { url = "https://files.pythonhosted.org/packages/80/0f/088cbb3020d44428964a6c97fe1edfb1b9550396bf6d278330281e8b709c/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:3dedcc22d73ec993f42055eff4fcfed9318d1eeb9a6606c55892a26964964e48", size = 226176, upload-time = "2026-04-02T09:27:00.437Z" }, + { url = "https://files.pythonhosted.org/packages/6a/9f/130394f9bbe06f4f63e22641d32fc9b202b7e251c9aef4db044324dac493/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:64f02c6841d7d83f832cd97ccf8eb8a906d06eb95d5276069175c696b024b60a", size = 217723, upload-time = "2026-04-02T09:27:02.021Z" }, + { url = "https://files.pythonhosted.org/packages/73/55/c469897448a06e49f8fa03f6caae97074fde823f432a98f979cc42b90e69/charset_normalizer-3.4.7-cp313-cp313-win32.whl", hash = "sha256:4042d5c8f957e15221d423ba781e85d553722fc4113f523f2feb7b188cc34c5e", size = 148085, upload-time = "2026-04-02T09:27:03.192Z" }, + { url = "https://files.pythonhosted.org/packages/5d/78/1b74c5bbb3f99b77a1715c91b3e0b5bdb6fe302d95ace4f5b1bec37b0167/charset_normalizer-3.4.7-cp313-cp313-win_amd64.whl", hash = "sha256:3946fa46a0cf3e4c8cb1cc52f56bb536310d34f25f01ca9b6c16afa767dab110", size = 158819, upload-time = "2026-04-02T09:27:04.454Z" }, + { url = "https://files.pythonhosted.org/packages/68/86/46bd42279d323deb8687c4a5a811fd548cb7d1de10cf6535d099877a9a9f/charset_normalizer-3.4.7-cp313-cp313-win_arm64.whl", hash = "sha256:80d04837f55fc81da168b98de4f4b797ef007fc8a79ab71c6ec9bc4dd662b15b", size = 147915, upload-time = "2026-04-02T09:27:05.971Z" }, + { url = "https://files.pythonhosted.org/packages/97/c8/c67cb8c70e19ef1960b97b22ed2a1567711de46c4ddf19799923adc836c2/charset_normalizer-3.4.7-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:c36c333c39be2dbca264d7803333c896ab8fa7d4d6f0ab7edb7dfd7aea6e98c0", size = 309234, upload-time = "2026-04-02T09:27:07.194Z" }, + { url = "https://files.pythonhosted.org/packages/99/85/c091fdee33f20de70d6c8b522743b6f831a2f1cd3ff86de4c6a827c48a76/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1c2aed2e5e41f24ea8ef1590b8e848a79b56f3a5564a65ceec43c9d692dc7d8a", size = 208042, upload-time = "2026-04-02T09:27:08.749Z" }, + { url = "https://files.pythonhosted.org/packages/87/1c/ab2ce611b984d2fd5d86a5a8a19c1ae26acac6bad967da4967562c75114d/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:54523e136b8948060c0fa0bc7b1b50c32c186f2fceee897a495406bb6e311d2b", size = 228706, upload-time = "2026-04-02T09:27:09.951Z" }, + { url = "https://files.pythonhosted.org/packages/a8/29/2b1d2cb00bf085f59d29eb773ce58ec2d325430f8c216804a0a5cd83cbca/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:715479b9a2802ecac752a3b0efa2b0b60285cf962ee38414211abdfccc233b41", size = 224727, upload-time = "2026-04-02T09:27:11.175Z" }, + { url = "https://files.pythonhosted.org/packages/47/5c/032c2d5a07fe4d4855fea851209cca2b6f03ebeb6d4e3afdb3358386a684/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bd6c2a1c7573c64738d716488d2cdd3c00e340e4835707d8fdb8dc1a66ef164e", size = 215882, upload-time = "2026-04-02T09:27:12.446Z" }, + { url = "https://files.pythonhosted.org/packages/2c/c2/356065d5a8b78ed04499cae5f339f091946a6a74f91e03476c33f0ab7100/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:c45e9440fb78f8ddabcf714b68f936737a121355bf59f3907f4e17721b9d1aae", size = 200860, upload-time = "2026-04-02T09:27:13.721Z" }, + { url = "https://files.pythonhosted.org/packages/0c/cd/a32a84217ced5039f53b29f460962abb2d4420def55afabe45b1c3c7483d/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3534e7dcbdcf757da6b85a0bbf5b6868786d5982dd959b065e65481644817a18", size = 211564, upload-time = "2026-04-02T09:27:15.272Z" }, + { url = "https://files.pythonhosted.org/packages/44/86/58e6f13ce26cc3b8f4a36b94a0f22ae2f00a72534520f4ae6857c4b81f89/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e8ac484bf18ce6975760921bb6148041faa8fef0547200386ea0b52b5d27bf7b", size = 211276, upload-time = "2026-04-02T09:27:16.834Z" }, + { url = "https://files.pythonhosted.org/packages/8f/fe/d17c32dc72e17e155e06883efa84514ca375f8a528ba2546bee73fc4df81/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:a5fe03b42827c13cdccd08e6c0247b6a6d4b5e3cdc53fd1749f5896adcdc2356", size = 201238, upload-time = "2026-04-02T09:27:18.229Z" }, + { url = "https://files.pythonhosted.org/packages/6a/29/f33daa50b06525a237451cdb6c69da366c381a3dadcd833fa5676bc468b3/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:2d6eb928e13016cea4f1f21d1e10c1cebd5a421bc57ddf5b1142ae3f86824fab", size = 230189, upload-time = "2026-04-02T09:27:19.445Z" }, + { url = "https://files.pythonhosted.org/packages/b6/6e/52c84015394a6a0bdcd435210a7e944c5f94ea1055f5cc5d56c5fe368e7b/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e74327fb75de8986940def6e8dee4f127cc9752bee7355bb323cc5b2659b6d46", size = 211352, upload-time = "2026-04-02T09:27:20.79Z" }, + { url = "https://files.pythonhosted.org/packages/8c/d7/4353be581b373033fb9198bf1da3cf8f09c1082561e8e922aa7b39bf9fe8/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:d6038d37043bced98a66e68d3aa2b6a35505dc01328cd65217cefe82f25def44", size = 227024, upload-time = "2026-04-02T09:27:22.063Z" }, + { url = "https://files.pythonhosted.org/packages/30/45/99d18aa925bd1740098ccd3060e238e21115fffbfdcb8f3ece837d0ace6c/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7579e913a5339fb8fa133f6bbcfd8e6749696206cf05acdbdca71a1b436d8e72", size = 217869, upload-time = "2026-04-02T09:27:23.486Z" }, + { url = "https://files.pythonhosted.org/packages/5c/05/5ee478aa53f4bb7996482153d4bfe1b89e0f087f0ab6b294fcf92d595873/charset_normalizer-3.4.7-cp314-cp314-win32.whl", hash = "sha256:5b77459df20e08151cd6f8b9ef8ef1f961ef73d85c21a555c7eed5b79410ec10", size = 148541, upload-time = "2026-04-02T09:27:25.146Z" }, + { url = "https://files.pythonhosted.org/packages/48/77/72dcb0921b2ce86420b2d79d454c7022bf5be40202a2a07906b9f2a35c97/charset_normalizer-3.4.7-cp314-cp314-win_amd64.whl", hash = "sha256:92a0a01ead5e668468e952e4238cccd7c537364eb7d851ab144ab6627dbbe12f", size = 159634, upload-time = "2026-04-02T09:27:26.642Z" }, + { url = "https://files.pythonhosted.org/packages/c6/a3/c2369911cd72f02386e4e340770f6e158c7980267da16af8f668217abaa0/charset_normalizer-3.4.7-cp314-cp314-win_arm64.whl", hash = "sha256:67f6279d125ca0046a7fd386d01b311c6363844deac3e5b069b514ba3e63c246", size = 148384, upload-time = "2026-04-02T09:27:28.271Z" }, + { url = "https://files.pythonhosted.org/packages/94/09/7e8a7f73d24dba1f0035fbbf014d2c36828fc1bf9c88f84093e57d315935/charset_normalizer-3.4.7-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:effc3f449787117233702311a1b7d8f59cba9ced946ba727bdc329ec69028e24", size = 330133, upload-time = "2026-04-02T09:27:29.474Z" }, + { url = "https://files.pythonhosted.org/packages/8d/da/96975ddb11f8e977f706f45cddd8540fd8242f71ecdb5d18a80723dcf62c/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fbccdc05410c9ee21bbf16a35f4c1d16123dcdeb8a1d38f33654fa21d0234f79", size = 216257, upload-time = "2026-04-02T09:27:30.793Z" }, + { url = "https://files.pythonhosted.org/packages/e5/e8/1d63bf8ef2d388e95c64b2098f45f84758f6d102a087552da1485912637b/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:733784b6d6def852c814bce5f318d25da2ee65dd4839a0718641c696e09a2960", size = 234851, upload-time = "2026-04-02T09:27:32.44Z" }, + { url = "https://files.pythonhosted.org/packages/9b/40/e5ff04233e70da2681fa43969ad6f66ca5611d7e669be0246c4c7aaf6dc8/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a89c23ef8d2c6b27fd200a42aa4ac72786e7c60d40efdc76e6011260b6e949c4", size = 233393, upload-time = "2026-04-02T09:27:34.03Z" }, + { url = "https://files.pythonhosted.org/packages/be/c1/06c6c49d5a5450f76899992f1ee40b41d076aee9279b49cf9974d2f313d5/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6c114670c45346afedc0d947faf3c7f701051d2518b943679c8ff88befe14f8e", size = 223251, upload-time = "2026-04-02T09:27:35.369Z" }, + { url = "https://files.pythonhosted.org/packages/2b/9f/f2ff16fb050946169e3e1f82134d107e5d4ae72647ec8a1b1446c148480f/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:a180c5e59792af262bf263b21a3c49353f25945d8d9f70628e73de370d55e1e1", size = 206609, upload-time = "2026-04-02T09:27:36.661Z" }, + { url = "https://files.pythonhosted.org/packages/69/d5/a527c0cd8d64d2eab7459784fb4169a0ac76e5a6fc5237337982fd61347e/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3c9a494bc5ec77d43cea229c4f6db1e4d8fe7e1bbffa8b6f0f0032430ff8ab44", size = 220014, upload-time = "2026-04-02T09:27:38.019Z" }, + { url = "https://files.pythonhosted.org/packages/7e/80/8a7b8104a3e203074dc9aa2c613d4b726c0e136bad1cc734594b02867972/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8d828b6667a32a728a1ad1d93957cdf37489c57b97ae6c4de2860fa749b8fc1e", size = 218979, upload-time = "2026-04-02T09:27:39.37Z" }, + { url = "https://files.pythonhosted.org/packages/02/9a/b759b503d507f375b2b5c153e4d2ee0a75aa215b7f2489cf314f4541f2c0/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:cf1493cd8607bec4d8a7b9b004e699fcf8f9103a9284cc94962cb73d20f9d4a3", size = 209238, upload-time = "2026-04-02T09:27:40.722Z" }, + { url = "https://files.pythonhosted.org/packages/c2/4e/0f3f5d47b86bdb79256e7290b26ac847a2832d9a4033f7eb2cd4bcf4bb5b/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0c96c3b819b5c3e9e165495db84d41914d6894d55181d2d108cc1a69bfc9cce0", size = 236110, upload-time = "2026-04-02T09:27:42.33Z" }, + { url = "https://files.pythonhosted.org/packages/96/23/bce28734eb3ed2c91dcf93abeb8a5cf393a7b2749725030bb630e554fdd8/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:752a45dc4a6934060b3b0dab47e04edc3326575f82be64bc4fc293914566503e", size = 219824, upload-time = "2026-04-02T09:27:43.924Z" }, + { url = "https://files.pythonhosted.org/packages/2c/6f/6e897c6984cc4d41af319b077f2f600fc8214eb2fe2d6bcb79141b882400/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:8778f0c7a52e56f75d12dae53ae320fae900a8b9b4164b981b9c5ce059cd1fcb", size = 233103, upload-time = "2026-04-02T09:27:45.348Z" }, + { url = "https://files.pythonhosted.org/packages/76/22/ef7bd0fe480a0ae9b656189ec00744b60933f68b4f42a7bb06589f6f576a/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ce3412fbe1e31eb81ea42f4169ed94861c56e643189e1e75f0041f3fe7020abe", size = 225194, upload-time = "2026-04-02T09:27:46.706Z" }, + { url = "https://files.pythonhosted.org/packages/c5/a7/0e0ab3e0b5bc1219bd80a6a0d4d72ca74d9250cb2382b7c699c147e06017/charset_normalizer-3.4.7-cp314-cp314t-win32.whl", hash = "sha256:c03a41a8784091e67a39648f70c5f97b5b6a37f216896d44d2cdcb82615339a0", size = 159827, upload-time = "2026-04-02T09:27:48.053Z" }, + { url = "https://files.pythonhosted.org/packages/7a/1d/29d32e0fb40864b1f878c7f5a0b343ae676c6e2b271a2d55cc3a152391da/charset_normalizer-3.4.7-cp314-cp314t-win_amd64.whl", hash = "sha256:03853ed82eeebbce3c2abfdbc98c96dc205f32a79627688ac9a27370ea61a49c", size = 174168, upload-time = "2026-04-02T09:27:49.795Z" }, + { url = "https://files.pythonhosted.org/packages/de/32/d92444ad05c7a6e41fb2036749777c163baf7a0301a040cb672d6b2b1ae9/charset_normalizer-3.4.7-cp314-cp314t-win_arm64.whl", hash = "sha256:c35abb8bfff0185efac5878da64c45dafd2b37fb0383add1be155a763c1f083d", size = 153018, upload-time = "2026-04-02T09:27:51.116Z" }, + { url = "https://files.pythonhosted.org/packages/01/1b/ef725f8eb19b5a261b30f78efa9252ef9d017985cb499102f6f49834cd12/charset_normalizer-3.4.7-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:177a0ba5f0211d488e295aaf82707237e331c24788d8d76c96c5a41594723217", size = 299121, upload-time = "2026-04-02T09:28:14.372Z" }, + { url = "https://files.pythonhosted.org/packages/a3/22/2f12878fbc680fbbb52386cd39a379801f62eaca74fc8b323381325f0f04/charset_normalizer-3.4.7-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e0d51f618228538a3e8f46bd246f87a6cd030565e015803691603f55e12afb5", size = 200612, upload-time = "2026-04-02T09:28:16.162Z" }, + { url = "https://files.pythonhosted.org/packages/bc/b6/10c84e789126ca97d4a7228863a30481e786980a8b8cfcbf4f30658ca63c/charset_normalizer-3.4.7-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:14265bfe1f09498b9d8ec91e9ec9fa52775edf90fcbde092b25f4a33d444fea9", size = 221041, upload-time = "2026-04-02T09:28:17.554Z" }, + { url = "https://files.pythonhosted.org/packages/21/7b/c414866a138400b2e81973d006da7f694cfeaf895ef07d2cba9a8743841a/charset_normalizer-3.4.7-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:87fad7d9ba98c86bcb41b2dc8dbb326619be2562af1f8ff50776a39e55721c5a", size = 216323, upload-time = "2026-04-02T09:28:18.863Z" }, + { url = "https://files.pythonhosted.org/packages/2e/92/bdcf94997e06b223d826df3abed45a5ad6e17f609b7df9d25cd23b5bde30/charset_normalizer-3.4.7-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f22dec1690b584cea26fade98b2435c132c1b5f68e39f5a0b7627cd7ae31f1dc", size = 208419, upload-time = "2026-04-02T09:28:20.332Z" }, + { url = "https://files.pythonhosted.org/packages/1a/64/3f9142293c88b1b10e199649ed1330f070c2a68e305335a5819fa7f25fa7/charset_normalizer-3.4.7-cp39-cp39-manylinux_2_31_armv7l.whl", hash = "sha256:d61f00a0869d77422d9b2aba989e2d24afa6ffd552af442e0e58de4f35ea6d00", size = 195016, upload-time = "2026-04-02T09:28:21.657Z" }, + { url = "https://files.pythonhosted.org/packages/c1/d1/d8a6b7dd5c5636b76ce0d080bc57d8e56c7bbd6bc2ac941529a35e41d84a/charset_normalizer-3.4.7-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6370e8686f662e6a3941ee48ed4742317cafbe5707e36406e9df792cdb535776", size = 206115, upload-time = "2026-04-02T09:28:23.259Z" }, + { url = "https://files.pythonhosted.org/packages/dd/8c/60ebe912379627d023eb96995b40bc50308729f210f43d66109ca0a7bbd2/charset_normalizer-3.4.7-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:a6c5863edfbe888d9eff9c8b8087354e27618d9da76425c119293f11712a6319", size = 204022, upload-time = "2026-04-02T09:28:24.779Z" }, + { url = "https://files.pythonhosted.org/packages/d5/2a/41816ceda78a551cbfdfbeab6f3891152b0e3f758ce6580c2c18c829f774/charset_normalizer-3.4.7-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:ed065083d0898c9d5b4bbec7b026fd755ff7454e6e8b73a67f8c744b13986e24", size = 195914, upload-time = "2026-04-02T09:28:26.181Z" }, + { url = "https://files.pythonhosted.org/packages/8f/9b/7c7f4b7f11525fcbdfba752455314ac60646bae91cdd671d531c1f7a97c6/charset_normalizer-3.4.7-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:2cd4a60d0e2fb04537162c62bbbb4182f53541fe0ede35cdf270a1c1e723cc42", size = 222159, upload-time = "2026-04-02T09:28:27.504Z" }, + { url = "https://files.pythonhosted.org/packages/9f/57/301682e7469bdbfa2ce219a804f0668b2266ab8520570d85d3b3ef483ea3/charset_normalizer-3.4.7-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:813c0e0132266c08eb87469a642cb30aaff57c5f426255419572aaeceeaa7bf4", size = 206154, upload-time = "2026-04-02T09:28:28.848Z" }, + { url = "https://files.pythonhosted.org/packages/20/ec/90339ff5cdc598b265748c1f231c7d7fbd9123a92cee10f757e0b1448de4/charset_normalizer-3.4.7-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:07d9e39b01743c3717745f4c530a6349eadbfa043c7577eef86c502c15df2c67", size = 217423, upload-time = "2026-04-02T09:28:30.248Z" }, + { url = "https://files.pythonhosted.org/packages/2e/e7/a7a6147f8e3375676309cf584b25c72a3bab784ea4085b0011fa07b23aeb/charset_normalizer-3.4.7-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:c0f081d69a6e58272819b70288d3221a6ee64b98df852631c80f293514d3b274", size = 210604, upload-time = "2026-04-02T09:28:31.736Z" }, + { url = "https://files.pythonhosted.org/packages/1a/62/d9340c7a79c393e57807d7fb6c57e82060687891f81b74d3201958b919c1/charset_normalizer-3.4.7-cp39-cp39-win32.whl", hash = "sha256:8751d2787c9131302398b11e6c8068053dcb55d5a8964e114b6e196cf16cb366", size = 144631, upload-time = "2026-04-02T09:28:33.158Z" }, + { url = "https://files.pythonhosted.org/packages/21/e7/92901117e2ddc8facfe8235a3ecd4eb482185b2ad5d5b6606b37c1afea06/charset_normalizer-3.4.7-cp39-cp39-win_amd64.whl", hash = "sha256:12a6fff75f6bc66711b73a2f0addfc4c8c15a20e805146a02d147a318962c444", size = 154710, upload-time = "2026-04-02T09:28:34.557Z" }, + { url = "https://files.pythonhosted.org/packages/cc/4f/e1fb138201ad9a32499dd9a98aa4a5a5441fbf7f56b52b619a54b7ee8777/charset_normalizer-3.4.7-cp39-cp39-win_arm64.whl", hash = "sha256:bb8cc7534f51d9a017b93e3e85b260924f909601c3df002bcdb58ddb4dc41a5c", size = 143716, upload-time = "2026-04-02T09:28:35.908Z" }, + { url = "https://files.pythonhosted.org/packages/db/8f/61959034484a4a7c527811f4721e75d02d653a35afb0b6054474d8185d4c/charset_normalizer-3.4.7-py3-none-any.whl", hash = "sha256:3dce51d0f5e7951f8bb4900c257dad282f49190fdbebecd4ba99bcc41fef404d", size = 61958, upload-time = "2026-04-02T09:28:37.794Z" }, +] + +[[package]] +name = "idna" +version = "3.15" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/82/77/7b3966d0b9d1d31a36ddf1746926a11dface89a83409bf1483f0237aa758/idna-3.15.tar.gz", hash = "sha256:ca962446ea538f7092a95e057da437618e886f4d349216d2b1e294abfdb65fdc", size = 199245, upload-time = "2026-05-12T22:45:57.011Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/23/408243171aa9aaba178d3e2559159c24c1171a641aa83b67bdd3394ead8e/idna-3.15-py3-none-any.whl", hash = "sha256:048adeaf8c2d788c40fee287673ccaa74c24ffd8dcf09ffa555a2fbb59f10ac8", size = 72340, upload-time = "2026-05-12T22:45:55.733Z" }, +] + +[[package]] +name = "requests" +version = "2.32.5" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +dependencies = [ + { name = "certifi", marker = "python_full_version < '3.10'" }, + { name = "charset-normalizer", marker = "python_full_version < '3.10'" }, + { name = "idna", marker = "python_full_version < '3.10'" }, + { name = "urllib3", version = "2.6.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" }, +] + +[[package]] +name = "requests" +version = "2.34.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", +] +dependencies = [ + { name = "certifi", marker = "python_full_version >= '3.10'" }, + { name = "charset-normalizer", marker = "python_full_version >= '3.10'" }, + { name = "idna", marker = "python_full_version >= '3.10'" }, + { name = "urllib3", version = "2.7.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ac/c3/e2a2b89f2d3e2179abd6d00ebd70bff6273f37fb3e0cc209f48b39d00cbf/requests-2.34.2.tar.gz", hash = "sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed", size = 142856, upload-time = "2026-05-14T19:25:27.735Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0", size = 73075, upload-time = "2026-05-14T19:25:26.443Z" }, +] + +[[package]] +name = "test-project" +version = "0.0.1" +source = { virtual = "." } +dependencies = [ + { name = "requests", version = "2.32.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "requests", version = "2.34.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, +] + +[package.metadata] +requires-dist = [{ name = "requests" }] + +[[package]] +name = "urllib3" +version = "2.6.3" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload-time = "2026-01-07T16:24:43.925Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" }, +] + +[[package]] +name = "urllib3" +version = "2.7.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", +] +sdist = { url = "https://files.pythonhosted.org/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", size = 433602, upload-time = "2026-05-07T16:13:18.596Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" }, +] diff --git a/tests/uv/lock/uv_lock_to_requirements_tests.bzl b/tests/uv/lock/uv_lock_to_requirements_tests.bzl new file mode 100644 index 0000000000..1b947877c9 --- /dev/null +++ b/tests/uv/lock/uv_lock_to_requirements_tests.bzl @@ -0,0 +1,289 @@ +"""Tests for uv_lock_to_requirements.""" + +load("@rules_testing//lib:test_suite.bzl", "test_suite") +load("//python/uv/private:uv_lock_to_requirements.bzl", "uv_lock_to_requirements") # buildifier: disable=bzl-visibility + +_tests = [] + +def _test_empty(env): + got = uv_lock_to_requirements(json.decode("""{"package":[]}""")) + env.expect.that_str(got).equals("") + +_tests.append(_test_empty) + +def _test_simple_package(env): + got = uv_lock_to_requirements(json.decode("""{"package":[ + {"name":"foo","version":"0.0.1","source":{"registry":"https://pypi.org/simple"},"wheels":[{"hash":"sha256:deadbeef","url":"https://example.org/foo.whl"}]} + ]}""")) + env.expect.that_str(got).equals("""\ +foo==0.0.1 \\ + --hash=sha256:deadbeef +""") + +_tests.append(_test_simple_package) + +def _test_package_with_markers(env): + got = uv_lock_to_requirements(json.decode("""{"package":[ + {"name":"foo","version":"0.0.1","source":{"registry":"https://pypi.org/simple"},"resolution-markers":["python_full_version < '3.10'"],"wheels":[{"hash":"sha256:deadbeef","url":"https://example.org/foo.whl"}]} + ]}""")) + env.expect.that_str(got).equals("""\ +foo==0.0.1 ; python_full_version < '3.10' \\ + --hash=sha256:deadbeef +""") + +_tests.append(_test_package_with_markers) + +def _test_package_with_multiple_markers(env): + got = uv_lock_to_requirements(json.decode("""{"package":[ + {"name":"foo","version":"0.0.1","source":{"registry":"https://pypi.org/simple"},"resolution-markers":["python_full_version == '3.10.*'","python_full_version >= '3.14'"],"wheels":[{"hash":"sha256:deadbeef","url":"https://example.org/foo.whl"}]} + ]}""")) + env.expect.that_str(got).equals("""\ +foo==0.0.1 ; python_full_version == '3.10.*' or python_full_version >= '3.14' \\ + --hash=sha256:deadbeef +""") + +_tests.append(_test_package_with_multiple_markers) + +def _test_package_with_deps(env): + got = uv_lock_to_requirements(json.decode("""{"package":[ + {"name":"bar","version":"0.0.1","source":{"registry":"https://pypi.org/simple"},"wheels":[{"hash":"sha256:baadbeef","url":"https://example.org/bar.whl"}]}, + {"name":"foo","version":"0.0.1","source":{"registry":"https://pypi.org/simple"},"dependencies":[{"name":"bar"}],"wheels":[{"hash":"sha256:deadbeef","url":"https://example.org/foo.whl"}]} + ]}""")) + env.expect.that_str(got).equals("""\ +bar==0.0.1 \\ + --hash=sha256:baadbeef + # via foo + +foo==0.0.1 \\ + --hash=sha256:deadbeef +""") + +_tests.append(_test_package_with_deps) + +def _test_package_with_optional_deps(env): + got = uv_lock_to_requirements(json.decode("""{"package":[ + {"name":"bar","version":"0.0.1","source":{"registry":"https://pypi.org/simple"},"wheels":[{"hash":"sha256:baadbeef","url":"https://example.org/bar.whl"}]}, + {"name":"foo","version":"0.0.1","source":{"registry":"https://pypi.org/simple"},"optional-dependencies":{"extra1":[{"name":"bar"}]},"wheels":[{"hash":"sha256:deadbeef","url":"https://example.org/foo.whl"}]} + ]}""")) + env.expect.that_str(got).equals("""\ +bar==0.0.1 \\ + --hash=sha256:baadbeef + # via foo + +foo[extra1]==0.0.1 \\ + --hash=sha256:deadbeef +""") + +_tests.append(_test_package_with_optional_deps) + +def _test_self_edge_excluded(env): + got = uv_lock_to_requirements(json.decode("""{"package":[ + {"name":"pydantic","version":"2.0.0","source":{"registry":"https://pypi.org/simple"},"dependencies":[{"name":"pydantic","extra":["email"]}],"wheels":[{"hash":"sha256:deadbeef","url":"https://example.org/pydantic.whl"}]} + ]}""")) + env.expect.that_str(got).equals("""\ +pydantic==2.0.0 \\ + --hash=sha256:deadbeef +""") + +_tests.append(_test_self_edge_excluded) + +def _test_multiple_dependents(env): + got = uv_lock_to_requirements(json.decode("""{"package":[ + {"name":"common","version":"1.0.0","source":{"registry":"https://pypi.org/simple"},"wheels":[{"hash":"sha256:aaaa","url":"https://example.org/common.whl"}]}, + {"name":"pkg_a","version":"0.1.0","source":{"registry":"https://pypi.org/simple"},"dependencies":[{"name":"common"}],"wheels":[{"hash":"sha256:bbbb","url":"https://example.org/a.whl"}]}, + {"name":"pkg_b","version":"0.2.0","source":{"registry":"https://pypi.org/simple"},"dependencies":[{"name":"common"}],"wheels":[{"hash":"sha256:cccc","url":"https://example.org/b.whl"}]} + ]}""")) + env.expect.that_str(got).equals("""\ +common==1.0.0 \\ + --hash=sha256:aaaa + # via + # pkg_a + # pkg_b + +pkg_a==0.1.0 \\ + --hash=sha256:bbbb + +pkg_b==0.2.0 \\ + --hash=sha256:cccc +""") + +_tests.append(_test_multiple_dependents) + +def _test_git_source_skipped(env): + got = uv_lock_to_requirements(json.decode("""{"package":[ + {"name":"foo","version":"0.1.0","source":{"git":"https://github.com/org/foo.git"}}, + {"name":"bar","version":"0.0.1","source":{"registry":"https://pypi.org/simple"},"wheels":[{"hash":"sha256:deadbeef","url":"https://example.org/bar.whl"}]} + ]}""")) + env.expect.that_str(got).equals("""\ +bar==0.0.1 \\ + --hash=sha256:deadbeef +""") + +_tests.append(_test_git_source_skipped) + +def _test_virtual_source_skipped(env): + got = uv_lock_to_requirements(json.decode("""{"package":[ + {"name":"virtual-pkg","version":"0.0.0","source":{"virtual":true}}, + {"name":"foo","version":"0.0.1","source":{"registry":"https://pypi.org/simple"},"wheels":[{"hash":"sha256:deadbeef","url":"https://example.org/foo.whl"}]} + ]}""")) + env.expect.that_str(got).equals("""\ +foo==0.0.1 \\ + --hash=sha256:deadbeef +""") + +_tests.append(_test_virtual_source_skipped) + +def _test_sdist_hash(env): + got = uv_lock_to_requirements(json.decode("""{"package":[ + {"name":"bar","version":"0.0.1","source":{"registry":"https://pypi.org/simple"},"sdist":{"hash":"sha256:deadb00f","url":"https://example.org/bar.tar.gz"}} + ]}""")) + env.expect.that_str(got).equals("""\ +bar==0.0.1 \\ + --hash=sha256:deadb00f +""") + +_tests.append(_test_sdist_hash) + +def _test_wheel_and_sdist_hashes(env): + got = uv_lock_to_requirements(json.decode("""{"package":[ + {"name":"foo","version":"0.0.1","source":{"registry":"https://pypi.org/simple"},"sdist":{"hash":"sha256:feedcafe","url":"https://example.org/foo.tar.gz"},"wheels":[{"hash":"sha256:deadbeef","url":"https://example.org/foo.whl"}]} + ]}""")) + env.expect.that_str(got).equals("""\ +foo==0.0.1 \\ + --hash=sha256:deadbeef \\ + --hash=sha256:feedcafe +""") + +_tests.append(_test_wheel_and_sdist_hashes) + +def _test_multiple_versions_same_package(env): + got = uv_lock_to_requirements(json.decode("""{"package":[ + {"name":"foo","version":"0.0.1","source":{"registry":"https://pypi.org/simple"},"resolution-markers":["python_full_version < '3.10'"],"wheels":[{"hash":"sha256:deadbeef","url":"https://example.org/foo-0.0.1.whl"}]}, + {"name":"foo","version":"0.0.2","source":{"registry":"https://pypi.org/simple"},"resolution-markers":["python_full_version >= '3.10'"],"wheels":[{"hash":"sha256:deadb11f","url":"https://example.org/foo-0.0.2.whl"}]} + ]}""")) + env.expect.that_str(got).equals("""\ +foo==0.0.1 ; python_full_version < '3.10' \\ + --hash=sha256:deadbeef + +foo==0.0.2 ; python_full_version >= '3.10' \\ + --hash=sha256:deadb11f +""") + +_tests.append(_test_multiple_versions_same_package) + +def _test_dep_with_extras(env): + got = uv_lock_to_requirements(json.decode("""{"package":[ + {"name":"foo","version":"0.0.1","source":{"registry":"https://pypi.org/simple"},"wheels":[{"hash":"sha256:deadbeef","url":"https://example.org/foo.whl"}]}, + {"name":"bar","version":"0.0.2","source":{"registry":"https://pypi.org/simple"},"dependencies":[{"name":"foo","extra":["extra1"]}],"wheels":[{"hash":"sha256:baadbeef","url":"https://example.org/bar.whl"}]} + ]}""")) + env.expect.that_str(got).equals("""\ +foo[extra1]==0.0.1 \\ + --hash=sha256:deadbeef + # via bar + +bar==0.0.2 \\ + --hash=sha256:baadbeef +""") + +_tests.append(_test_dep_with_extras) + +def _test_multiple_hashes_from_wheels(env): + got = uv_lock_to_requirements(json.decode("""{"package":[ + {"name":"foo","version":"0.0.1","source":{"registry":"https://pypi.org/simple"},"wheels":[ + {"hash":"sha256:aaaa","url":"https://example.org/foo-0.0.1-cp39.whl"}, + {"hash":"sha256:bbbb","url":"https://example.org/foo-0.0.1-py3-none-any.whl"} + ]} + ]}""")) + env.expect.that_str(got).equals("""\ +foo==0.0.1 \\ + --hash=sha256:aaaa \\ + --hash=sha256:bbbb +""") + +_tests.append(_test_multiple_hashes_from_wheels) + +def _test_package_no_hashes_no_deps(env): + got = uv_lock_to_requirements(json.decode("""{"package":[ + {"name":"foo","version":"0.0.1","source":{"registry":"https://pypi.org/simple"}} + ]}""")) + env.expect.that_str(got).equals("""\ +foo==0.0.1 +""") + +_tests.append(_test_package_no_hashes_no_deps) + +def _test_package_with_multiple_optional_deps(env): + got = uv_lock_to_requirements(json.decode("""{"package":[ + {"name":"bar","version":"0.0.1","source":{"registry":"https://pypi.org/simple"},"wheels":[{"hash":"sha256:baadbeef","url":"https://example.org/bar.whl"}]}, + {"name":"baz","version":"0.0.2","source":{"registry":"https://pypi.org/simple"},"wheels":[{"hash":"sha256:deadc0de","url":"https://example.org/baz.whl"}]}, + {"name":"foo","version":"0.0.3","source":{"registry":"https://pypi.org/simple"},"optional-dependencies":{"extra1":[{"name":"bar"}],"extra2":[{"name":"baz"}]},"wheels":[{"hash":"sha256:feedcafe","url":"https://example.org/foo.whl"}]} + ]}""")) + env.expect.that_str(got).equals("""\ +bar==0.0.1 \\ + --hash=sha256:baadbeef + # via foo + +baz==0.0.2 \\ + --hash=sha256:deadc0de + # via foo + +foo[extra1,extra2]==0.0.3 \\ + --hash=sha256:feedcafe +""") + +_tests.append(_test_package_with_multiple_optional_deps) + +def _test_dep_with_multiple_extras(env): + got = uv_lock_to_requirements(json.decode("""{"package":[ + {"name":"foo","version":"0.0.1","source":{"registry":"https://pypi.org/simple"},"wheels":[{"hash":"sha256:deadbeef","url":"https://example.org/foo.whl"}]}, + {"name":"bar","version":"0.0.2","source":{"registry":"https://pypi.org/simple"},"dependencies":[{"name":"foo","extra":["extra1","extra2"]}],"wheels":[{"hash":"sha256:baadbeef","url":"https://example.org/bar.whl"}]} + ]}""")) + env.expect.that_str(got).equals("""\ +foo[extra1,extra2]==0.0.1 \\ + --hash=sha256:deadbeef + # via bar + +bar==0.0.2 \\ + --hash=sha256:baadbeef +""") + +_tests.append(_test_dep_with_multiple_extras) + +def _test_extras_from_multiple_dependents(env): + got = uv_lock_to_requirements(json.decode("""{"package":[ + {"name":"common","version":"1.0.0","source":{"registry":"https://pypi.org/simple"},"wheels":[{"hash":"sha256:aaaa","url":"https://example.org/common.whl"}]}, + {"name":"pkg_a","version":"0.1.0","source":{"registry":"https://pypi.org/simple"},"dependencies":[{"name":"common","extra":["extra1"]}],"wheels":[{"hash":"sha256:bbbb","url":"https://example.org/a.whl"}]}, + {"name":"pkg_b","version":"0.2.0","source":{"registry":"https://pypi.org/simple"},"dependencies":[{"name":"common","extra":["extra2"]}],"wheels":[{"hash":"sha256:cccc","url":"https://example.org/b.whl"}]} + ]}""")) + env.expect.that_str(got).equals("""\ +common[extra1,extra2]==1.0.0 \\ + --hash=sha256:aaaa + # via + # pkg_a + # pkg_b + +pkg_a==0.1.0 \\ + --hash=sha256:bbbb + +pkg_b==0.2.0 \\ + --hash=sha256:cccc +""") + +_tests.append(_test_extras_from_multiple_dependents) + +def _test_requires_dist_extras(env): + """Test that extras from metadata.requires-dist are included.""" + got = uv_lock_to_requirements(json.decode("""{"package":[ + {"name":"pytest-bazel","version":"0.1.6","source":{"registry":"https://pypi.org/simple"},"wheels":[{"hash":"sha256:a29e80e1d67c3db801bdd4d0b6b742f2bfb48cd6841caa33401458e5c4e29c21","url":"https://example.org/pytest_bazel-0.1.6.whl"}]}, + {"name":"root-pkg","version":"0.0.0","source":{"virtual":"."},"dependencies":[{"name":"pytest-bazel"}],"metadata":{"requires-dist":[{"name":"pytest-bazel","extras":["all"]}]}} + ]}""")) + env.expect.that_str(got).equals("""\ +pytest-bazel[all]==0.1.6 \\ + --hash=sha256:a29e80e1d67c3db801bdd4d0b6b742f2bfb48cd6841caa33401458e5c4e29c21 + # via root-pkg +""") + +_tests.append(_test_requires_dist_extras) + +def uv_lock_to_requirements_test_suite(name): + test_suite(name = name, basic_tests = _tests) diff --git a/tests/uv/lock/workspaces/BUILD.bazel b/tests/uv/lock/workspaces/BUILD.bazel new file mode 100644 index 0000000000..aa1fabe2e3 --- /dev/null +++ b/tests/uv/lock/workspaces/BUILD.bazel @@ -0,0 +1,27 @@ +load("@bazel_skylib//rules:diff_test.bzl", "diff_test") +load("//python/uv:lock.bzl", "lock") +load("//tests/support:support.bzl", "NOT_WINDOWS") + +# This test verifies that the `lock` rule automatically passes `--project` to `uv pip compile` based +# on the package directory, so that `[tool.uv]` settings from a `pyproject.toml` in the same +# directory are applied. It will exclude a particular dependency from the lock-file, so it will be +# easy to see if we have any issues. +lock( + name = "requirements", + srcs = [ + "pyproject.toml", + "//tests/uv/lock/workspaces/packages", + ], + out = "requirements.txt", + # It seems that the CI remote executors for the RBE do not have network + # connectivity due to current CI setup. + tags = ["no-remote-exec"], +) + +diff_test( + name = "requirements_test", + timeout = "short", + file1 = ":requirements", + file2 = "requirements.txt", + target_compatible_with = NOT_WINDOWS, +) diff --git a/tests/uv/lock/workspaces/packages/BUILD.bazel b/tests/uv/lock/workspaces/packages/BUILD.bazel new file mode 100644 index 0000000000..5461287d9f --- /dev/null +++ b/tests/uv/lock/workspaces/packages/BUILD.bazel @@ -0,0 +1,5 @@ +filegroup( + name = "packages", + srcs = ["//tests/uv/lock/workspaces/packages/foo:pyproject.toml"], + visibility = ["//tests/uv/lock/workspaces:__pkg__"], +) diff --git a/tests/uv/lock/workspaces/packages/foo/BUILD.bazel b/tests/uv/lock/workspaces/packages/foo/BUILD.bazel new file mode 100644 index 0000000000..d89a7b784e --- /dev/null +++ b/tests/uv/lock/workspaces/packages/foo/BUILD.bazel @@ -0,0 +1 @@ +exports_files(["pyproject.toml"]) diff --git a/tests/uv/lock/workspaces/packages/foo/pyproject.toml b/tests/uv/lock/workspaces/packages/foo/pyproject.toml new file mode 100644 index 0000000000..f26ea85b51 --- /dev/null +++ b/tests/uv/lock/workspaces/packages/foo/pyproject.toml @@ -0,0 +1,7 @@ +[project] +name = "foo" +version = "0.0.0" +dependencies = ["black"] + +[tool.uv] +no-build-isolation = true diff --git a/tests/uv/lock/workspaces/pyproject.toml b/tests/uv/lock/workspaces/pyproject.toml new file mode 100644 index 0000000000..01e1e2e076 --- /dev/null +++ b/tests/uv/lock/workspaces/pyproject.toml @@ -0,0 +1,10 @@ +[project] +name = "test" +version = "0.0.0" +dependencies = ["requests"] + +[tool.uv] +no-build-isolation = true + +[tool.uv.workspace] +members = ["packages/*"] diff --git a/tests/uv/lock/workspaces/requirements.txt b/tests/uv/lock/workspaces/requirements.txt new file mode 100644 index 0000000000..5ce35d7571 --- /dev/null +++ b/tests/uv/lock/workspaces/requirements.txt @@ -0,0 +1,242 @@ +# This file was autogenerated by uv via the following command: +# bazel run //tests/uv/lock/workspaces:requirements.update +black==26.5.1 \ + --hash=sha256:0e48b87e03bf109288e55cfceadcfa15ff5470aca2851a851950ed2926f450d7 \ + --hash=sha256:1037d5ac7b7b310b2632ad867ec8d0e4c4819dcdb0b820f63135da746a24e418 \ + --hash=sha256:1ef92b76f7733f282fd096ea406200b5a286c42947412b0eaff3a74e3616cefe \ + --hash=sha256:1f7ea64ebfa01b50f693508fc39f875e264446d3b097088f84f203b9d09618a0 \ + --hash=sha256:22f2cd76d069cc54c71f10360744ba8983fbb616903b4304a85b734915c8e1b4 \ + --hash=sha256:2b36cf2ddf5566e205f6535f782a62194a184d33e175b64ae8c40b1737522be3 \ + --hash=sha256:30d3c14661f2792e9142cce3eeeb1cbc175b3eb5f733be0c8eeb99651e52b0c3 \ + --hash=sha256:32d5ea7f6c8bdfa6e648326ebca1f02b0764e2a029edc6f8dce2627e19d468c3 \ + --hash=sha256:3915f256e75a2d7cf88d8953d37f780455dc586cc72dee059c528fe77f581217 \ + --hash=sha256:4ad6fa01f941920f54f2bbb35f3df7673428a0ef98a0b0840c2eaef3b110efa8 \ + --hash=sha256:4ed7f7da04046d2e488437170797d3b4a4ad83906683bcb7dfc68b673bbce5e2 \ + --hash=sha256:5119fa92ae61f786e8c3662fd60aece1d0a2dd5cca5d0c79417a95e7a4272a59 \ + --hash=sha256:577f21094ea469ef92ec1adaf2c9441a226d2144d01a5be2fa823cecf6543e50 \ + --hash=sha256:58b4bd92cf88aacf83d88479c8f9caee044b1ec55f2451a337354a7ea2590a22 \ + --hash=sha256:5c34b25da232ead53a6f335b76dbea124f4d152ad568b9080d6f944bc2b34b52 \ + --hash=sha256:87ed5c6f450580a2f6790bc7cbfb016dfc73bc750249762268a3695361315eef \ + --hash=sha256:89c93167a74d3a75dfaa38a5c7cca015537d5820dd7f17d63267d674a61cae90 \ + --hash=sha256:96ae2c733b2aabdd9986e2c5df628ff3473676cd1c5faded1ff496cf6d74083c \ + --hash=sha256:9942db8888e06943c5dde66ca0037dcff82a2a4ec1ad0ada9e0d2ee9d9823893 \ + --hash=sha256:9d98d4137277c75dfb898ec8d846c4fd68ba1e9cf77f95e2865c203dc18f4c3d \ + --hash=sha256:a1dca32d9f1784af512a13410ec204c6f7f0aa9797a111c42e1c03449821c264 \ + --hash=sha256:dd321f668053961824bcc1be1cc1df748b2d7e4fa28086b08331e577b0100a73 \ + --hash=sha256:e1a26503279b6b310669fb0b219c39e4820b77e8189fe80f522bb511f247db0a \ + --hash=sha256:e88976690a64b0af98312ca958415849cb42423423c5f2ee74af4b49a97a2168 \ + --hash=sha256:ea8d16dc41655aa113cd64665e7219446cd7e4ff2248d7178eaa905190c86b18 \ + --hash=sha256:ecb3e624844c798144e9bd986954e0adc81d8911a1f30f375e1252fe26e8c294 \ + --hash=sha256:ed1a20af114c301a0269bf01163d51dbef72737fd65f850001e7cbe7f3c7abae + # via foo (tests/uv/lock/workspaces/packages/foo/pyproject.toml) +certifi==2025.1.31 \ + --hash=sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651 \ + --hash=sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe + # via requests +charset-normalizer==3.4.7 \ + --hash=sha256:007d05ec7321d12a40227aae9e2bc6dca73f3cb21058999a1df9e193555a9dcc \ + --hash=sha256:03853ed82eeebbce3c2abfdbc98c96dc205f32a79627688ac9a27370ea61a49c \ + --hash=sha256:07d9e39b01743c3717745f4c530a6349eadbfa043c7577eef86c502c15df2c67 \ + --hash=sha256:08e721811161356f97b4059a9ba7bafb23ea5ee2255402c42881c214e173c6b4 \ + --hash=sha256:0c96c3b819b5c3e9e165495db84d41914d6894d55181d2d108cc1a69bfc9cce0 \ + --hash=sha256:0ea948db76d31190bf08bd371623927ee1339d5f2a0b4b1b4a4439a65298703c \ + --hash=sha256:0f7eb884681e3938906ed0434f20c63046eacd0111c4ba96f27b76084cd679f5 \ + --hash=sha256:12a6fff75f6bc66711b73a2f0addfc4c8c15a20e805146a02d147a318962c444 \ + --hash=sha256:12d8baf840cc7889b37c7c770f478adea7adce3dcb3944d02ec87508e2dcf153 \ + --hash=sha256:14265bfe1f09498b9d8ec91e9ec9fa52775edf90fcbde092b25f4a33d444fea9 \ + --hash=sha256:16d971e29578a5e97d7117866d15889a4a07befe0e87e703ed63cd90cb348c01 \ + --hash=sha256:177a0ba5f0211d488e295aaf82707237e331c24788d8d76c96c5a41594723217 \ + --hash=sha256:1a87ca9d5df6fe460483d9a5bbf2b18f620cbed41b432e2bddb686228282d10b \ + --hash=sha256:1c2a768fdd44ee4a9339a9b0b130049139b8ce3c01d2ce09f67f5a68048d477c \ + --hash=sha256:1c2aed2e5e41f24ea8ef1590b8e848a79b56f3a5564a65ceec43c9d692dc7d8a \ + --hash=sha256:1dc8b0ea451d6e69735094606991f32867807881400f808a106ee1d963c46a83 \ + --hash=sha256:1efde3cae86c8c273f1eb3b287be7d8499420cf2fe7585c41d370d3e790054a5 \ + --hash=sha256:202389074300232baeb53ae2569a60901f7efadd4245cf3a3bf0617d60b439d7 \ + --hash=sha256:203104ed3e428044fd943bc4bf45fa73c0730391f9621e37fe39ecf477b128cb \ + --hash=sha256:2257141f39fe65a3fdf38aeccae4b953e5f3b3324f4ff0daf9f15b8518666a2c \ + --hash=sha256:298930cec56029e05497a76988377cbd7457ba864beeea92ad7e844fe74cd1f1 \ + --hash=sha256:2cd4a60d0e2fb04537162c62bbbb4182f53541fe0ede35cdf270a1c1e723cc42 \ + --hash=sha256:2d6eb928e13016cea4f1f21d1e10c1cebd5a421bc57ddf5b1142ae3f86824fab \ + --hash=sha256:2fe249cb4651fd12605b7288b24751d8bfd46d35f12a20b1ba33dea122e690df \ + --hash=sha256:30b8d1d8c52a48c2c5690e152c169b673487a2a58de1ec7393196753063fcd5e \ + --hash=sha256:320ade88cfb846b8cd6b4ddf5ee9e80ee0c1f52401f2456b84ae1ae6a1a5f207 \ + --hash=sha256:3534e7dcbdcf757da6b85a0bbf5b6868786d5982dd959b065e65481644817a18 \ + --hash=sha256:36836d6ff945a00b88ba1e4572d721e60b5b8c98c155d465f56ad19d68f23734 \ + --hash=sha256:38c0109396c4cfc574d502df99742a45c72c08eff0a36158b6f04000043dbf38 \ + --hash=sha256:3946fa46a0cf3e4c8cb1cc52f56bb536310d34f25f01ca9b6c16afa767dab110 \ + --hash=sha256:3bec022aec2c514d9cf199522a802bd007cd588ab17ab2525f20f9c34d067c18 \ + --hash=sha256:3c9a494bc5ec77d43cea229c4f6db1e4d8fe7e1bbffa8b6f0f0032430ff8ab44 \ + --hash=sha256:3dce51d0f5e7951f8bb4900c257dad282f49190fdbebecd4ba99bcc41fef404d \ + --hash=sha256:3dedcc22d73ec993f42055eff4fcfed9318d1eeb9a6606c55892a26964964e48 \ + --hash=sha256:4042d5c8f957e15221d423ba781e85d553722fc4113f523f2feb7b188cc34c5e \ + --hash=sha256:481551899c856c704d58119b5025793fa6730adda3571971af568f66d2424bb5 \ + --hash=sha256:4dc1e73c36828f982bfe79fadf5919923f8a6f4df2860804db9a98c48824ce8d \ + --hash=sha256:4e5163c14bffd570ef2affbfdd77bba66383890797df43dc8b4cc7d6f500bf53 \ + --hash=sha256:511ef87c8aec0783e08ac18565a16d435372bc1ac25a91e6ac7f5ef2b0bff790 \ + --hash=sha256:532bc9bf33a68613fd7d65e4b1c71a6a38d7d42604ecf239c77392e9b4e8998c \ + --hash=sha256:54523e136b8948060c0fa0bc7b1b50c32c186f2fceee897a495406bb6e311d2b \ + --hash=sha256:5649fd1c7bade02f320a462fdefd0b4bd3ce036065836d4f42e0de958038e116 \ + --hash=sha256:56be790f86bfb2c98fb742ce566dfb4816e5a83384616ab59c49e0604d49c51d \ + --hash=sha256:5b77459df20e08151cd6f8b9ef8ef1f961ef73d85c21a555c7eed5b79410ec10 \ + --hash=sha256:5ed6ab538499c8644b8a3e18debabcd7ce684f3fa91cf867521a7a0279cab2d6 \ + --hash=sha256:6178f72c5508bfc5fd446a5905e698c6212932f25bcdd4b47a757a50605a90e2 \ + --hash=sha256:6370e8686f662e6a3941ee48ed4742317cafbe5707e36406e9df792cdb535776 \ + --hash=sha256:64f02c6841d7d83f832cd97ccf8eb8a906d06eb95d5276069175c696b024b60a \ + --hash=sha256:65bcd23054beab4d166035cabbc868a09c1a49d1efe458fe8e4361215df40265 \ + --hash=sha256:66671f93accb62ed07da56613636f3641f1a12c13046ce91ffc923721f23c008 \ + --hash=sha256:6696b7688f54f5af4462118f0bfa7c1621eeb87154f77fa04b9295ce7a8f2943 \ + --hash=sha256:6785f414ae0f3c733c437e0f3929197934f526d19dfaa75e18fdb4f94c6fb374 \ + --hash=sha256:67f6279d125ca0046a7fd386d01b311c6363844deac3e5b069b514ba3e63c246 \ + --hash=sha256:6c114670c45346afedc0d947faf3c7f701051d2518b943679c8ff88befe14f8e \ + --hash=sha256:6e0d51f618228538a3e8f46bd246f87a6cd030565e015803691603f55e12afb5 \ + --hash=sha256:6ed74185b2db44f41ef35fd1617c5888e59792da9bbc9190d6c7300617182616 \ + --hash=sha256:708838739abf24b2ceb208d0e22403dd018faeef86ddac04319a62ae884c4f15 \ + --hash=sha256:715479b9a2802ecac752a3b0efa2b0b60285cf962ee38414211abdfccc233b41 \ + --hash=sha256:733784b6d6def852c814bce5f318d25da2ee65dd4839a0718641c696e09a2960 \ + --hash=sha256:750e02e074872a3fad7f233b47734166440af3cdea0add3e95163110816d6752 \ + --hash=sha256:752a45dc4a6934060b3b0dab47e04edc3326575f82be64bc4fc293914566503e \ + --hash=sha256:7579e913a5339fb8fa133f6bbcfd8e6749696206cf05acdbdca71a1b436d8e72 \ + --hash=sha256:7641bb8895e77f921102f72833904dcd9901df5d6d72a2ab8f31d04b7e51e4e7 \ + --hash=sha256:7804338df6fcc08105c7745f1502ba68d900f45fd770d5bdd5288ddccb8a42d8 \ + --hash=sha256:80d04837f55fc81da168b98de4f4b797ef007fc8a79ab71c6ec9bc4dd662b15b \ + --hash=sha256:813c0e0132266c08eb87469a642cb30aaff57c5f426255419572aaeceeaa7bf4 \ + --hash=sha256:82b271f5137d07749f7bf32f70b17ab6eaabedd297e75dce75081a24f76eb545 \ + --hash=sha256:84c018e49c3bf790f9c2771c45e9313a08c2c2a6342b162cd650258b57817706 \ + --hash=sha256:8751d2787c9131302398b11e6c8068053dcb55d5a8964e114b6e196cf16cb366 \ + --hash=sha256:8778f0c7a52e56f75d12dae53ae320fae900a8b9b4164b981b9c5ce059cd1fcb \ + --hash=sha256:87fad7d9ba98c86bcb41b2dc8dbb326619be2562af1f8ff50776a39e55721c5a \ + --hash=sha256:8d828b6667a32a728a1ad1d93957cdf37489c57b97ae6c4de2860fa749b8fc1e \ + --hash=sha256:8e385e4267ab76874ae30db04c627faaaf0b509e1ccc11a95b3fc3e83f855c00 \ + --hash=sha256:92a0a01ead5e668468e952e4238cccd7c537364eb7d851ab144ab6627dbbe12f \ + --hash=sha256:94e1885b270625a9a828c9793b4d52a64445299baa1fea5a173bf1d3dd9a1a5a \ + --hash=sha256:a180c5e59792af262bf263b21a3c49353f25945d8d9f70628e73de370d55e1e1 \ + --hash=sha256:a277ab8928b9f299723bc1a2dabb1265911b1a76341f90a510368ca44ad9ab66 \ + --hash=sha256:a5fe03b42827c13cdccd08e6c0247b6a6d4b5e3cdc53fd1749f5896adcdc2356 \ + --hash=sha256:a6c5863edfbe888d9eff9c8b8087354e27618d9da76425c119293f11712a6319 \ + --hash=sha256:a89c23ef8d2c6b27fd200a42aa4ac72786e7c60d40efdc76e6011260b6e949c4 \ + --hash=sha256:adb2597b428735679446b46c8badf467b4ca5f5056aae4d51a19f9570301b1ad \ + --hash=sha256:ae196f021b5e7c78e918242d217db021ed2a6ace2bc6ae94c0fc596221c7f58d \ + --hash=sha256:ae89db9e5f98a11a4bf50407d4363e7b09b31e55bc117b4f7d80aab97ba009e5 \ + --hash=sha256:aed52fea0513bac0ccde438c188c8a471c4e0f457c2dd20cdbf6ea7a450046c7 \ + --hash=sha256:aef65cd602a6d0e0ff6f9930fcb1c8fec60dd2cfcb6facaf4bdb0e5873042db0 \ + --hash=sha256:af21eb4409a119e365397b2adbaca4c9ccab56543a65d5dbd9f920d6ac29f686 \ + --hash=sha256:b14b2d9dac08e28bb8046a1a0434b1750eb221c8f5b87a68f4fa11a6f97b5e34 \ + --hash=sha256:bb6d88045545b26da47aa879dd4a89a71d1dce0f0e549b1abcb31dfe4a8eac49 \ + --hash=sha256:bb8cc7534f51d9a017b93e3e85b260924f909601c3df002bcdb58ddb4dc41a5c \ + --hash=sha256:bc17a677b21b3502a21f66a8cc64f5bfad4df8a0b8434d661666f8ce90ac3af1 \ + --hash=sha256:bd6c2a1c7573c64738d716488d2cdd3c00e340e4835707d8fdb8dc1a66ef164e \ + --hash=sha256:bd9b23791fe793e4968dba0c447e12f78e425c59fc0e3b97f6450f4781f3ee60 \ + --hash=sha256:c03a41a8784091e67a39648f70c5f97b5b6a37f216896d44d2cdcb82615339a0 \ + --hash=sha256:c0f081d69a6e58272819b70288d3221a6ee64b98df852631c80f293514d3b274 \ + --hash=sha256:c35abb8bfff0185efac5878da64c45dafd2b37fb0383add1be155a763c1f083d \ + --hash=sha256:c36c333c39be2dbca264d7803333c896ab8fa7d4d6f0ab7edb7dfd7aea6e98c0 \ + --hash=sha256:c45e9440fb78f8ddabcf714b68f936737a121355bf59f3907f4e17721b9d1aae \ + --hash=sha256:c593052c465475e64bbfe5dbd81680f64a67fdc752c56d7a0ae205dc8aeefe0f \ + --hash=sha256:cdd68a1fb318e290a2077696b7eb7a21a49163c455979c639bf5a5dcdc46617d \ + --hash=sha256:ce3412fbe1e31eb81ea42f4169ed94861c56e643189e1e75f0041f3fe7020abe \ + --hash=sha256:cf1493cd8607bec4d8a7b9b004e699fcf8f9103a9284cc94962cb73d20f9d4a3 \ + --hash=sha256:cf29836da5119f3c8a8a70667b0ef5fdca3bb12f80fd06487cfa575b3909b393 \ + --hash=sha256:d4a48e5b3c2a489fae013b7589308a40146ee081f6f509e047e0e096084ceca1 \ + --hash=sha256:d560742f3c0d62afaccf9f41fe485ed69bd7661a241f86a3ef0f0fb8b1a397af \ + --hash=sha256:d6038d37043bced98a66e68d3aa2b6a35505dc01328cd65217cefe82f25def44 \ + --hash=sha256:d61f00a0869d77422d9b2aba989e2d24afa6ffd552af442e0e58de4f35ea6d00 \ + --hash=sha256:d635aab80466bc95771bb78d5370e74d36d1fe31467b6b29b8b57b2a3cd7d22c \ + --hash=sha256:dca4bbc466a95ba9c0234ef56d7dd9509f63da22274589ebd4ed7f1f4d4c54e3 \ + --hash=sha256:dd915403e231e6b1809fe9b6d9fc55cf8fb5e02765ac625d9cd623342a7905d7 \ + --hash=sha256:e044c39e41b92c845bc815e5ae4230804e8e7bc29e399b0437d64222d92809dd \ + --hash=sha256:e060d01aec0a910bdccb8be71faf34e7799ce36950f8294c8bf612cba65a2c9e \ + --hash=sha256:e1421b502d83040e6d7fb2fb18dff63957f720da3d77b2fbd3187ceb63755d7b \ + --hash=sha256:e17b8d5d6a8c47c85e68ca8379def1303fd360c3e22093a807cd34a71cd082b8 \ + --hash=sha256:e5f4d355f0a2b1a31bc3edec6795b46324349c9cb25eed068049e4f472fb4259 \ + --hash=sha256:e712b419df8ba5e42b226c510472b37bd57b38e897d3eca5e8cfd410a29fa859 \ + --hash=sha256:e74327fb75de8986940def6e8dee4f127cc9752bee7355bb323cc5b2659b6d46 \ + --hash=sha256:e80c8378d8f3d83cd3164da1ad2df9e37a666cdde7b1cb2298ed0b558064be30 \ + --hash=sha256:e8ac484bf18ce6975760921bb6148041faa8fef0547200386ea0b52b5d27bf7b \ + --hash=sha256:eca9705049ad3c7345d574e3510665cb2cf844c2f2dcfe675332677f081cbd46 \ + --hash=sha256:ed065083d0898c9d5b4bbec7b026fd755ff7454e6e8b73a67f8c744b13986e24 \ + --hash=sha256:edac0f1ab77644605be2cbba52e6b7f630731fc42b34cb0f634be1a6eface56a \ + --hash=sha256:effc3f449787117233702311a1b7d8f59cba9ced946ba727bdc329ec69028e24 \ + --hash=sha256:f22dec1690b584cea26fade98b2435c132c1b5f68e39f5a0b7627cd7ae31f1dc \ + --hash=sha256:f495a1652cf3fbab2eb0639776dad966c2fb874d79d87ca07f9d5f059b8bd215 \ + --hash=sha256:f496c9c3cc02230093d8330875c4c3cdfc3b73612a5fd921c65d39cbcef08063 \ + --hash=sha256:f59099f9b66f0d7145115e6f80dd8b1d847176df89b234a5a6b3f00437aa0832 \ + --hash=sha256:f59ad4c0e8f6bba240a9bb85504faa1ab438237199d4cce5f622761507b8f6a6 \ + --hash=sha256:fbccdc05410c9ee21bbf16a35f4c1d16123dcdeb8a1d38f33654fa21d0234f79 \ + --hash=sha256:fea24543955a6a729c45a73fe90e08c743f0b3334bbf3201e6c4bc1b0c7fa464 + # via requests +click==8.4.1 \ + --hash=sha256:482be17c6991b8c19c5429a1e995d9b0efdbb63172824c41f99965dc0ade8ec2 \ + --hash=sha256:918b5633eddf6b41c32d4f454bf0de810065c74e3f7dbf8ee5452f8be88d3e96 + # via black +idna==3.10 \ + --hash=sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9 \ + --hash=sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3 + # via requests +mypy-extensions==1.1.0 \ + --hash=sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505 \ + --hash=sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558 + # via black +packaging==26.2 \ + --hash=sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e \ + --hash=sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661 + # via black +pathspec==1.1.1 \ + --hash=sha256:17db5ecd524104a120e173814c90367a96a98d07c45b2e10c2f3919fff91bf5a \ + --hash=sha256:a00ce642f577bf7f473932318056212bc4f8bfdf53128c78bbd5af0b9b20b189 + # via black +platformdirs==4.10.0 \ + --hash=sha256:31e761a6a0ca04faf7353ea759bdba55652be214725111e5aac52dfa29d4bef7 \ + --hash=sha256:fb516cdb12eb0d857d0cd85a7c57cea4d060bee4578d6cf5a14dfdf8cbf8784a + # via black +pytokens==0.4.1 \ + --hash=sha256:0fc71786e629cef478cbf29d7ea1923299181d0699dbe7c3c0f4a583811d9fc1 \ + --hash=sha256:11edda0942da80ff58c4408407616a310adecae1ddd22eef8c692fe266fa5009 \ + --hash=sha256:140709331e846b728475786df8aeb27d24f48cbcf7bcd449f8de75cae7a45083 \ + --hash=sha256:24afde1f53d95348b5a0eb19488661147285ca4dd7ed752bbc3e1c6242a304d1 \ + --hash=sha256:26cef14744a8385f35d0e095dc8b3a7583f6c953c2e3d269c7f82484bf5ad2de \ + --hash=sha256:27b83ad28825978742beef057bfe406ad6ed524b2d28c252c5de7b4a6dd48fa2 \ + --hash=sha256:292052fe80923aae2260c073f822ceba21f3872ced9a68bb7953b348e561179a \ + --hash=sha256:29d1d8fb1030af4d231789959f21821ab6325e463f0503a61d204343c9b355d1 \ + --hash=sha256:2a44ed93ea23415c54f3face3b65ef2b844d96aeb3455b8a69b3df6beab6acc5 \ + --hash=sha256:30f51edd9bb7f85c748979384165601d028b84f7bd13fe14d3e065304093916a \ + --hash=sha256:34bcc734bd2f2d5fe3b34e7b3c0116bfb2397f2d9666139988e7a3eb5f7400e3 \ + --hash=sha256:3ad72b851e781478366288743198101e5eb34a414f1d5627cdd585ca3b25f1db \ + --hash=sha256:3f901fe783e06e48e8cbdc82d631fca8f118333798193e026a50ce1b3757ea68 \ + --hash=sha256:42f144f3aafa5d92bad964d471a581651e28b24434d184871bd02e3a0d956037 \ + --hash=sha256:4a14d5f5fc78ce85e426aa159489e2d5961acf0e47575e08f35584009178e321 \ + --hash=sha256:4a58d057208cb9075c144950d789511220b07636dd2e4708d5645d24de666bdc \ + --hash=sha256:4e691d7f5186bd2842c14813f79f8884bb03f5995f0575272009982c5ac6c0f7 \ + --hash=sha256:5502408cab1cb18e128570f8d598981c68a50d0cbd7c61312a90507cd3a1276f \ + --hash=sha256:584c80c24b078eec1e227079d56dc22ff755e0ba8654d8383b2c549107528918 \ + --hash=sha256:5ad948d085ed6c16413eb5fec6b3e02fa00dc29a2534f088d3302c47eb59adf9 \ + --hash=sha256:670d286910b531c7b7e3c0b453fd8156f250adb140146d234a82219459b9640c \ + --hash=sha256:682fa37ff4d8e95f7df6fe6fe6a431e8ed8e788023c6bcc0f0880a12eab80ad1 \ + --hash=sha256:6d6c4268598f762bc8e91f5dbf2ab2f61f7b95bdc07953b602db879b3c8c18e1 \ + --hash=sha256:79fc6b8699564e1f9b521582c35435f1bd32dd06822322ec44afdeba666d8cb3 \ + --hash=sha256:8bdb9d0ce90cbf99c525e75a2fa415144fd570a1ba987380190e8b786bc6ef9b \ + --hash=sha256:8fcb9ba3709ff77e77f1c7022ff11d13553f3c30299a9fe246a166903e9091eb \ + --hash=sha256:941d4343bf27b605e9213b26bfa1c4bf197c9c599a9627eb7305b0defcfe40c1 \ + --hash=sha256:967cf6e3fd4adf7de8fc73cd3043754ae79c36475c1c11d514fc72cf5490094a \ + --hash=sha256:970b08dd6b86058b6dc07efe9e98414f5102974716232d10f32ff39701e841c4 \ + --hash=sha256:97f50fd18543be72da51dd505e2ed20d2228c74e0464e4262e4899797803d7fa \ + --hash=sha256:9bd7d7f544d362576be74f9d5901a22f317efc20046efe2034dced238cbbfe78 \ + --hash=sha256:add8bf86b71a5d9fb5b89f023a80b791e04fba57960aa790cc6125f7f1d39dfe \ + --hash=sha256:b35d7e5ad269804f6697727702da3c517bb8a5228afa450ab0fa787732055fc9 \ + --hash=sha256:b49750419d300e2b5a3813cf229d4e5a4c728dae470bcc89867a9ad6f25a722d \ + --hash=sha256:d31b97b3de0f61571a124a00ffe9a81fb9939146c122c11060725bd5aea79975 \ + --hash=sha256:d70e77c55ae8380c91c0c18dea05951482e263982911fc7410b1ffd1dadd3440 \ + --hash=sha256:d9907d61f15bf7261d7e775bd5d7ee4d2930e04424bab1972591918497623a16 \ + --hash=sha256:da5baeaf7116dced9c6bb76dc31ba04a2dc3695f3d9f74741d7910122b456edc \ + --hash=sha256:dc74c035f9bfca0255c1af77ddd2d6ae8419012805453e4b0e7513e17904545d \ + --hash=sha256:dcafc12c30dbaf1e2af0490978352e0c4041a7cde31f4f81435c2a5e8b9cabb6 \ + --hash=sha256:ee44d0f85b803321710f9239f335aafe16553b39106384cef8e6de40cb4ef2f6 \ + --hash=sha256:f66a6bbe741bd431f6d741e617e0f39ec7257ca1f89089593479347cc4d13324 + # via black +requests==2.32.3 \ + --hash=sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760 \ + --hash=sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6 + # via test (tests/uv/lock/workspaces/pyproject.toml) +urllib3==2.3.0 \ + --hash=sha256:1cee9ad369867bfdbbb48b7dd50374c0967a0bb7710050facf0dd6911440e3df \ + --hash=sha256:f8c5449b3cf0861679ce7e0503c7b44b5ec981bec0d1d3795a07f1ba96f0204d + # via requests diff --git a/tests/validate_test_main/BUILD.bazel b/tests/validate_test_main/BUILD.bazel new file mode 100644 index 0000000000..94163a2fde --- /dev/null +++ b/tests/validate_test_main/BUILD.bazel @@ -0,0 +1,10 @@ +load("//python:py_test.bzl", "py_test") + +py_test( + name = "validate_test_main_test", + srcs = ["validate_test_main_test.py"], + main = "validate_test_main_test.py", + deps = [ + "//python/private:py_test_main_validator_lib", + ], +) diff --git a/tests/validate_test_main/validate_test_main_test.py b/tests/validate_test_main/validate_test_main_test.py new file mode 100644 index 0000000000..4cbc55c937 --- /dev/null +++ b/tests/validate_test_main/validate_test_main_test.py @@ -0,0 +1,261 @@ +#!/usr/bin/env python3 +import ast +import textwrap +import unittest + +from python.private.py_test_main_validator import module_runs_tests + + +def _runs_tests(source: str) -> bool: + tree = ast.parse(textwrap.dedent(source)) + return module_runs_tests(tree) + + +class ModuleRunsTestsTest(unittest.TestCase): + def test_only_definitions_is_rejected(self): + self.assertFalse( + _runs_tests( + """ + import unittest + + class MyTest(unittest.TestCase): + def test_foo(self): + self.assertTrue(True) + """ + ) + ) + + def test_definitions_with_assignments_is_rejected(self): + self.assertFalse( + _runs_tests( + """ + import unittest + + CONSTANT = 5 + + class MyTest(unittest.TestCase): + def test_foo(self): + pass + + def helper(): pass + """ + ) + ) + + def test_global_statement_with_definition_is_rejected(self): + self.assertFalse( + _runs_tests( + """ + global x + + class MyTest: + def test_foo(self): + pass + """ + ) + ) + + def test_assert_statement_with_definition_is_rejected(self): + # A bare `assert` with an inert condition runs no tests. + self.assertFalse( + _runs_tests( + """ + assert True + + class MyTest: + def test_foo(self): + pass + """ + ) + ) + + def test_assert_statement_with_active_expression_runs_tests(self): + # An assert whose condition runs a call actually runs the tests. + self.assertTrue( + _runs_tests( + """ + import unittest + + class MyTest(unittest.TestCase): + def test_foo(self): + pass + + assert unittest.main() + """ + ) + ) + + @unittest.skipUnless( + hasattr(ast, "TypeAlias"), "PEP 695 type aliases require Python 3.12+" + ) + def test_type_alias_with_definition_is_rejected(self): + self.assertFalse( + _runs_tests( + """ + type Alias = int + + class MyTest: + def test_foo(self): + pass + """ + ) + ) + + def test_type_checking_block_with_definition_is_rejected(self): + # `if TYPE_CHECKING:` guarding typing-only imports is inert; with a test + # definition and no runner, the module is still rejected. + self.assertFalse( + _runs_tests( + """ + from typing import TYPE_CHECKING + import unittest + + if TYPE_CHECKING: + from typing import Any + + class MyTest(unittest.TestCase): + def test_foo(self): + pass + """ + ) + ) + + def test_try_except_import_error_with_definition_is_rejected(self): + # `try: import foo except ImportError:` optional imports are inert. + self.assertFalse( + _runs_tests( + """ + try: + import foo + except ImportError: + foo = None + + import unittest + + class MyTest(unittest.TestCase): + def test_foo(self): + pass + """ + ) + ) + + def test_definitions_with_active_assignment_runs_tests(self): + # An assignment whose value runs a call actually runs the tests. + self.assertTrue( + _runs_tests( + """ + import unittest + + class MyTest(unittest.TestCase): + def test_foo(self): + pass + + exit_code = unittest.main() + """ + ) + ) + + def test_definitions_with_active_expression_runs_tests(self): + self.assertTrue( + _runs_tests( + """ + import sys + import unittest + + class MyTest(unittest.TestCase): + def test_foo(self): + pass + + sys.exit(unittest.main()) + """ + ) + ) + + def test_if_block_invoking_runner_runs_tests(self): + # A runner call inside an `if` body must still count as active. + self.assertTrue( + _runs_tests( + """ + import sys + import unittest + + class MyTest(unittest.TestCase): + def test_foo(self): + pass + + if "--run" in sys.argv: + unittest.main() + """ + ) + ) + + def test_import_only_module_is_allowed(self): + # A module that defines nothing and only imports other modules is not + # the "defined but never run" case, so it is allowed. + self.assertTrue( + _runs_tests( + """ + import my_tests + from my_pkg.tests import suite + """ + ) + ) + + def test_imports_and_assignments_without_definitions_is_allowed(self): + self.assertTrue( + _runs_tests( + """ + import my_tests + + CONSTANT = 5 + """ + ) + ) + + def test_empty_module_is_allowed(self): + self.assertTrue(_runs_tests("")) + + def test_docstring_only_is_allowed(self): + self.assertTrue(_runs_tests('"""A module docstring."""')) + + def test_if_name_main_guard_runs_tests(self): + self.assertTrue( + _runs_tests( + """ + import unittest + + class MyTest(unittest.TestCase): + def test_foo(self): + pass + + if __name__ == "__main__": + unittest.main() + """ + ) + ) + + def test_bare_call_runs_tests(self): + self.assertTrue( + _runs_tests( + """ + import pytest + pytest.main() + """ + ) + ) + + def test_top_level_loop_runs_tests(self): + self.assertTrue( + _runs_tests( + """ + def f(): pass + + for _ in range(1): + f() + """ + ) + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/venv_site_packages_libs/app_files_building/app_files_building_tests.bzl b/tests/venv_site_packages_libs/app_files_building/app_files_building_tests.bzl index 65124076c6..663ab6963e 100644 --- a/tests/venv_site_packages_libs/app_files_building/app_files_building_tests.bzl +++ b/tests/venv_site_packages_libs/app_files_building/app_files_building_tests.bzl @@ -2,10 +2,9 @@ load("@rules_testing//lib:analysis_test.bzl", "analysis_test") load("@rules_testing//lib:test_suite.bzl", "test_suite") -load("//python:py_info.bzl", "PyInfo") +load("//python:py_info.bzl", "PyInfo", "VenvSymlinkEntry", "VenvSymlinkKind") load("//python:py_library.bzl", "py_library") load("//python/private:common_labels.bzl", "labels") # buildifier: disable=bzl-visibility -load("//python/private:py_info.bzl", "VenvSymlinkEntry", "VenvSymlinkKind") # buildifier: disable=bzl-visibility load("//python/private:venv_runfiles.bzl", "build_link_map", "get_venv_symlinks") # buildifier: disable=bzl-visibility load("//tests/support:support.bzl", "SUPPORTS_BZLMOD") diff --git a/tests/venv_site_packages_libs/importlib_metadata_test.py b/tests/venv_site_packages_libs/importlib_metadata_test.py index 963d43b6e0..eb16a6b6af 100644 --- a/tests/venv_site_packages_libs/importlib_metadata_test.py +++ b/tests/venv_site_packages_libs/importlib_metadata_test.py @@ -1,4 +1,6 @@ import importlib.metadata +import pathlib +import sys import unittest @@ -10,13 +12,79 @@ def test_importlib_metadata_files(self): len(files), 0, "importlib.metadata.files returned empty list" ) - # Verify it contains some expected files. - # The RECORD file lists paths relative to the installation root (site-packages). - # whl_with_data1-1.0.data/purelib/data_overlap.py should be installed as data_overlap.py - # whl_with_data1-1.0.data/platlib/whl_with_data1/platlib_file.txt should be whl_with_data1/platlib_file.txt + # Verify it contains expected files. + # The RECORD file lists paths relative to the installation root + # (site-packages). + # Per PEP 376 and PEP 427: + # - purelib and platlib files are installed directly under + # site-packages: + # whl_with_data1-1.0.data/purelib/data_overlap.py should be + # installed as data_overlap.py, and + # whl_with_data1-1.0.data/platlib/whl_with_data1/platlib_file.txt + # should be whl_with_data1/platlib_file.txt. + # - scripts, headers, and data files installed outside site-packages + # are recorded relative to site-packages traversing up to the venv + # root (e.g. ../../../bin/ on POSIX, ../../Scripts/ on Windows). + if sys.platform == "win32": + scripts_prefix = "../../Scripts/" + headers_prefix = "../../Include/" + data_prefix = "../../" + else: + scripts_prefix = "../../../bin/" + headers_prefix = "../../../include/" + data_prefix = "../../../" - file_names = [f.name for f in files] - self.assertIn("data_overlap.py", file_names) + expected_paths = sorted( + [ + scripts_prefix + "data_overlap.sh", + data_prefix + "bin/data_overlap.sh", + scripts_prefix + "overlap/both.sh", + scripts_prefix + "overlap/script1.sh", + scripts_prefix + "whl_script.sh", + scripts_prefix + "whl_with_data1_script", + headers_prefix + "data_overlap.h", + data_prefix + "include/data_overlap.h", + headers_prefix + "overlap/both.h", + headers_prefix + "overlap/header1.h", + headers_prefix + "whl_with_data1/header_file.h", + data_prefix + "overlap/both.txt", + data_prefix + "overlap/data1.txt", + data_prefix + "site-packages/data_overlap.py", + data_prefix + "whl_with_data1/data_data_file.txt", + data_prefix + "whl_with_data1/data_data_file.txt", + "data_overlap.py", + "whl_with_data1/data_file.txt", + "whl_with_data1/platlib_file.txt", + ] + ) + file_paths = sorted(str(f).replace("\\", "/") for f in files) + self.assertEqual(file_paths, expected_paths) + + for f in files: + resolved = pathlib.Path(f.locate()) + if resolved.exists(): + self.assertTrue( + resolved.is_file(), + f"Expected {resolved} to be a regular file", + ) + + # Verify file content can be read both as binary and as text + content = f.read_binary() + self.assertIsNotNone(content) + + text = f.read_text(encoding="utf-8") + self.assertIsNotNone(text) + else: + # On Windows, venv bin scripts have a .bat extension appended. + bat_resolved = resolved.parent / (resolved.name + ".bat") + self.assertTrue( + bat_resolved.exists(), + f"Expected file {f} (resolved to {resolved} or {bat_resolved}) to exist", + ) + self.assertTrue( + bat_resolved.is_file(), + f"Expected {bat_resolved} to be a regular file", + ) if __name__ == "__main__": 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/build_defs/python/private/BUILD.bazel b/tools/build_defs/python/private/BUILD.bazel index 746545640d..e189b18880 100644 --- a/tools/build_defs/python/private/BUILD.bazel +++ b/tools/build_defs/python/private/BUILD.bazel @@ -20,8 +20,9 @@ filegroup( visibility = ["//python:__subpackages__"], ) +# keep bzl_library( - name = "py_internal_renamed_bzl", + name = "py_internal_renamed", srcs = ["py_internal_renamed.bzl"], - visibility = ["//python/private:__pkg__"], + visibility = ["//:__subpackages__"], ) diff --git a/tools/precompiler/BUILD.bazel b/tools/precompiler/BUILD.bazel index 268f41b032..e055980c72 100644 --- a/tools/precompiler/BUILD.bazel +++ b/tools/precompiler/BUILD.bazel @@ -14,6 +14,7 @@ load("@bazel_skylib//rules:common_settings.bzl", "string_list_flag") load("//python/private:py_interpreter_program.bzl", "py_interpreter_program") # buildifier: disable=bzl-visibility +load("//python/private:visibility.bzl", "NOT_ACTUALLY_PUBLIC") # buildifier: disable=bzl-visibility filegroup( name = "distribution", @@ -25,11 +26,9 @@ py_interpreter_program( name = "precompiler", execution_requirements = ":execution_requirements", main = "precompiler.py", - visibility = [ - # Not actually public. Only public so rules_python-generated toolchains - # are able to reference it. - "//visibility:public", - ], + # Not actually public. Only public so rules_python-generated toolchains + # are able to reference it. + visibility = NOT_ACTUALLY_PUBLIC, ) string_list_flag( diff --git a/tools/private/BUILD.bazel b/tools/private/BUILD.bazel index adc8de3b0f..ae6951c245 100644 --- a/tools/private/BUILD.bazel +++ b/tools/private/BUILD.bazel @@ -1,3 +1,5 @@ +load("@bazel_skylib//:bzl_library.bzl", "bzl_library") + package( default_visibility = ["//:__subpackages__"], ) @@ -8,3 +10,9 @@ filegroup( "//tools/private/zipapp:distribution", ], ) + +bzl_library( + name = "publish_deps", + srcs = ["publish_deps.bzl"], + deps = ["//python/uv/private:lock"], +) diff --git a/tools/private/debug/print_defined_toolchains.sh b/tools/private/debug/print_defined_toolchains.sh new file mode 100755 index 0000000000..1694dc975c --- /dev/null +++ b/tools/private/debug/print_defined_toolchains.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Programmatically probe which repository target name is resolved successfully inside this workspace +if bazel query @pythons_hub//... >/dev/null 2>&1; then + HUB_REPO="@pythons_hub" +elif bazel query @@rules_python++python+pythons_hub//... >/dev/null 2>&1; then + HUB_REPO="@@rules_python++python+pythons_hub" +else + HUB_REPO="@@+python+pythons_hub" +fi + +# Query standard toolchains inside the resolved hub repository, excluding CC and Exec Tools toolchains. +bazel query "kind('toolchain', ${HUB_REPO}//...) - filter('_py_cc_toolchain$', ${HUB_REPO}//...) - filter('_py_exec_tools_toolchain$', ${HUB_REPO}//...)" "$@" diff --git a/tools/private/gazelle/BUILD.bazel b/tools/private/gazelle/BUILD.bazel new file mode 100644 index 0000000000..2a38e8f3a1 --- /dev/null +++ b/tools/private/gazelle/BUILD.bazel @@ -0,0 +1,21 @@ +load("@bazel_gazelle//:def.bzl", "gazelle", "gazelle_binary") + +package( + default_visibility = ["//:__subpackages__"], +) + +gazelle_binary( + name = "gazelle_bin", + languages = [ + "@bazel_skylib_gazelle_plugin//bzl", + ], + # Marked manual to avoid building in CI, which fails due to copt settings. + tags = ["manual"], +) + +gazelle( + name = "gazelle", + gazelle = ":gazelle_bin", + # Marked manual to avoid building in CI, which fails due to copt settings. + tags = ["manual"], +) diff --git a/tools/private/release/BUILD.bazel b/tools/private/release/BUILD.bazel index 31cc3a0239..5f80d1130a 100644 --- a/tools/private/release/BUILD.bazel +++ b/tools/private/release/BUILD.bazel @@ -1,12 +1,40 @@ -load("@rules_python//python:defs.bzl", "py_binary") +load("@rules_python//python:defs.bzl", "py_binary", "py_library") package(default_visibility = ["//visibility:public"]) +py_library( + name = "changelog_news", + srcs = ["changelog_news.py"], +) + +py_library( + name = "release_lib", + srcs = glob( + ["*.py"], + exclude = [ + "changelog_news.py", + "mock_gh.py", + ], + ), + deps = [ + ":changelog_news", + "@dev_pip//packaging", + ], +) + +py_library( + name = "mock_gh", + srcs = ["mock_gh.py"], + deps = [ + ":release_lib", + ], +) + py_binary( name = "release", srcs = ["release.py"], main = "release.py", deps = [ - "@dev_pip//packaging", + ":release_lib", ], ) diff --git a/tools/private/release/__init__.py b/tools/private/release/__init__.py new file mode 100644 index 0000000000..e68f0391f8 --- /dev/null +++ b/tools/private/release/__init__.py @@ -0,0 +1 @@ +"""Release tools package.""" diff --git a/tools/private/release/add_backports.py b/tools/private/release/add_backports.py new file mode 100644 index 0000000000..63c70af7d7 --- /dev/null +++ b/tools/private/release/add_backports.py @@ -0,0 +1,118 @@ +"""Subcommand to add PRs to the release tracking issue backports checklist.""" + +from tools.private.release.gh import GitHub +from tools.private.release.release_issue import ( + add_backports_to_body, + add_rc_task_to_body, + add_sync_changelog_task_to_body, + parse_checklist_state, +) + + +class AddBackports: + """Class to add PRs to the release tracking issue.""" + + def __init__(self, args, gh: GitHub): + self.args = args + self.gh = gh + + def run(self) -> int: + """Executes the add-backports subcommand.""" + args = self.args + + issue_num = args.issue + if not issue_num: + print( + "No issue specified. Trying to auto-discover active release" + " tracking issue..." + ) + try: + open_issues = self.gh.get_open_tracking_issues() + if not open_issues: + print("Error: No open release tracking issues found.") + return 1 + if len(open_issues) > 1: + print( + "Error: Multiple open release tracking issues found." + " Cannot determine active one:" + ) + for issue in open_issues: + print(f"- #{issue['number']}: {issue['title']}") + return 1 + issue_num = open_issues[0]["number"] + print(f"Auto-discovered active release tracking issue: #{issue_num}") + except Exception as e: + print(f"Error auto-discovering tracking issue: {e}") + return 1 + + resolved_prs = [] + for pr_ref in args.prs: + try: + pr_num = self.gh.resolve_pr_number(pr_ref) + resolved_prs.append(pr_num) + except Exception as e: + print(f"Error resolving PR ref '{pr_ref}': {e}") + return 1 + + print( + f"Adding backports {resolved_prs} (resolved from {args.prs}) to tracking issue #{issue_num}..." + ) + try: + body = self.gh.get_issue_body(issue_num) + items_to_add = [{"ref": f"#{pr}"} for pr in resolved_prs] + body = add_backports_to_body(body, items_to_add) + for pr in resolved_prs: + body = add_sync_changelog_task_to_body(body, pr) + state = parse_checklist_state(body) + rc_tags = state.get("rc_tags", {}) + has_pending_rc = any( + not task.checked and task.status != "done" for task in rc_tags.values() + ) + next_rc_num = max(rc_tags.keys()) + 1 if rc_tags else 0 + if not has_pending_rc: + print( + f"No pending RC task found. Adding 'Tag" + f" RC{next_rc_num}' to checklist..." + ) + body = add_rc_task_to_body(body, next_rc_num) + except ValueError as e: + print(f"Error: {e}") + return 1 + except Exception as e: + print(f"Failed to update tracking issue: {e}") + return 1 + + try: + self.gh.update_issue_body(issue_num, body) + print("Successfully updated tracking issue checklist.") + except Exception as e: + print(f"Failed to update tracking issue body: {e}") + return 1 + + return 0 + + @classmethod + def add_parser(cls, subparsers): + """Adds parser for add-backports subcommand.""" + parser = subparsers.add_parser( + "add-backports", + help="Add PRs to the release tracking issue backports checklist.", + ) + parser.add_argument( + "prs", + type=str, + nargs="+", + help="PR references (numbers, #numbers, or URLs) to add (positional, space-separated).", + ) + parser.add_argument( + "--issue", + type=int, + help="The tracking issue number. If omitted, will try to auto-discover the active release tracking issue.", + ) + parser.set_defaults(command=cls.run_from_args) + + @classmethod + def run_from_args(cls, args): + """Instantiates and runs the command from parsed args.""" + gh = GitHub() + return cls(args, gh).run() diff --git a/tools/private/release/backport_create_releases.py b/tools/private/release/backport_create_releases.py new file mode 100644 index 0000000000..a6d2beabdf --- /dev/null +++ b/tools/private/release/backport_create_releases.py @@ -0,0 +1,237 @@ +"""Subcommand to initiate releases for verified backports.""" + +import argparse +import pathlib +import re +from dataclasses import dataclass + +from tools.private.release.backport_prepare import parse_backport_metadata +from tools.private.release.gh import GitHub +from tools.private.release.release_issue import ( + add_backports_to_body, + add_sync_changelog_task_to_body, + parse_metadata_line, + update_task_in_body, +) + + +@dataclass +class BackportTasksState: + # Map of minor version (e.g. "1.7") to verification status (e.g. "success", "failed-conflict") + verify: dict[str, str] + # Map of full version (e.g. "1.7.1") to tuple of (status, release_issue_number_or_None) + release: dict[str, tuple[str, str | None]] + + +def parse_backport_tasks(body) -> BackportTasksState: + """Parses tasks from backport issue body.""" + verify_tasks = {} + release_tasks = {} + + lines = body.splitlines() + for line in lines: + parsed = parse_metadata_line(line) + if not parsed: + continue + name = parsed["name"] + meta = parsed["metadata"] + + verify_match = re.match(r"Verify apply (\d+\.\d+)", name, re.IGNORECASE) + if verify_match: + minor = verify_match.group(1) + verify_tasks[minor] = meta.get("status", "pending") + continue + + release_match = re.match(r"Track Release (\d+\.\d+\.\d+)", name, re.IGNORECASE) + if release_match: + version = release_match.group(1) + status = meta.get("status", "pending") + issue_num = meta.get("release_issue") + release_tasks[version] = (status, issue_num) + + return BackportTasksState(verify=verify_tasks, release=release_tasks) + + +def is_release_eligible(version, target_minors, verify_statuses): + """Checks if a release version is eligible based on verification statuses.""" + version_minor = ".".join(version.split(".")[:2]) + v_minor_parsed = [int(x) for x in version_minor.split(".")] + + for minor in target_minors: + minor_parsed = [int(x) for x in minor.split(".")] + if minor_parsed >= v_minor_parsed: + status = verify_statuses.get(minor) + if status != "success": + return ( + False, + f"Blocked by verification failure on {minor} (status: {status})", + ) + + return True, "Eligible" + + +def _load_release_template() -> str: + """Loads the release tracking issue template.""" + template_path = pathlib.Path(".github/ISSUE_TEMPLATE/release_tracking_template.md") + if not template_path.exists(): + raise FileNotFoundError(f"Template file not found at {template_path}") + return template_path.read_text(encoding="utf-8") + + +class BackportCreateReleases: + """Class to initiate releases for verified backports.""" + + def __init__(self, args, gh: GitHub): + self._args = args + self._gh = gh + + def run(self) -> int: + """Executes the backport-create-releases subcommand.""" + args = self._args + issue_num = args.issue + if not issue_num: + raise ValueError("--issue is required.") + + print(f"Reading backport issue #{issue_num}...") + body = self._gh.get_issue_body(issue_num) + try: + metadata = parse_backport_metadata(body) + except ValueError as e: + e.add_note(f"Failed to parse backport metadata from issue #{issue_num}") + raise + pr_ref = metadata.pr + + pr_num = self._gh.resolve_pr_number(pr_ref) + + # Parse tasks + tasks_state = parse_backport_tasks(body) + verify_statuses = tasks_state.verify + release_tasks = tasks_state.release + + target_minors = sorted( + list(verify_statuses.keys()), key=lambda m: [int(x) for x in m.split(".")] + ) + + # We need the templates for release issues + template_content = _load_release_template() + + updated_body = body + changes_made = False + + for version, (status, release_issue) in release_tasks.items(): + # If status is success, we already created the issue. + # We check the actual checklist item status, but the helper returns it. + # The status in metadata is 'success' when done. + if status == "success": + print(f"Release for {version} already initiated: {release_issue}") + continue + + eligible, reason = is_release_eligible( + version, target_minors, verify_statuses + ) + + task_name = f"Track Release {version}" + + if eligible: + print(f"Initiating release for {version}...") + + # Check if release issue already exists + existing_issues = self._gh.get_open_tracking_issues(version) + if existing_issues: + new_issue_num = existing_issues[0]["number"] + print( + f"Release issue for {version} already exists: #{new_issue_num}. Reusing it." + ) + else: + # Create the issue + is_first_release = version.endswith(".0") + if is_first_release: + issue_template = template_content + else: + lines = template_content.splitlines() + lines = [ + line for line in lines if not re.search(r"Tag RC\d+", line) + ] + issue_template = "\n".join(lines) + if template_content.endswith("\n"): + issue_template += "\n" + + if args.dry_run: + print( + f"[DRY RUN] Would create release tracking issue for {version} (without RC tasks)" + ) + new_issue_num = "" + else: + new_issue_num = self._gh.create_release_tracking_issue( + version, issue_template + ) + print( + f"Created release tracking issue #{new_issue_num} for {version}" + ) + + # Add the backport PR to the new release issue + print( + f"Adding PR #{pr_num} to release issue #{new_issue_num} checklist..." + ) + rel_body = self._gh.get_issue_body(new_issue_num) + rel_body = add_backports_to_body( + rel_body, [{"ref": f"#{pr_num}"}] + ) + rel_body = add_sync_changelog_task_to_body(rel_body, pr_num) + self._gh.update_issue_body(new_issue_num, rel_body) + + # Update task in backport issue + metadata = {"status": "success", "release_issue": f"#{new_issue_num}"} + updated_body = update_task_in_body( + updated_body, task_name, checked=True, metadata=metadata + ) + changes_made = True + else: + print(f"Release for {version} is not eligible: {reason}") + metadata = {"status": "error-later-release-did-not-apply"} + + if status != "error-later-release-did-not-apply": + updated_body = update_task_in_body( + updated_body, task_name, checked=False, metadata=metadata + ) + changes_made = True + + if changes_made: + if args.dry_run: + print( + f"[DRY RUN] Would update backport issue #{issue_num} body:\n{updated_body}" + ) + else: + self._gh.update_issue_body(issue_num, updated_body) + print(f"Updated backport issue #{issue_num}") + else: + print("No changes needed for backport issue.") + + return 0 + + @classmethod + def add_parser(cls, subparsers): + """Adds parser for backport-create-releases subcommand.""" + parser = subparsers.add_parser( + "backport-create-releases", + help="Initiate releases for verified backports.", + ) + parser.add_argument( + "--issue", + type=int, + required=True, + help="The backport tracking issue number (required).", + ) + parser.add_argument( + "--dry-run", + action=argparse.BooleanOptionalAction, + default=True, + help="Perform a dry run (default: True). Use --no-dry-run to actually execute.", + ) + parser.set_defaults(command=cls.run_from_args) + + @classmethod + def run_from_args(cls, args): + """Instantiates and runs the command from parsed args.""" + gh = GitHub() + return cls(args, gh).run() diff --git a/tools/private/release/backport_prepare.py b/tools/private/release/backport_prepare.py new file mode 100644 index 0000000000..459e91f703 --- /dev/null +++ b/tools/private/release/backport_prepare.py @@ -0,0 +1,305 @@ +"""Subcommand to prepare backport tracking issue and verify cherry-picks.""" + +import argparse +import datetime +import re +from dataclasses import dataclass + +from tools.private.release import changelog_news +from tools.private.release.gh import BACKPORT_LABEL, GitHub +from tools.private.release.git import Git +from tools.private.release.utils import determine_next_version + + +@dataclass +class BackportMetadata: + pr: str + from_minor: str + to_minor: str + + +def parse_backport_metadata(body) -> BackportMetadata: + """Parses backport metadata from issue body.""" + pr_match = re.search(r"^\s*\*\s*PR:\s*(\S+)", body, re.MULTILINE | re.IGNORECASE) + from_match = re.search( + r"^\s*\*\s*From version:\s*(\S+)", body, re.MULTILINE | re.IGNORECASE + ) + to_match = re.search( + r"^\s*\*\s*To version:\s*(\S+)", body, re.MULTILINE | re.IGNORECASE + ) + + if not pr_match or not from_match or not to_match: + raise ValueError( + "Missing metadata in issue body. Need PR, From version, and To version." + ) + + return BackportMetadata( + pr=pr_match.group(1), + from_minor=from_match.group(1), + to_minor=to_match.group(1), + ) + + +def get_target_branches(git, remote, from_minor, to_minor): + """Identifies target release branches in the given range.""" + branches = git.get_remote_branches(remote) + target_branches = [] + + # Parse version strings to compare (only major and minor) + from_v = [int(x) for x in from_minor.split(".")][:2] + to_v = [int(x) for x in to_minor.split(".")][:2] + + for branch in branches: + match = re.match(r"^release/(\d+)\.(\d+)$", branch) + if match: + major = int(match.group(1)) + minor = int(match.group(2)) + v = [major, minor] + if v >= from_v and v <= to_v: + target_branches.append(branch) + + # Sort branches by version + target_branches.sort(key=lambda b: [int(x) for x in b.split("/")[1].split(".")]) + return target_branches + + +def get_latest_release_branch(git, remote): + """Determines the latest release branch.""" + branches = git.get_remote_branches(remote) + release_branches = [] + for branch in branches: + match = re.match(r"^release/(\d+)\.(\d+)$", branch) + if match: + release_branches.append(branch) + if not release_branches: + return None + release_branches.sort(key=lambda b: [int(x) for x in b.split("/")[1].split(".")]) + return release_branches[-1] + + +class BackportPrepare: + """Class to prepare backport tracking issue and verify cherry-picks.""" + + def __init__(self, args, git: Git, gh: GitHub): + self.args = args + self.git = git + self.gh = gh + + def run(self) -> int: + """Executes the backport-prepare subcommand.""" + args = self.args + print("Fetching remote to verify release branches...") + self.git.fetch(args.remote, tags=True, force=True) + + issue_num = args.issue + + if issue_num: + # Triggered by comment, read from issue + print(f"Reading metadata from issue #{issue_num}...") + body = self.gh.get_issue_body(issue_num) + try: + metadata = parse_backport_metadata(body) + except ValueError as e: + e.add_note(f"Failed to parse backport metadata from issue #{issue_num}") + raise + pr_ref = metadata.pr + from_minor = metadata.from_minor + to_minor = metadata.to_minor + else: + # Triggered manually, use args + pr_ref = args.pr + from_minor = args.from_minor + to_minor = args.to_minor + + if not pr_ref or not from_minor: + raise ValueError( + "PR and From version are required if not running from an issue." + ) + + if not to_minor: + latest_branch = get_latest_release_branch(self.git, args.remote) + if not latest_branch: + raise RuntimeError( + "Could not determine latest release branch and --to-minor was not provided." + ) + to_minor = latest_branch.split("/")[1] + print(f"Auto-determined latest minor version: {to_minor}") + + # Resolve PR to merge commit + try: + pr_num = self.gh.resolve_pr_number(pr_ref) + print(f"Resolving PR #{pr_num} info...") + pr_info = self.gh.get_pr_info(pr_num) + except Exception as e: + e.add_note(f"Failed to resolve PR info for {pr_ref}") + raise + + if pr_info.get("state") != "MERGED": + raise ValueError( + f"PR #{pr_num} is not merged (state: {pr_info.get('state')})." + ) + merge_commit = pr_info.get("mergeCommit") + if not merge_commit or "oid" not in merge_commit: + raise ValueError(f"PR #{pr_num} has no merge commit SHA.") + pr_sha = merge_commit["oid"] + print(f"Resolved PR #{pr_num} to merge commit {pr_sha[:8]}") + + target_branches = get_target_branches( + self.git, args.remote, from_minor, to_minor + ) + if not target_branches: + raise ValueError( + f"No release branches found in range {from_minor} to {to_minor}" + ) + + print(f"Identified target branches: {target_branches}") + + # Verify workspace is clean + if self.git.status(): + raise RuntimeError("Workspace is dirty. Aborting.") + + current_branch = self.git.get_current_branch() + verify_results = {} # branch -> (success, reason) + version_map = {} # branch -> next_version + + try: + for branch in target_branches: + minor_ver = branch.split("/")[1] + print(f"Verifying application on {branch}...") + self.git.checkout(branch, track_remote=args.remote) + + # Determine next version + next_version = determine_next_version(branch) + version_map[branch] = next_version + + # Verify apply (cherry-pick + news) + try: + self.git.cherry_pick(pr_sha) + + # Try news update + try: + release_date = datetime.date.today().strftime("%Y-%m-%d") + changelog_news.update_changelog(next_version, release_date) + verify_results[branch] = (True, "Success") + print(f"Verification successful for {branch}") + except Exception as e: + verify_results[branch] = ( + False, + f"Changelog update failed: {e}", + ) + print( + f"Verification failed for {branch} (changelog update): {e}" + ) + except Exception as e: + verify_results[branch] = (False, f"Cherry-pick failed: {e}") + print(f"Verification failed for {branch} (cherry-pick): {e}") + finally: + # Always abort/reset + try: + self.git.cherry_pick_abort() + except Exception: + pass + self.git.reset_hard(reset_to=f"{args.remote}/{branch}") + finally: + # Restore original branch + if current_branch: + self.git.checkout(current_branch) + + # Generate issue content + body_lines = [ + f"* PR: #{pr_num}", + f"* From version: {from_minor}", + f"* To version: {to_minor}", + "", + "## Tasks", + "", + ] + + # Add Verify tasks + for branch in target_branches: + minor_ver = branch.split("/")[1] + success, reason = verify_results[branch] + if success: + body_lines.append(f"- [x] Verify apply {minor_ver} | status=success") + else: + status = "failed-conflict" + if "Changelog" in reason: + status = "failed-changelog" + body_lines.append(f"- [ ] Verify apply {minor_ver} | status={status}") + + # Add Release tasks + for branch in target_branches: + next_version = version_map[branch] + body_lines.append(f"- [ ] Track Release {next_version}") + + new_body = "\n".join(body_lines) + + if issue_num: + if args.dry_run: + print( + f"[DRY RUN] Would update issue #{issue_num} with body:\n{new_body}" + ) + else: + self.gh.update_issue_body(issue_num, new_body) + print(f"Successfully updated issue #{issue_num}") + else: + title = f"Backport: #{pr_num}" + if args.dry_run: + print( + f"[DRY RUN] Would create issue with title '{title}' and body:\n{new_body}" + ) + else: + new_issue_num = self.gh.create_issue( + title, new_body, labels=[BACKPORT_LABEL] + ) + print(f"Created backport tracking issue #{new_issue_num}") + + return 0 + + @classmethod + def add_parser(cls, subparsers): + """Adds parser for backport-prepare subcommand.""" + parser = subparsers.add_parser( + "backport-prepare", + help="Prepare backport tracking issue and verify cherry-picks.", + ) + parser.add_argument( + "--issue", + type=int, + help="The backport tracking issue number (if running from an existing issue).", + ) + parser.add_argument( + "--pr", + type=str, + help="PR reference to backport (required if not running from issue).", + ) + parser.add_argument( + "--from-minor", + type=str, + help="Oldest minor version to target (inclusive) (e.g. 1.7) (required if not running from issue).", + ) + parser.add_argument( + "--to-minor", + type=str, + help="Newest minor version to target (inclusive) (optional, defaults to latest).", + ) + parser.add_argument( + "--remote", + type=str, + default="origin", + help="The git remote (default: origin).", + ) + parser.add_argument( + "--dry-run", + action=argparse.BooleanOptionalAction, + default=True, + help="Perform a dry run (default: True). Use --no-dry-run to actually execute.", + ) + parser.set_defaults(command=cls.run_from_args) + + @classmethod + def run_from_args(cls, args): + """Instantiates and runs the command from parsed args.""" + git = Git(".") + gh = GitHub() + return cls(args, git, gh).run() diff --git a/tools/private/release/changelog_news.py b/tools/private/release/changelog_news.py new file mode 100644 index 0000000000..6381aacb34 --- /dev/null +++ b/tools/private/release/changelog_news.py @@ -0,0 +1,368 @@ +"""Utility functions for handling news entries and merging them into CHANGELOG.md.""" + +import pathlib +import re + + +def _get_sub_category(content): + """Extracts the sub-category in parentheses from the entry content.""" + match = re.match(r"^(?:\*|-)\s*\(([^)]+)\)", content) + if match: + return match.group(1).lower() + return "" + + +def is_news_file(path): + """Checks if a file path is a valid news file.""" + path = pathlib.Path(path) + if not path.is_file(): + return False + if path.suffix != ".md": + return False + parts = path.name.split(".") + if len(parts) < 3: + return False + return True + + +def _get_news_files(news_dir): + """Returns a list of news files matching the ..md pattern.""" + news_path = pathlib.Path(news_dir) + if not news_path.exists(): + return [] + + return [p for p in news_path.iterdir() if is_news_file(p)] + + +def _format_entry(content): + """Formats news entry content as a markdown bullet list item. + + Continuation lines are indented with two spaces. + """ + lines = content.strip().splitlines() + if not lines: + return "" + + formatted_lines = [] + first_line = lines[0] + if not (first_line.startswith("* ") or first_line.startswith("- ")): + formatted_lines.append(f"* {first_line}") + else: + formatted_lines.append(first_line) + + for line in lines[1:]: + if not line: + formatted_lines.append("") + elif line.startswith("* ") or line.startswith("- "): + formatted_lines.append(line) + elif line.startswith(" "): + formatted_lines.append(line) + else: + formatted_lines.append(f" {line}") + + return "\n".join(formatted_lines) + + +def _parse_new_files(news_files): + """Parses news files and groups them by category.""" + entries = {} + for p in news_files: + if not is_news_file(p): + continue + parts = p.name.split(".") + category = parts[1].lower() + + content = p.read_text(encoding="utf-8").strip() + + if not content: + continue + + formatted_content = _format_entry(content) + if not formatted_content: + continue + + if category not in entries: + entries[category] = [] + entries[category].append(formatted_content) + + return entries + + +def generate_release_block(version, release_date, news_entries): + """Generates the markdown block for the release.""" + header_version = version.replace(".", "-") + lines = [ + f"{{#v{header_version}}}", + f"## [{version}] - {release_date}", + "", + f"[{version}]: https://github.com/bazel-contrib/rules_python/releases/tag/{version}", + "", + ] + + # Standard categories in preferred order + category_order = ["removed", "changed", "fixed", "added"] + # Add any other categories found + for cat in news_entries: + if cat not in category_order: + category_order.append(cat) + + has_entries = False + for cat in category_order: + if cat in news_entries and news_entries[cat]: + has_entries = True + lines.append(f"{{#v{header_version}-{cat}}}") + lines.append(f"### {cat.capitalize()}") + + # Sort entries by sub-category, then by content + sorted_entries = sorted( + news_entries[cat], key=lambda e: (_get_sub_category(e), e) + ) + + for entry in sorted_entries: + lines.append(entry) + lines.append("") + + if not has_entries: + lines.append("No notable changes.") + lines.append("") + + return "\n".join(lines) + + +def _parse_simple_version(ver_str): + """Parses a version string (X-Y-Z or X.Y.Z) into a tuple of ints.""" + normalized = ver_str.replace("-", ".") + return tuple(int(x) for x in normalized.split(".")) + + +def _find_insertion_point(changelog_content, new_version): + """Finds the character index to insert the new version block. + + It should be inserted before the first version in the changelog that is + smaller than new_version. + """ + # Find all version anchors: {#vX-Y-Z} + matches = list(re.finditer(r"\{#v(?P\d+-\d+-\d+)\}", changelog_content)) + + parsed_new_ver = _parse_simple_version(new_version) + + for m in matches: + ver_str = m.group("ver") + parsed_ver = _parse_simple_version(ver_str) + if parsed_ver < parsed_new_ver: + return m.start() + + raise ValueError( + f"Could not find a version in CHANGELOG.md smaller than {new_version} to insert before." + ) + + +def _add_news_to_changelog(input_path, output_path, version, entries, release_date): + """Adds or merges news entries into CHANGELOG.md.""" + input_path = pathlib.Path(input_path) + output_path = pathlib.Path(output_path) + changelog_content = input_path.read_text(encoding="utf-8") + + if version == "unreleased": + header_version = "unreleased" + version_anchor = "{#unreleased}" + category_anchor_fmt = "{{#unreleased-{cat}}}" + category_anchor_pattern = r"\{#unreleased-(?P[a-z]+)\}" + else: + header_version = version.replace(".", "-") + version_anchor = f"{{#v{header_version}}}" + category_anchor_fmt = "{{#v" + header_version + "-{cat}}}" + category_anchor_pattern = ( + r"\{#v" + re.escape(header_version) + r"-(?P[a-z]+)\}" + ) + + version_exists = version_anchor in changelog_content + + if version_exists: + if not entries and version != "unreleased": + print( + f"Version {version} already exists and no news entries found" + " to merge. Doing nothing." + ) + output_path.write_text(changelog_content, encoding="utf-8") + return + + print(f"Version {version} already exists in changelog. Merging news entries...") + # Extract the existing version block + pattern = ( + r"(?P" + + re.escape(version_anchor) + + r")(?P.*?)(?=\n\s*\{#v\d+-\d+-\d+\}|\Z)" + ) + match = re.search(pattern, changelog_content, re.DOTALL) + if not match: + raise RuntimeError( + f"Could not find content for existing version {version} in CHANGELOG.md" + ) + + content_block = match.group("content") + + # Strip the "Unreleased changes..." sentence for Unreleased preview + if version == "unreleased": + content_block = re.sub( + r"Unreleased\s+changes\s+are\s+tracked\s+as\s+individual\s+files\s+in\s+the\s+\[news/\]\(\./news\)\s+directory,\s+or\s+view\s+the\s+\[latest\s+generated\s+changelog\]\(https://rules-python\.readthedocs\.io/en/latest/changelog\.html\)\.\s*\n*", + "", + content_block, + ) + + # Parse existing categories + match_cat = re.search(category_anchor_pattern, content_block) + if match_cat: + header_end_idx = match_cat.start() + header_str = content_block[:header_end_idx] + categories_str = content_block[header_end_idx:] + else: + header_str = content_block + categories_str = "" + + existing_entries = {} + if categories_str: + cat_matches = list(re.finditer(category_anchor_pattern, categories_str)) + for i, m in enumerate(cat_matches): + cat = m.group("cat") + start_idx = m.end() + end_idx = ( + cat_matches[i + 1].start() + if i + 1 < len(cat_matches) + else len(categories_str) + ) + cat_content = categories_str[start_idx:end_idx].strip() + + lines = cat_content.splitlines() + cat_entries = [] + current_entry = [] + for line in lines: + if not line.strip() or line.strip().startswith("### "): + continue + if line.startswith("* ") or line.startswith("- "): + if current_entry: + cat_entries.append("\n".join(current_entry)) + current_entry = [line] + else: + if current_entry: + current_entry.append(line) + if current_entry: + cat_entries.append("\n".join(current_entry)) + existing_entries[cat] = cat_entries + + # Merge news entries + merged_entries = dict(existing_entries) + for cat, cat_entries in entries.items(): + if cat not in merged_entries: + merged_entries[cat] = [] + merged_entries[cat].extend(cat_entries) + + # Reconstruct categories + reconstructed_lines = [] + category_order = ["removed", "changed", "fixed", "added"] + for cat in merged_entries: + if cat not in category_order: + category_order.append(cat) + + for cat in category_order: + if cat in merged_entries and merged_entries[cat]: + reconstructed_lines.append(category_anchor_fmt.format(cat=cat)) + reconstructed_lines.append(f"### {cat.capitalize()}") + + sorted_entries = sorted( + merged_entries[cat], key=lambda e: (_get_sub_category(e), e) + ) + + for entry in sorted_entries: + reconstructed_lines.append(entry) + reconstructed_lines.append("") + + new_categories_str = "\n".join(reconstructed_lines) + new_release_block = ( + header_str.rstrip() + "\n\n" + new_categories_str.strip() + "\n" + ) + if version == "unreleased" and not new_categories_str.strip(): + new_release_block = ( + header_str.rstrip() + "\n\nNo notable unreleased changes.\n" + ) + + # Replace in changelog_content + new_content = re.sub( + pattern, + r"\g\n" + new_release_block.strip() + "\n", + changelog_content, + count=1, + flags=re.DOTALL, + ) + output_path.write_text(new_content, encoding="utf-8") + + else: + print( + f"Version {version} does not exist in changelog. Creating new" + " release section from news entries..." + ) + new_release_block = generate_release_block(version, release_date, entries) + + # Find insertion point + insertion_point = _find_insertion_point(changelog_content, version) + + new_content = ( + changelog_content[:insertion_point] + + new_release_block + + "\n\n" + + changelog_content[insertion_point:] + ) + output_path.write_text(new_content, encoding="utf-8") + + +def merge_new_into_changelog( + changelog_path, + output_path, + news_dir, + version, + release_date, + delete_news=False, + news_files=None, +): + """Merges news entries from news_dir into changelog_path and writes to output_path.""" + if news_files is None: + news_files = _get_news_files(news_dir) + else: + news_files = [pathlib.Path(f) for f in news_files] + entries = _parse_new_files(news_files) + _add_news_to_changelog( + input_path=changelog_path, + output_path=output_path, + version=version, + entries=entries, + release_date=release_date, + ) + if delete_news: + for p in news_files: + if p.exists(): + p.unlink() + if news_files: + print(f"Removed {len(news_files)} processed news files.") + + +def update_changelog( + version, + release_date, + changelog_path="CHANGELOG.md", + output_path=None, + news_dir="news", + delete_news=True, + news_files=None, +): + """Performs the version replacements in CHANGELOG.md.""" + if output_path is None: + output_path = changelog_path + merge_new_into_changelog( + changelog_path=changelog_path, + output_path=output_path, + news_dir=news_dir, + version=version, + release_date=release_date, + delete_news=delete_news, + news_files=news_files, + ) diff --git a/tools/private/release/complete_prepare.py b/tools/private/release/complete_prepare.py new file mode 100644 index 0000000000..e585a53e8c --- /dev/null +++ b/tools/private/release/complete_prepare.py @@ -0,0 +1,114 @@ +"""Subcommand to mark preparation task as complete.""" + +import re + +from tools.private.release.gh import GitHub +from tools.private.release.release_issue import ( + parse_checklist_state, + update_task_in_body, +) +from tools.private.release.utils import set_github_output + + +class CompletePrepare: + """Class to mark preparation task as complete.""" + + def __init__(self, args, gh: GitHub): + self.args = args + self.gh = gh + + def run(self) -> int: + """Executes the complete-prepare subcommand (Phase 2 PR merged).""" + args = self.args + pr_number = args.pr + issue_number = args.issue + + if not pr_number and not issue_number: + print("Error: Either --pr or --issue must be provided.") + return 1 + + if not pr_number and issue_number: + issue_body = self.gh.get_issue_body(issue_number) + state = parse_checklist_state(issue_body) + prep_task = state.get("prepare_release") + if not prep_task or not prep_task.pr: + print( + f"Error: Could not find PR reference for 'Prepare Release'" + f" in issue #{issue_number}." + ) + return 1 + pr_number = int(prep_task.pr.lstrip("#")) + + print(f"Completing preparation for PR #{pr_number}...") + + pr_info = self.gh.get_pr_info(pr_number) + if not pr_info or pr_info.get("state") != "MERGED": + state = pr_info.get("state", "UNKNOWN") if pr_info else "NOT_FOUND" + print(f"Error: PR #{pr_number} is not merged yet (state: {state}).") + return 1 + + if not issue_number: + # Resolve issue number from PR body + pr_body = pr_info.get("body") or "" + match = re.search(r"Work towards #(\d+)", pr_body) + if not match: + match = re.search(r"#(\d+)", pr_body) + if not match: + print( + f"Error: Could not determine tracking issue number from PR" + f" #{pr_number} body: {pr_body}" + ) + return 1 + + issue_number = int(match.group(1)) + print(f"Resolved tracking issue #{issue_number} from PR #{pr_number} body.") + + commit_sha = pr_info["mergeCommit"]["oid"] + short_commit = commit_sha[:8] + print( + f"PR #{pr_number} merged at commit {commit_sha}. Updating tracking issue..." + ) + + # Update checklist: mark Prepare Release as done (checked) and set SUCCESS + body = self.gh.get_issue_body(issue_number) + metadata = { + "status": "done", + "pr": f"#{pr_number}", + "commit": short_commit, + } + updated_body = update_task_in_body( + body, "Prepare Release", checked=True, metadata=metadata + ) + self.gh.update_issue_body(issue_number, updated_body) + print("Prepare Release task marked complete successfully!") + + set_github_output("issue", str(issue_number)) + + return 0 + + @classmethod + def add_parser(cls, subparsers): + """Adds parser for complete-prepare subcommand.""" + parser = subparsers.add_parser( + "complete-prepare", + help="Mark the Prepare Release task as complete in the tracking issue.", + ) + parser.add_argument( + "--pr", + type=int, + required=False, + help="The merged preparation PR number.", + ) + parser.add_argument( + "--issue", + type=int, + required=False, + help="The release tracking issue number.", + ) + parser.set_defaults(command=cls.run_from_args) + + @classmethod + def run_from_args(cls, args): + """Instantiates and runs the command from parsed args.""" + gh = GitHub() + return cls(args, gh).run() diff --git a/tools/private/release/complete_sync_changelog.py b/tools/private/release/complete_sync_changelog.py new file mode 100644 index 0000000000..9c8d9028f0 --- /dev/null +++ b/tools/private/release/complete_sync_changelog.py @@ -0,0 +1,97 @@ +"""Subcommand to mark sync changelog tasks as complete.""" + +import re + +from tools.private.release.gh import GitHub +from tools.private.release.release_issue import ( + parse_checklist_state, + update_task_in_body, +) + + +class CompleteSyncChangelog: + """Class to mark sync changelog tasks as complete.""" + + def __init__(self, args, gh: GitHub): + self.args = args + self.gh = gh + + def run(self) -> int: + """Executes the complete-sync-changelog subcommand.""" + args = self.args + print(f"Completing sync changelog for PR #{args.pr}...") + + pr_info = self.gh.get_pr_info(args.pr) + if not pr_info or pr_info.get("state") != "MERGED": + state = pr_info.get("state", "UNKNOWN") + print(f"Error: PR #{args.pr} is not merged yet (state: {state}).") + return 1 + + # Resolve issue number from PR body using Release-Tracking-Issue: # + pr_body = pr_info.get("body") or "" + match = re.search(r"Release-Tracking-Issue:\s*#(\d+)", pr_body) + if not match: + print( + f"Error: Could not find 'Release-Tracking-Issue: #' in" + f" PR #{args.pr} body: {pr_body}" + ) + return 1 + + issue_num = int(match.group(1)) + print(f"Resolved tracking issue #{issue_num} from PR #{args.pr} body.") + + commit_sha = pr_info["mergeCommit"]["oid"] + short_commit = commit_sha[:8] + print( + f"PR #{args.pr} merged at commit {commit_sha}. Updating tracking issue..." + ) + + # Update checklist: mark all Sync Changelog tasks pointing to this PR as done + body = self.gh.get_issue_body(issue_num) + state = parse_checklist_state(body) + sync_changelogs = state.get("sync_changelogs", {}) + + updated_any = False + for pr_num, task in sync_changelogs.items(): + # Check if this task points to our merged PR + task_pr = task.metadata.get("pr") + if task_pr == f"#{args.pr}": + print(f"Marking task '{task.name}' as complete...") + metadata = { + "status": "done", + "pr": f"#{args.pr}", + "commit": short_commit, + } + body = update_task_in_body( + body, task.name, checked=True, metadata=metadata + ) + updated_any = True + + if not updated_any: + print(f"Warning: No 'Sync Changelog' tasks found pointing to PR #{args.pr}") + return 0 + + self.gh.update_issue_body(issue_num, body) + print("Sync changelog tasks marked complete successfully!") + return 0 + + @classmethod + def add_parser(cls, subparsers): + """Adds parser for complete-sync-changelog subcommand.""" + parser = subparsers.add_parser( + "complete-sync-changelog", + help="Mark the Sync Changelog tasks as complete in the tracking issue.", + ) + parser.add_argument( + "--pr", + type=int, + required=True, + help="The merged sync changelog PR number.", + ) + parser.set_defaults(command=cls.run_from_args) + + @classmethod + def run_from_args(cls, args): + """Instantiates and runs the command from parsed args.""" + gh = GitHub() + return cls(args, gh).run() diff --git a/tools/private/release/create_rc.py b/tools/private/release/create_rc.py new file mode 100644 index 0000000000..0987bb3e43 --- /dev/null +++ b/tools/private/release/create_rc.py @@ -0,0 +1,211 @@ +"""Subcommand to tag and push the next release candidate.""" + +import os +import traceback +from argparse import Namespace + +from tools.private.release.gh import GH_REACTION_THUMBS_DOWN, GitHub +from tools.private.release.git import Git +from tools.private.release.process_backports import ProcessBackports +from tools.private.release.release_issue import ( + RELEASE_TITLE_RE, + add_rc_task_to_body, + parse_backports, + parse_checklist_state, + update_task_in_body, +) +from tools.private.release.utils import ( + REPO_URL, + get_latest_rc_tag, + set_github_output, +) + + +class CreateRc: + """Class to tag and push the next release candidate.""" + + def __init__(self, args, git: Git, gh: GitHub): + self.args = args + self.git = git + self.gh = gh + + def run(self) -> int: + """Executes the create-rc subcommand.""" + args = self.args + exit_code = 0 + try: + exit_code = self._run_internal() + except Exception as e: + print(f"Unexpected error: {e}") + traceback.print_exc() + exit_code = 1 + + if exit_code != 0 and args.triggering_comment: + print(f"Reacting with thumbs-down to comment {args.triggering_comment}...") + try: + self.gh.add_comment_reaction( + args.triggering_comment, GH_REACTION_THUMBS_DOWN + ) + except Exception as e: + print(f"Failed to add reaction to comment: {e}") + + return exit_code + + def _run_internal(self) -> int: + """Internal implementation of create-rc.""" + args = self.args + + # Try to process pending backports first + print("Processing pending backports before creating RC...") + backport_args = Namespace( + issue=args.issue, + remote=args.remote, + add=None, + triggering_comment=None, + dry_run=False, + ) + pb = ProcessBackports(backport_args, self.git, self.gh) + backports_exit_code = pb.run() + if backports_exit_code != 0: + print("Error: Processing backports failed. Aborting RC creation.") + return backports_exit_code + + body = self.gh.get_issue_body(args.issue) + state = parse_checklist_state(body) + + if ( + state["prepare_release"].status != "done" + or state["create_branch"].status != "done" + ): + print( + "Error: Preconditions not met (release must be prepared and" + " branch created)." + ) + return 1 + + # Gating: RC tagging is blocked if any backport is unchecked OR does not have status=done + backports = parse_backports(body) + conflicting_or_pending = [ + b + for b in backports + if (b.checked and b.status != "done") + or (not b.checked and b.status != "ignore") + ] + if conflicting_or_pending: + print( + f"Gating RC tagging: {len(conflicting_or_pending)} backports" + " are still unfinished, failed, or in conflict." + ) + return 1 + + # Resolve version and branch + issue_title = self.gh.get_issue_title(args.issue) + version_match = RELEASE_TITLE_RE.search(issue_title) + if not version_match: + print(f"Error: Could not parse version from issue title: {issue_title}") + return 1 + + version = version_match.group(1) + branch_version = ".".join(version.split(".")[:2]) + branch_name = f"release/{branch_version}" + + # Determine next RC tag + self.git.fetch(args.remote) + self.git.fetch(args.remote, tags=True, force=True) + latest_rc = get_latest_rc_tag(version, remote=args.remote) + + if not latest_rc: + next_rc_num = 0 + next_rc = f"{version}-rc0" + else: + rc_num = int(latest_rc.split("-rc")[-1]) + next_rc_num = rc_num + 1 + next_rc = f"{version}-rc{next_rc_num}" + + # Precheck: next RC number must exist and be unchecked in the checklist + rc_tags = state.get("rc_tags", {}) + if next_rc_num not in rc_tags: + print(f"Task 'Tag RC{next_rc_num}' not found in checklist. Adding it...") + body = add_rc_task_to_body(body, next_rc_num) + self.gh.update_issue_body(args.issue, body) + else: + target_rc_task = rc_tags[next_rc_num] + if target_rc_task.checked or target_rc_task.status == "done": + print( + f"Error: Task 'Tag RC{next_rc_num}' is already marked done in" + " the checklist." + ) + return 1 + + target_ref = f"{args.remote}/{branch_name}" + commit_sha = self.git.get_commit_sha(target_ref) + + print(f"Tagging and pushing next RC: {next_rc}...") + self.git.tag(next_rc, target_ref) + self.git.push(args.remote, next_rc) + + set_github_output("tag_name", next_rc) + + # Check off the appropriate "Tag RC{N}" task in the checklist + print(f"Checking off Tag RC{next_rc_num} task...") + metadata = {"status": "done", "tag": next_rc, "commit": commit_sha[:8]} + task_name = f"Tag RC{next_rc_num}" + updated_body = update_task_in_body( + body, task_name, checked=True, metadata=metadata + ) + self.gh.update_issue_body(args.issue, updated_body) + + tag_url = f"{REPO_URL}/releases/tag/{next_rc}" + bcr_entry_url = f"https://registry.bazel.build/modules/rules_python/{next_rc}" + bcr_search_url = f"https://github.com/bazelbuild/bazel-central-registry/pulls?q=is%3Apr+rules_python+{next_rc}" + if run_id := os.environ.get("GITHUB_RUN_ID"): + release_workflow_url = f"{REPO_URL}/actions/runs/{run_id}" + else: + release_workflow_url = ( + f"{REPO_URL}/actions/workflows/release_create_rc.yaml" + ) + branch_url = f"{REPO_URL}/tree/{branch_name}" + comment_body = f"""**New Release Candidate Tagged!** 🐍🌿 + +Release Candidate **{next_rc}** has been successfully generated and tagged on branch [`{branch_name}`]({branch_url}). + +- [Github Release {next_rc}]({tag_url}) +- [BCR Entry {next_rc}]({bcr_entry_url}) +- [BCR PRs]({bcr_search_url}) +- [Release workflow status]({release_workflow_url})""" + self.gh.post_issue_comment(args.issue, comment_body) + print("RC creation completed successfully!") + return 0 + + @classmethod + def add_parser(cls, subparsers): + """Adds parser for create-rc subcommand.""" + parser = subparsers.add_parser( + "create-rc", + help="Tags the next RC on the release branch if no backports remain.", + ) + parser.add_argument( + "--issue", + type=int, + required=True, + help="The tracking issue number (required).", + ) + parser.add_argument( + "--remote", + type=str, + required=True, + help="The git remote to push the RC tag to (required).", + ) + parser.add_argument( + "--triggering-comment", + type=int, + help="The ID of the comment that triggered this run (optional).", + ) + parser.set_defaults(command=cls.run_from_args) + + @classmethod + def run_from_args(cls, args): + """Instantiates and runs the command from parsed args.""" + git = Git(".") + gh = GitHub() + return cls(args, git, gh).run() diff --git a/tools/private/release/create_release_branch.py b/tools/private/release/create_release_branch.py new file mode 100644 index 0000000000..7a9d7eec37 --- /dev/null +++ b/tools/private/release/create_release_branch.py @@ -0,0 +1,131 @@ +"""Subcommand to create a release branch from a merged PR commit.""" + +from tools.private.release.gh import GitHub +from tools.private.release.git import Git +from tools.private.release.release_issue import ( + RELEASE_TITLE_RE, + parse_checklist_state, + update_task_in_body, +) +from tools.private.release.utils import REPO_URL + + +class CreateReleaseBranch: + """Class to create a release branch from a merged PR commit.""" + + def __init__(self, args, git: Git, gh: GitHub): + self.args = args + self.git = git + self.gh = gh + + def run(self) -> int: + """Executes the create-release-branch subcommand.""" + args = self.args + print(f"Evaluating branch creation for tracking issue #{args.issue}...") + body = self.gh.get_issue_body(args.issue) + state = parse_checklist_state(body) + + if ( + state["prepare_release"].status != "done" + or not state["prepare_release"].commit + ): + print( + "Error: Prepare Release task is not marked 'done' with a valid" + " commit SHA." + ) + return 1 + + if state["create_branch"].checked: + print("Release branch has already been created and checked. Skipping.") + return 0 + + # Extract version from issue title + issue_title = self.gh.get_issue_title(args.issue) + version_match = RELEASE_TITLE_RE.search(issue_title) + if not version_match: + print(f"Error: Could not parse version from issue title: {issue_title}") + return 1 + + version = version_match.group(1) + branch_version = ".".join(version.split(".")[:2]) + branch_name = f"release/{branch_version}" + + commit_sha = state["prepare_release"].commit + print(f"Cutting branch {branch_name} from commit {commit_sha}...") + + # Create and push branch without affecting local checkout + self.git.fetch(args.remote) + + if self.git.remote_branch_exists(args.remote, branch_name): + remote_ref = f"{args.remote}/{branch_name}" + remote_sha = self.git.get_commit_sha(remote_ref) + if remote_sha == commit_sha: + print( + f"Branch {branch_name} already exists on {args.remote} and" + f" points to {commit_sha}. Skipping push." + ) + elif self.git.is_ancestor(remote_ref, commit_sha): + print( + f"Branch {branch_name} exists on {args.remote} but can be" + f" fast-forwarded to {commit_sha}. Pushing..." + ) + ref_spec = f"{commit_sha}:refs/heads/{branch_name}" + self.git.push(args.remote, ref_spec) + else: + print( + f"Error: Branch {branch_name} already exists on" + f" {args.remote} at {remote_sha[:8]}, which is not an" + f" ancestor of {commit_sha[:8]}. Cannot fast-forward." + ) + return 1 + else: + print(f"Branch {branch_name} does not exist on {args.remote}. Pushing...") + ref_spec = f"{commit_sha}:refs/heads/{branch_name}" + self.git.push(args.remote, ref_spec) + print( + f"Successfully pushed branch {branch_name} pointing to" + f" {commit_sha} to {args.remote}" + ) + + # Update tracking issue checklist + print("Updating tracking issue checklist...") + branch_url = f"{REPO_URL}/tree/{branch_name}" + metadata = { + "status": "done", + "branch_url": branch_url, + "commit": commit_sha[:8], + } + updated_body = update_task_in_body( + body, "Create Release branch", checked=True, metadata=metadata + ) + self.gh.update_issue_body(args.issue, updated_body) + print("Create Release branch task marked complete successfully!") + return 0 + + @classmethod + def add_parser(cls, subparsers): + """Adds parser for create-release-branch subcommand.""" + parser = subparsers.add_parser( + "create-release-branch", + help="Create the release branch pointing to the merged PR commit.", + ) + parser.add_argument( + "--issue", + type=int, + required=True, + help="The tracking issue number (required).", + ) + parser.add_argument( + "--remote", + type=str, + required=True, + help="The git remote to create the branch on (required).", + ) + parser.set_defaults(command=cls.run_from_args) + + @classmethod + def run_from_args(cls, args): + """Instantiates and runs the command from parsed args.""" + git = Git(".") + gh = GitHub() + return cls(args, git, gh).run() diff --git a/tools/private/release/create_release_issue.py b/tools/private/release/create_release_issue.py new file mode 100644 index 0000000000..72b71aa716 --- /dev/null +++ b/tools/private/release/create_release_issue.py @@ -0,0 +1,67 @@ +"""Subcommand to create a release tracking issue.""" + +import pathlib +import re + +from tools.private.release.gh import GitHub +from tools.private.release.utils import determine_next_version, semver_type + + +class CreateReleaseIssue: + """Class to create a release tracking issue.""" + + def __init__(self, args, gh: GitHub): + self.args = args + self.gh = gh + + def run(self) -> int: + """Executes the create-release-issue subcommand.""" + version = self.args.version + if version is None: + version = determine_next_version() + + # Concurrency check + open_issues = self.gh.get_open_tracking_issues() + if open_issues: + print("Error: A release is already in progress. Active tracking issues:") + for issue in open_issues: + print(f"- {issue['title']}: {issue['url']}") + return 1 + + template_path = pathlib.Path( + ".github/ISSUE_TEMPLATE/release_tracking_template.md" + ) + if not template_path.exists(): + raise FileNotFoundError(f"Template file not found at {template_path}") + template_content = template_path.read_text(encoding="utf-8") + + is_first_release = version.endswith(".0") + if not is_first_release: + # Patch release: remove RC tasks + lines = template_content.splitlines() + lines = [line for line in lines if not re.search(r"Tag RC\d+", line)] + template_content = "\n".join(lines) + + issue_num = self.gh.create_release_tracking_issue(version, template_content) + print(f"Created tracking issue #{issue_num} for v{version}") + return 0 + + @classmethod + def add_parser(cls, subparsers): + """Adds parser for create-release-issue subcommand.""" + parser = subparsers.add_parser( + "create-release-issue", + help="Search for open releases and create a new tracking issue.", + ) + parser.add_argument( + "--version", + type=semver_type, + help="The release version (e.g., 0.38.0). If not provided, determined automatically.", + ) + parser.set_defaults(command=cls.run_from_args) + + @classmethod + def run_from_args(cls, args): + """Instantiates and runs the command from parsed args.""" + gh = GitHub() + return cls(args, gh).run() diff --git a/tools/private/release/determine_next_version.py b/tools/private/release/determine_next_version.py new file mode 100644 index 0000000000..541daddfe7 --- /dev/null +++ b/tools/private/release/determine_next_version.py @@ -0,0 +1,30 @@ +"""Subcommand to determine the next version.""" + +from tools.private.release.utils import determine_next_version + + +class DetermineNextVersion: + """Class to determine the next version.""" + + def __init__(self, args, git=None, gh=None): + self.args = args + + def run(self) -> int: + """Executes the determine-next-version subcommand.""" + version = determine_next_version() + print(version) + return 0 + + @classmethod + def add_parser(cls, subparsers): + """Adds parser for determine-next-version subcommand.""" + parser = subparsers.add_parser( + "determine-next-version", + help="Determine the next version and print it, without making any changes.", + ) + parser.set_defaults(command=cls.run_from_args) + + @classmethod + def run_from_args(cls, args): + """Instantiates and runs the command from parsed args.""" + return cls(args).run() diff --git a/tools/private/release/gh.py b/tools/private/release/gh.py new file mode 100644 index 0000000000..c21041384e --- /dev/null +++ b/tools/private/release/gh.py @@ -0,0 +1,542 @@ +"""GitHub CLI helper functions for the release tool.""" + +import enum +import json +import os +import re +import tempfile +from typing import TypedDict + +from tools.private.release.release_issue import BackportTask +from tools.private.release.shell import run_cmd + +# GitHub label types +RELEASE_LABEL = "type: release" +BACKPORT_LABEL = "type: backport-pr" +RELEASE_PREPARED_LABEL = "release-prepared" + +# GitHub reaction types +# See: https://docs.github.com/en/rest/reactions/reactions?apiVersion=2022-11-28#about-reactions +GH_REACTION_THUMBS_UP = "+1" +GH_REACTION_THUMBS_DOWN = "-1" +GH_REACTION_LAUGH = "laugh" +GH_REACTION_CONFUSED = "confused" +GH_REACTION_HEART = "heart" +GH_REACTION_HOORAY = "hooray" +GH_REACTION_ROCKET = "rocket" +GH_REACTION_EYES = "eyes" + + +class BackportTaskStatus(str, enum.Enum): + """Status strings for backport tasks on a release tracking issue.""" + + PENDING = "pending" + DONE = "done" + RESOLVED = "resolved" + OPEN_PR = "open-pr" + DRAFT_PR = "draft-pr" + ERROR_NOT_FOUND = "error-not-found" + ERROR_CLOSED_PR = "error-closed-pr" + ERROR_NO_MERGE_COMMIT = "error-no-merge-commit" + ERROR_UNKNOWN = "error-unknown" + ERROR_RESOLUTION_FAILED = "error-resolution-failed" + ERROR_MERGE_CONFLICT = "error-merge-conflict" + ERROR_INVALID_PR = "error-invalid-pr" + IGNORE = "ignore" + + def __str__(self) -> str: + return self.value + + +class IssueDict(TypedDict, total=False): + """In-memory representation of a GitHub Issue object. + + See GitHub API docs: + https://docs.github.com/en/rest/issues/issues#get-an-issue + """ + + number: int + title: str + body: str + labels: list[str] + url: str + + +class AutoMergeDict(TypedDict, total=False): + """Representation of auto-merge status on a Pull Request. + + See GitHub API docs: + https://docs.github.com/en/rest/pulls/pulls#get-a-pull-request + """ + + merge_method: str + + +class PrDict(TypedDict, total=False): + """In-memory representation of a GitHub Pull Request object. + + See GitHub API docs: + https://docs.github.com/en/rest/pulls/pulls#get-a-pull-request + """ + + number: int + title: str + body: str + base: str + head: str + labels: list[str] + url: str + state: str + isDraft: bool + mergeCommit: dict[str, str] + auto_merge: AutoMergeDict | None + + +class MultipleTrackingIssuesError(ValueError): + """Raised when multiple open tracking issues are found for a version.""" + + pass + + +class NoTrackingIssueError(ValueError): + """Raised when no open tracking issue is found for a version.""" + + pass + + +class GitHub: + """GitHub CLI helper class for the release tool.""" + + def __init__(self, repo: str = "bazel-contrib/rules_python"): + """Initializes the GitHub helper. + + Args: + repo: The GitHub repository to operate on. + """ + self.repo = repo + + def _run_gh( + self, *args: str, check: bool = True, capture_output: bool = True + ) -> str | None: + """Runs a 'gh' command. + + Args: + *args: Arguments for 'gh' (excluding 'gh'). + check: If True, raises CalledProcessError on failure. + capture_output: If True, captures and returns stdout. + + Returns: + The stdout of the command, stripped, or None. + """ + return run_cmd("gh", *args, check=check, capture_output=capture_output) + + def _gh_issue( + self, *args: str, check: bool = True, capture_output: bool = True + ) -> str | None: + """Runs a 'gh issue' command.""" + return self._run_gh( + "issue", + *args, + f"--repo={self.repo}", + check=check, + capture_output=capture_output, + ) + + def _gh_pr( + self, *args: str, check: bool = True, capture_output: bool = True + ) -> str | None: + """Runs a 'gh pr' command.""" + return self._run_gh( + "pr", + *args, + f"--repo={self.repo}", + check=check, + capture_output=capture_output, + ) + + def list_issues( + self, + *, + fields: str, + label: str | None = None, + state: str | None = None, + search: str | None = None, + ) -> list[IssueDict]: + """Helper to list issues using gh CLI. + + Args: + fields: Comma-separated list of fields to return. + label: Filter by label. + state: Filter by state ('open', 'closed', 'all'). + search: Search query. + + Returns: + A list of issue dictionaries. + """ + cmd = ["list", f"--json={fields}"] + if label: + cmd.append(f"--label={label}") + if state: + cmd.append(f"--state={state}") + if search: + cmd.append(f"--search={search}") + + output = self._gh_issue(*cmd) + return json.loads(output) if output else [] + + def get_open_tracking_issues(self, version: str | None = None) -> list[IssueDict]: + """Finds open tracking issues for release. + + Args: + version: Optional specific version to match (e.g., "1.0.0"). + + Returns: + List of matching open release tracking issue dictionaries. + """ + search = f"Release {version}" if version else None + return self.list_issues( + fields="number,title,url", + label=RELEASE_LABEL, + state="open", + search=search, + ) + + def get_release_tracking_issue(self, version: str) -> int: + """Finds the single open tracking issue for a given version. + + Args: + version: Version string (e.g. "1.0.0"). + + Returns: + The issue number. + + Raises: + NoTrackingIssueError: If no open tracking issue is found. + MultipleTrackingIssuesError: If multiple open tracking issues are found. + """ + issues = self.get_open_tracking_issues(version) + matching = [i for i in issues if i["title"] == f"Release {version}"] + if not matching: + raise NoTrackingIssueError( + f"No open tracking issue found for Release {version}" + ) + if len(matching) > 1: + raise MultipleTrackingIssuesError( + f"Multiple open tracking issues found for Release {version}: " + + ", ".join(str(i["number"]) for i in matching) + ) + return matching[0]["number"] + + def create_issue( + self, title: str, body: str, labels: list[str] | None = None + ) -> int: + """Creates an issue using gh CLI. + + Args: + title: Title of the issue. + body: Body of the issue. + labels: List of labels to add. + + Returns: + The issue number. + """ + cmd = ["create", f"--title={title}", f"--body={body}"] + if labels: + for label in labels: + cmd.append(f"--label={label}") + + output = self._gh_issue(*cmd) + if not output: + raise RuntimeError("gh issue create returned no output") + # output is URL: https://github.com/owner/repo/issues/123 + return int(output.rstrip("/").split("/")[-1]) + + def create_release_tracking_issue(self, version: str, template_content: str) -> int: + """Creates a release tracking issue from a template. + + Args: + version: Release version string (e.g., "1.0.0"). + template_content: Content of the issue template markdown file. + + Returns: + The created issue number. + """ + title = f"Release {version}" + # Strip YAML frontmatter if present + issue_body = template_content + if template_content.startswith("---"): + parts = template_content.split("---", 2) + if len(parts) >= 3: + issue_body = parts[2].strip() + + return self.create_issue(title=title, body=issue_body, labels=[RELEASE_LABEL]) + + def get_issue_body(self, issue_num: int) -> str: + """Gets the body content of an issue. + + Args: + issue_num: The issue number. + + Returns: + The body string of the issue. + """ + output = self._gh_issue("view", str(issue_num), "--json=body") + if not output: + return "" + data = json.loads(output) + return data.get("body", "") + + def get_issue_title(self, issue_num: int) -> str: + """Gets the title of an issue. + + Args: + issue_num: The issue number. + + Returns: + The title string of the issue. + """ + output = self._gh_issue("view", str(issue_num), "--json=title") + if not output: + return "" + data = json.loads(output) + return data.get("title", "") + + def update_issue_body(self, issue_num: int, body: str) -> None: + """Updates the body of an issue. + + Args: + issue_num: The issue number. + body: The new body content. + """ + with tempfile.NamedTemporaryFile(mode="w", delete=False, encoding="utf-8") as f: + f.write(body) + f.flush() + temp_path = f.name + + try: + self._gh_issue( + "edit", str(issue_num), f"--body-file={temp_path}", capture_output=False + ) + finally: + if os.path.exists(temp_path): + os.remove(temp_path) + + def resolve_pr_number(self, pr_ref: str) -> int: + """Resolves a PR reference (number, #number, or GitHub URL) to a PR number. + + Args: + pr_ref: PR number string (e.g., "123", "#123") or URL. + + Returns: + The integer PR number. + + Raises: + ValueError: If the PR reference cannot be resolved or is for another repo. + """ + clean_ref = pr_ref.lstrip("#") + if clean_ref.isdigit(): + return int(clean_ref) + + if pr_ref.startswith("http"): + pattern = rf"github\.com/{re.escape(self.repo)}/pull/(\d+)(/|\?|\Z)" + match = re.search(pattern, pr_ref, re.IGNORECASE) + if match: + return int(match.group(1)) + raise ValueError( + f"URL is not for the configured repository ({self.repo}): {pr_ref}" + ) + + raise ValueError(f"Could not resolve PR reference: {pr_ref}") + + def get_pr_info(self, pr_num: int) -> PrDict: + """Gets info about a PR using gh CLI. + + Args: + pr_num: The PR number. + + Returns: + Dictionary containing PR fields (state, isDraft, mergeCommit, etc.). + """ + output = self._gh_pr("view", str(pr_num), "--json=state,isDraft,mergeCommit") + return json.loads(output) if output else {} + + def get_pr_comments(self, pr_num: int) -> list[dict]: + """Gets all comments for a PR using gh CLI. + + Args: + pr_num: The PR number. + + Returns: + List of comment objects (with body, author, etc.). + """ + output = self._gh_pr("view", str(pr_num), "--json=comments") + if not output: + return [] + data = json.loads(output) + return data.get("comments", []) + + def create_pr( + self, + title: str, + body: str, + base: str = "main", + labels: list[str] | None = None, + ) -> str: + """Creates a pull request. + + Args: + title: Title of the PR. + body: Body of the PR. + base: Base branch to merge into (default: "main"). + labels: Optional list of labels to add. + + Returns: + The URL of the created PR. + """ + cmd = [ + "create", + f"--title={title}", + f"--body={body}", + f"--base={base}", + ] + if labels: + for label in labels: + cmd.append(f"--label={label}") + output = self._gh_pr(*cmd) + return output if output else "" + + def enable_auto_merge(self, pr_num: int, method: str = "squash") -> None: + """Enables auto-merge for a PR. + + Args: + pr_num: The PR number. + method: The merge method ('squash', 'rebase', or 'merge'). + """ + cmd = ["merge", str(pr_num), "--auto"] + if method == "squash": + cmd.append("--squash") + elif method == "rebase": + cmd.append("--rebase") + elif method == "merge": + cmd.append("--merge") + self._gh_pr(*cmd, capture_output=False) + + def get_open_pr(self, branch_name: str) -> PrDict | None: + """Finds an open PR for the given branch. + + Args: + branch_name: The head branch name to search for. + + Returns: + Dictionary with 'number' and 'url' if an open PR exists, else None. + """ + cmd = [ + "list", + f"--head={branch_name}", + "--state=open", + "--json=number,url", + ] + output = self._gh_pr(*cmd) + prs = json.loads(output) if output else [] + return prs[0] if prs else None + + def post_issue_comment(self, issue_num: int, comment_body: str) -> None: + """Posts a comment on an issue or PR. + + Args: + issue_num: The issue or PR number. + comment_body: The body content of the comment. + """ + self._gh_issue( + "comment", + str(issue_num), + f"--body={comment_body}", + capture_output=False, + ) + + def add_comment_reaction(self, comment_id: int, reaction: str) -> None: + """Adds a reaction to an issue or PR comment. + + Args: + comment_id: The comment ID (note: gh api endpoint needed for comment reactions). + reaction: The reaction type (e.g., "+1", "-1", "rocket"). + """ + self._run_gh( + "api", + f"repos/{self.repo}/issues/comments/{comment_id}/reactions", + "-f", + f"content={reaction}", + capture_output=False, + ) + + def get_merge_commits_for_prs( + self, pending_items: list[BackportTask] + ) -> list[BackportTask]: + """Resolves PR references in pending backports to their merge commit SHAs. + + Updates item.status based on PR state if it cannot be resolved. + + Args: + pending_items: A list of BackportTask items to resolve. + + Returns: + The list of resolved BackportTask items. + """ + return resolve_merge_commits_for_prs(self, pending_items) + + +def resolve_merge_commits_for_prs( + gh_client: GitHub, pending_items: list[BackportTask] +) -> list[BackportTask]: + """Resolves PR references in pending backports to their merge commit SHAs. + + Updates item.status based on PR state if it cannot be resolved. + + Args: + gh_client: The GitHub client. + pending_items: A list of BackportTask items to resolve. + + Returns: + The list of resolved BackportTask items. + """ + resolved_items = [] + for item in pending_items: + pr_num = int(item.pr_ref.lstrip("#")) + print(f"Resolving PR #{pr_num} to merge commit...") + try: + pr_info = gh_client.get_pr_info(pr_num) + if not pr_info: + print(f"PR #{pr_num} not found. Gating.") + item.status = BackportTaskStatus.ERROR_NOT_FOUND + else: + state = pr_info.get("state") + is_draft = pr_info.get("isDraft", False) + if state == "OPEN" or is_draft: + print( + f"PR #{pr_num} is open or draft (state: {state}," + f" draft: {is_draft}). Ignoring." + ) + item.status = ( + BackportTaskStatus.OPEN_PR + if not is_draft + else BackportTaskStatus.DRAFT_PR + ) + elif state == "CLOSED": + print(f"PR #{pr_num} is closed but not merged. Gating.") + item.status = BackportTaskStatus.ERROR_CLOSED_PR + elif state == "MERGED": + merge_commit = pr_info.get("mergeCommit") + if merge_commit and "oid" in merge_commit: + item.commit = merge_commit["oid"] + item.status = BackportTaskStatus.RESOLVED + else: + print(f"PR #{pr_num} has no merge commit SHA. Gating.") + item.status = BackportTaskStatus.ERROR_NO_MERGE_COMMIT + else: + print(f"PR #{pr_num} has unknown state: {state}. Gating.") + item.status = BackportTaskStatus.ERROR_UNKNOWN + except Exception as e: + print(f"Error resolving PR #{pr_num}: {e}. Gating.") + item.status = BackportTaskStatus.ERROR_RESOLUTION_FAILED + resolved_items.append(item) + return resolved_items diff --git a/tools/private/release/git.py b/tools/private/release/git.py new file mode 100644 index 0000000000..3e7e3cb893 --- /dev/null +++ b/tools/private/release/git.py @@ -0,0 +1,421 @@ +"""Git helper functions for the release tool.""" + +import subprocess + +from tools.private.release.shell import run_cmd + + +class Git: + """Git helper class for the release tool. + + Operates on a specific git repository path. + """ + + def __init__(self, repo: str): + """Initializes the Git helper. + + Args: + repo: The path to the git repository. + """ + self._repo = repo + + def _run_git( + self, *args: str, check: bool = True, capture_output: bool = True + ) -> str | None: + """Runs a git command in the repository directory. + + Args: + *args: Arguments passed to the git command. + check: If True, raises CalledProcessError on failure. + capture_output: If True, captures and returns stdout. + + Returns: + The stdout of the command, stripped, or None if capture_output is + False. + """ + return run_cmd( + "git", + *args, + check=check, + capture_output=capture_output, + cwd=self._repo, + ) + + def get_tags(self) -> list[str]: + """Returns a list of all git tags in the repository. + + Returns: + A list of tag names (strings). + """ + output = self._run_git("tag") + return output.splitlines() if output else [] + + def checkout( + self, + ref: str, + create_branch: bool = False, + track_remote: str | None = None, + ) -> None: + """Checks out a git reference (tag, branch, or commit). + + Args: + ref: The git reference (tag, branch, or commit) to checkout. + create_branch: If True, creates the branch before checking it out. + track_remote: If specified, checks out the branch tracking this + remote's corresponding branch. + """ + cmd = ["checkout"] + if create_branch: + cmd.append("-b") + + should_reset_hard = False + if track_remote: + if self.branch_exists(ref): + cmd.append(ref) + should_reset_hard = True + else: + cmd.extend(["--track", f"{track_remote}/{ref}"]) + else: + cmd.append(ref) + self._run_git(*cmd, capture_output=False) + + if should_reset_hard: + self.reset_hard(reset_to=f"{track_remote}/{ref}") + + def add(self, *files: str) -> None: + """Stages files for commit. + + Args: + *files: Paths to files to stage. + """ + self._run_git("add", *files, capture_output=False) + + def add_modified_and_deleted(self) -> None: + """Stages all modified and deleted tracked files.""" + self._run_git("add", "--update", capture_output=False) + + def commit(self, message: str, amend: bool = False, no_edit: bool = False) -> None: + """Commits staged changes, optionally amending the previous commit. + + Args: + message: The commit message. + amend: If True, amends the previous commit. + no_edit: If True, uses the existing commit message without editing. + """ + cmd = ["commit"] + if amend: + cmd.append("--amend") + if no_edit: + cmd.append("--no-edit") + if message: + cmd.extend(["-m", message]) + self._run_git(*cmd, capture_output=False) + + def push( + self, + remote: str, + ref: str, + set_upstream: bool = False, + force: bool = False, + ) -> None: + """Pushes a reference to a remote repository. + + Args: + remote: The remote repository name (e.g., 'origin'). + ref: The reference to push (e.g., a branch name). + set_upstream: If True, sets the upstream tracking branch. + force: If True, force pushes the changes. + """ + cmd = ["push"] + if set_upstream: + cmd.append("--set-upstream") + if force: + cmd.append("--force") + cmd.extend([remote, ref]) + self._run_git(*cmd, capture_output=False) + + def fetch( + self, + remote: str = "origin", + refspec: str | None = None, + tags: bool = False, + force: bool = False, + ) -> None: + """Fetches updates from a remote repository. + + Args: + remote: The remote repository name. Defaults to 'origin'. + refspec: The refspec to fetch. + tags: If True, fetches all tags. + force: If True, force fetches updates. + """ + cmd = ["fetch", remote] + if refspec: + cmd.append(refspec) + if tags: + cmd.append("--tags") + if force: + cmd.append("--force") + self._run_git(*cmd, capture_output=False) + + def merge(self, commit_ref: str, ff_only: bool = True) -> None: + """Merges a commit into the current branch. + + Args: + commit_ref: The commit reference to merge. + ff_only: If True, only allows fast-forward merges. + """ + cmd = ["merge", commit_ref] + if ff_only: + cmd.append("--ff-only") + self._run_git(*cmd, capture_output=False) + + def tag(self, tag_name: str, commit_ref: str) -> None: + """Creates a local tag pointing to a specific commit. + + Args: + tag_name: The name of the tag to create. + commit_ref: The commit reference the tag should point to. + """ + self._run_git("tag", tag_name, commit_ref, capture_output=False) + + def cherry_pick(self, sha: str) -> None: + """Cherry-picks a commit. + + Args: + sha: The commit SHA to cherry-pick. + """ + self._run_git("cherry-pick", "-x", sha, capture_output=False) + + def cherry_pick_abort(self) -> None: + """Aborts an in-progress cherry-pick operation.""" + self._run_git("cherry-pick", "--abort", capture_output=False) + + def reset_hard(self, *, reset_to: str = "HEAD") -> None: + """Resets the index and working tree to a specific reference. + + Args: + reset_to: The git reference to reset to. Defaults to 'HEAD'. + """ + self._run_git("reset", "--hard", reset_to, capture_output=False) + + def status(self) -> str: + """Returns the output of git status --porcelain. + + Returns: + The porcelain status output. + """ + output = self._run_git("status", "--porcelain") + return output if output else "" + + def get_commit_sha(self, ref: str = "HEAD", short: bool = False) -> str: + """Returns the commit SHA of a given reference. + + Args: + ref: The git reference. Defaults to 'HEAD'. + short: If True, returns a short SHA. + + Returns: + The commit SHA. + """ + cmd = ["rev-parse"] + if short: + cmd.append("--short") + cmd.append(ref) + output = self._run_git(*cmd) + return output if output else "" + + def get_commit_message(self, ref: str = "HEAD") -> str: + """Returns the commit message of a given reference. + + Args: + ref: The git reference. Defaults to 'HEAD'. + + Returns: + The commit message. + """ + output = self._run_git("log", "-1", "--format=%B", ref) + return output if output else "" + + def branch_exists(self, branch_name: str) -> bool: + """Returns True if a local branch exists. + + Args: + branch_name: The name of the branch to check. + + Returns: + True if the branch exists, False otherwise. + """ + try: + self._run_git("show-ref", "--verify", f"refs/heads/{branch_name}") + return True + except subprocess.CalledProcessError: + return False + + def tag_exists(self, tag_name: str) -> bool: + """Returns True if a local tag exists. + + Args: + tag_name: The name of the tag to check. + + Returns: + True if the tag exists, False otherwise. + """ + try: + self._run_git("show-ref", "--verify", f"refs/tags/{tag_name}") + return True + except subprocess.CalledProcessError: + return False + + def sort_commits_chronologically(self, shas: list[str]) -> list[str]: + """Sorts a list of commit SHAs chronologically (oldest first). + + Args: + shas: A list of commit SHAs to sort. + + Returns: + The sorted list of commit SHAs. + """ + output = self._run_git("log", "--no-walk", "--reverse", "--format=%H", *shas) + return output.splitlines() if output else [] + + def get_current_branch(self) -> str: + """Returns the current git branch name. + + Returns: + The current branch name. + """ + output = self._run_git("rev-parse", "--abbrev-ref", "HEAD") + return output if output else "" + + def remote_branch_exists(self, remote: str, branch_name: str) -> bool: + """Returns True if a remote branch exists. + + Args: + remote: The name of the remote. + branch_name: The name of the branch. + + Returns: + True if the remote branch exists, False otherwise. + """ + try: + self._run_git( + "show-ref", + "--verify", + f"refs/remotes/{remote}/{branch_name}", + ) + return True + except subprocess.CalledProcessError: + return False + + def is_ancestor(self, ancestor: str, descendant: str) -> bool: + """Returns True if ancestor is an ancestor of descendant. + + Args: + ancestor: The commit reference that might be an ancestor. + descendant: The commit reference that might be a descendant. + + Returns: + True if ancestor is an ancestor of descendant, False otherwise. + """ + try: + self._run_git("merge-base", "--is-ancestor", ancestor, descendant) + return True + except subprocess.CalledProcessError: + return False + + def get_remote_tags(self, remote: str) -> list[str]: + """Returns a list of tags present on the specified remote repository. + + Args: + remote: The name of the git remote to query (e.g., 'origin', + 'upstream'). + + Returns: + A list of tag names (strings) found on the remote, excluding peeled + tags. + """ + output = self._run_git("ls-remote", "--tags", remote) + tags = [] + if not output: + return tags + for line in output.splitlines(): + if not line: + continue + parts = line.split() + if len(parts) < 2: + continue + ref = parts[1] + if ref.startswith("refs/tags/"): + tag = ref[len("refs/tags/") :] + # Skip peeled tags (e.g. tag^{}) to avoid + # duplicate tag names in the output. + if not tag.endswith("^{}"): + tags.append(tag) + return tags + + def get_modified_files(self, ref: str) -> list[str]: + """Returns a list of files modified in a given reference. + + Args: + ref: The git reference. + + Returns: + A list of file paths. + """ + output = self._run_git("show", "--name-only", "--format=", ref) + return [line for line in output.splitlines() if line.strip()] if output else [] + + def diff(self) -> str: + """Returns the diff of unstaged changes. + + Returns: + The diff output as a string. + """ + output = self._run_git("diff") + return output if output else "" + + def apply(self, patch_file: str) -> None: + """Applies a patch file. + + Args: + patch_file: The path to the patch file. + """ + self._run_git("apply", patch_file, capture_output=False) + + def apply_check(self, patch_file: str) -> bool: + """Verifies if a patch can be applied cleanly. + + Args: + patch_file: The path to the patch file. + + Returns: + True if the patch can be applied cleanly, False otherwise. + """ + try: + self._run_git("apply", "--check", patch_file, capture_output=False) + return True + except subprocess.CalledProcessError: + return False + + def get_remote_branches(self, remote: str = "origin") -> list[str]: + """Returns a list of remote branches. + + Args: + remote: The name of the remote. + + Returns: + A list of branch names (without the remote prefix). + """ + output = self._run_git("branch", "-r") + branches = [] + if not output: + return branches + for line in output.splitlines(): + line = line.strip() + if "->" in line: + continue + parts = line.split("/") + if len(parts) >= 2 and parts[0] == remote: + branches.append("/".join(parts[1:])) + return branches diff --git a/tools/private/release/mock_gh.py b/tools/private/release/mock_gh.py new file mode 100644 index 0000000000..0b5b672517 --- /dev/null +++ b/tools/private/release/mock_gh.py @@ -0,0 +1,162 @@ +"""In-memory fake for GitHub API.""" + +import re + +from tools.private.release.gh import ( + RELEASE_LABEL, + IssueDict, + MultipleTrackingIssuesError, + NoTrackingIssueError, + PrDict, + resolve_merge_commits_for_prs, +) + + +class MockGitHub: + def __init__(self, repo: str = "bazel-contrib/rules_python"): + self.repo = repo + self.issues: dict[int, IssueDict] = {} + self.next_issue_num = 1001 + self.prs: dict[int, PrDict] = {} # num -> pr_info + self.issue_comments: dict[int, list[str]] = {} + self.reactions: dict[int, list[str]] = {} + self.pr_comments: dict[int, list[dict]] = {} + + def post_issue_comment(self, issue_num: int, comment_body: str) -> None: + self.issue_comments.setdefault(issue_num, []).append(comment_body) + + def add_comment_reaction(self, comment_id: int, reaction: str) -> None: + self.reactions.setdefault(comment_id, []).append(reaction) + + def enable_auto_merge(self, pr_num: int, method: str = "squash") -> None: + if pr_num not in self.prs: + self.create_pr(title="", body="") + self.prs[pr_num]["auto_merge"] = {"merge_method": method} + + def create_issue( + self, title: str, body: str, labels: list[str] | None = None + ) -> int: + issue_num = self.next_issue_num + self.next_issue_num += 1 + self.issues[issue_num] = { + "title": title, + "body": body, + "labels": labels or [], + "number": issue_num, + "url": f"https://github.com/{self.repo}/issues/{issue_num}", + } + return issue_num + + def create_release_tracking_issue(self, version: str, template_content: str) -> int: + # Strip YAML frontmatter if present (simplified copy from gh.py) + issue_body = template_content + if template_content.startswith("---"): + parts = template_content.split("---", 2) + if len(parts) >= 3: + issue_body = parts[2].strip() + + return self.create_issue( + title=f"Release {version}", body=issue_body, labels=[RELEASE_LABEL] + ) + + def get_issue_body(self, issue_num: int) -> str: + if issue_num not in self.issues: + raise ValueError(f"Issue #{issue_num} not found in MockGitHub") + return self.issues[issue_num]["body"] + + def get_issue_title(self, issue_num: int) -> str: + if issue_num not in self.issues: + raise ValueError(f"Issue #{issue_num} not found in MockGitHub") + return self.issues[issue_num]["title"] + + def update_issue_body(self, issue_num: int, body: str): + if issue_num not in self.issues: + raise ValueError(f"Issue #{issue_num} not found in MockGitHub") + self.issues[issue_num]["body"] = body + + def resolve_pr_number(self, pr_ref: str) -> int: + # Real algorithm copy (doesn't require RPCs) + clean_ref = pr_ref.lstrip("#") + if clean_ref.isdigit(): + return int(clean_ref) + + if pr_ref.startswith("http"): + pattern = rf"github\.com/{re.escape(self.repo)}/pull/(\d+)(/|\?|\Z)" + match = re.search(pattern, pr_ref, re.IGNORECASE) + if match: + return int(match.group(1)) + raise ValueError( + f"URL is not for the configured repository ({self.repo}): {pr_ref}" + ) + raise ValueError(f"Could not resolve PR ref: {pr_ref}") + + def get_release_tracking_issue(self, version: str) -> int: + search_title = f"Release {version}" + matching = [ + num + for num, issue in self.issues.items() + if issue["title"] == search_title + and RELEASE_LABEL in issue.get("labels", []) + ] + if not matching: + raise NoTrackingIssueError( + f"No open tracking issue found for Release {version}" + ) + if len(matching) > 1: + raise MultipleTrackingIssuesError( + f"Multiple open tracking issues found for Release {version}" + ) + return matching[0] + + def create_pr( + self, + title: str, + body: str, + base: str = "main", + labels: list[str] | None = None, + ) -> str: + pr_num = self.next_issue_num + self.next_issue_num += 1 + url = f"https://github.com/{self.repo}/pull/{pr_num}" + self.prs[pr_num] = { + "title": title, + "body": body, + "base": base, + "labels": labels or [], + "number": pr_num, + "url": url, + "state": "OPEN", + } + return url + + def get_open_pr(self, branch_name: str) -> PrDict | None: + for pr in self.prs.values(): + if pr.get("head") == branch_name and pr.get("state") == "OPEN": + return pr + return None + + def get_open_tracking_issues(self, version: str | None = None) -> list[IssueDict]: + results = [] + for issue in self.issues.values(): + if RELEASE_LABEL in issue["labels"]: + if version: + if issue["title"] == f"Release {version}": + results.append(issue) + else: + results.append(issue) + return results + + def get_pr_info(self, pr_num: int) -> PrDict: + if pr_num in self.prs: + return self.prs[pr_num] + return { + "state": "MERGED", + "mergeCommit": {"oid": f"mock_merge_sha_{pr_num}"}, + } + + 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/on_pr_merged.py b/tools/private/release/on_pr_merged.py new file mode 100644 index 0000000000..8d451580fb --- /dev/null +++ b/tools/private/release/on_pr_merged.py @@ -0,0 +1,113 @@ +"""Subcommand to handle PR merge event by processing backports.""" + +import argparse +import re + +from tools.private.release.gh import GitHub +from tools.private.release.git import Git +from tools.private.release.process_backports import ProcessBackports +from tools.private.release.release_issue import parse_backports + + +class OnPrMerged: + """Class to handle PR merge event.""" + + def __init__(self, args, git: Git, gh: GitHub): + self.args = args + self.git = git + self.gh = gh + + def run(self) -> int: + """Executes the on-pr-merged subcommand.""" + args = self.args + pr_num = args.pr + pr_ref = f"#{pr_num}" + + print(f"Verifying PR {pr_ref} has backport comment...") + try: + comments = self.gh.get_pr_comments(pr_num) + has_comment = any( + re.match( + r"^\s*/backport(\s|$)", + comment.get("body") or "", + re.IGNORECASE, + ) + for comment in comments + ) + if not has_comment: + print(f"PR {pr_ref} does not have a /backport comment. Skipping.") + return 1 + except Exception as e: + print(f"Error checking PR comments: {e}") + return 1 + + print(f"Searching for active release tracking issue containing PR {pr_ref}...") + open_issues = self.gh.get_open_tracking_issues() + if not open_issues: + print("No open release tracking issues found.") + return 1 + + found_issue = None + for issue in open_issues: + issue_num = issue["number"] + body = self.gh.get_issue_body(issue_num) + backports = parse_backports(body) + + if any(item.pr_ref == pr_ref for item in backports): + if found_issue: + print( + f"Error: PR {pr_ref} found in multiple open release" + f" tracking issues: #{found_issue} and #{issue_num}" + ) + return 1 + found_issue = issue_num + + if not found_issue: + print(f"PR {pr_ref} not found in any active release tracking issue.") + return 1 + + print(f"Found PR {pr_ref} in tracking issue #{found_issue}") + + # Now run ProcessBackports for this issue + process_args = argparse.Namespace( + issue=found_issue, + remote=args.remote, + add=None, + triggering_comment=None, + dry_run=args.dry_run, + ) + print(f"Processing backports for issue #{found_issue}...") + return ProcessBackports(process_args, self.git, self.gh).run() + + @classmethod + def add_parser(cls, subparsers): + """Adds parser for on-pr-merged subcommand.""" + parser = subparsers.add_parser( + "on-pr-merged", + help="Handle PR merge event by processing backports.", + ) + parser.add_argument( + "pr", + type=int, + help="PR number that was merged.", + ) + parser.add_argument( + "--remote", + type=str, + required=True, + help="The git remote to push changes to (required).", + ) + parser.add_argument( + "--dry-run", + action=argparse.BooleanOptionalAction, + default=True, + help="Perform a dry run (default: True). Use --no-dry-run to actually execute.", + ) + parser.set_defaults(command=cls.run_from_args) + + @classmethod + def run_from_args(cls, args): + """Instantiates and runs the command from parsed args.""" + git = Git(".") + gh = GitHub() + return cls(args, git, gh).run() diff --git a/tools/private/release/prepare.py b/tools/private/release/prepare.py new file mode 100644 index 0000000000..00412a8600 --- /dev/null +++ b/tools/private/release/prepare.py @@ -0,0 +1,240 @@ +"""Subcommand to prepare the release (updates changelog, placeholders).""" + +import argparse +import datetime +import pathlib + +from tools.private.release import changelog_news +from tools.private.release.gh import ( + RELEASE_PREPARED_LABEL, + GitHub, + MultipleTrackingIssuesError, + NoTrackingIssueError, +) +from tools.private.release.git import Git +from tools.private.release.release_issue import ( + parse_checklist_state, + update_task_in_body, +) +from tools.private.release.utils import ( + determine_next_version, + replace_version_next, + semver_type, +) + + +class Prepare: + """Class to prepare the release.""" + + def __init__(self, args, git: Git, gh: GitHub): + self.args = args + self.git = git + self.gh = gh + + def run(self) -> int: + """Executes the prepare subcommand.""" + args = self.args + print("Fetching upstream to verify fresh release history...") + self.git.fetch(tags=True, force=True) + + # Run pre-check: verify there are no local edits + status = self.git.status() + if status: + print( + "Error: Local edits detected. Workspace must be completely clean" + " before running release preparation." + ) + for line in status.splitlines(): + print(f" {line}") + return 1 + print("Pre-check passed: Workspace is clean.") + + version = args.version + if version is None: + version = determine_next_version() + + print(f"Running preparation pipeline for {version}...") + + # 1. Find or create tracking issue (EARLY) + # We do this before any write operations (branch creation, commit, push) + issue_num = args.issue + + if not issue_num: + try: + issue_num = self.gh.get_release_tracking_issue(version) + print(f"Tracking issue: #{issue_num}") + except MultipleTrackingIssuesError as e: + print(f"Error: {e}") + return 1 + except NoTrackingIssueError: + # Not found, we need the template + template_path = pathlib.Path( + ".github/ISSUE_TEMPLATE/release_tracking_template.md" + ) + if not template_path.exists(): + raise FileNotFoundError( + f"Template file not found at {template_path}" + ) + template_content = template_path.read_text(encoding="utf-8") + + if args.dry_run: + print( + f"[DRY RUN] No active tracking issue found for" + f" {version}. Would create a new one." + ) + print(f"[DRY RUN] Title: Release {version}\n{template_content}") + issue_num = None # Keep it None for dry-run prints later + else: + print( + f"No active tracking issue found for {version}." + " Creating a new one..." + ) + issue_num = self.gh.create_release_tracking_issue( + version, template_content + ) + print(f"Tracking issue: #{issue_num}") + else: + print(f"Tracking issue: #{issue_num}") + + branch_name = f"prepare-{version}" + + # 2. Interleaved git and write operations + + # --- Branch selection/creation --- + if self.git.branch_exists(branch_name): + if args.dry_run: + print( + f"[DRY RUN] Branch {branch_name} already exists. Would" + " checkout existing branch." + ) + else: + print(f"Branch {branch_name} already exists. Checking it out...") + self.git.checkout(branch_name) + else: + if args.dry_run: + print(f"[DRY RUN] Would create and checkout branch {branch_name}") + else: + self.git.checkout(branch_name, create_branch=True) + + # --- Update files --- + if args.dry_run: + print( + f"[DRY RUN] Would update CHANGELOG.md and version placeholders" + f" for {version}" + ) + else: + print("Updating changelog and placeholders...") + release_date = datetime.date.today().strftime("%Y-%m-%d") + changelog_news.update_changelog(version, release_date) + replace_version_next(version) + + # --- Commit and Push --- + if args.dry_run: + print(f"[DRY RUN] Would push branch {branch_name} to origin") + else: + modified_files = self.git.status() + if modified_files: + # Stage all modified and deleted tracked files + self.git.add_modified_and_deleted() + self.git.commit(f"Prepare release {version}") + else: + print("No files modified by the release tool. Nothing to commit.") + + print(f"Pushing branch {branch_name} to origin...") + # Force push to overwrite the remote branch if it already exists (e.g. from a previous run) + self.git.push("origin", branch_name, set_upstream=True, force=True) + + # --- Create PR --- + # Determine if we need to create a PR or reuse an existing one + open_pr = self.gh.get_open_pr(branch_name) + associated_pr = None + + if not open_pr and issue_num: + body = self.gh.get_issue_body(issue_num) + state = parse_checklist_state(body) + associated_pr = state["prepare_release"].pr + + if open_pr: + pr_num = open_pr["number"] + pr_url = open_pr["url"] + print(f"Open Pull Request already exists: {pr_url} (PR #{pr_num})") + elif associated_pr: + pr_num = associated_pr.lstrip("#") + pr_url = f"https://github.com/bazel-contrib/rules_python/pull/{pr_num}" + print( + f"PR #{pr_num} is already associated in tracking issue" + f" #{issue_num}. Using it." + ) + else: + if args.dry_run: + target_issue = f"#{issue_num}" if issue_num else "" + print( + f"[DRY RUN] Would create Pull Request for branch" + f" {branch_name} targeting issue {target_issue}" + ) + pr_num = "" + else: + pr_url = self.gh.create_pr( + title=f"Prepare release v{version}", + body=f"Work towards #{issue_num}", + base="main", + labels=[RELEASE_PREPARED_LABEL], + ) + pr_num = pr_url.split("/")[-1] + print(f"Created Pull Request: {pr_url} (PR #{pr_num})") + + # --- Update checklist --- + if args.dry_run: + target_issue = f"#{issue_num}" if issue_num else "" + print( + f"[DRY RUN] Would update tracking issue {target_issue} checklist" + " 'Prepare Release' task status to PENDING" + ) + else: + print( + f"Updating tracking issue #{issue_num} checklist 'Prepare" + " Release' task status to PENDING..." + ) + body = self.gh.get_issue_body(issue_num) + metadata = {"status": "pending", "pr": f"#{pr_num}"} + updated_body = update_task_in_body( + body, "Prepare Release", checked=False, metadata=metadata + ) + self.gh.update_issue_body(issue_num, updated_body) + print("Preparation pipeline completed successfully!") + + return 0 + + @classmethod + def add_parser(cls, subparsers): + """Adds parser for prepare subcommand.""" + parser = subparsers.add_parser( + "prepare", + help="Prepare the release (updates changelog, placeholders).", + ) + parser.add_argument( + "version", + nargs="?", + type=semver_type, + help="The new release version (e.g., 0.28.0). If not provided, " + "it will be determined automatically.", + ) + parser.add_argument( + "--issue", + type=int, + help="The tracking issue number (optional, triggers automated branch/PR pipeline).", + ) + parser.add_argument( + "--dry-run", + action=argparse.BooleanOptionalAction, + default=True, + help="Perform a dry run (default: True). Use --no-dry-run to actually execute.", + ) + parser.set_defaults(command=cls.run_from_args) + + @classmethod + def run_from_args(cls, args): + """Instantiates and runs the command from parsed args.""" + git = Git(".") + gh = GitHub() + return cls(args, git, gh).run() diff --git a/tools/private/release/process_backports.py b/tools/private/release/process_backports.py new file mode 100644 index 0000000000..4ad846c0ac --- /dev/null +++ b/tools/private/release/process_backports.py @@ -0,0 +1,631 @@ +"""Subcommand to process pending backports.""" + +import argparse +import datetime +import hashlib +import os +import tempfile +from dataclasses import dataclass +from typing import Any + +from tools.private.release import changelog_news +from tools.private.release.gh import GH_REACTION_THUMBS_DOWN, GitHub +from tools.private.release.git import Git +from tools.private.release.release_issue import ( + RELEASE_TITLE_RE, + add_backports_to_body, + add_rc_task_to_body, + add_sync_changelog_task_to_body, + parse_backports, + parse_checklist_state, + update_task_in_body, +) +from tools.private.release.utils import ( + get_latest_rc_tag, + parse_pr_list, + replace_version_next, +) + + +@dataclass +class CherryPickAndUpdatePrsResult: + # List of PR references that failed to cherry-pick. + failed_prs: list[str] + # List of news files collected from the successful cherry-picks. + collected_news_files: list[str] + # List of PR numbers that were successfully cherry-picked. + successful_pr_nums: list[int] + # List of tuples mapping successful PR numbers to their version marker diffs. + collected_diffs: list[tuple[int, str]] + # The updated checklist body for the release tracking issue. + body: str + + +class ProcessBackports: + """Class to process pending backports.""" + + def __init__(self, args, git: Git, gh: GitHub): + self.args = args + self.git = git + self.gh = gh + + def _process_pr_commit_infos( + self, pr_commit_infos, body, issue, dry_run + ) -> tuple[list[str], dict[str, Any], list[str], list[str], str]: + shas = [] + sha_to_item = {} + failed_prs = [] + ignored_prs = [] + for item in pr_commit_infos: + if item.commit: + sha = item.commit + sha_to_item[sha] = item + shas.append(sha) + elif item.status in ("open-pr", "draft-pr"): + print(f"PR {item.pr_ref} is open or draft. Ignoring.") + ignored_prs.append(item.pr_ref) + else: + failed_prs.append(item.pr_ref) + status_to_set = item.status or "error-unmerged-pr" + if dry_run: + print( + f"[DRY RUN] Would update tracking issue checklist for" + f" unresolved PR {item.pr_ref} to status={status_to_set}" + ) + else: + print( + f"Updating tracking issue checklist for unresolved PR" + f" {item.pr_ref}..." + ) + try: + body = update_task_in_body( + body, + item.pr_ref, + checked=False, + metadata={"status": status_to_set}, + ) + self.gh.update_issue_body(issue, body) + except Exception as e: + print( + f"ERROR: Failed to update tracking issue for" + f" unresolved PR {item.pr_ref}: {e}" + ) + return shas, sha_to_item, failed_prs, ignored_prs, body + + def _cherry_pick_and_update_prs( + self, + sorted_shas, + sha_to_item, + body, + issue, + remote, + dry_run, + version, + branch_name, + next_rc_suffix, + ) -> CherryPickAndUpdatePrsResult: + failed_prs = [] + collected_news_files = [] + successful_pr_nums = [] + collected_diffs = [] + for sha in sorted_shas: + item = sha_to_item[sha] + print(f"Cherry-picking {item.pr_ref} / {sha}...") + try: + self.git.cherry_pick(sha) + + # Collect news files before they are deleted by update_changelog + modified_files = self.git.get_modified_files("HEAD") + for f in modified_files: + if changelog_news.is_news_file(f): + collected_news_files.append(f) + + # Replace version markers FIRST to isolate diff + print(f"Replacing version markers for PR {item.pr_ref}...") + replace_version_next(version) + + # Get diff of unstaged changes (version marker replacement) + diff_content = self.git.diff() + + # Perform news processing (merging news/ files into the changelog) + print(f"Merging news fragments into changelog for PR {item.pr_ref}...") + release_date = datetime.date.today().strftime("%Y-%m-%d") + changelog_news.update_changelog(version, release_date) + + # Stage changelog changes, news/ deletions, and version placeholder updates + self.git.add_modified_and_deleted() + + # Amend cherry-pick commit to include news merging and deletions, + # and reference the release tracking issue. + print(f"Amending cherry-pick commit for PR {item.pr_ref}...") + current_msg = self.git.get_commit_message("HEAD") + new_msg = f"{current_msg.strip()}\n\nWork towards #{issue}" + self.git.commit(new_msg, amend=True) + + try: + pr_num = self.gh.resolve_pr_number(item.pr_ref) + if diff_content: + collected_diffs.append((pr_num, diff_content)) + successful_pr_nums.append(pr_num) + except Exception as e: + print( + f"Warning: Failed to resolve PR number for {item.pr_ref}: {e}" + ) + + if not dry_run: + # Push amended commit + self.git.push(remote, branch_name) + + new_sha = self.git.get_commit_sha("HEAD", short=True) + metadata = { + "status": "done", + "rc": next_rc_suffix, + "commit": new_sha, + } + print(f"Updating tracking issue checklist for PR {item.pr_ref}...") + try: + body = update_task_in_body( + body, item.pr_ref, checked=True, metadata=metadata + ) + self.gh.update_issue_body(issue, body) + except Exception as e: + print( + f"ERROR: Failed to update tracking issue for PR" + f" {item.pr_ref}: {e}" + ) + print(f"Success: backported {item.pr_ref} / {sha} to {branch_name}") + else: + print( + f"[DRY RUN] Success: {item.pr_ref} / {sha} can be" + f" backported without error." + ) + print( + f"[DRY RUN] Would update tracking issue checklist for" + f" PR {item.pr_ref} to status=done" + ) + except Exception as e: + print(f"ERROR: Conflict or error on {sha}: {e}. Aborting.") + try: + self.git.cherry_pick_abort() + except Exception: + pass + failed_prs.append(item.pr_ref) + + if dry_run: + print( + f"[DRY RUN] Would update tracking issue checklist for" + f" failed PR {item.pr_ref} to status=error-merge-conflict" + ) + else: + print( + f"Updating tracking issue checklist for failed PR" + f" {item.pr_ref}..." + ) + try: + body = update_task_in_body( + body, + item.pr_ref, + checked=False, + metadata={"status": "error-merge-conflict"}, + ) + self.gh.update_issue_body(issue, body) + print( + f"Updated back port of {item.pr_ref} to" + f" status=error-merge-conflict (unchecked)" + ) + except Exception as e: + print( + f"ERROR: Failed to update tracking issue for" + f" failed PR {item.pr_ref}: {e}" + ) + return CherryPickAndUpdatePrsResult( + failed_prs=failed_prs, + collected_news_files=collected_news_files, + successful_pr_nums=successful_pr_nums, + collected_diffs=collected_diffs, + body=body, + ) + + def _sync_changelog_to_main( + self, + version: str, + collected_news_files: list[str], + successful_pr_nums: list[int], + collected_diffs: list[tuple[int, str]], + release_branch: str, + ) -> None: + args = self.args + sorted_prs = sorted(successful_pr_nums) + prs_str = ",".join(str(n) for n in sorted_prs) + prs_hash = hashlib.sha256(prs_str.encode()).hexdigest()[:7] + + main_branch = "main" + backport_branch = f"prepare-{version}-backports-{prs_hash}" + + print(f"Syncing changelog to {main_branch} via branch {backport_branch}...") + + self.git.fetch(args.remote, refspec=main_branch) + self.git.checkout(main_branch, track_remote=args.remote) + main_start_sha = self.git.get_commit_sha("HEAD") + + failed_version_sync_prs = [] + try: + if args.dry_run: + print( + f"[DRY RUN] Would create and checkout branch {backport_branch} from {main_branch}" + ) + else: + if self.git.branch_exists(backport_branch): + self.git.checkout(backport_branch) + self.git.reset_hard(reset_to=main_branch) + else: + self.git.checkout(backport_branch, create_branch=True) + + print( + f"Updating CHANGELOG.md and removing news files on {backport_branch}..." + ) + release_date = datetime.date.today().strftime("%Y-%m-%d") + changelog_news.update_changelog( + version, + release_date, + news_files=collected_news_files, + delete_news=True, + ) + + # Apply version marker diffs + failed_version_sync_prs = self._apply_version_marker_diffs(collected_diffs) + + if args.dry_run: + print( + f"[DRY RUN] Would commit: 'chore(release): sync changelog for v{version} backports'" + ) + print(f"[DRY RUN] Would push {backport_branch} to {args.remote}") + print( + f"[DRY RUN] Would create PR to {main_branch} with label 'type: sync-changelog'" + ) + print( + f"[DRY RUN] Would update tracking issue #{args.issue} checklist tasks 'Sync Changelog #' to PENDING" + ) + print("[DRY RUN] Diff of changes:") + print(self.git.status()) + else: + self.git.add_modified_and_deleted() + self.git.commit( + f"chore(release): sync changelog for v{version} backports" + ) + self.git.push( + args.remote, backport_branch, set_upstream=True, force=True + ) + + pr_title = f"chore(release): sync changelog for v{version} backports" + pr_body_lines = [ + "Updates CHANGELOG.md and removes news files for backports:", + ] + for pr_num in sorted_prs: + pr_body_lines.append(f"- #{pr_num}") + + if failed_version_sync_prs: + pr_body_lines.append("") + pr_body_lines.append( + "Warning: These PRs failed to update their version markers:" + ) + for pr_num in sorted(failed_version_sync_prs): + pr_body_lines.append(f"- #{pr_num}") + + pr_body_lines.append("") + pr_body_lines.append(f"Work towards #{args.issue}") + pr_body_lines.append(f"Release-Tracking-Issue: #{args.issue}") + pr_body = "\n".join(pr_body_lines) + + print(f"Creating PR to {main_branch}...") + pr_url = self.gh.create_pr( + title=pr_title, + body=pr_body, + base=main_branch, + labels=["type: sync-changelog"], + ) + print(f"Created PR: {pr_url}") + + try: + pr_num = int(pr_url.split("/")[-1]) + print(f"Enabling auto-merge for PR #{pr_num}...") + self.gh.enable_auto_merge(pr_num) + + print( + f"Updating tracking issue #{args.issue} checklist with" + " Sync Changelog tasks..." + ) + issue_body = self.gh.get_issue_body(args.issue) + for pr in successful_pr_nums: + task_name = f"Sync Changelog #{pr}" + metadata = {"status": "pending", "pr": f"#{pr_num}"} + issue_body = update_task_in_body( + issue_body, + task_name, + checked=False, + metadata=metadata, + ) + self.gh.update_issue_body(args.issue, issue_body) + except Exception as e: + print( + f"Warning: Failed to update tracking issue or enable" + f" auto-merge: {e}" + ) + finally: + if args.dry_run: + self.git.reset_hard(reset_to=main_start_sha) + self.git.checkout(release_branch) + + def _apply_version_marker_diffs( + self, + collected_diffs: list[tuple[int, str]], + ) -> list[int]: + """Applies version marker diffs on main branch and returns failed PR numbers.""" + args = self.args + failed_version_sync_prs = [] + if not collected_diffs: + return failed_version_sync_prs + + with tempfile.TemporaryDirectory() as temp_dir: + print(f"Applying {len(collected_diffs)} version marker patches...") + for pr_num, diff_content in collected_diffs: + if args.dry_run: + print( + f"[DRY RUN] Would check and apply version marker patch for PR #{pr_num}" + ) + + patch_filepath = os.path.join(temp_dir, f"{pr_num}.patch") + with open(patch_filepath, "w", encoding="utf-8") as f: + f.write(diff_content) + + if self.git.apply_check(patch_filepath): + if args.dry_run: + print( + f"[DRY RUN] Version marker patch for PR #{pr_num} applies cleanly." + ) + else: + print(f"Applying version marker patch for PR #{pr_num}...") + self.git.apply(patch_filepath) + else: + print( + f"Warning: Version marker patch for PR #{pr_num} could not be applied cleanly to main. Skipping." + ) + failed_version_sync_prs.append(pr_num) + return failed_version_sync_prs + + def run(self) -> int: + """Executes the process-backports subcommand.""" + args = self.args + exit_code = 0 + try: + exit_code = self._run_internal() + except Exception as e: + print(f"Unexpected error: {e}") + exit_code = 1 + + if exit_code != 0 and args.triggering_comment: + print(f"Reacting with thumbs-down to comment {args.triggering_comment}...") + try: + self.gh.add_comment_reaction( + args.triggering_comment, GH_REACTION_THUMBS_DOWN + ) + except Exception as e: + print(f"Failed to add reaction to comment: {e}") + + return exit_code + + def _run_internal(self) -> int: + """Internal implementation of process-backports.""" + args = self.args + body = self.gh.get_issue_body(args.issue) + + if args.add: + items_to_add: list[dict[str, Any]] = [] + for pr_ref in args.add: + try: + pr_num = self.gh.resolve_pr_number(pr_ref) + items_to_add.append({"ref": f"#{pr_num}"}) + except Exception as e: + print(f"Warning: PR ref '{pr_ref}' is invalid: {e}") + items_to_add.append( + { + "ref": pr_ref, + "metadata": {"status": "error-invalid-pr"}, + } + ) + + print(f"Adding backports {items_to_add} to tracking issue #{args.issue}...") + try: + body = add_backports_to_body(body, items_to_add) + for item in items_to_add: + if ( + "metadata" in item + and item["metadata"].get("status") == "error-invalid-pr" + ): + continue + pr_num = int(item["ref"].lstrip("#")) + body = add_sync_changelog_task_to_body(body, pr_num) + state = parse_checklist_state(body) + rc_tags = state.get("rc_tags", {}) + has_pending_rc = any( + not task.checked and task.status != "done" + for task in rc_tags.values() + ) + next_rc_num = max(rc_tags.keys()) + 1 if rc_tags else 0 + if not has_pending_rc: + print( + f"No pending RC task found. Adding 'Tag" + f" RC{next_rc_num}' to checklist..." + ) + body = add_rc_task_to_body(body, next_rc_num) + except ValueError as e: + print(f"Error: {e}") + return 1 + + if not args.dry_run: + self.gh.update_issue_body(args.issue, body) + print("Successfully updated tracking issue checklist.") + else: + print( + "[DRY RUN] Would update tracking issue checklist with new" + " backports." + ) + if not has_pending_rc: + print(f"[DRY RUN] Would add 'Tag RC{next_rc_num}' to checklist.") + + items = parse_backports(body) + + pending_items = [ + item + for item in items + if not item.checked and not item.status.startswith("error-") + ] + + if not pending_items: + print("No pending backports found.") + return 0 + + print(f"Found {len(pending_items)} pending backports to process.") + + # Determine branch name from issue title + issue_title = self.gh.get_issue_title(args.issue) + version_match = RELEASE_TITLE_RE.search(issue_title) + if not version_match: + print(f"Error: Could not parse version from issue title: {issue_title}") + return 1 + + version = version_match.group(1) + branch_version = ".".join(version.split(".")[:2]) + branch_name = f"release/{branch_version}" + + # Determine next RC tag to write to backport metadata + self.git.fetch(args.remote, tags=True, force=True) + latest_rc = get_latest_rc_tag(version, remote=args.remote) + if not latest_rc: + next_rc_suffix = "rc0" + else: + rc_num = int(latest_rc.split("-rc")[-1]) + next_rc_suffix = f"rc{rc_num + 1}" + + # Resolve PRs to merge commits using gh helper. + pr_commit_infos = self.gh.get_merge_commits_for_prs(pending_items) + + shas, sha_to_item, failed_prs, ignored_prs, body = ( + self._process_pr_commit_infos( + pr_commit_infos, body, args.issue, args.dry_run + ) + ) + + if not shas: + print("No valid merge commits to process.") + if failed_prs: + print("Failed PRs:") + for pr in failed_prs: + print(f"- {pr}") + return 1 + return 0 + + # Verify workspace is clean before proceeding + if self.git.status(): + print( + "ERROR: Git workspace is dirty. Please commit or stash changes" + " before running backports." + ) + return 1 + + # Sort chronologically using git helper + sorted_shas = self.git.sort_commits_chronologically(shas) + + self.git.fetch(args.remote) + self.git.checkout(branch_name, track_remote=args.remote) + start_sha = self.git.get_commit_sha("HEAD") + + collected_news_files = [] + successful_pr_nums = [] + collected_diffs = [] + try: + result = self._cherry_pick_and_update_prs( + sorted_shas, + sha_to_item, + body, + args.issue, + args.remote, + args.dry_run, + version, + branch_name, + next_rc_suffix, + ) + failed_prs.extend(result.failed_prs) + collected_news_files.extend(result.collected_news_files) + successful_pr_nums.extend(result.successful_pr_nums) + collected_diffs.extend(result.collected_diffs) + body = result.body + finally: + if args.dry_run: + print(f"[DRY RUN] Resetting branch {branch_name} to {start_sha}") + self.git.reset_hard(reset_to=start_sha) + + if successful_pr_nums: + self._sync_changelog_to_main( + version, + collected_news_files, + successful_pr_nums, + collected_diffs, + branch_name, + ) + + if failed_prs: + print("ERROR: One or more cherry-picks/resolutions failed:") + for pr in failed_prs: + print(f"- {pr}") + return 1 + + if args.dry_run: + print("Dry run completed successfully. No errors found.") + else: + print("All backports successfully processed!") + return 0 + + @classmethod + def add_parser(cls, subparsers): + """Adds parser for process-backports subcommand.""" + parser = subparsers.add_parser( + "process-backports", + help="Cherry-pick pending backports listed in the tracking issue.", + ) + parser.add_argument( + "--issue", + type=int, + required=True, + help="The tracking issue number (required).", + ) + parser.add_argument( + "--remote", + type=str, + required=True, + help="The git remote to push changes to (required).", + ) + parser.add_argument( + "--add", + type=parse_pr_list, + help="PR references (numbers, #numbers, or URLs, comma/space separated) to add before processing.", + ) + parser.add_argument( + "--triggering-comment", + type=int, + help="The ID of the comment that triggered this run (optional).", + ) + parser.add_argument( + "--dry-run", + action=argparse.BooleanOptionalAction, + default=True, + help="Perform a dry run (default: True). Use --no-dry-run to actually execute.", + ) + parser.set_defaults(command=cls.run_from_args) + + @classmethod + def run_from_args(cls, args): + """Instantiates and runs the command from parsed args.""" + git = Git(".") + gh = GitHub() + return cls(args, git, gh).run() diff --git a/tools/private/release/promote.py b/tools/private/release/promote.py new file mode 100644 index 0000000000..44ccd5ba52 --- /dev/null +++ b/tools/private/release/promote.py @@ -0,0 +1,246 @@ +"""Subcommand to promote a release candidate to final release.""" + +import argparse +import os +import urllib.parse + +from tools.private.release.gh import GitHub +from tools.private.release.git import Git +from tools.private.release.release_issue import ( + RELEASE_TITLE_RE, + update_task_in_body, +) +from tools.private.release.utils import ( + REPO_URL, + determine_next_version, + get_latest_rc_tag, + semver_type, + set_github_output, +) + + +class Promote: + """Class to promote a release candidate to final release.""" + + def __init__(self, args, git: Git, gh: GitHub): + self.args = args + self.git = git + self.gh = gh + + def run(self) -> int: + """Executes the promote-rc subcommand (Phase 3).""" + args = self.args + # Fetch from remote to ensure we have the latest tags + self.git.fetch(args.remote, tags=True, force=True) + + version = args.version + if version is None: + if args.issue: + issue_title = self.gh.get_issue_title(args.issue) + version_match = RELEASE_TITLE_RE.search(issue_title) + if version_match: + version = version_match.group(1) + print( + f"Resolved version {version} from tracking issue" + f" #{args.issue} title." + ) + else: + print( + f"Error: Could not parse version from issue title:" + f" {issue_title}" + ) + return 1 + else: + version = determine_next_version() + + # Verify final tag doesn't already exist + if self.git.tag_exists(version): + print(f"Error: Final tag {version} already exists.") + return 1 + + is_first_release = version.endswith(".0") + + # Verify issue can be found + issue_num = args.issue + if not issue_num: + try: + issue_num = self.gh.get_release_tracking_issue(version) + except ValueError as e: + print(f"Error: {e}") + return 1 + except Exception as e: + print(f"Error: Unexpected error finding tracking issue: {e}") + return 1 + + if is_first_release: + latest_rc = get_latest_rc_tag(version, remote=args.remote) + if not latest_rc: + print(f"Error: No release candidate tags found matching {version}-rc*") + return 1 + 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...") + body = self.gh.get_issue_body(issue_num) + + # Determine release branch name + branch_version = ".".join(version.split(".")[:2]) + branch_name = f"release/{branch_version}" + remote_branch = f"{args.remote}/{branch_name}" + + print(f"Fetching remote branch {remote_branch}...") + self.git.fetch(args.remote, refspec=branch_name) + try: + branch_sha = self.git.get_commit_sha(remote_branch) + except Exception as e: + print( + f"Error: Could not get commit SHA for remote branch" + f" {remote_branch}: {e}" + ) + return 1 + + if is_first_release: + assert rc_commit_sha is not None # type assert + if rc_commit_sha != branch_sha: + print( + 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": rc_commit_sha[:8], + } + try: + updated_body = update_task_in_body( + body, "Tag Final", checked=False, metadata=metadata + ) + except ValueError as e: + print(f"Error: Tracking issue #{issue_num} is malformed: {e}") + return 1 + + if not args.dry_run: + self.gh.update_issue_body(issue_num, updated_body) + print(f"Updated tracking issue #{issue_num} with error status.") + else: + print( + f"[DRY RUN] Would update tracking issue #{issue_num} with" + f" error status." + ) + return 1 + commit_sha = rc_commit_sha + else: + # Patch release: tag branch head directly + commit_sha = branch_sha + latest_rc = None + + # Verify issue is in the right format by trying to prepare the update (for success case) + metadata = {"status": "done", "tag": version, "commit": commit_sha[:8]} + try: + updated_body = update_task_in_body( + body, "Tag Final", checked=True, metadata=metadata + ) + except ValueError as e: + print(f"Error: Tracking issue #{issue_num} is malformed: {e}") + return 1 + + # All pre-conditions met, perform modifications + promote_source = ( + latest_rc + if is_first_release + else f"head of {branch_name} ({commit_sha[:8]})" + ) + if args.dry_run: + print( + f"[DRY RUN] Pre-conditions passed successfully for promoting" + f" {promote_source} to {version}." + ) + print(f"[DRY RUN] Would tag commit {commit_sha[:8]} as {version}") + print(f"[DRY RUN] Would push tag {version} to {args.remote}") + print(f"[DRY RUN] Would update tracking issue #{issue_num} checklist") + print(f"[DRY RUN] Would post comment to tracking issue #{issue_num}") + return 0 + + print( + f"Promoting {promote_source} to final release {version} (commit" + f" {commit_sha[:8]}) using tracking issue #{issue_num}..." + ) + + # Tag the specific commit without checkout, and push to remote + self.git.tag(version, commit_sha) + self.git.push(args.remote, version) + + set_github_output("version", version) + + print(f"Updating tracking issue #{issue_num} checklist...") + self.gh.update_issue_body(issue_num, updated_body) + + print(f"Posting comment to tracking issue #{issue_num}...") + + branch_url = f"{REPO_URL}/tree/{branch_name}" + release_url = f"{REPO_URL}/releases/tag/{version}" + bcr_entry_url = f"https://registry.bazel.build/modules/rules_python/{version}" + bcr_query = ( + f'is:pr ("bazel-contrib/rules_python" in:title) ("@{version}" in:title)' + ) + bcr_search_url = f"https://github.com/bazelbuild/bazel-central-registry/pulls?q={urllib.parse.quote(bcr_query)}" + + if run_id := os.environ.get("GITHUB_RUN_ID"): + release_workflow_url = f"{REPO_URL}/actions/runs/{run_id}" + else: + release_workflow_url = f"{REPO_URL}/actions/workflows/release_promote.yaml" + + comment_body = f"""**New Release Tagged!** 🐍🌿 + +Version **{version}** has been successfully generated and tagged on branch [`{branch_name}`]({branch_url}). + +- [Github Release {version}]({release_url}) +- [BCR Entry {version}]({bcr_entry_url}) +- [BCR PRs]({bcr_search_url}) +- [Release workflow status]({release_workflow_url})""" + self.gh.post_issue_comment(issue_num, comment_body) + + return 0 + + @classmethod + def add_parser(cls, subparsers): + """Adds parser for promote-rc subcommand.""" + parser = subparsers.add_parser( + "promote", + help="Promote the latest RC to final release.", + ) + parser.add_argument( + "version", + nargs="?", + type=semver_type, + help="The final version to release (e.g., 0.38.0).", + ) + parser.add_argument( + "--issue", + type=int, + help="The tracking issue number (optional).", + ) + parser.add_argument( + "--remote", + type=str, + required=True, + help="The git remote to push the final tag to (required).", + ) + parser.add_argument( + "--dry-run", + action=argparse.BooleanOptionalAction, + default=True, + help="Perform a dry run (default: True). Use --no-dry-run to actually execute.", + ) + parser.set_defaults(command=cls.run_from_args) + + @classmethod + def run_from_args(cls, args): + """Instantiates and runs the command from parsed args.""" + git = Git(".") + gh = GitHub() + return cls(args, git, gh).run() diff --git a/tools/private/release/release.py b/tools/private/release/release.py index 6fce0ff3b0..7f1896ecf4 100644 --- a/tools/private/release/release.py +++ b/tools/private/release/release.py @@ -1,196 +1,77 @@ """A tool to perform release steps.""" import argparse -import datetime -import fnmatch import os -import pathlib -import re -import subprocess - -from packaging.version import parse as parse_version - -_EXCLUDE_PATTERNS = [ - "./.git/*", - "./.github/*", - "./.bazelci/*", - "./.bcr/*", - "./bazel-*/*", - "./CONTRIBUTING.md", - "./RELEASING.md", - "./tools/private/release/*", - "./tests/tools/private/release/*", +import sys + +from tools.private.release.add_backports import AddBackports +from tools.private.release.backport_create_releases import BackportCreateReleases +from tools.private.release.backport_prepare import BackportPrepare +from tools.private.release.complete_prepare import CompletePrepare +from tools.private.release.complete_sync_changelog import CompleteSyncChangelog +from tools.private.release.create_rc import CreateRc +from tools.private.release.create_release_branch import CreateReleaseBranch +from tools.private.release.create_release_issue import CreateReleaseIssue +from tools.private.release.determine_next_version import DetermineNextVersion +from tools.private.release.on_pr_merged import OnPrMerged +from tools.private.release.prepare import Prepare +from tools.private.release.process_backports import ProcessBackports +from tools.private.release.promote import Promote + +cmds = [ + DetermineNextVersion, + CreateReleaseIssue, + Prepare, + CompletePrepare, + CompleteSyncChangelog, + CreateReleaseBranch, + AddBackports, + ProcessBackports, + OnPrMerged, + CreateRc, + Promote, + BackportPrepare, + BackportCreateReleases, ] -def _iter_version_placeholder_files(): - for root, dirs, files in os.walk(".", topdown=True): - # Filter directories - dirs[:] = [ - d - for d in dirs - if not any( - fnmatch.fnmatch(os.path.join(root, d), pattern) - for pattern in _EXCLUDE_PATTERNS - ) - ] - - for filename in files: - filepath = os.path.join(root, filename) - if any(fnmatch.fnmatch(filepath, pattern) for pattern in _EXCLUDE_PATTERNS): - continue - - yield filepath - - -def _get_git_tags(): - """Runs a git command and returns the output.""" - return subprocess.check_output(["git", "tag"]).decode("utf-8").splitlines() - - -def get_latest_version(): - """Gets the latest version from git tags.""" - tags = _get_git_tags() - # The packaging module can parse PEP440 versions, including RCs. - # It has a good understanding of version precedence. - versions = [ - (tag, parse_version(tag)) - for tag in tags - if re.match(r"^\d+\.\d+\.\d+(rc\d+)?$", tag.strip()) - ] - if not versions: - raise RuntimeError("No git tags found matching X.Y.Z or X.Y.ZrcN format.") - - versions.sort(key=lambda v: v[1]) - latest_tag, latest_version = versions[-1] - - if latest_version.is_prerelease: - raise ValueError(f"The latest version is a pre-release version: {latest_tag}") - - # After all that, we only want to consider stable versions for the release. - stable_versions = [tag for tag, version in versions if not version.is_prerelease] - if not stable_versions: - raise ValueError("No stable git tags found matching X.Y.Z format.") - - # The versions are already sorted, so the last one is the latest. - return stable_versions[-1] - - -def should_increment_minor(): - """Checks if the minor version should be incremented.""" - for filepath in _iter_version_placeholder_files(): - try: - with open(filepath, "r") as f: - content = f.read() - except (IOError, UnicodeDecodeError): - # Ignore binary files or files with read errors - continue - - if "VERSION_NEXT_FEATURE" in content: - return True - return False - - -def determine_next_version(): - """Determines the next version based on git tags and placeholders.""" - latest_version = get_latest_version() - major, minor, patch = [int(n) for n in latest_version.split(".")] - - if should_increment_minor(): - return f"{major}.{minor + 1}.0" - else: - return f"{major}.{minor}.{patch + 1}" - - -def update_changelog(version, release_date, changelog_path="CHANGELOG.md"): - """Performs the version replacements in CHANGELOG.md.""" - - header_version = version.replace(".", "-") - - changelog_path_obj = pathlib.Path(changelog_path) - lines = changelog_path_obj.read_text().splitlines() - - new_lines = [] - after_template = False - before_already_released = True - for line in lines: - if "END_UNRELEASED_TEMPLATE" in line: - after_template = True - if re.match("#v[1-9]-", line): - before_already_released = False - - if after_template and before_already_released: - line = line.replace("## Unreleased", f"## [{version}] - {release_date}") - line = line.replace("v0-0-0", f"v{header_version}") - line = line.replace("0.0.0", version) - - new_lines.append(line) - - changelog_path_obj.write_text("\n".join(new_lines)) - - -def replace_version_next(version): - """Replaces all VERSION_NEXT_* placeholders with the new version.""" - for filepath in _iter_version_placeholder_files(): - try: - with open(filepath, "r") as f: - content = f.read() - except (IOError, UnicodeDecodeError): - # Ignore binary files or files with read errors - continue - - if "VERSION_NEXT_FEATURE" in content or "VERSION_NEXT_PATCH" in content: - new_content = content.replace("VERSION_NEXT_FEATURE", version) - new_content = new_content.replace("VERSION_NEXT_PATCH", version) - with open(filepath, "w") as f: - f.write(new_content) - - -def _semver_type(value): - if not re.match(r"^\d+\.\d+\.\d+(rc\d+)?$", value): - raise argparse.ArgumentTypeError( - f"'{value}' is not a valid semantic version (X.Y.Z or X.Y.ZrcN)" - ) - return value - - def create_parser(): - """Creates the argument parser.""" + """Creates the argument parser with subcommands.""" parser = argparse.ArgumentParser( description="Automate release steps for rules_python." ) - parser.add_argument( - "version", - nargs="?", - type=_semver_type, - help="The new release version (e.g., 0.28.0). If not provided, " - "it will be determined automatically.", + + subparsers = parser.add_subparsers( + dest="command", required=True, help="Subcommands" ) + + for cmd in cmds: + cmd.add_parser(subparsers) + return parser def main(): - # Change to the workspace root so the script can be run using `bazel run` + print(f"sys.argv: {sys.argv}") if "BUILD_WORKSPACE_DIRECTORY" in os.environ: os.chdir(os.environ["BUILD_WORKSPACE_DIRECTORY"]) parser = create_parser() args = parser.parse_args() - version = args.version - if version is None: - print("No version provided, determining next version automatically...") - version = determine_next_version() - print(f"Determined next version: {version}") - - print("Updating changelog ...") - release_date = datetime.date.today().strftime("%Y-%m-%d") - update_changelog(version, release_date) - - print("Replacing VERSION_NEXT placeholders ...") - replace_version_next(version) - - print("Done") + exit_code = 1 + try: + # args.command is the run_from_args classmethod of the selected command + exit_code = args.command(args) + except Exception as e: + sys.stdout.flush() + print(f"Fatal error: {e}", file=sys.stderr) + if hasattr(e, "__notes__"): + for note in e.__notes__: + print(note, file=sys.stderr) + sys.exit(1) + + sys.exit(exit_code if exit_code is not None else 0) if __name__ == "__main__": diff --git a/tools/private/release/release_issue.py b/tools/private/release/release_issue.py new file mode 100644 index 0000000000..9c73d653d5 --- /dev/null +++ b/tools/private/release/release_issue.py @@ -0,0 +1,389 @@ +import re +from typing import Any + + +class BackportTask: + """Represents a backport task from the tracking issue checklist.""" + + def __init__( + self, + pr_ref: str, + checked: bool, + status: str, + rc: str | None = None, + commit: str | None = None, + metadata: dict[str, str] | None = None, + ): + """Initializes a BackportTask. + + Args: + pr_ref: The PR reference (e.g. '#123'). + checked: Whether the checklist item is checked. + status: The status of the backport (e.g. 'pending', 'done', + 'error-merge-conflict'). + rc: The release candidate version this PR was backported to. + commit: The cherry-pick commit SHA. + metadata: Raw metadata parsed from the checklist line. + """ + self.pr_ref = pr_ref + self.checked = checked + self.status = status + self.rc = rc + self.commit = commit + self.metadata = metadata or {} + + def __repr__(self): + return ( + f"BackportTask(pr_ref={self.pr_ref!r}, checked={self.checked!r}, " + f"status={self.status!r}, rc={self.rc!r}, commit={self.commit!r})" + ) + + +class ReleaseTask: + """Represents a release task from the tracking issue checklist.""" + + def __init__( + self, + name: str, + checked: bool, + status: str | None = None, + pr: str | None = None, + commit: str | None = None, + branch: str | None = None, + tag: str | None = None, + metadata: dict[str, str] | None = None, + ): + """Initializes a ReleaseTask. + + Args: + name: The name of the task (e.g. 'Prepare Release'). + checked: Whether the checklist item is checked. + status: The status of the task (e.g. 'pending', 'done'). + pr: The associated PR reference (e.g. '#123'). + commit: The associated commit SHA. + branch: The associated branch name. + tag: The associated tag name. + metadata: Raw metadata parsed from the checklist line. + """ + self.name = name + self.checked = checked + self.status = status + self.pr = pr + self.commit = commit + self.branch = branch + self.tag = tag + self.metadata = metadata or {} + + def __repr__(self): + return ( + f"ReleaseTask(name={self.name!r}, checked={self.checked!r}, " + f"status={self.status!r}, pr={self.pr!r}, commit={self.commit!r}, " + f"branch={self.branch!r}, tag={self.tag!r})" + ) + + +RELEASE_TITLE_RE = re.compile(r"Release (\d+\.\d+\.\d+)", re.IGNORECASE) + + +def parse_metadata_line(line): + """Parses a checklist line with optional | key=value metadata.""" + match = re.match(r"^\s*-\s*\[([ xX])\]\s+([^|]+)(?:\s*\|\s*(.*))?$", line) + if not match: + return None + + checked = match.group(1).lower() == "x" + name = match.group(2).strip() + metadata_str = match.group(3) + + metadata = {} + if metadata_str: + for k, v in re.findall(r"(\w+)\s*=\s*(\S+)", metadata_str): + metadata[k] = v + + return { + "checked": checked, + "name": name, + "metadata": metadata, + "original_line": line, + } + + +def format_metadata_line(checked, name, metadata): + """Formats a checklist line with space-separated key=value metadata.""" + check_str = "x" if checked else " " + if not metadata: + return f"- [{check_str}] {name}" + + metadata_pairs = [] + for k, v in metadata.items(): + if k == "commit" or k.endswith("_commit"): + # The 'commit' key (and keys ending with '_commit') is special-cased with + # a space after '=' so that GitHub autolinks the commit SHA. Autolinking + # requires certain characters to precede the value. + metadata_pairs.append(f"{k}= {v}") + else: + metadata_pairs.append(f"{k}={v}") + metadata_str = " ".join(metadata_pairs) + return f"- [{check_str}] {name} | {metadata_str}" + + +def update_task_in_body(body, task_name, checked, metadata): + """Updates a specific task's checked state and metadata in the issue body.""" + lines = body.splitlines() + updated_lines = [] + found = False + + for line in lines: + parsed = parse_metadata_line(line) + if parsed and parsed["name"].lower() == task_name.lower(): + updated_lines.append(format_metadata_line(checked, task_name, metadata)) + found = True + else: + updated_lines.append(line) + + if not found: + raise ValueError( + f"Task '{task_name}' not found in issue body. " + f"Expected format: '- [ ] {task_name}' or '- [x] {task_name}' (optionally followed by '| key=value')" + ) + + return "\n".join(updated_lines) + + +def parse_checklist_state(body): + """Parses the main checklist tasks and their metadata. + + Returns: + A dict containing ReleaseTask objects for 'prepare_release', + 'create_branch', 'tag_final', and a dict of RC tags. + """ + state = { + "prepare_release": ReleaseTask("Prepare Release", False), + "create_branch": ReleaseTask("Create Release branch", False), + "tag_final": ReleaseTask("Tag Final", False), + "rc_tags": {}, # Dynamically mapped: int -> ReleaseTask + "sync_changelogs": {}, # Dynamically mapped: int -> ReleaseTask + } + + lines = body.splitlines() + for line in lines: + parsed = parse_metadata_line(line) + if not parsed: + continue + + name = parsed["name"].strip() + meta = parsed["metadata"] + checked = parsed["checked"] + name_lower = name.lower() + + if "prepare release" in name_lower: + state["prepare_release"] = ReleaseTask( + name=name, + checked=checked, + status=meta.get("status"), + pr=meta.get("pr"), + commit=meta.get("commit"), + metadata=meta, + ) + elif "create release branch" in name_lower: + state["create_branch"] = ReleaseTask( + name=name, + checked=checked, + status=meta.get("status"), + branch=meta.get("branch"), + commit=meta.get("commit"), + metadata=meta, + ) + elif "tag final" in name_lower: + state["tag_final"] = ReleaseTask( + name=name, + checked=checked, + status=meta.get("status"), + tag=meta.get("tag"), + commit=meta.get("commit"), + metadata=meta, + ) + else: + # Match Tag RC + rc_match = re.match(r"Tag RC(\d+)", name, re.IGNORECASE) + if rc_match: + rc_num = int(rc_match.group(1)) + state["rc_tags"][rc_num] = ReleaseTask( + name=name, + checked=checked, + status=meta.get("status"), + tag=meta.get("tag"), + commit=meta.get("commit"), + metadata=meta, + ) + else: + # Match Sync Changelog # + sync_match = re.match(r"Sync Changelog #(\d+)", name, re.IGNORECASE) + if sync_match: + pr_num = int(sync_match.group(1)) + state["sync_changelogs"][pr_num] = ReleaseTask( + name=name, + checked=checked, + status=meta.get("status"), + pr=meta.get("pr"), + commit=meta.get("commit"), + metadata=meta, + ) + + return state + + +def parse_backports(body): + """Parses the ## Backports checklist section.""" + body = body.replace("\r\n", "\n") + match = re.search( + r"## Backports\n(.*?)(?=\n##|\n---|\Z)", body, re.DOTALL | re.IGNORECASE + ) + if not match: + return [] + + section_content = match.group(1) + items = [] + lines = section_content.splitlines() + + for line in lines: + parsed = parse_metadata_line(line) + if parsed: + items.append( + BackportTask( + pr_ref=parsed["name"], + checked=parsed["checked"], + status=parsed["metadata"].get("status", "pending"), + rc=parsed["metadata"].get("rc"), + commit=parsed["metadata"].get("commit"), + metadata=parsed["metadata"], + ) + ) + return items + + +def add_backports_to_body(body: str, items: list[dict[str, Any]]) -> str: + """Adds new backport checklist items to the ## Backports section. + + Args: + body: The issue body. + items: A list of dicts, where each dict has a 'ref' key (str) and + optional 'metadata' key (dict). + """ + body = body.replace("\r\n", "\n") + # Find the Backports section + pattern = r"(## Backports\n)(.*?)(?=\n##|\n---|\Z)" + match = re.search(pattern, body, re.DOTALL | re.IGNORECASE) + if not match: + raise ValueError("Could not find '## Backports' section in issue body.") + + section_content = match.group(2) + + # Parse existing backports to avoid duplicates + existing_items = parse_backports(body) + existing_refs = {item.pr_ref for item in existing_items} + + new_lines = [] + for item in items: + ref = item["ref"] + # Normalize numeric refs to #numeric + if not ref.startswith("#") and ref.isdigit(): + ref = f"#{ref}" + + if ref in existing_refs: + print(f"PR {ref} is already in the backports list. Skipping.") + continue + existing_refs.add(ref) + + metadata = item.get("metadata", {}) + new_lines.append( + format_metadata_line(checked=False, name=ref, metadata=metadata) + ) + + if not new_lines: + return body + + # Append new lines to the section content. + section_content_clean = section_content.rstrip("\n") + separator = "\n" if section_content_clean else "" + updated_section = section_content_clean + separator + "\n".join(new_lines) + "\n\n" + + # Replace the old section with the updated one + start, end = match.span(2) + return body[:start] + updated_section + body[end:] + + +def add_rc_task_to_body(body: str, rc_num: int) -> str: + """Adds a new 'Tag RC' task to the checklist in the issue body.""" + body = body.replace("\r\n", "\n") + lines = body.splitlines() + + # Find the index of the last "Tag RC" line + last_rc_idx = -1 + for i, line in enumerate(lines): + parsed = parse_metadata_line(line) + if parsed and re.match(r"Tag RC\d+", parsed["name"], re.IGNORECASE): + last_rc_idx = i + + if last_rc_idx == -1: + # If no RC task found (unexpected, but fallback to before "Tag Final") + for i, line in enumerate(lines): + parsed = parse_metadata_line(line) + if parsed and parsed["name"].lower() == "tag final": + last_rc_idx = i - 1 + break + + if last_rc_idx == -1: + raise ValueError("Could not find a place to insert the new RC task.") + + new_task_line = f"- [ ] Tag RC{rc_num}" + lines.insert(last_rc_idx + 1, new_task_line) + + return "\n".join(lines) + + +def add_sync_changelog_task_to_body(body: str, pr_num: int) -> str: + """Adds a new 'Sync Changelog #' task to the checklist in the issue body.""" + body = body.replace("\r\n", "\n") + + # Check if already exists + task_name = f"Sync Changelog #{pr_num}" + lines = body.splitlines() + for line in lines: + parsed = parse_metadata_line(line) + if parsed and parsed["name"].lower() == task_name.lower(): + print(f"Task '{task_name}' already exists. Skipping.") + return body + + # Find the index of the last "Sync Changelog #" line + last_sync_idx = -1 + for i, line in enumerate(lines): + parsed = parse_metadata_line(line) + if parsed and re.match(r"Sync Changelog #\d+", parsed["name"], re.IGNORECASE): + last_sync_idx = i + + if last_sync_idx == -1: + # If no Sync Changelog task found, insert before "Tag Final" + for i, line in enumerate(lines): + parsed = parse_metadata_line(line) + if parsed and parsed["name"].lower() == "tag final": + last_sync_idx = i - 1 + break + + if last_sync_idx == -1: + # If "Tag Final" not found, insert after "Create Release branch" + for i, line in enumerate(lines): + parsed = parse_metadata_line(line) + if parsed and parsed["name"].lower() == "create release branch": + last_sync_idx = i + break + + if last_sync_idx == -1: + raise ValueError( + "Could not find a place to insert the new Sync Changelog task." + ) + + new_task_line = f"- [ ] Sync Changelog #{pr_num}" + lines.insert(last_sync_idx + 1, new_task_line) + + return "\n".join(lines) diff --git a/tools/private/release/shell.py b/tools/private/release/shell.py new file mode 100644 index 0000000000..0093a28574 --- /dev/null +++ b/tools/private/release/shell.py @@ -0,0 +1,31 @@ +"""Shell utility functions for the release tool.""" + +import shlex +import subprocess + + +def run_cmd(*args, check=True, capture_output=True, cwd=None): + """Runs a command as a subprocess with separate arguments (prints command). + + If the command fails, it raises the CalledProcessError after attaching + a detailed note explaining the failure to preserve the stack trace. + """ + cmd = [str(arg) for arg in args] + cwd_suffix = f" (cwd: {cwd})" if cwd else "" + print(f"> {shlex.join(cmd)}{cwd_suffix}") + try: + result = subprocess.run( + cmd, + check=check, + stdout=subprocess.PIPE if capture_output else None, + stderr=subprocess.PIPE if capture_output else None, + universal_newlines=True, + cwd=cwd, + ) + return result.stdout.strip() if capture_output else None + except subprocess.CalledProcessError as e: + note = f"Error running command: {shlex.join(cmd)}{cwd_suffix}" + if capture_output: + note += f"\nStdout: {e.stdout}\nStderr: {e.stderr}" + e.add_note(note) + raise diff --git a/tools/private/release/utils.py b/tools/private/release/utils.py new file mode 100644 index 0000000000..ab1e0acef3 --- /dev/null +++ b/tools/private/release/utils.py @@ -0,0 +1,193 @@ +"""Utility functions for the release tool.""" + +import argparse +import fnmatch +import os +import re + +from packaging.version import parse as parse_version + +from tools.private.release.git import Git + +REPO_URL = "https://github.com/bazel-contrib/rules_python" + + +def semver_type(value): + """Argparse type validator for semantic versions.""" + if not re.match(r"^\d+\.\d+\.\d+(rc\d+)?$", value): + raise argparse.ArgumentTypeError( + f"'{value}' is not a valid semantic version (X.Y.Z or X.Y.ZrcN)" + ) + return value + + +_EXCLUDE_PATTERNS = [ + "./.agents/*", + "./.git/*", + "./.github/*", + "./.bazelci/*", + "./.bcr/*", + "./bazel-*/*", + "./CONTRIBUTING.md", + "./RELEASING.md", + "./tools/private/release/*", + "./tests/tools/private/release/*", +] + + +def _iter_version_placeholder_files(): + for root, dirs, files in os.walk(".", topdown=True): + # Filter directories + dirs[:] = [ + d + for d in dirs + if not any( + fnmatch.fnmatch(os.path.join(root, d), pattern) + for pattern in _EXCLUDE_PATTERNS + ) + ] + + for filename in files: + filepath = os.path.join(root, filename) + if any(fnmatch.fnmatch(filepath, pattern) for pattern in _EXCLUDE_PATTERNS): + continue + + yield filepath + + +def get_latest_version(): + """Gets the latest version from git tags.""" + git = Git(".") + tags = git.get_tags() + versions = [ + (tag, parse_version(tag)) + for tag in tags + if re.match(r"^\d+\.\d+\.\d+(rc\d+)?$", tag.strip()) + ] + if not versions: + raise RuntimeError("No git tags found matching X.Y.Z or X.Y.ZrcN format.") + + versions.sort(key=lambda v: v[1]) + latest_tag, latest_version = versions[-1] + + if latest_version.is_prerelease: + raise ValueError(f"The latest version is a pre-release version: {latest_tag}") + + stable_versions = [tag for tag, version in versions if not version.is_prerelease] + if not stable_versions: + raise ValueError("No stable git tags found matching X.Y.Z format.") + + return stable_versions[-1] + + +def get_latest_rc_tag(version, remote=None): + """Queries git tags and returns the highest RC tag for the version.""" + git = Git(".") + if remote: + tags = git.get_remote_tags(remote) + else: + tags = git.get_tags() + pattern = rf"^{re.escape(version)}-rc\d+$" + rc_tags = [tag.strip() for tag in tags if re.match(pattern, tag.strip())] + if not rc_tags: + return None + rc_tags.sort(key=parse_version) + return rc_tags[-1] + + +def should_increment_minor(): + """Checks if the minor version should be incremented.""" + for filepath in _iter_version_placeholder_files(): + try: + with open(filepath, "r") as f: + content = f.read() + except (IOError, UnicodeDecodeError): + continue + + if "VERSION_NEXT_FEATURE" in content: + return True + return False + + +def determine_next_version(branch_name=None): + """Determines the next version based on git tags and the current branch.""" + git = Git(".") + if branch_name is None: + branch_name = git.get_current_branch() + + if branch_name: + release_match = re.match(r"^release/(\d+)\.(\d+)$", branch_name) + if release_match: + branch_major = int(release_match.group(1)) + branch_minor = int(release_match.group(2)) + print( + f"Detected release branch: {branch_name} (targeting" + f" {branch_major}.{branch_minor}.x)" + ) + + tags = git.get_tags() + matching_patches = [] + for tag in tags: + tag = tag.strip() + m = re.match(rf"^{branch_major}\.{branch_minor}\.(\d+)$", tag) + if m: + matching_patches.append(int(m.group(1))) + + if matching_patches: + latest_patch = max(matching_patches) + next_version = f"{branch_major}.{branch_minor}.{latest_patch + 1}" + print( + f"Latest tag on this branch is" + f" {branch_major}.{branch_minor}.{latest_patch}. Next" + f" version: {next_version}" + ) + return next_version + else: + next_version = f"{branch_major}.{branch_minor}.0" + print( + f"No stable tags found for {branch_major}.{branch_minor}.x." + f" Next version: {next_version}" + ) + return next_version + + latest_version = get_latest_version() + major, minor, patch = [int(n) for n in latest_version.split(".")] + + if should_increment_minor(): + return f"{major}.{minor + 1}.0" + else: + return f"{major}.{minor}.{patch + 1}" + + +def replace_version_next(version): + """Replaces all VERSION_NEXT_* placeholders with the new version.""" + for filepath in _iter_version_placeholder_files(): + try: + with open(filepath, "r") as f: + content = f.read() + except (IOError, UnicodeDecodeError): + continue + + if "VERSION_NEXT_FEATURE" in content or "VERSION_NEXT_PATCH" in content: + new_content = content.replace("VERSION_NEXT_FEATURE", version) + new_content = new_content.replace("VERSION_NEXT_PATCH", version) + with open(filepath, "w") as f: + f.write(new_content) + + +def parse_pr_list(value: str) -> list[str]: + """Parses a comma or space separated list of PR references. + + PR references can be numbers (optionally prefixed with '#') or URLs. + """ + if not value: + return [] + # Split by space and/or comma + return [p for p in re.split(r"[\s,]+", value.strip()) if p] + + +def set_github_output(name: str, value: str) -> None: + """Sets a GitHub Actions output parameter if GITHUB_OUTPUT is set.""" + if github_output := os.environ.get("GITHUB_OUTPUT"): + with open(github_output, "a", encoding="utf-8") as f: + f.write(f"{name}={value}\n") diff --git a/tools/private/reviewbot/antigravity_review.py b/tools/private/reviewbot/antigravity_review.py new file mode 100644 index 0000000000..4ae0ef1690 --- /dev/null +++ b/tools/private/reviewbot/antigravity_review.py @@ -0,0 +1,88 @@ +# /// script +# requires-python = ">=3.11" +# dependencies = [ +# "google-antigravity", +# ] +# /// +import argparse +import asyncio +import subprocess +from pathlib import Path + +from google.antigravity import Agent, CapabilitiesConfig, LocalAgentConfig +from google.antigravity.models import GeminiAPIEndpoint, ModelTarget +from google.antigravity.types import BuiltinTools + + +def parse_args(): + parser = argparse.ArgumentParser() + parser.add_argument("--prompt", required=True, help="Path to prompt file") + parser.add_argument("--diff-file", help="Path to pre-computed diff file") + return parser.parse_args() + + +def get_pr_diff(diff_file: str | None = None) -> str: + """Fetches the git diff for the current pull request against origin/main.""" + if diff_file and Path(diff_file).exists(): + return Path(diff_file).read_text() + try: + return subprocess.check_output( + ["git", "diff", "origin/main...HEAD"], text=True, stderr=subprocess.DEVNULL + ) + except Exception: + return "No diff could be automatically extracted via git commands." + + +async def main(): + args = parse_args() + + # Read prompt file and pre-hydrate with the exact PR code diff + base_prompt = Path(args.prompt).read_text() + diff_text = get_pr_diff(args.diff_file) + prompt = ( + f"{base_prompt}\n\n## Pull Request Git Diff\n" + f"Here is the exact code diff for this pull request:\n```diff\n{diff_text}\n```" + ) + + # General coordinator instructions for the reviewer agent. + system_instructions = ( + "You are a code review assistant. Use your available skills to perform " + "reviews on pull requests." + ) + + # Create the default endpoint picking up GEMINI_API_KEY from the environment. + endpoint = GeminiAPIEndpoint() + + # Initialize the Antigravity Agent in read-only mode for security. + # Register the review-pr skill from the local reviewbot folder. + # Provide a comprehensive prioritized cascade across Gemini 3 models + # to automatically fall back if any model hits free-tier quota limits (429) + # or temporary unavailability. + config = LocalAgentConfig( + models=[ + ModelTarget(name="gemini-3.5-flash", endpoint=endpoint), + ModelTarget(name="gemini-3.1-pro-preview", endpoint=endpoint), + ModelTarget(name="gemini-3.1-flash-lite", endpoint=endpoint), + ModelTarget(name="gemini-3-pro-preview", endpoint=endpoint), + ModelTarget(name="gemini-flash-latest", endpoint=endpoint), + ModelTarget(name="gemini-pro-latest", endpoint=endpoint), + ], + system_instructions=system_instructions, + skills_paths=[str(Path(__file__).parent / "skills" / "review-pr" / "SKILL.md")], + capabilities=CapabilitiesConfig( + enabled_tools=BuiltinTools.read_only(), + ), + ) + + async with Agent(config) as agent: + response = await agent.chat(prompt) + report = await response.text() + + print("--- REVIEW REPORT GENERATED ---") + print(report) + + # TODO: Use GITHUB_TOKEN to post the report back to the PR comments. + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/tools/private/reviewbot/prompt.txt b/tools/private/reviewbot/prompt.txt new file mode 100644 index 0000000000..e406164af1 --- /dev/null +++ b/tools/private/reviewbot/prompt.txt @@ -0,0 +1,3 @@ +Use the review-pr skill to review the files modified in this pull request. +Summarize your findings and suggest specific, actionable improvements. +Group your findings into clear, descriptive nits or suggestions. diff --git a/tools/private/reviewbot/skills/review-pr/SKILL.md b/tools/private/reviewbot/skills/review-pr/SKILL.md new file mode 100644 index 0000000000..915a526686 --- /dev/null +++ b/tools/private/reviewbot/skills/review-pr/SKILL.md @@ -0,0 +1,54 @@ +--- +name: review-pr +description: Perform a read-only code review on a pull request. +--- + +# review-pr + +IMPORTANT: The exact git diff for the pull request is pre-provided right in your prompt. Analyze this provided diff directly in a single pass without calling exploratory directory listing or file reading tools unless you specifically need surrounding lines of context from a modified file. + +You are an expert Starlark, Python, and Bazel code reviewer. Analyze the changed files for +correctness, edge cases, and performance. Focus strictly on logical +correctness, concurrency safety, system architecture, performance bottlenecks, +and resource management. Do not comment on style nits or formatting issues +that an automated formatter can handle. Be constructive and concise. + +For every issue or improvement you identify, you MUST output the finding in the +GitHub Actions workflow command warning format. Specify the exact file path +and line numbers that the comment applies to. + +Format each finding exactly as a single line to stdout matching this template: +`::warning file={file_path},line={line_number},endLine={end_line},title={category}::{comment_body}` + +Where: +* `file_path` is the relative file path from the repository root. +* `line_number` is the starting line number in the file where the comment applies. +* `end_line` is the ending line number in the file where the comment applies (equal to line_number if the issue is on a single line). +* `category` is a short tag for the type of issue (e.g., "Error Handling", "Correctness", "Performance"). +* `comment_body` is your constructive and concise feedback. + +Do not write any markdown commentary outside of these GHA command formatted lines. + +Follow these checklists during your review: + +### General Quality & Architecture Checklist +* **PR Description Audit**: Verify the description contains the Why + (business/technical reason), a brief high level overview of changes, + Issue/Bug Link, and explicit Testing Evidence. +* **Separation of Concerns**: Suggest extracting large hardcoded data structures + (e.g., massive templates, complex regexes) to resource files. +* **Logic Correctness**: Verify calculations, negative values, division-by-zero, + and null safety before member access. +* **Error Handling**: Flag silent failures (e.g., empty except blocks) and + unconditional defaults that override configs. +* **Deterministic Operations**: Sort collections/keys to guarantee + reproducible/deterministic execution. + +### Skeptical Critic (Adversarial Specialist Review) +* **Dynamic Filtering**: Filter the PR diff and run only the specialist checks + that have relevant files changed (e.g. skip the C++ checks if only Python + files are modified). +* **Specialist Review Pillars**: Run parallel audits focusing on: + 1. Crash Regression: Null safety and resource lifecycle. + 2. Performance & Latency: Thread bottlenecks, locks, and network calls. + 3. Test Integrity: Coverage validity, change detectors defense. diff --git a/tools/private/sync_downloader_configs.py b/tools/private/sync_downloader_configs.py new file mode 100755 index 0000000000..7e8527b989 --- /dev/null +++ b/tools/private/sync_downloader_configs.py @@ -0,0 +1,37 @@ +#!/usr/bin/env python3 + +"""Synchronizes downloader_config.cfg across subworkspaces.""" + +import sys +from pathlib import Path + + +def main(): + repo_root = Path(__file__).resolve().parent.parent.parent + canonical = repo_root / "downloader_config.cfg" + + subworkspaces = [ + repo_root / "gazelle" / "downloader_config.cfg", + repo_root / "sphinxdocs" / "downloader_config.cfg", + ] + + with open(canonical, "r", encoding="utf-8") as f: + canonical_content = f.read() + + changed = False + for sub in subworkspaces: + if sub.exists(): + with open(sub, "r", encoding="utf-8") as f: + old_content = f.read() + if old_content != canonical_content: + with open(sub, "w", encoding="utf-8") as f: + f.write(canonical_content) + print(f"Updated {sub.relative_to(repo_root)}") + changed = True + + if changed: + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/tools/private/update_deps/BUILD.bazel b/tools/private/update_deps/BUILD.bazel index beecf82189..8746d82c17 100644 --- a/tools/private/update_deps/BUILD.bazel +++ b/tools/private/update_deps/BUILD.bazel @@ -13,7 +13,8 @@ # limitations under the License. load("//python:py_binary.bzl", "py_binary") load("//python:py_library.bzl", "py_library") -load("//python:py_test.bzl", "py_test") +load("//tests/support:support.bzl", "SUPPORTS_BZLMOD") +load("//tests/support/pytest_test:pytest_test.bzl", "pytest_test") licenses(["notice"]) @@ -34,10 +35,10 @@ py_binary( name = "update_coverage_deps", srcs = ["update_coverage_deps.py"], data = [ - "//python/private:coverage_deps", + "//python/private:coverage_deps_filegroup", ], env = { - "UPDATE_FILE": "$(rlocationpath //python/private:coverage_deps)", + "UPDATE_FILE": "$(rlocationpath //python/private:coverage_deps_filegroup)", }, imports = ["../../.."], deps = [ @@ -65,10 +66,11 @@ py_binary( ], ) -py_test( +pytest_test( name = "update_file_test", srcs = ["update_file_test.py"], imports = ["../../.."], + target_compatible_with = SUPPORTS_BZLMOD, deps = [ ":update_file", ], 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 fcb44fcc7c..74ac657bad 100755 --- a/tools/private/update_deps/update_coverage_deps.py +++ b/tools/private/update_deps/update_coverage_deps.py @@ -111,16 +111,16 @@ 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 not in _supported_platforms: + if platform and platform not in _supported_platforms: return None return Dep( name=name, - platform=_supported_platforms[platform], + platform=_supported_platforms[platform] if platform else "", python=python_version, url=url, sha256=digests["sha256"], @@ -170,6 +170,7 @@ def main(): data = json.loads(response.read().decode("utf-8")) urls = [] + default_url = None for u in data["urls"]: if u["yanked"]: continue @@ -177,6 +178,10 @@ def main(): if not u["filename"].endswith(".whl"): continue + if u["filename"].endswith("py3-none-any.whl"): + default_url = _map(name=args.name, platform="", **u) + continue + if u["python_version"] not in args.py: continue @@ -196,7 +201,13 @@ def main(): # Update the coverage_deps, which are used to register deps update_file( path=args.update_file, - snippet=f"_coverage_deps = {repr(Deps(urls))}\n", + snippet="\n".join( + [ + f"_default = {repr(default_url)}", + f"_coverage_deps = {repr(Deps(urls))}", + "", + ] + ), start_marker="# START: maintained by 'bazel run //tools/private/update_deps:update_coverage_deps '", end_marker="# END: maintained by 'bazel run //tools/private/update_deps:update_coverage_deps '", dry_run=args.dry_run, diff --git a/tools/private/update_deps/update_file_test.py b/tools/private/update_deps/update_file_test.py index a3cb1c0a6d..b1d6545ead 100644 --- a/tools/private/update_deps/update_file_test.py +++ b/tools/private/update_deps/update_file_test.py @@ -12,14 +12,13 @@ # See the License for the specific language governing permissions and # limitations under the License. -import unittest +import pytest from tools.private.update_deps.update_file import replace_snippet, unified_diff -class TestReplaceSnippet(unittest.TestCase): - def test_replace_simple(self): - current = """\ +def test_replace_simple(): + current = """\ Before the snippet # Start marker @@ -30,15 +29,14 @@ def test_replace_simple(self): After the snippet """ - snippet = "Replaced" # noqa: F841 - got = replace_snippet( - current=current, - snippet="Replaced", - start_marker="# Start marker", - end_marker="# End marker", - ) - - want = """\ + got = replace_snippet( + current=current, + snippet="Replaced", + start_marker="# Start marker", + end_marker="# End marker", + ) + + want = """\ Before the snippet # Start marker @@ -47,10 +45,11 @@ def test_replace_simple(self): After the snippet """ - self.assertEqual(want, got) + assert got == want - def test_replace_indented(self): - current = """\ + +def test_replace_indented(): + current = """\ Before the snippet # Start marker @@ -59,14 +58,14 @@ def test_replace_indented(self): After the snippet """ - got = replace_snippet( - current=current, - snippet=" Replaced", - start_marker="# Start marker", - end_marker="# End marker", - ) - - want = """\ + got = replace_snippet( + current=current, + snippet=" Replaced", + start_marker="# Start marker", + end_marker="# End marker", + ) + + want = """\ Before the snippet # Start marker @@ -75,45 +74,42 @@ def test_replace_indented(self): After the snippet """ - self.assertEqual(want, got) - - def test_raises_if_start_is_not_found(self): - with self.assertRaises(RuntimeError) as exc: - replace_snippet( - current="foo", - snippet="", - start_marker="start", - end_marker="end", - ) - - self.assertEqual(exc.exception.args[0], "Start marker 'start' was not found") - - def test_raises_if_end_is_not_found(self): - with self.assertRaises(RuntimeError) as exc: - replace_snippet( - current="start", - snippet="", - start_marker="start", - end_marker="end", - ) - - self.assertEqual(exc.exception.args[0], "End marker 'end' was not found") - - -class TestUnifiedDiff(unittest.TestCase): - def test_diff(self): - give_a = """\ + assert got == want + + +def test_raises_if_start_is_not_found(): + with pytest.raises(RuntimeError, match="Start marker 'start' was not found"): + replace_snippet( + current="foo", + snippet="", + start_marker="start", + end_marker="end", + ) + + +def test_raises_if_end_is_not_found(): + with pytest.raises(RuntimeError, match="End marker 'end' was not found"): + replace_snippet( + current="start", + snippet="", + start_marker="start", + end_marker="end", + ) + + +def test_diff(): + give_a = """\ First line second line Third line """ - give_b = """\ + give_b = """\ First line Second line Third line """ - got = unified_diff("filename", give_a, give_b) - want = """\ + got = unified_diff("filename", give_a, give_b) + want = """\ --- a/filename +++ b/filename @@ -1,3 +1,3 @@ @@ -121,8 +117,4 @@ def test_diff(self): -second line +Second line Third line""" - self.assertEqual(want, got) - - -if __name__ == "__main__": - unittest.main() + assert got == want 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/private/zipapp/BUILD.bazel b/tools/private/zipapp/BUILD.bazel index 7420776ef3..45bd126c6e 100644 --- a/tools/private/zipapp/BUILD.bazel +++ b/tools/private/zipapp/BUILD.bazel @@ -1,5 +1,6 @@ load("//python:py_library.bzl", "py_library") load("//python/private:py_interpreter_program.bzl", "py_interpreter_program") # buildifier: disable=bzl-visibility +load("//python/private:visibility.bzl", "NOT_ACTUALLY_PUBLIC") # buildifier: disable=bzl-visibility package( default_visibility = ["//:__subpackages__"], @@ -8,11 +9,9 @@ package( py_interpreter_program( name = "zipper", main = "zipper.py", - visibility = [ - # Not actually public. Only public so rules_python-generated toolchains - # are able to reference it. - "//visibility:public", - ], + # Not actually public. Only public so rules_python-generated toolchains + # are able to reference it. + visibility = NOT_ACTUALLY_PUBLIC, ) py_library( @@ -23,11 +22,9 @@ py_library( py_interpreter_program( name = "exe_zip_maker", main = "exe_zip_maker.py", - visibility = [ - # Not actually public. Only public so rules_python-generated toolchains - # are able to reference it. - "//visibility:public", - ], + # Not actually public. Only public so rules_python-generated toolchains + # are able to reference it. + visibility = NOT_ACTUALLY_PUBLIC, ) py_library( @@ -38,11 +35,9 @@ py_library( py_interpreter_program( name = "zip_main_maker", main = "zip_main_maker.py", - visibility = [ - # Not actually public. Only public so rules_python-generated toolchains - # are able to reference it. - "//visibility:public", - ], + # Not actually public. Only public so rules_python-generated toolchains + # are able to reference it. + visibility = NOT_ACTUALLY_PUBLIC, ) py_library( diff --git a/tools/publish/requirements_darwin.txt b/tools/publish/requirements_darwin.txt index 6d8de8b4c9..93f9d51eee 100644 --- a/tools/publish/requirements_darwin.txt +++ b/tools/publish/requirements_darwin.txt @@ -6,180 +6,197 @@ backports-tarfile==1.2.0 \ --hash=sha256:77e284d754527b01fb1e6fa8a1afe577858ebe4e9dad8919e34c862cb399bc34 \ --hash=sha256:d75e02c268746e1b8144c278978b6e98e85de6ad16f8e4b0844a154557eca991 # via jaraco-context -certifi==2025.10.5 \ - --hash=sha256:0f212c2744a9bb6de0c56639a6f68afe01ecd92d91f14ae897c4fe7bbeeef0de \ - --hash=sha256:47c09d31ccf2acf0be3f701ea53595ee7e0b8fa08801c6624be771df09ae7b43 +certifi==2026.7.22 \ + --hash=sha256:62f22742b58a1a33014a2b6b706588a8d7e2a88ae7bd1a6ebe8c992928483775 \ + --hash=sha256:741e2c3b351ddf169a738da9f2c048608ff7f2c5cc02f1ebc6b118bb090d5d55 # via requests -charset-normalizer==3.4.3 \ - --hash=sha256:00237675befef519d9af72169d8604a067d92755e84fe76492fef5441db05b91 \ - --hash=sha256:02425242e96bcf29a49711b0ca9f37e451da7c70562bc10e8ed992a5a7a25cc0 \ - --hash=sha256:027b776c26d38b7f15b26a5da1044f376455fb3766df8fc38563b4efbc515154 \ - --hash=sha256:07a0eae9e2787b586e129fdcbe1af6997f8d0e5abaa0bc98c0e20e124d67e601 \ - --hash=sha256:0cacf8f7297b0c4fcb74227692ca46b4a5852f8f4f24b3c766dd94a1075c4884 \ - --hash=sha256:0e78314bdc32fa80696f72fa16dc61168fda4d6a0c014e0380f9d02f0e5d8a07 \ - --hash=sha256:0f2be7e0cf7754b9a30eb01f4295cc3d4358a479843b31f328afd210e2c7598c \ - --hash=sha256:13faeacfe61784e2559e690fc53fa4c5ae97c6fcedb8eb6fb8d0a15b475d2c64 \ - --hash=sha256:14c2a87c65b351109f6abfc424cab3927b3bdece6f706e4d12faaf3d52ee5efe \ - --hash=sha256:1606f4a55c0fd363d754049cdf400175ee96c992b1f8018b993941f221221c5f \ - --hash=sha256:16a8770207946ac75703458e2c743631c79c59c5890c80011d536248f8eaa432 \ - --hash=sha256:18343b2d246dc6761a249ba1fb13f9ee9a2bcd95decc767319506056ea4ad4dc \ - --hash=sha256:18b97b8404387b96cdbd30ad660f6407799126d26a39ca65729162fd810a99aa \ - --hash=sha256:1bb60174149316da1c35fa5233681f7c0f9f514509b8e399ab70fea5f17e45c9 \ - --hash=sha256:1e8ac75d72fa3775e0b7cb7e4629cec13b7514d928d15ef8ea06bca03ef01cae \ - --hash=sha256:1ef99f0456d3d46a50945c98de1774da86f8e992ab5c77865ea8b8195341fc19 \ - --hash=sha256:2001a39612b241dae17b4687898843f254f8748b796a2e16f1051a17078d991d \ - --hash=sha256:23b6b24d74478dc833444cbd927c338349d6ae852ba53a0d02a2de1fce45b96e \ - --hash=sha256:252098c8c7a873e17dd696ed98bbe91dbacd571da4b87df3736768efa7a792e4 \ - --hash=sha256:257f26fed7d7ff59921b78244f3cd93ed2af1800ff048c33f624c87475819dd7 \ - --hash=sha256:2c322db9c8c89009a990ef07c3bcc9f011a3269bc06782f916cd3d9eed7c9312 \ - --hash=sha256:30a96e1e1f865f78b030d65241c1ee850cdf422d869e9028e2fc1d5e4db73b92 \ - --hash=sha256:30d006f98569de3459c2fc1f2acde170b7b2bd265dc1943e87e1a4efe1b67c31 \ - --hash=sha256:31a9a6f775f9bcd865d88ee350f0ffb0e25936a7f930ca98995c05abf1faf21c \ - --hash=sha256:320e8e66157cc4e247d9ddca8e21f427efc7a04bbd0ac8a9faf56583fa543f9f \ - --hash=sha256:34a7f768e3f985abdb42841e20e17b330ad3aaf4bb7e7aeeb73db2e70f077b99 \ - --hash=sha256:3653fad4fe3ed447a596ae8638b437f827234f01a8cd801842e43f3d0a6b281b \ - --hash=sha256:3cd35b7e8aedeb9e34c41385fda4f73ba609e561faedfae0a9e75e44ac558a15 \ - --hash=sha256:3cfb2aad70f2c6debfbcb717f23b7eb55febc0bb23dcffc0f076009da10c6392 \ - --hash=sha256:416175faf02e4b0810f1f38bcb54682878a4af94059a1cd63b8747244420801f \ - --hash=sha256:41d1fc408ff5fdfb910200ec0e74abc40387bccb3252f3f27c0676731df2b2c8 \ - --hash=sha256:42e5088973e56e31e4fa58eb6bd709e42fc03799c11c42929592889a2e54c491 \ - --hash=sha256:4ca4c094de7771a98d7fbd67d9e5dbf1eb73efa4f744a730437d8a3a5cf994f0 \ - --hash=sha256:511729f456829ef86ac41ca78c63a5cb55240ed23b4b737faca0eb1abb1c41bc \ - --hash=sha256:53cd68b185d98dde4ad8990e56a58dea83a4162161b1ea9272e5c9182ce415e0 \ - --hash=sha256:585f3b2a80fbd26b048a0be90c5aae8f06605d3c92615911c3a2b03a8a3b796f \ - --hash=sha256:5b413b0b1bfd94dbf4023ad6945889f374cd24e3f62de58d6bb102c4d9ae534a \ - --hash=sha256:5d8d01eac18c423815ed4f4a2ec3b439d654e55ee4ad610e153cf02faf67ea40 \ - --hash=sha256:6aab0f181c486f973bc7262a97f5aca3ee7e1437011ef0c2ec04b5a11d16c927 \ - --hash=sha256:6cf8fd4c04756b6b60146d98cd8a77d0cdae0e1ca20329da2ac85eed779b6849 \ - --hash=sha256:6fb70de56f1859a3f71261cbe41005f56a7842cc348d3aeb26237560bfa5e0ce \ - --hash=sha256:6fce4b8500244f6fcb71465d4a4930d132ba9ab8e71a7859e6a5d59851068d14 \ - --hash=sha256:70bfc5f2c318afece2f5838ea5e4c3febada0be750fcf4775641052bbba14d05 \ - --hash=sha256:73dc19b562516fc9bcf6e5d6e596df0b4eb98d87e4f79f3ae71840e6ed21361c \ - --hash=sha256:74d77e25adda8581ffc1c720f1c81ca082921329452eba58b16233ab1842141c \ - --hash=sha256:78deba4d8f9590fe4dae384aeff04082510a709957e968753ff3c48399f6f92a \ - --hash=sha256:86df271bf921c2ee3818f0522e9a5b8092ca2ad8b065ece5d7d9d0e9f4849bcc \ - --hash=sha256:88ab34806dea0671532d3f82d82b85e8fc23d7b2dd12fa837978dad9bb392a34 \ - --hash=sha256:8999f965f922ae054125286faf9f11bc6932184b93011d138925a1773830bbe9 \ - --hash=sha256:8dcfc373f888e4fb39a7bc57e93e3b845e7f462dacc008d9749568b1c4ece096 \ - --hash=sha256:939578d9d8fd4299220161fdd76e86c6a251987476f5243e8864a7844476ba14 \ - --hash=sha256:96b2b3d1a83ad55310de8c7b4a2d04d9277d5591f40761274856635acc5fcb30 \ - --hash=sha256:a2d08ac246bb48479170408d6c19f6385fa743e7157d716e144cad849b2dd94b \ - --hash=sha256:b256ee2e749283ef3ddcff51a675ff43798d92d746d1a6e4631bf8c707d22d0b \ - --hash=sha256:b5e3b2d152e74e100a9e9573837aba24aab611d39428ded46f4e4022ea7d1942 \ - --hash=sha256:b89bc04de1d83006373429975f8ef9e7932534b8cc9ca582e4db7d20d91816db \ - --hash=sha256:bd28b817ea8c70215401f657edef3a8aa83c29d447fb0b622c35403780ba11d5 \ - --hash=sha256:c60e092517a73c632ec38e290eba714e9627abe9d301c8c8a12ec32c314a2a4b \ - --hash=sha256:c6dbd0ccdda3a2ba7c2ecd9d77b37f3b5831687d8dc1b6ca5f56a4880cc7b7ce \ - --hash=sha256:c6e490913a46fa054e03699c70019ab869e990270597018cef1d8562132c2669 \ - --hash=sha256:c6f162aabe9a91a309510d74eeb6507fab5fff92337a15acbe77753d88d9dcf0 \ - --hash=sha256:c6fd51128a41297f5409deab284fecbe5305ebd7e5a1f959bee1c054622b7018 \ - --hash=sha256:cc34f233c9e71701040d772aa7490318673aa7164a0efe3172b2981218c26d93 \ - --hash=sha256:cc9370a2da1ac13f0153780040f465839e6cccb4a1e44810124b4e22483c93fe \ - --hash=sha256:ccf600859c183d70eb47e05a44cd80a4ce77394d1ac0f79dbd2dd90a69a3a049 \ - --hash=sha256:ce571ab16d890d23b5c278547ba694193a45011ff86a9162a71307ed9f86759a \ - --hash=sha256:cf1ebb7d78e1ad8ec2a8c4732c7be2e736f6e5123a4146c5b89c9d1f585f8cef \ - --hash=sha256:d0e909868420b7049dafd3a31d45125b31143eec59235311fc4c57ea26a4acd2 \ - --hash=sha256:d22dbedd33326a4a5190dd4fe9e9e693ef12160c77382d9e87919bce54f3d4ca \ - --hash=sha256:d716a916938e03231e86e43782ca7878fb602a125a91e7acb8b5112e2e96ac16 \ - --hash=sha256:d79c198e27580c8e958906f803e63cddb77653731be08851c7df0b1a14a8fc0f \ - --hash=sha256:d95bfb53c211b57198bb91c46dd5a2d8018b3af446583aab40074bf7988401cb \ - --hash=sha256:e28e334d3ff134e88989d90ba04b47d84382a828c061d0d1027b1b12a62b39b1 \ - --hash=sha256:ec557499516fc90fd374bf2e32349a2887a876fbf162c160e3c01b6849eaf557 \ - --hash=sha256:fb6fecfd65564f208cbf0fba07f107fb661bcd1a7c389edbced3f7a493f70e37 \ - --hash=sha256:fb731e5deb0c7ef82d698b0f4c5bb724633ee2a489401594c5c88b02e6cb15f7 \ - --hash=sha256:fb7f67a1bfa6e40b438170ebdc8158b78dc465a5a67b6dde178a46987b244a72 \ - --hash=sha256:fd10de089bcdcd1be95a2f73dbe6254798ec1bda9f450d5828c96f93e2536b9c \ - --hash=sha256:fdabf8315679312cfa71302f9bd509ded4f2f263fb5b765cf1433b39106c3cc9 +charset-normalizer==3.4.9 \ + --hash=sha256:0327fcd59a935777d83410750c50600ee9571af2846f71ce40f25b13da1ef380 \ + --hash=sha256:03d07803992c6c7bbc976327f34b18b6160327fc81cb82c9d504720ac0be3b62 \ + --hash=sha256:04ce310cb89c15df659582aee80a0603788732a5e017d5bd5c81158106ce249c \ + --hash=sha256:0d861473f743244d349b50f850d10eb87aeb22bbdcc8e64f79273c94af5a8226 \ + --hash=sha256:0e94703ec9684807f20cfb5eed95c70f67f2a8f21ad620146d7b5a13677b93e5 \ + --hash=sha256:0fa1aec2d32bcc03c8fa0f6f1712caad1adc38509f31142112e5c9daf5b9c833 \ + --hash=sha256:16b65ea0f2465b6fb52aa22de5eca612aa964ddfec00a912e26f4656cbef890b \ + --hash=sha256:16d10d789dd9bcca1173c95af82c58433122564b7bc39385124be735a35cbe99 \ + --hash=sha256:19ac87f93086ce37b86e098888555c4b4bc48102279bae3350098c0ed664b501 \ + --hash=sha256:1d22856ffbe153a602df38e4a5464f0b748a54002e0d69ac6d2ad0a197cc99ec \ + --hash=sha256:21e764fd1e70b6a3e205a0e46f3051701f98a8cb3fad66eeb80e48bb502f8698 \ + --hash=sha256:231ddcbb35e2ff8973e1365db41fe0572662893b99a05deb183b68ad4c0c8bd4 \ + --hash=sha256:253a4a220747e8b5faf57ec320c4f5efb0cef05f647420bf267143ec15dba10a \ + --hash=sha256:280081916dc341820640489a66e4696049401ef1cf6dd672f672e70ad915aca3 \ + --hash=sha256:2a441ea71902098ffe78c5abe6c494f44160b4af614ed16c3d9a3b1d17fd8ee2 \ + --hash=sha256:304b13570067b2547562e308af560b3963857b1fa90bd6afd978130130fe2d6a \ + --hash=sha256:32286a2c8d167e897177b673176c1e3e00d4057caf5d2b64eef9a3666b03018e \ + --hash=sha256:33bdcc2a32c0a0e861f60841a512c8acc658c87c2ac59d89e3a46dacf7d866e4 \ + --hash=sha256:375b83ed0aecfce76c16d198fbc21f3b11b337d68662bea0a995046682a11419 \ + --hash=sha256:3c09a49d6cde137258beb3d551994a2927fd35ad5cf96aed573f61bbd67c5f84 \ + --hash=sha256:3d92613ec25e43b05f042302531ec0f00b8445190e43325880cbd6ab7c2581da \ + --hash=sha256:40a126142a56b2dfc0aacbad1de8310cbf60da7656db0e6b16eebd48e3e93519 \ + --hash=sha256:416c229f77e5ea25b3dfd4b582f8d73d7e43c22320302b9ab128a2d3a0b38efe \ + --hash=sha256:432786d3561e69aeeae6c7e8648964ce0ad05736120135601f87ac26b9c83381 \ + --hash=sha256:43b9e366a31fdd1c87d0eb08f579b4a82b723ea54338f040d6b4e518a026ea29 \ + --hash=sha256:440eede837960000d74978f0eba527be106b5b9aee0daf779d395276ed0b0614 \ + --hash=sha256:45b0cc4e3556cd875e09102988d1ab8356c998b596c9fced84547c8138b487a0 \ + --hash=sha256:476743fe6dfe14a2da12e3ac79125dc84a3b2cf8094369a47a1529b0cd8549fe \ + --hash=sha256:4773092f8019072343a7447203308b176e10199920eb02d6195e81bbb3274c29 \ + --hash=sha256:4b3dac63058cc36820b0dd072f89898604e2d39686fe05321729d00d8ac185a0 \ + --hash=sha256:4d1c96a7a18b9690a4d46df09e3e3382406ae3213727cd1019ebade1c4a81917 \ + --hash=sha256:51307f5c71007673a2bf8232ad973483d281e74cb99c8c5a990af1eefa6277d9 \ + --hash=sha256:51447e9aa2684679af07ca5021c3db526e0284347ebf4ffcec1154c3350cfe32 \ + --hash=sha256:58150c9f9b9a552505912d182ccdf26f6396fb6094816ceebcbb20eecabaed94 \ + --hash=sha256:5b10cd92fc5c498b35a8635df6d5a100207f88b63a4dc1de7ef9a548e1e2cd63 \ + --hash=sha256:5e226f6218febc71f6c1fc2fafb91c226f75bdc1d8fb12d66823716e891608fd \ + --hash=sha256:609b3ba8fcc0fb5ab7af00719d0fb6ad0cb518e48e7712d12fd68f1327951198 \ + --hash=sha256:60f44ade2cf573dad7a277e6f8ca9a51a21dda572b13bd7d8539bb3cd5dbedde \ + --hash=sha256:611057cc5d5c0afc743ba8be6bd828c17e0aaa8643f9d0a9b9bb7dea80eb8012 \ + --hash=sha256:6366a16e1a25018694d6a5d784d09b046edc9eac40ea2b54065c3052672516a1 \ + --hash=sha256:65a7ff3f705e57d392f7261b6d0550fe137c3019477431f1c355e0db0a7d3e15 \ + --hash=sha256:673611bbd43f0810bec0b0f028ddeaaa501190339cac411f347ac76917c3ae7b \ + --hash=sha256:67830fc78e67501f47bb950471b2dcb9b35b140084429318e862895a8e89c993 \ + --hash=sha256:68ce9f4d6b26d5ccbf7fd4459bf75f74a0a146677ebba80597df60cbdb20e6f4 \ + --hash=sha256:68e5f26a1ad57ded6d1cfb85331d1c1a195314756471d97758c48498bb4dcdf5 \ + --hash=sha256:69b157c5d3292bcd443faca052f3096f637f1e074b98212a933c074ae23dc3b8 \ + --hash=sha256:75286256590a6320cf106a0d28970d3560aad9ee09aa7b34fb40524792436d35 \ + --hash=sha256:78841cccf1af7b40f6f716338d50c0902dbe88d9f800b3c973b7a9a0a693a642 \ + --hash=sha256:78fa18e436a1a0e58dbd7e02fc4473f3f32cceb12df9dfca542d075961c307d2 \ + --hash=sha256:79580094b00d1789d1f93ea55bc43cb2f611910c72235b7657f3482ddcc1b22d \ + --hash=sha256:7b86a2b16095d250c6f58b3d9b2eee6f4147754344f3dab0922f7c9bf7d226c9 \ + --hash=sha256:83aed2c10721ddd90f68140685391b50811a880af20654c59af6b6c66c40513c \ + --hash=sha256:84fd18bcc17526fc2b3c1af7d2b9217d32c9c04448c16ec693b9b4f1985c3d33 \ + --hash=sha256:871ff67ea1aad4dfd91736464934d56b32dac49f9fbe16cddba36198a7b3a0db \ + --hash=sha256:898f0e9068ca27d37f8e83a5b962821df851532e6c4a7d615c1c033f9da6eedf \ + --hash=sha256:8a79d9f4d8001473a30c163556b3c3bfebec837495a412dde78b51672f6134f9 \ + --hash=sha256:8c041122946b7ba21bb32c45b1aa57b1be35527690aeb3c5c234521085632eee \ + --hash=sha256:90c44bc373b7687f6948b693cceaea1348ae0975d7474746559494468e3c1d84 \ + --hash=sha256:9104ed0bd76a429d46f9ec0dbc9b08ad1d2dcdf2b00a5a0daa1c145329b35b44 \ + --hash=sha256:920079c3f7456fa213e0829ed2073aaa727fd39d889ead5b4f35d0de5460d04f \ + --hash=sha256:93d59d504b230e83c7a843251681959a0b6a9cd76f6e146ce1b8a80eb8739af9 \ + --hash=sha256:9b2aff1c7b3884512b9512c3eaadd9bab39fb45042ffaaa1dd08ff2b9f8109d9 \ + --hash=sha256:9b8e0f3107e2200b76f6054de99016eac3ee6762713587b36baaa7e4bd2ae177 \ + --hash=sha256:9bb41182d93ea91f60b4bc8fbf4c820c69ef8a12ab2d917f3f1834f1acad07e8 \ + --hash=sha256:9cdef90ae47919cae358d8ab15797a800ed41da7aba5d72419fb510729e2ed4b \ + --hash=sha256:a1786910334ed46ab1dd73222f2cd1e05c2c3bb39f6dddb4f8b36fc382058a39 \ + --hash=sha256:a4cfde78a9f2880208d16a93b795726a3017d5977e08d1e162a7a31322479c41 \ + --hash=sha256:a4fbdde9dd4a9ce5fd52c2b3a347bb50cc89483ef783f1cb00d408c13f7a96c0 \ + --hash=sha256:aa99adc8f081b475a12843953db36831eaf83ec33eb46a90629ca6a5de45a616 \ + --hash=sha256:ac351b3b8014eead140e77e9717e2992c6bbe30b63bc3422422eb84865412e3d \ + --hash=sha256:ad41ba96094304aa090f5a30cb6e4fb3b3f1c264c523394b4c39bbacc4dc92ba \ + --hash=sha256:b5314963fce9b0b12743891de876e724997864ee22aa496f903f426c7e2fa5b2 \ + --hash=sha256:bcf74c1df76758a395bf0af608c04c82257523f55c9868b334f06270d0f2112b \ + --hash=sha256:bd47ba7fc3ca94896759ea0109775132d3e7ab921fbf54038e1bab2e46c313c9 \ + --hash=sha256:c0323c9daef75ef2e5083624b4585018a0c9d5e3b40f607eed81a311270b934b \ + --hash=sha256:c1225416b463483160e4af85d5fc3a9690ccb53fd4b1865a6437825f5ede3209 \ + --hash=sha256:c1c948747b03be832dceed96ca815cef7360de9aa19d37c730f8e3f6101aca48 \ + --hash=sha256:c25fe15c70c59eb7c5ce8c06a1f3fa1da0ecc5ea1e7a5922c40fd2fa9b0d5046 \ + --hash=sha256:cc1b0fff8ead343dae06305f954eb8468ba0ec1a97881f42489d198e4ce3c632 \ + --hash=sha256:cd6280cf040f233bd7d3407b743b4b4c74f70e8e1c4199cb112a62c941c0772a \ + --hash=sha256:cd6c3d4b783c556fa00bf540854e42f135e2f256abd29669fcd0da0f2dec79c2 \ + --hash=sha256:d4d6fcde76f94f5cb9e43e9e9a61f16dacefd228cbbf6f1a09bd9b219a92f1a1 \ + --hash=sha256:ddf4af30b417d9fe16481e9b81c27ab2a7cde1ff7ba3e85653b02db7d145dc7b \ + --hash=sha256:df115d4d83168fdf2cae48ef1ff6d1cb4c466364e30861b37121de0f3bf1b990 \ + --hash=sha256:df7276909358e5635ae203673ab7e509ddd224225a8d6b0790bf13eb2bde1cc5 \ + --hash=sha256:e4fd89cc178bced6ad29cb3e6dd4aa63fa5017c3524dbd0b25998fb64a87cc8b \ + --hash=sha256:e9701d0049d92c16703a42771b98d560b95248949f23f8cf7b4eddd201814fb9 \ + --hash=sha256:ee2f2a527e3c1a6e6411eb4209642e138b544a2d72fe5d0d76daf77b24063534 \ + --hash=sha256:f7fb7d750cfa0a070d2c24e831fd3481019a60dd317ea2b39acbcebc08b6ed81 \ + --hash=sha256:f840ed6d8ecba8255df8c42b87fadeda98ddfc6eeec05e2dc66e26d46dd6f58a \ + --hash=sha256:f86c6358749bd4fda175388691e3ba8c46e24c5347d0afd20f9b7edfc9faf07d \ + --hash=sha256:fa36ec09ef71d158186bc79e359ff5fdd6e7996fe8ab638f00d6b93139ba4fcf \ + --hash=sha256:fe2c7201c642b7c308f1675355ad7ff7b66acfe3541625efe5a3ad38f29d6115 # via requests -docutils==0.22.2 \ - --hash=sha256:9fdb771707c8784c8f2728b67cb2c691305933d68137ef95a75db5f4dfbc213d \ - --hash=sha256:b0e98d679283fc3bb0ead8a5da7f501baa632654e7056e9c5846842213d674d8 +docutils==0.23 \ + --hash=sha256:25d013af9bf23bc1c7b2b093dff4208166c53a94786c9e447808335ef1185fea \ + --hash=sha256:746f5060322511280a1e50eb76846ed6bf2342984b2ac04dc42caa1a8d78799e # via readme-renderer -idna==3.10 \ - --hash=sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9 \ - --hash=sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3 +id==1.6.1 \ + --hash=sha256:d0732d624fb46fd4e7bc4e5152f00214450953b9e772c182c1c22964def1a069 \ + --hash=sha256:f5ec41ed2629a508f5d0988eda142e190c9c6da971100612c4de9ad9f9b237ca + # via twine +idna==3.18 \ + --hash=sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2 \ + --hash=sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848 # via requests -importlib-metadata==8.7.0 \ - --hash=sha256:d13b81ad223b890aa16c5471f2ac3056cf76c5f10f82d6f9292f0b415f389000 \ - --hash=sha256:e5dd1551894c77868a30651cef00984d50e1002d06942a7101d34870c5f02afd - # via - # keyring - # twine +importlib-metadata==9.0.0 \ + --hash=sha256:2d21d1cc5a017bd0559e36150c21c830ab1dc304dedd1b7ea85d20f45ef3edd7 \ + --hash=sha256:a4f57ab599e6a2e3016d7595cfd72eb4661a5106e787a95bcc90c7105b831efc + # via keyring jaraco-classes==3.4.0 \ --hash=sha256:47a024b51d0239c0dd8c8540c6c7f484be3b8fcf0b2d85c13825780d3b3f3acd \ --hash=sha256:f662826b6bed8cace05e7ff873ce0f9283b5c924470fe664fff1c2f00f581790 # via keyring -jaraco-context==6.0.1 \ - --hash=sha256:9bae4ea555cf0b14938dc0aee7c9f32ed303aa20a3b73e7dc80111628792d1b3 \ - --hash=sha256:f797fc481b490edb305122c9181830a3a5b76d84ef6d1aef2fb9b47ab956f9e4 +jaraco-context==6.1.2 \ + --hash=sha256:bf8150b79a2d5d91ae48629d8b427a8f7ba0e1097dd6202a9059f29a36379535 \ + --hash=sha256:f1a6c9d391e661cc5b8d39861ff077a7dc24dc23833ccee564b234b81c82dfe3 # via keyring -jaraco-functools==4.3.0 \ - --hash=sha256:227ff8ed6f7b8f62c56deff101545fa7543cf2c8e7b82a7c2116e672f29c26e8 \ - --hash=sha256:cfd13ad0dd2c47a3600b439ef72d8615d482cedcff1632930d6f28924d92f294 +jaraco-functools==4.6.0 \ + --hash=sha256:880c577ec9720b3a052d5bc611fb9f2269b3d87902ef42440df443b88e443280 \ + --hash=sha256:99e3dc0060c5cbe8fcd1cdb36258e2a65ca40f1566b2033b12abb1bb44dd3c30 # via keyring -keyring==25.6.0 \ - --hash=sha256:0b39998aa941431eb3d9b0d4b2460bc773b9df6fed7621c2dfb291a7e0187a66 \ - --hash=sha256:552a3f7af126ece7ed5c89753650eec89c7eaae8617d0aa4d9ad2b75111266bd +keyring==25.7.0 \ + --hash=sha256:be4a0b195f149690c166e850609a477c532ddbfbaed96a404d4e43f8d5e2689f \ + --hash=sha256:fe01bd85eb3f8fb3dd0405defdeac9a5b4f6f0439edbb3149577f244a2e8245b # via twine -markdown-it-py==4.0.0 \ - --hash=sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147 \ - --hash=sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3 +markdown-it-py==4.2.0 \ + --hash=sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49 \ + --hash=sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a # via rich mdurl==0.1.2 \ --hash=sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8 \ --hash=sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba # via markdown-it-py -more-itertools==10.8.0 \ - --hash=sha256:52d4362373dcf7c52546bc4af9a86ee7c4579df9a8dc268be0a2f949d376cc9b \ - --hash=sha256:f638ddf8a1a0d134181275fb5d58b086ead7c6a72429ad725c67503f13ba30bd +more-itertools==11.1.0 \ + --hash=sha256:48e8f4d9e7e5878571ecf6f2b4e57634f93cd474cc8cfbd2376f2d11b396e30d \ + --hash=sha256:4b65538ae22f6fed0ce4874efd317463a7489796a0939fa66824dd542125a192 # via # jaraco-classes # jaraco-functools -nh3==0.3.0 \ - --hash=sha256:0649464ac8eee018644aacbc103874ccbfac80e3035643c3acaab4287e36e7f5 \ - --hash=sha256:16f8670201f7e8e0e05ed1a590eb84bfa51b01a69dd5caf1d3ea57733de6a52f \ - --hash=sha256:1adeb1062a1c2974bc75b8d1ecb014c5fd4daf2df646bbe2831f7c23659793f9 \ - --hash=sha256:37d3003d98dedca6cd762bf88f2e70b67f05100f6b949ffe540e189cc06887f9 \ - --hash=sha256:389d93d59b8214d51c400fb5b07866c2a4f79e4e14b071ad66c92184fec3a392 \ - --hash=sha256:3f1b4f8a264a0c86ea01da0d0c390fe295ea0bcacc52c2103aca286f6884f518 \ - --hash=sha256:423201bbdf3164a9e09aa01e540adbb94c9962cc177d5b1cbb385f5e1e79216e \ - --hash=sha256:634e34e6162e0408e14fb61d5e69dbaea32f59e847cfcfa41b66100a6b796f62 \ - --hash=sha256:6d68fa277b4a3cf04e5c4b84dd0c6149ff7d56c12b3e3fab304c525b850f613d \ - --hash=sha256:7275fdffaab10cc5801bf026e3c089d8de40a997afc9e41b981f7ac48c5aa7d5 \ - --hash=sha256:7852f038a054e0096dac12b8141191e02e93e0b4608c4b993ec7d4ffafea4e49 \ - --hash=sha256:7c915060a2c8131bef6a29f78debc29ba40859b6dbe2362ef9e5fd44f11487c2 \ - --hash=sha256:80fe20171c6da69c7978ecba33b638e951b85fb92059259edd285ff108b82a6d \ - --hash=sha256:a537ece1bf513e5a88d8cff8a872e12fe8d0f42ef71dd15a5e7520fecd191bbb \ - --hash=sha256:af5aa8127f62bbf03d68f67a956627b1bd0469703a35b3dad28d0c1195e6c7fb \ - --hash=sha256:b0612ccf5de8a480cf08f047b08f9d3fecc12e63d2ee91769cb19d7290614c23 \ - --hash=sha256:ba0caa8aa184196daa6e574d997a33867d6d10234018012d35f86d46024a2a95 \ - --hash=sha256:bae63772408fd63ad836ec569a7c8f444dd32863d0c67f6e0b25ebbd606afa95 \ - --hash=sha256:c7a32a7f0d89f7d30cb8f4a84bdbd56d1eb88b78a2434534f62c71dac538c450 \ - --hash=sha256:ce5e7185599f89b0e391e2f29cc12dc2e206167380cea49b33beda4891be2fe1 \ - --hash=sha256:d8ba24cb31525492ea71b6aac11a4adac91d828aadeff7c4586541bf5dc34d2f \ - --hash=sha256:d97d3efd61404af7e5721a0e74d81cdbfc6e5f97e11e731bb6d090e30a7b62b2 \ - --hash=sha256:e90883f9f85288f423c77b3f5a6f4486375636f25f793165112679a7b6363b35 \ - --hash=sha256:e9e6a7e4d38f7e8dda9edd1433af5170c597336c1a74b4693c5cb75ab2b30f2a \ - --hash=sha256:ec6cfdd2e0399cb79ba4dcffb2332b94d9696c52272ff9d48a630c5dca5e325a \ - --hash=sha256:f416c35efee3e6a6c9ab7716d9e57aa0a49981be915963a82697952cba1353e1 +nh3==0.3.6 \ + --hash=sha256:082675ff87b9385ec430ffe6d5847ba7456cc39b73720cd4add472f9f4cffd56 \ + --hash=sha256:2411e8c3cee81a1ddd62c2a5d50585c28aa5566d373ad1db92536b95ddb24ef2 \ + --hash=sha256:25c733bee928530556b1db0ea46c52cf5aa686146e38e60a6fc7cb801ef91cec \ + --hash=sha256:2f90d9a0cfdbee218994fdaaeeb5a0fde62d08f35e4eef0378ec1e2200172fd0 \ + --hash=sha256:34d2b0d934156b87ee114f599a3ba9b8b9e17b5d79652ba3a13fa50903de965e \ + --hash=sha256:36d06341bd501240d320f5942481ed5e6846136b666e1ba4faf802b78ebc875f \ + --hash=sha256:43bc1ed3fa0716295fabee29ba42b2667e4a51d140b0a68e092170a765474fa6 \ + --hash=sha256:44673b27010051ab5a5e438a86ec31bbda61d4a77d7e900af6b7be3037c1abae \ + --hash=sha256:455469a29951edc92bc48b47ac2281c3f2609e6c4f6a047056449f8c2c23facf \ + --hash=sha256:4713502748f564fee0633b37b3403783ce0a3af3a3d148ad91025a5bdadb7bc6 \ + --hash=sha256:5276ef17bdba9ad8040575c74072008b13aae429436e9d0429e718bb5f90f4da \ + --hash=sha256:597a8e843bea00b2eb5520658dc24a9bb032e7fc9e7c2c0c4cd29420220c9796 \ + --hash=sha256:69bbb92865a693d909db3a700d3c01537533844d0948c1e9323561ce06ecda41 \ + --hash=sha256:69f365963f63a1e9bff53bdbb3c542c7c2efed3e163c9d5d83a772a2ac468c21 \ + --hash=sha256:82ca5bf427ad1b216b65ede1a2e2d87dc49bec417ceba0f297213107d3cd9d78 \ + --hash=sha256:889932a97fb4abb6f95fef1914c0d269ebfb60011e67121c1163059b9449dbb4 \ + --hash=sha256:905f877dc66dd7aea4a76e54bcb26acb5ff8216f720c0017ccf63e0e6035698e \ + --hash=sha256:a43ebd7543555c3ac1bc353023d0794e75cb76f6f18f19c32e95441496c0cc25 \ + --hash=sha256:d14bf7982e7a77c0c775634c29c07ce08b38a046df73e1c1f139b3e82f18a38e \ + --hash=sha256:e196fa70c2ff2eb4de7d3df3108f8f358c1d69dff20d45b11f20a5aa227ffb6d \ + --hash=sha256:e1b160831c9cdb06a6c79c2f9cdb11386602938f9af260d1c457a85add4f6f69 \ + --hash=sha256:e6b7beece07525dc6e6b0fc2f104442de2ba328360ad00e50cbe2e1fd620447d \ + --hash=sha256:edb2b4a1a27523e6cc7c417f8d21ce3d005243548b93e56b762b66b0c7f589f9 \ + --hash=sha256:f2f14b7ae1fca99c4a66c981aac3974e7fbc1ca30a12673d223ae1df76680917 \ + --hash=sha256:f338ac7d594c067679f1e99b4f5ec3906842979560f9d8f15d6bdfa39a353b10 \ + --hash=sha256:f3736c9dd3d1856f80cd031715b84ca75cda2bbb1ac802c3da26bfce590838d7 \ + --hash=sha256:f5ed5fe84aee7f39db95c214a7421bf0499fbf500fec6d86a4e29bfc37971438 # via readme-renderer -pkginfo==1.12.1.2 \ - --hash=sha256:5cd957824ac36f140260964eba3c6be6442a8359b8c48f4adf90210f33a04b7b \ - --hash=sha256:c783ac885519cab2c34927ccfa6bf64b5a704d7c69afaea583dd9b7afe969343 +packaging==26.3 \ + --hash=sha256:94edc256424af38762eb31306eed28beb9f0efc50a8837492c9d6fd6004aed79 \ + --hash=sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c # via twine -pygments==2.19.2 \ - --hash=sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887 \ - --hash=sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b +pygments==2.20.0 \ + --hash=sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f \ + --hash=sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176 # via # readme-renderer # rich -readme-renderer==44.0 \ - --hash=sha256:2fbca89b81a08526aadf1357a8c2ae889ec05fb03f5da67f9769c9a592166151 \ - --hash=sha256:8712034eabbfa6805cacf1402b4eeb2a73028f72d1166d6f5cb7f9c047c5d1e1 +readme-renderer==45.0 \ + --hash=sha256:030a8fac74904f8fba11ad1bb6964e3f76e896dc7e5e71f16af190c9056696d1 \ + --hash=sha256:3385ed220117104a2bceb4a9dac8c5fdf6d1f96890d7ea2a9c7174fd5c84091f # via twine -requests==2.33.0 \ - --hash=sha256:3324635456fa185245e24865e810cecec7b4caf933d7eb133dcde67d48cee69b \ - --hash=sha256:c7ebc5e8b0f21837386ad0e1c8fe8b829fa5f544d8df3b2253bff14ef29d7652 +requests==2.34.2 \ + --hash=sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0 \ + --hash=sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed # via # requests-toolbelt # twine @@ -191,21 +208,22 @@ rfc3986==2.0.0 \ --hash=sha256:50b1502b60e289cb37883f3dfd34532b8873c7de9f49bb546641ce9cbd256ebd \ --hash=sha256:97aacf9dbd4bfd829baad6e6309fa6573aaf1be3f6fa735c8ab05e46cecb261c # via twine -rich==14.1.0 \ - --hash=sha256:536f5f1785986d6dbdea3c75205c473f970777b4a0d6c6dd1b696aa05a3fa04f \ - --hash=sha256:e497a48b844b0320d45007cdebfeaeed8db2a4f4bcf49f15e455cfc4af11eaa8 +rich==15.0.0 \ + --hash=sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb \ + --hash=sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36 # via twine -twine==5.1.1 \ - --hash=sha256:215dbe7b4b94c2c50a7315c0275d2258399280fbb7d04182c7e55e24b5f93997 \ - --hash=sha256:9aa0825139c02b3434d913545c7b847a21c835e11597f5255842d457da2322db +twine==7.0.0 \ + --hash=sha256:85cdb29c518efef867360ae4acd4b0dfd61c8654a22fca08e6f8539f05022177 \ + --hash=sha256:b854164df26db268af05f49aa5c0344b10e27a494343ff05b1e0bad3b135f5a7 # via -r tools/publish/requirements.in -urllib3==2.6.3 \ - --hash=sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed \ - --hash=sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4 +urllib3==2.7.0 \ + --hash=sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c \ + --hash=sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897 # via + # id # requests # twine -zipp==3.23.0 \ - --hash=sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e \ - --hash=sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166 +zipp==4.1.0 \ + --hash=sha256:25ad4e16390cd314347dd8f1de67a2ac538ae658ed4ab9db16029c07c188e97f \ + --hash=sha256:4cb57381f544315db7688e976e922a2b18cdb513d21cc194eb42232ba2a3e602 # via importlib-metadata diff --git a/tools/publish/requirements_linux.txt b/tools/publish/requirements_linux.txt index 8865eb3ecb..00d46f9ff0 100644 --- a/tools/publish/requirements_linux.txt +++ b/tools/publish/requirements_linux.txt @@ -6,253 +6,282 @@ backports-tarfile==1.2.0 \ --hash=sha256:77e284d754527b01fb1e6fa8a1afe577858ebe4e9dad8919e34c862cb399bc34 \ --hash=sha256:d75e02c268746e1b8144c278978b6e98e85de6ad16f8e4b0844a154557eca991 # via jaraco-context -certifi==2025.10.5 \ - --hash=sha256:0f212c2744a9bb6de0c56639a6f68afe01ecd92d91f14ae897c4fe7bbeeef0de \ - --hash=sha256:47c09d31ccf2acf0be3f701ea53595ee7e0b8fa08801c6624be771df09ae7b43 +certifi==2026.7.22 \ + --hash=sha256:62f22742b58a1a33014a2b6b706588a8d7e2a88ae7bd1a6ebe8c992928483775 \ + --hash=sha256:741e2c3b351ddf169a738da9f2c048608ff7f2c5cc02f1ebc6b118bb090d5d55 # via requests -cffi==2.0.0 \ - --hash=sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb \ - --hash=sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b \ - --hash=sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f \ - --hash=sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9 \ - --hash=sha256:0cf2d91ecc3fcc0625c2c530fe004f82c110405f101548512cce44322fa8ac44 \ - --hash=sha256:0f6084a0ea23d05d20c3edcda20c3d006f9b6f3fefeac38f59262e10cef47ee2 \ - --hash=sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c \ - --hash=sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75 \ - --hash=sha256:1cd13c99ce269b3ed80b417dcd591415d3372bcac067009b6e0f59c7d4015e65 \ - --hash=sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e \ - --hash=sha256:1f72fb8906754ac8a2cc3f9f5aaa298070652a0ffae577e0ea9bd480dc3c931a \ - --hash=sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e \ - --hash=sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25 \ - --hash=sha256:2081580ebb843f759b9f617314a24ed5738c51d2aee65d31e02f6f7a2b97707a \ - --hash=sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe \ - --hash=sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b \ - --hash=sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91 \ - --hash=sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592 \ - --hash=sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187 \ - --hash=sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c \ - --hash=sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1 \ - --hash=sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94 \ - --hash=sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba \ - --hash=sha256:3e837e369566884707ddaf85fc1744b47575005c0a229de3327f8f9a20f4efeb \ - --hash=sha256:3f4d46d8b35698056ec29bca21546e1551a205058ae1a181d871e278b0b28165 \ - --hash=sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529 \ - --hash=sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca \ - --hash=sha256:4647afc2f90d1ddd33441e5b0e85b16b12ddec4fca55f0d9671fef036ecca27c \ - --hash=sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6 \ - --hash=sha256:53f77cbe57044e88bbd5ed26ac1d0514d2acf0591dd6bb02a3ae37f76811b80c \ - --hash=sha256:5eda85d6d1879e692d546a078b44251cdd08dd1cfb98dfb77b670c97cee49ea0 \ - --hash=sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743 \ - --hash=sha256:61d028e90346df14fedc3d1e5441df818d095f3b87d286825dfcbd6459b7ef63 \ - --hash=sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5 \ - --hash=sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5 \ - --hash=sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4 \ - --hash=sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d \ - --hash=sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b \ - --hash=sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93 \ - --hash=sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205 \ - --hash=sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27 \ - --hash=sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512 \ - --hash=sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d \ - --hash=sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c \ - --hash=sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037 \ - --hash=sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26 \ - --hash=sha256:89472c9762729b5ae1ad974b777416bfda4ac5642423fa93bd57a09204712322 \ - --hash=sha256:8ea985900c5c95ce9db1745f7933eeef5d314f0565b27625d9a10ec9881e1bfb \ - --hash=sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c \ - --hash=sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8 \ - --hash=sha256:9332088d75dc3241c702d852d4671613136d90fa6881da7d770a483fd05248b4 \ - --hash=sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414 \ - --hash=sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9 \ - --hash=sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664 \ - --hash=sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9 \ - --hash=sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775 \ - --hash=sha256:b18a3ed7d5b3bd8d9ef7a8cb226502c6bf8308df1525e1cc676c3680e7176739 \ - --hash=sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc \ - --hash=sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062 \ - --hash=sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe \ - --hash=sha256:b882b3df248017dba09d6b16defe9b5c407fe32fc7c65a9c69798e6175601be9 \ - --hash=sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92 \ - --hash=sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5 \ - --hash=sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13 \ - --hash=sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d \ - --hash=sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26 \ - --hash=sha256:cb527a79772e5ef98fb1d700678fe031e353e765d1ca2d409c92263c6d43e09f \ - --hash=sha256:cf364028c016c03078a23b503f02058f1814320a56ad535686f90565636a9495 \ - --hash=sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b \ - --hash=sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6 \ - --hash=sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c \ - --hash=sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef \ - --hash=sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5 \ - --hash=sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18 \ - --hash=sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad \ - --hash=sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3 \ - --hash=sha256:de8dad4425a6ca6e4e5e297b27b5c824ecc7581910bf9aee86cb6835e6812aa7 \ - --hash=sha256:e11e82b744887154b182fd3e7e8512418446501191994dbf9c9fc1f32cc8efd5 \ - --hash=sha256:e6e73b9e02893c764e7e8d5bb5ce277f1a009cd5243f8228f75f842bf937c534 \ - --hash=sha256:f73b96c41e3b2adedc34a7356e64c8eb96e03a3782b535e043a986276ce12a49 \ - --hash=sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2 \ - --hash=sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5 \ - --hash=sha256:fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453 \ - --hash=sha256:fe562eb1a64e67dd297ccc4f5addea2501664954f2692b69a76449ec7913ecbf +cffi==2.1.1 \ + --hash=sha256:046bfc24911b37851ee1b51aab8bffe713d89c68c6a057b09484ce9fd5f69b4e \ + --hash=sha256:06c72bb76605a4b0cd0aad6930b69d4baf7dd5d806cfc409b824191099700e66 \ + --hash=sha256:0beceaabe56af686895136a2de78db54ecd8e4046b236b8fd6d6cb61389e9bf2 \ + --hash=sha256:154852545011f779917b11c78db2358d095da62a9a172b78ad0a583ee5adc0d0 \ + --hash=sha256:194cffa889098ced9976c3fc6340305e43f6303657d298da55366907c05c22d6 \ + --hash=sha256:19ee6127ee34de7d83ce3d371ebc5ed91addbdcc39f9ab15ce4eb35a4e534971 \ + --hash=sha256:1a18a57b58cfb21fc28d72e876acf10eaed67a1ed96226f92af4df681d571c4c \ + --hash=sha256:1aa5645c30469b09530c4ebca77ebf8f17618293c58f8549cb1a543a50236e7d \ + --hash=sha256:1dea0e4d7d4f11f619fe8c1d76caf49e24405b4b5743c0e3be16a500ecd930c9 \ + --hash=sha256:208f941bb9d18e768138677f0a6d2ce01f590df56043dda1df1535ac57c88517 \ + --hash=sha256:210019b6c7cf07f081b4c54635c8cf744377001350e29cc0f81c4377b4797735 \ + --hash=sha256:246fa40ce8645a614ff682e0b70f37134e460eaf93a775e0cbe3cca585a67a80 \ + --hash=sha256:25792eac27877609e7bb06d42ff88278a6624fff2ba9bbb523c09616b117e80f \ + --hash=sha256:27350daa11d4f10c540e6e89dada4c54feb7256ad03e9a4dc075ebad7ba360d1 \ + --hash=sha256:28907ab9bfb6aa13184cfc17c6b8e1023c5ab6fd7076d8c20a35e59fe04f8f29 \ + --hash=sha256:2ae64be792b8966f2c69538199728b290e34726562896df1e5dc8ffd8d8188e8 \ + --hash=sha256:31348097ff5bbe827ccc41795d4dd099d9f0625e7def00ee653c137a490c2a6c \ + --hash=sha256:3143d81e29e1e20a9ce10901ec369012947876596f75a222235965f2b7ae832e \ + --hash=sha256:3222ba5d678f80a030e6afbcc33dc1ae5cb45facabb61cee2c7016b8432fde48 \ + --hash=sha256:3311ed60d36f83378794e1009ac6258bafbf81f7888b4caa7b35a521e3f95813 \ + --hash=sha256:334644fbac4eff73d985a17a91226df55d0f394160c4cfb880e084c8f7161cac \ + --hash=sha256:34e261f78cb6ceaaa36f42f2613f4380d94d9c759a9c73c769ee6e0247364632 \ + --hash=sha256:363e05fa78e15116c3c32c210ee36884fd6b9afa6d440e47112c3bd511d64cb6 \ + --hash=sha256:398aff33cee2767e3e781d2554c54bd0dff386bb437581e0d8011fde1a942ec1 \ + --hash=sha256:3d22a20b1fb1632cc72c22f95f7b0d2961c3e1c235f245ba4c606c4771035659 \ + --hash=sha256:42a494cee34437f05546455144f2b5d9ac09b1face62bcfce597d2e521066688 \ + --hash=sha256:42e2f76b9455f5a9a844f770bf3e200ed3da0e15f5df3db9c31fe80b04b3d004 \ + --hash=sha256:42f6930c31dc7f50732c9ae793c2786c7b6b044195967bbdde40bb9be81c4cc0 \ + --hash=sha256:456a61fa52d579ebf9df2e9552ead5129855dbaff6c1e5a9b1bc408809bdc062 \ + --hash=sha256:471cee653ae88de62096552e6d24ccb4a5adb8c8c9f10b5054d0122c15bf2779 \ + --hash=sha256:49cbc70e6542d4ccccb936558d1064a8012541e78f821f955cff24e357776c94 \ + --hash=sha256:4a7c934f7360e8cd64fe9efadcbd10c7c6364f531e432b9a4bf5ccbc9e0e8b50 \ + --hash=sha256:4be96343e422f2dfcd12ab5c9f5aebe03f82f737c6bffeca6830b3875cb44aab \ + --hash=sha256:4f42141fc14250de6dde5ee7ea4432be017252d91f19c5ad043c084cea629cac \ + --hash=sha256:507a24c282e0f42f8ed737cf048572cbf580468da5555764a8331735e9c736b6 \ + --hash=sha256:51b31d1c98274844cfd7838ce00bfc27c7423a4dc00fc0772fc3331c2cc90676 \ + --hash=sha256:58acb8ab8e295e6c5ea12f888cbb13cf21511ef2a3303a23f4325c29d17fe5c1 \ + --hash=sha256:5a59cc1c4442bc3d5c703bf720b51138d0bfc173618807c9ee2490a7541dd3d9 \ + --hash=sha256:5bb4e7ea95dcd6a014a6fef62e62467d67d8e582326443f3d68e71d6320a9fcf \ + --hash=sha256:5c58fe613dc5e5336357eff555824a314d8e43282600435c8d1cb6a7a2fedd13 \ + --hash=sha256:5e7cecbaadb83884793e05828cee59b210b24583b9c7425d0ba6a754fe22eb4e \ + --hash=sha256:616f097f2fe415bc92a247f02e11f634e1f9e9a83d327e3c915c15089c87869e \ + --hash=sha256:63bbfd5ded17c4840ac07cd8f1c21ba9d9708141f840b324f422f41b207e3973 \ + --hash=sha256:64faea20f4e2613363a1a9b9c7dd73058f3ecd00133a511e72ad7c511658f527 \ + --hash=sha256:661c298b4821edebead0c91edd2b00374d67ad7c5a1f7a91d4442633b79d6a72 \ + --hash=sha256:68e62fe11f30d5ca8289242866f0a5291402d8529ca2178ab8afc5c9694ae890 \ + --hash=sha256:6a8dddef476fab96d066d578fc88526767b836ab5ab21754e1d5bf3879c31c7c \ + --hash=sha256:6e192623c49c94421616a5778fba35cf0d5a8d000650c1967ef4448ee5cdd990 \ + --hash=sha256:7225e4514edb64eb6740324353e0da0711954fd8d7da4576755b1c6e09b697cd \ + --hash=sha256:75f80557d1389eddbd0de2681f6a390a0c5338c31ddaa821381c203fc3fd50d9 \ + --hash=sha256:770de9db11e84213beec501cfcaa013b019820ca881e03344dea5844f7876d94 \ + --hash=sha256:7750c6449dff7864bb9bb27ddfb0267756189201a3afc911d82b3caacd70dfc3 \ + --hash=sha256:7bde5e4cc5c10140859842b9d383af292b22639a4dffb725314baf45968cef80 \ + --hash=sha256:7ce713ace7c0e4520535b42b77eaa742c16dab813978064913e5a3cf82973b41 \ + --hash=sha256:7da0c5eff80f0197f3b3d1232ec5a682a9325f4ae9016a78f5f5ca35f9ced1f5 \ + --hash=sha256:7dbb61fe3a7699468030f71bbe5f8a0e326a151daa91beb11a6fc1f980c55e1c \ + --hash=sha256:811bd1e21d32de12efca32393a0ab3f5133b54fce9bd44b8bd77ab07da14bf6a \ + --hash=sha256:8ef53b2de9bcb9197d31854256575d59dbac0cba72ac627bb291ef5eceb74be4 \ + --hash=sha256:937c0052c05a31ca1daf18de3158eed4dbfcb9cc107adbea227728d647be701e \ + --hash=sha256:9d2055050ea716bd38b7f7f1579c275386646b4894c155a3e2f3cd62ed41b7c6 \ + --hash=sha256:9f8d177621de5cb38ee3e731eda45d421db093ec0739f46a5594babda7987a98 \ + --hash=sha256:a2d7755bef5a12ed488f4ef1f1b69ee9191d7396083b755a5d2295f6edb4768b \ + --hash=sha256:a48d62ab9d6f4f98c983223a547af44be6ca3691074c31cecced6facd3ba2dc1 \ + --hash=sha256:a4f00aa42f75d6e4595e8866e748cc1705adc0cddfeb2ca86d0d03993d63ba03 \ + --hash=sha256:a6e721d4b0e45d5b65e87534470e67b18dcd092c83f68fba09f152b9cbc061af \ + --hash=sha256:a730a083190634c65cca36ba5f489531576ebd79bcd5c8e172130f6453127231 \ + --hash=sha256:a931079504ecc49efed7744c476a5c343a92fabf66dec2db95edb1b2fdc770e2 \ + --hash=sha256:aa9511c62d14da7aacc9b4bf51f3f697a621e83b2d6919008243c3aad168eea3 \ + --hash=sha256:ab36d55f9ed2d067327667c2fea18dda018eb628dd6347aa01dda6cf1f5d3836 \ + --hash=sha256:ad2c86c495b899d862ea0f4b42891b8713a3bd45dd4105c7fd51c2a72f39f3a5 \ + --hash=sha256:aeae0e330c9f6acd681f647d46cefd30c29f93e3392882e792e82080c9691399 \ + --hash=sha256:b0431303acaea1089ad4b3e9ce4e6518193def1118d4073ca848635ee4ea2e96 \ + --hash=sha256:b5bdfd1c873d4e093aabc0ca84c4ca6dbc4f752afb5c86f146d9742580c9da2e \ + --hash=sha256:baed1e86cc735622097354b9d1281406caf42ff42a886d29faa8e8d1630333be \ + --hash=sha256:c1453022f490d2459a11819d83ad1d586e9ff65a12ac3e705ffebd46d3685dcf \ + --hash=sha256:c26608d2222fb1e94487e4a387d85f13eb55d5ed725cb25a0c589ac4ee60e7bc \ + --hash=sha256:c7659f22557c5a0bc4855cd635f55edec690cc008a40768527762cb9fb263455 \ + --hash=sha256:c8c69575568085ba0b1b10c0249d779a214aea6f6522e949a0fc9fb0fcb449d0 \ + --hash=sha256:c8d2c9fd1f2d16f780d15127abb050d13d1a76c03a4bd87d7e4980e45e511e12 \ + --hash=sha256:ca82be1a1d406ecfe1d25dc16cb33488e5a16bf4438c9fb590484ea29d92478b \ + --hash=sha256:cc572dace3f60ef98d7b12ff411d20f5362feb31a0439eab0085bbfd349982d7 \ + --hash=sha256:d18e5ac0f2f03f4f518d3e23db0f0cad7faa1da8620e9c09461d443bbf6e6692 \ + --hash=sha256:d28630f5854ab07ab1fd4aba756de52326c82e6be15d414b12793f1975048b54 \ + --hash=sha256:d9c275eaacd24aa73f94ffd6de08fc3f932424d8b6c376f4bed7cde376fe7bc3 \ + --hash=sha256:da0e573f9f97159390c89d9f1a9e41908b66d408cc5b58d08cf3847d844c531b \ + --hash=sha256:dd31f52ea1086513bb9df30f8fcee9b8918323ae067a3d5b78bc826a000712be \ + --hash=sha256:dddad92b554513a31f272570678ba307fb9f618f05e3d4a5eacafff9eae03e1d \ + --hash=sha256:df423d40ee8654634421812bc3b196da3f9bd7d32929da813f8394c4348a5358 \ + --hash=sha256:df913725b79db7bcf03448f36b7bf8815363417d5b58deecf9305e3e30f0f21a \ + --hash=sha256:e0bcb7e0f677f543555d2adff3bf19c05f66cdb4796e5ff602442ab2fe3c4ef7 \ + --hash=sha256:e2d65b31f36619cda3999b78b2aa9632e76b78448e7a56fc4240824200e7c4fc \ + --hash=sha256:e6e8cff14d6fb0be70a09c0bdc58096f501952d04624ebf867e0e56da2df8960 \ + --hash=sha256:f16c709686a78c727bbbf059f92b0bf41c6fc60deec706d2dc19f529175a6125 \ + --hash=sha256:f24fb43132a4c6b4cb4eb029492919b2db645be6808d738f244fd146c03c32cb \ + --hash=sha256:f53e442b08449d42821fa4a4fba000095af9f62742a500f978a9f557ec44339a \ + --hash=sha256:f5cfbc5fe74540d335175b656c725d74d90e3730c626d92575eea35029d9afaa \ + --hash=sha256:f81b3b8f3d4e343550fa4baa0e479bba9f2d29ce9c2e9b51d1ce1718d7442fcf \ + --hash=sha256:f8ec5e643a9a937f64e1999eb9f75d072263751912dc5cd06d3c85f8f44be7c3 \ + --hash=sha256:fb92203a88b3d3053034db775110081c49d28be6551923805e039924093761e4 \ + --hash=sha256:fcd22650c908d7b7da162bbfaab594a1227a15d1643a98c68b122ac642fa2264 # via cryptography -charset-normalizer==3.4.3 \ - --hash=sha256:00237675befef519d9af72169d8604a067d92755e84fe76492fef5441db05b91 \ - --hash=sha256:02425242e96bcf29a49711b0ca9f37e451da7c70562bc10e8ed992a5a7a25cc0 \ - --hash=sha256:027b776c26d38b7f15b26a5da1044f376455fb3766df8fc38563b4efbc515154 \ - --hash=sha256:07a0eae9e2787b586e129fdcbe1af6997f8d0e5abaa0bc98c0e20e124d67e601 \ - --hash=sha256:0cacf8f7297b0c4fcb74227692ca46b4a5852f8f4f24b3c766dd94a1075c4884 \ - --hash=sha256:0e78314bdc32fa80696f72fa16dc61168fda4d6a0c014e0380f9d02f0e5d8a07 \ - --hash=sha256:0f2be7e0cf7754b9a30eb01f4295cc3d4358a479843b31f328afd210e2c7598c \ - --hash=sha256:13faeacfe61784e2559e690fc53fa4c5ae97c6fcedb8eb6fb8d0a15b475d2c64 \ - --hash=sha256:14c2a87c65b351109f6abfc424cab3927b3bdece6f706e4d12faaf3d52ee5efe \ - --hash=sha256:1606f4a55c0fd363d754049cdf400175ee96c992b1f8018b993941f221221c5f \ - --hash=sha256:16a8770207946ac75703458e2c743631c79c59c5890c80011d536248f8eaa432 \ - --hash=sha256:18343b2d246dc6761a249ba1fb13f9ee9a2bcd95decc767319506056ea4ad4dc \ - --hash=sha256:18b97b8404387b96cdbd30ad660f6407799126d26a39ca65729162fd810a99aa \ - --hash=sha256:1bb60174149316da1c35fa5233681f7c0f9f514509b8e399ab70fea5f17e45c9 \ - --hash=sha256:1e8ac75d72fa3775e0b7cb7e4629cec13b7514d928d15ef8ea06bca03ef01cae \ - --hash=sha256:1ef99f0456d3d46a50945c98de1774da86f8e992ab5c77865ea8b8195341fc19 \ - --hash=sha256:2001a39612b241dae17b4687898843f254f8748b796a2e16f1051a17078d991d \ - --hash=sha256:23b6b24d74478dc833444cbd927c338349d6ae852ba53a0d02a2de1fce45b96e \ - --hash=sha256:252098c8c7a873e17dd696ed98bbe91dbacd571da4b87df3736768efa7a792e4 \ - --hash=sha256:257f26fed7d7ff59921b78244f3cd93ed2af1800ff048c33f624c87475819dd7 \ - --hash=sha256:2c322db9c8c89009a990ef07c3bcc9f011a3269bc06782f916cd3d9eed7c9312 \ - --hash=sha256:30a96e1e1f865f78b030d65241c1ee850cdf422d869e9028e2fc1d5e4db73b92 \ - --hash=sha256:30d006f98569de3459c2fc1f2acde170b7b2bd265dc1943e87e1a4efe1b67c31 \ - --hash=sha256:31a9a6f775f9bcd865d88ee350f0ffb0e25936a7f930ca98995c05abf1faf21c \ - --hash=sha256:320e8e66157cc4e247d9ddca8e21f427efc7a04bbd0ac8a9faf56583fa543f9f \ - --hash=sha256:34a7f768e3f985abdb42841e20e17b330ad3aaf4bb7e7aeeb73db2e70f077b99 \ - --hash=sha256:3653fad4fe3ed447a596ae8638b437f827234f01a8cd801842e43f3d0a6b281b \ - --hash=sha256:3cd35b7e8aedeb9e34c41385fda4f73ba609e561faedfae0a9e75e44ac558a15 \ - --hash=sha256:3cfb2aad70f2c6debfbcb717f23b7eb55febc0bb23dcffc0f076009da10c6392 \ - --hash=sha256:416175faf02e4b0810f1f38bcb54682878a4af94059a1cd63b8747244420801f \ - --hash=sha256:41d1fc408ff5fdfb910200ec0e74abc40387bccb3252f3f27c0676731df2b2c8 \ - --hash=sha256:42e5088973e56e31e4fa58eb6bd709e42fc03799c11c42929592889a2e54c491 \ - --hash=sha256:4ca4c094de7771a98d7fbd67d9e5dbf1eb73efa4f744a730437d8a3a5cf994f0 \ - --hash=sha256:511729f456829ef86ac41ca78c63a5cb55240ed23b4b737faca0eb1abb1c41bc \ - --hash=sha256:53cd68b185d98dde4ad8990e56a58dea83a4162161b1ea9272e5c9182ce415e0 \ - --hash=sha256:585f3b2a80fbd26b048a0be90c5aae8f06605d3c92615911c3a2b03a8a3b796f \ - --hash=sha256:5b413b0b1bfd94dbf4023ad6945889f374cd24e3f62de58d6bb102c4d9ae534a \ - --hash=sha256:5d8d01eac18c423815ed4f4a2ec3b439d654e55ee4ad610e153cf02faf67ea40 \ - --hash=sha256:6aab0f181c486f973bc7262a97f5aca3ee7e1437011ef0c2ec04b5a11d16c927 \ - --hash=sha256:6cf8fd4c04756b6b60146d98cd8a77d0cdae0e1ca20329da2ac85eed779b6849 \ - --hash=sha256:6fb70de56f1859a3f71261cbe41005f56a7842cc348d3aeb26237560bfa5e0ce \ - --hash=sha256:6fce4b8500244f6fcb71465d4a4930d132ba9ab8e71a7859e6a5d59851068d14 \ - --hash=sha256:70bfc5f2c318afece2f5838ea5e4c3febada0be750fcf4775641052bbba14d05 \ - --hash=sha256:73dc19b562516fc9bcf6e5d6e596df0b4eb98d87e4f79f3ae71840e6ed21361c \ - --hash=sha256:74d77e25adda8581ffc1c720f1c81ca082921329452eba58b16233ab1842141c \ - --hash=sha256:78deba4d8f9590fe4dae384aeff04082510a709957e968753ff3c48399f6f92a \ - --hash=sha256:86df271bf921c2ee3818f0522e9a5b8092ca2ad8b065ece5d7d9d0e9f4849bcc \ - --hash=sha256:88ab34806dea0671532d3f82d82b85e8fc23d7b2dd12fa837978dad9bb392a34 \ - --hash=sha256:8999f965f922ae054125286faf9f11bc6932184b93011d138925a1773830bbe9 \ - --hash=sha256:8dcfc373f888e4fb39a7bc57e93e3b845e7f462dacc008d9749568b1c4ece096 \ - --hash=sha256:939578d9d8fd4299220161fdd76e86c6a251987476f5243e8864a7844476ba14 \ - --hash=sha256:96b2b3d1a83ad55310de8c7b4a2d04d9277d5591f40761274856635acc5fcb30 \ - --hash=sha256:a2d08ac246bb48479170408d6c19f6385fa743e7157d716e144cad849b2dd94b \ - --hash=sha256:b256ee2e749283ef3ddcff51a675ff43798d92d746d1a6e4631bf8c707d22d0b \ - --hash=sha256:b5e3b2d152e74e100a9e9573837aba24aab611d39428ded46f4e4022ea7d1942 \ - --hash=sha256:b89bc04de1d83006373429975f8ef9e7932534b8cc9ca582e4db7d20d91816db \ - --hash=sha256:bd28b817ea8c70215401f657edef3a8aa83c29d447fb0b622c35403780ba11d5 \ - --hash=sha256:c60e092517a73c632ec38e290eba714e9627abe9d301c8c8a12ec32c314a2a4b \ - --hash=sha256:c6dbd0ccdda3a2ba7c2ecd9d77b37f3b5831687d8dc1b6ca5f56a4880cc7b7ce \ - --hash=sha256:c6e490913a46fa054e03699c70019ab869e990270597018cef1d8562132c2669 \ - --hash=sha256:c6f162aabe9a91a309510d74eeb6507fab5fff92337a15acbe77753d88d9dcf0 \ - --hash=sha256:c6fd51128a41297f5409deab284fecbe5305ebd7e5a1f959bee1c054622b7018 \ - --hash=sha256:cc34f233c9e71701040d772aa7490318673aa7164a0efe3172b2981218c26d93 \ - --hash=sha256:cc9370a2da1ac13f0153780040f465839e6cccb4a1e44810124b4e22483c93fe \ - --hash=sha256:ccf600859c183d70eb47e05a44cd80a4ce77394d1ac0f79dbd2dd90a69a3a049 \ - --hash=sha256:ce571ab16d890d23b5c278547ba694193a45011ff86a9162a71307ed9f86759a \ - --hash=sha256:cf1ebb7d78e1ad8ec2a8c4732c7be2e736f6e5123a4146c5b89c9d1f585f8cef \ - --hash=sha256:d0e909868420b7049dafd3a31d45125b31143eec59235311fc4c57ea26a4acd2 \ - --hash=sha256:d22dbedd33326a4a5190dd4fe9e9e693ef12160c77382d9e87919bce54f3d4ca \ - --hash=sha256:d716a916938e03231e86e43782ca7878fb602a125a91e7acb8b5112e2e96ac16 \ - --hash=sha256:d79c198e27580c8e958906f803e63cddb77653731be08851c7df0b1a14a8fc0f \ - --hash=sha256:d95bfb53c211b57198bb91c46dd5a2d8018b3af446583aab40074bf7988401cb \ - --hash=sha256:e28e334d3ff134e88989d90ba04b47d84382a828c061d0d1027b1b12a62b39b1 \ - --hash=sha256:ec557499516fc90fd374bf2e32349a2887a876fbf162c160e3c01b6849eaf557 \ - --hash=sha256:fb6fecfd65564f208cbf0fba07f107fb661bcd1a7c389edbced3f7a493f70e37 \ - --hash=sha256:fb731e5deb0c7ef82d698b0f4c5bb724633ee2a489401594c5c88b02e6cb15f7 \ - --hash=sha256:fb7f67a1bfa6e40b438170ebdc8158b78dc465a5a67b6dde178a46987b244a72 \ - --hash=sha256:fd10de089bcdcd1be95a2f73dbe6254798ec1bda9f450d5828c96f93e2536b9c \ - --hash=sha256:fdabf8315679312cfa71302f9bd509ded4f2f263fb5b765cf1433b39106c3cc9 +charset-normalizer==3.4.9 \ + --hash=sha256:0327fcd59a935777d83410750c50600ee9571af2846f71ce40f25b13da1ef380 \ + --hash=sha256:03d07803992c6c7bbc976327f34b18b6160327fc81cb82c9d504720ac0be3b62 \ + --hash=sha256:04ce310cb89c15df659582aee80a0603788732a5e017d5bd5c81158106ce249c \ + --hash=sha256:0d861473f743244d349b50f850d10eb87aeb22bbdcc8e64f79273c94af5a8226 \ + --hash=sha256:0e94703ec9684807f20cfb5eed95c70f67f2a8f21ad620146d7b5a13677b93e5 \ + --hash=sha256:0fa1aec2d32bcc03c8fa0f6f1712caad1adc38509f31142112e5c9daf5b9c833 \ + --hash=sha256:16b65ea0f2465b6fb52aa22de5eca612aa964ddfec00a912e26f4656cbef890b \ + --hash=sha256:16d10d789dd9bcca1173c95af82c58433122564b7bc39385124be735a35cbe99 \ + --hash=sha256:19ac87f93086ce37b86e098888555c4b4bc48102279bae3350098c0ed664b501 \ + --hash=sha256:1d22856ffbe153a602df38e4a5464f0b748a54002e0d69ac6d2ad0a197cc99ec \ + --hash=sha256:21e764fd1e70b6a3e205a0e46f3051701f98a8cb3fad66eeb80e48bb502f8698 \ + --hash=sha256:231ddcbb35e2ff8973e1365db41fe0572662893b99a05deb183b68ad4c0c8bd4 \ + --hash=sha256:253a4a220747e8b5faf57ec320c4f5efb0cef05f647420bf267143ec15dba10a \ + --hash=sha256:280081916dc341820640489a66e4696049401ef1cf6dd672f672e70ad915aca3 \ + --hash=sha256:2a441ea71902098ffe78c5abe6c494f44160b4af614ed16c3d9a3b1d17fd8ee2 \ + --hash=sha256:304b13570067b2547562e308af560b3963857b1fa90bd6afd978130130fe2d6a \ + --hash=sha256:32286a2c8d167e897177b673176c1e3e00d4057caf5d2b64eef9a3666b03018e \ + --hash=sha256:33bdcc2a32c0a0e861f60841a512c8acc658c87c2ac59d89e3a46dacf7d866e4 \ + --hash=sha256:375b83ed0aecfce76c16d198fbc21f3b11b337d68662bea0a995046682a11419 \ + --hash=sha256:3c09a49d6cde137258beb3d551994a2927fd35ad5cf96aed573f61bbd67c5f84 \ + --hash=sha256:3d92613ec25e43b05f042302531ec0f00b8445190e43325880cbd6ab7c2581da \ + --hash=sha256:40a126142a56b2dfc0aacbad1de8310cbf60da7656db0e6b16eebd48e3e93519 \ + --hash=sha256:416c229f77e5ea25b3dfd4b582f8d73d7e43c22320302b9ab128a2d3a0b38efe \ + --hash=sha256:432786d3561e69aeeae6c7e8648964ce0ad05736120135601f87ac26b9c83381 \ + --hash=sha256:43b9e366a31fdd1c87d0eb08f579b4a82b723ea54338f040d6b4e518a026ea29 \ + --hash=sha256:440eede837960000d74978f0eba527be106b5b9aee0daf779d395276ed0b0614 \ + --hash=sha256:45b0cc4e3556cd875e09102988d1ab8356c998b596c9fced84547c8138b487a0 \ + --hash=sha256:476743fe6dfe14a2da12e3ac79125dc84a3b2cf8094369a47a1529b0cd8549fe \ + --hash=sha256:4773092f8019072343a7447203308b176e10199920eb02d6195e81bbb3274c29 \ + --hash=sha256:4b3dac63058cc36820b0dd072f89898604e2d39686fe05321729d00d8ac185a0 \ + --hash=sha256:4d1c96a7a18b9690a4d46df09e3e3382406ae3213727cd1019ebade1c4a81917 \ + --hash=sha256:51307f5c71007673a2bf8232ad973483d281e74cb99c8c5a990af1eefa6277d9 \ + --hash=sha256:51447e9aa2684679af07ca5021c3db526e0284347ebf4ffcec1154c3350cfe32 \ + --hash=sha256:58150c9f9b9a552505912d182ccdf26f6396fb6094816ceebcbb20eecabaed94 \ + --hash=sha256:5b10cd92fc5c498b35a8635df6d5a100207f88b63a4dc1de7ef9a548e1e2cd63 \ + --hash=sha256:5e226f6218febc71f6c1fc2fafb91c226f75bdc1d8fb12d66823716e891608fd \ + --hash=sha256:609b3ba8fcc0fb5ab7af00719d0fb6ad0cb518e48e7712d12fd68f1327951198 \ + --hash=sha256:60f44ade2cf573dad7a277e6f8ca9a51a21dda572b13bd7d8539bb3cd5dbedde \ + --hash=sha256:611057cc5d5c0afc743ba8be6bd828c17e0aaa8643f9d0a9b9bb7dea80eb8012 \ + --hash=sha256:6366a16e1a25018694d6a5d784d09b046edc9eac40ea2b54065c3052672516a1 \ + --hash=sha256:65a7ff3f705e57d392f7261b6d0550fe137c3019477431f1c355e0db0a7d3e15 \ + --hash=sha256:673611bbd43f0810bec0b0f028ddeaaa501190339cac411f347ac76917c3ae7b \ + --hash=sha256:67830fc78e67501f47bb950471b2dcb9b35b140084429318e862895a8e89c993 \ + --hash=sha256:68ce9f4d6b26d5ccbf7fd4459bf75f74a0a146677ebba80597df60cbdb20e6f4 \ + --hash=sha256:68e5f26a1ad57ded6d1cfb85331d1c1a195314756471d97758c48498bb4dcdf5 \ + --hash=sha256:69b157c5d3292bcd443faca052f3096f637f1e074b98212a933c074ae23dc3b8 \ + --hash=sha256:75286256590a6320cf106a0d28970d3560aad9ee09aa7b34fb40524792436d35 \ + --hash=sha256:78841cccf1af7b40f6f716338d50c0902dbe88d9f800b3c973b7a9a0a693a642 \ + --hash=sha256:78fa18e436a1a0e58dbd7e02fc4473f3f32cceb12df9dfca542d075961c307d2 \ + --hash=sha256:79580094b00d1789d1f93ea55bc43cb2f611910c72235b7657f3482ddcc1b22d \ + --hash=sha256:7b86a2b16095d250c6f58b3d9b2eee6f4147754344f3dab0922f7c9bf7d226c9 \ + --hash=sha256:83aed2c10721ddd90f68140685391b50811a880af20654c59af6b6c66c40513c \ + --hash=sha256:84fd18bcc17526fc2b3c1af7d2b9217d32c9c04448c16ec693b9b4f1985c3d33 \ + --hash=sha256:871ff67ea1aad4dfd91736464934d56b32dac49f9fbe16cddba36198a7b3a0db \ + --hash=sha256:898f0e9068ca27d37f8e83a5b962821df851532e6c4a7d615c1c033f9da6eedf \ + --hash=sha256:8a79d9f4d8001473a30c163556b3c3bfebec837495a412dde78b51672f6134f9 \ + --hash=sha256:8c041122946b7ba21bb32c45b1aa57b1be35527690aeb3c5c234521085632eee \ + --hash=sha256:90c44bc373b7687f6948b693cceaea1348ae0975d7474746559494468e3c1d84 \ + --hash=sha256:9104ed0bd76a429d46f9ec0dbc9b08ad1d2dcdf2b00a5a0daa1c145329b35b44 \ + --hash=sha256:920079c3f7456fa213e0829ed2073aaa727fd39d889ead5b4f35d0de5460d04f \ + --hash=sha256:93d59d504b230e83c7a843251681959a0b6a9cd76f6e146ce1b8a80eb8739af9 \ + --hash=sha256:9b2aff1c7b3884512b9512c3eaadd9bab39fb45042ffaaa1dd08ff2b9f8109d9 \ + --hash=sha256:9b8e0f3107e2200b76f6054de99016eac3ee6762713587b36baaa7e4bd2ae177 \ + --hash=sha256:9bb41182d93ea91f60b4bc8fbf4c820c69ef8a12ab2d917f3f1834f1acad07e8 \ + --hash=sha256:9cdef90ae47919cae358d8ab15797a800ed41da7aba5d72419fb510729e2ed4b \ + --hash=sha256:a1786910334ed46ab1dd73222f2cd1e05c2c3bb39f6dddb4f8b36fc382058a39 \ + --hash=sha256:a4cfde78a9f2880208d16a93b795726a3017d5977e08d1e162a7a31322479c41 \ + --hash=sha256:a4fbdde9dd4a9ce5fd52c2b3a347bb50cc89483ef783f1cb00d408c13f7a96c0 \ + --hash=sha256:aa99adc8f081b475a12843953db36831eaf83ec33eb46a90629ca6a5de45a616 \ + --hash=sha256:ac351b3b8014eead140e77e9717e2992c6bbe30b63bc3422422eb84865412e3d \ + --hash=sha256:ad41ba96094304aa090f5a30cb6e4fb3b3f1c264c523394b4c39bbacc4dc92ba \ + --hash=sha256:b5314963fce9b0b12743891de876e724997864ee22aa496f903f426c7e2fa5b2 \ + --hash=sha256:bcf74c1df76758a395bf0af608c04c82257523f55c9868b334f06270d0f2112b \ + --hash=sha256:bd47ba7fc3ca94896759ea0109775132d3e7ab921fbf54038e1bab2e46c313c9 \ + --hash=sha256:c0323c9daef75ef2e5083624b4585018a0c9d5e3b40f607eed81a311270b934b \ + --hash=sha256:c1225416b463483160e4af85d5fc3a9690ccb53fd4b1865a6437825f5ede3209 \ + --hash=sha256:c1c948747b03be832dceed96ca815cef7360de9aa19d37c730f8e3f6101aca48 \ + --hash=sha256:c25fe15c70c59eb7c5ce8c06a1f3fa1da0ecc5ea1e7a5922c40fd2fa9b0d5046 \ + --hash=sha256:cc1b0fff8ead343dae06305f954eb8468ba0ec1a97881f42489d198e4ce3c632 \ + --hash=sha256:cd6280cf040f233bd7d3407b743b4b4c74f70e8e1c4199cb112a62c941c0772a \ + --hash=sha256:cd6c3d4b783c556fa00bf540854e42f135e2f256abd29669fcd0da0f2dec79c2 \ + --hash=sha256:d4d6fcde76f94f5cb9e43e9e9a61f16dacefd228cbbf6f1a09bd9b219a92f1a1 \ + --hash=sha256:ddf4af30b417d9fe16481e9b81c27ab2a7cde1ff7ba3e85653b02db7d145dc7b \ + --hash=sha256:df115d4d83168fdf2cae48ef1ff6d1cb4c466364e30861b37121de0f3bf1b990 \ + --hash=sha256:df7276909358e5635ae203673ab7e509ddd224225a8d6b0790bf13eb2bde1cc5 \ + --hash=sha256:e4fd89cc178bced6ad29cb3e6dd4aa63fa5017c3524dbd0b25998fb64a87cc8b \ + --hash=sha256:e9701d0049d92c16703a42771b98d560b95248949f23f8cf7b4eddd201814fb9 \ + --hash=sha256:ee2f2a527e3c1a6e6411eb4209642e138b544a2d72fe5d0d76daf77b24063534 \ + --hash=sha256:f7fb7d750cfa0a070d2c24e831fd3481019a60dd317ea2b39acbcebc08b6ed81 \ + --hash=sha256:f840ed6d8ecba8255df8c42b87fadeda98ddfc6eeec05e2dc66e26d46dd6f58a \ + --hash=sha256:f86c6358749bd4fda175388691e3ba8c46e24c5347d0afd20f9b7edfc9faf07d \ + --hash=sha256:fa36ec09ef71d158186bc79e359ff5fdd6e7996fe8ab638f00d6b93139ba4fcf \ + --hash=sha256:fe2c7201c642b7c308f1675355ad7ff7b66acfe3541625efe5a3ad38f29d6115 # via requests -cryptography==46.0.7 \ - --hash=sha256:04959522f938493042d595a736e7dbdff6eb6cc2339c11465b3ff89343b65f65 \ - --hash=sha256:128c5edfe5e5938b86b03941e94fac9ee793a94452ad1365c9fc3f4f62216832 \ - --hash=sha256:1d25aee46d0c6f1a501adcddb2d2fee4b979381346a78558ed13e50aa8a59067 \ - --hash=sha256:24402210aa54baae71d99441d15bb5a1919c195398a87b563df84468160a65de \ - --hash=sha256:258514877e15963bd43b558917bc9f54cf7cf866c38aa576ebf47a77ddbc43a4 \ - --hash=sha256:35719dc79d4730d30f1c2b6474bd6acda36ae2dfae1e3c16f2051f215df33ce0 \ - --hash=sha256:397655da831414d165029da9bc483bed2fe0e75dde6a1523ec2fe63f3c46046b \ - --hash=sha256:3986ac1dee6def53797289999eabe84798ad7817f3e97779b5061a95b0ee4968 \ - --hash=sha256:420b1e4109cc95f0e5700eed79908cef9268265c773d3a66f7af1eef53d409ef \ - --hash=sha256:42a1e5f98abb6391717978baf9f90dc28a743b7d9be7f0751a6f56a75d14065b \ - --hash=sha256:462ad5cb1c148a22b2e3bcc5ad52504dff325d17daf5df8d88c17dda1f75f2a4 \ - --hash=sha256:506c4ff91eff4f82bdac7633318a526b1d1309fc07ca76a3ad182cb5b686d6d3 \ - --hash=sha256:5ad9ef796328c5e3c4ceed237a183f5d41d21150f972455a9d926593a1dcb308 \ - --hash=sha256:5d1c02a14ceb9148cc7816249f64f623fbfee39e8c03b3650d842ad3f34d637e \ - --hash=sha256:5e51be372b26ef4ba3de3c167cd3d1022934bc838ae9eaad7e644986d2a3d163 \ - --hash=sha256:60627cf07e0d9274338521205899337c5d18249db56865f943cbe753aa96f40f \ - --hash=sha256:65814c60f8cc400c63131584e3e1fad01235edba2614b61fbfbfa954082db0ee \ - --hash=sha256:73510b83623e080a2c35c62c15298096e2a5dc8d51c3b4e1740211839d0dea77 \ - --hash=sha256:7bbc6ccf49d05ac8f7d7b5e2e2c33830d4fe2061def88210a126d130d7f71a85 \ - --hash=sha256:80406c3065e2c55d7f49a9550fe0c49b3f12e5bfff5dedb727e319e1afb9bf99 \ - --hash=sha256:84d4cced91f0f159a7ddacad249cc077e63195c36aac40b4150e7a57e84fffe7 \ - --hash=sha256:8a469028a86f12eb7d2fe97162d0634026d92a21f3ae0ac87ed1c4a447886c83 \ - --hash=sha256:91bbcb08347344f810cbe49065914fe048949648f6bd5c2519f34619142bbe85 \ - --hash=sha256:935ce7e3cfdb53e3536119a542b839bb94ec1ad081013e9ab9b7cfd478b05006 \ - --hash=sha256:9694078c5d44c157ef3162e3bf3946510b857df5a3955458381d1c7cfc143ddb \ - --hash=sha256:a1529d614f44b863a7b480c6d000fe93b59acee9c82ffa027cfadc77521a9f5e \ - --hash=sha256:abad9dac36cbf55de6eb49badd4016806b3165d396f64925bf2999bcb67837ba \ - --hash=sha256:b36a4695e29fe69215d75960b22577197aca3f7a25b9cf9d165dcfe9d80bc325 \ - --hash=sha256:b7b412817be92117ec5ed95f880defe9cf18a832e8cafacf0a22337dc1981b4d \ - --hash=sha256:c5b1ccd1239f48b7151a65bc6dd54bcfcc15e028c8ac126d3fada09db0e07ef1 \ - --hash=sha256:cbd5fb06b62bd0721e1170273d3f4d5a277044c47ca27ee257025146c34cbdd1 \ - --hash=sha256:cdf1a610ef82abb396451862739e3fc93b071c844399e15b90726ef7470eeaf2 \ - --hash=sha256:cdfbe22376065ffcf8be74dc9a909f032df19bc58a699456a21712d6e5eabfd0 \ - --hash=sha256:d02c738dacda7dc2a74d1b2b3177042009d5cab7c7079db74afc19e56ca1b455 \ - --hash=sha256:d151173275e1728cf7839aaa80c34fe550c04ddb27b34f48c232193df8db5842 \ - --hash=sha256:d23c8ca48e44ee015cd0a54aeccdf9f09004eba9fc96f38c911011d9ff1bd457 \ - --hash=sha256:d3b99c535a9de0adced13d159c5a9cf65c325601aa30f4be08afd680643e9c15 \ - --hash=sha256:d5f7520159cd9c2154eb61eb67548ca05c5774d39e9c2c4339fd793fe7d097b2 \ - --hash=sha256:db0f493b9181c7820c8134437eb8b0b4792085d37dbb24da050476ccb664e59c \ - --hash=sha256:e06acf3c99be55aa3b516397fe42f5855597f430add9c17fa46bf2e0fb34c9bb \ - --hash=sha256:e4cfd68c5f3e0bfdad0d38e023239b96a2fe84146481852dffbcca442c245aa5 \ - --hash=sha256:ea42cbe97209df307fdc3b155f1b6fa2577c0defa8f1f7d3be7d31d189108ad4 \ - --hash=sha256:ebd6daf519b9f189f85c479427bbd6e9c9037862cf8fe89ee35503bd209ed902 \ - --hash=sha256:f247c8c1a1fb45e12586afbb436ef21ff1e80670b2861a90353d9b025583d246 \ - --hash=sha256:fbfd0e5f273877695cb93baf14b185f4878128b250cc9f8e617ea0c025dfb022 \ - --hash=sha256:fc9ab8856ae6cf7c9358430e49b368f3108f050031442eaeb6b9d87e4dcf4e4f \ - --hash=sha256:fcd8eac50d9138c1d7fc53a653ba60a2bee81a505f9f8850b6b2888555a45d0e \ - --hash=sha256:fdd1736fed309b4300346f88f74cd120c27c56852c3838cab416e7a166f67298 \ - --hash=sha256:ffca7aa1d00cf7d6469b988c581598f2259e46215e0140af408966a24cf086ce +cryptography==50.0.0 \ + --hash=sha256:031e2d5dd4bb9caa3ca9c82e5a197fd8ae680232cee62603d1a813f3f07e3d03 \ + --hash=sha256:06a32a980526a6ab9a4b9bf8f7385800791e2bb960903cb6b530e4817509a3b7 \ + --hash=sha256:07479a1cb08219ab719147e742e76090c9c773321959bb94946fffdd397a6437 \ + --hash=sha256:07949c449a1abcf60d1ee6e88956d89404c7df3c8258f46589e912988e551987 \ + --hash=sha256:105110f43a471dbd0060b9c9516cb8a6a79233631a04cc2ba16f28323ac6e025 \ + --hash=sha256:11b74db56cdbe3cdee6e3f6982ecb70334fa10dce99ed58bf7894aaaa3b2a037 \ + --hash=sha256:12b9c6996425c76ea6c457ace4f3073e715b8c545add07cd1a8f3a4f90691269 \ + --hash=sha256:1489e263a8048bb8b6a8bac662eb2d402ea5d2b7b4699b72f385f1e2772db105 \ + --hash=sha256:19736989797678c6af1e55cd49055cdbcb55d8f6b5583ac5335f933aba9101dc \ + --hash=sha256:1b4a266766514614f8aa60416e71f2fc6e575d36e7bdc90f644fadb2f4b75b95 \ + --hash=sha256:2a8183b489dc1f7f80f135780fadc1108f14b31b8a40411c7a5b17425f65f28b \ + --hash=sha256:37fdb0d0111f1e2ff07139dfb79f1b49531f8e213c46f1163dd7642979b58c47 \ + --hash=sha256:3f5735ffe4996d28b809371756219f5354864902a3b9e7c0b9ee87041209fc9c \ + --hash=sha256:49e7d93abdbd2990caced757e5fade25302f719c3c8fb6e6fff2dde98999fc41 \ + --hash=sha256:5e34edd123674534acd70147f0ca331eaa2c74e6325fb2028c886aa26ba0b68c \ + --hash=sha256:62598a8a57f815db4c6259a4e97d857dab56697e7de8e8ab02352ab74da1995d \ + --hash=sha256:65c2c3add92b45fd0709db8594536aea39c2a67af0e27ffcf049c498501140b7 \ + --hash=sha256:6ba6a53445bd3cfa809ef3ef5f1589aa6ba08784a1d962bf47d0940e871dab1c \ + --hash=sha256:6e7d61120573a7f2cd94cc095f9e81f6967c61ccdf194285aa143ecec8e0b708 \ + --hash=sha256:7cec5b856506da6defb290f30c9ee687d5f5e8cb0bd3f6459dde43b0b4fa40ef \ + --hash=sha256:80b63928fa35083b33966ce1efb70e5b9607181e49dcd1c22c8c005e319f667f \ + --hash=sha256:82148ec5bddac30b51a5b3c1945075f896fa022cb93f8e4a01e9f6ee95292c5f \ + --hash=sha256:828743d939e9629bc267b8e2d08d8bb67cd4319c771a33d4b18b22dd8fb7440a \ + --hash=sha256:8d89f3976b10b4ce31118de72329025f70d2c6ead14a8217c5514dd2c6d5a78f \ + --hash=sha256:8eb5e1172eb569ea8a872796576e6a67c276351728b6455d5beb01242b027c6a \ + --hash=sha256:900131fafd8aead39ac7dd3a7e833be754c17a95cfd91221636949fe4eb0aa8a \ + --hash=sha256:910d11e1a385c654bf738bf3e6b8e6ed5de0f5610fcae2be9e5b398d8081d20e \ + --hash=sha256:910e1d2668e7de9648f2bcee30e180db2a6b15c30f887d7c4c93ddf96e3992e3 \ + --hash=sha256:9aa87839c383bdbab6ef865787a1fb877af8dd03464c4400322726feaaadfc6d \ + --hash=sha256:a1b30560f2acc95aa8b2e06e716a13dbfc97314747b80d9707e307f77b40d6b3 \ + --hash=sha256:a91296cb61e8df6f86d0c19cc4068228da256bf59bf86049fbd821084565327f \ + --hash=sha256:b42a28c1844fd9de8f3f7d540e36b66f3a9c83fceac7170ebc7a6a19edd9dcae \ + --hash=sha256:bd1c592e4d5974f0d08d4888e432157adba757c66da0246918e43677fafa2d30 \ + --hash=sha256:c87f62a3d3b9888ed0fdde100ec06aa61ca9cd44bad9057d1dff9a516b5f5bb9 \ + --hash=sha256:c99c003e088647b8a5b7c145d6f78c335f6348332b62e142d411c4b63d1460b9 \ + --hash=sha256:ccdc4a71a4dabae05de219404f9f4abc38e3b58422177ff93d0da05967dafa07 \ + --hash=sha256:d24fead1d4d076e1bfb006dcec392074a3cd8d7b4fc8a595aa64073b2b7a96ba \ + --hash=sha256:d58c3db7cd6eed54e6c06744db55456b65ebd7492ddeae9c1e93cfca7aa857d3 \ + --hash=sha256:d764dcf130c428ef66786f866dd750f53182bc608813489915e9fc106bb0c82f \ + --hash=sha256:df2a58a472f332225671c35b0a830208b86d004f82baa8530fa3782c85646533 \ + --hash=sha256:e722f16708d854fe924790e051061f6704a472c3bac347b6fd88033ea8dd0dc5 \ + --hash=sha256:ecfed7367f965a0328cfbdd70da860f15441f002f613185668c6e6ebf5a0ac11 \ + --hash=sha256:eeac2acb5a20ed25e0ad6d1df9891a520b78b404266b6d11778f25d5d691a6c9 \ + --hash=sha256:f59e38625469987d7ef6d495323c55e7db6c212eaf6112267e0d3b565a2e9c9f \ + --hash=sha256:f89831ef99dd7dd169ab06d63a831adb9e20a87aac6d380266bbda5823349169 \ + --hash=sha256:fd9192b7b70c573d7f214eb1ae35e00d359f6f5e4b27c7e21e30de1fc6204645 # via secretstorage -docutils==0.22.2 \ - --hash=sha256:9fdb771707c8784c8f2728b67cb2c691305933d68137ef95a75db5f4dfbc213d \ - --hash=sha256:b0e98d679283fc3bb0ead8a5da7f501baa632654e7056e9c5846842213d674d8 +docutils==0.23 \ + --hash=sha256:25d013af9bf23bc1c7b2b093dff4208166c53a94786c9e447808335ef1185fea \ + --hash=sha256:746f5060322511280a1e50eb76846ed6bf2342984b2ac04dc42caa1a8d78799e # via readme-renderer -idna==3.10 \ - --hash=sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9 \ - --hash=sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3 +id==1.6.1 \ + --hash=sha256:d0732d624fb46fd4e7bc4e5152f00214450953b9e772c182c1c22964def1a069 \ + --hash=sha256:f5ec41ed2629a508f5d0988eda142e190c9c6da971100612c4de9ad9f9b237ca + # via twine +idna==3.18 \ + --hash=sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2 \ + --hash=sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848 # via requests -importlib-metadata==8.7.0 \ - --hash=sha256:d13b81ad223b890aa16c5471f2ac3056cf76c5f10f82d6f9292f0b415f389000 \ - --hash=sha256:e5dd1551894c77868a30651cef00984d50e1002d06942a7101d34870c5f02afd - # via - # keyring - # twine +importlib-metadata==9.0.0 \ + --hash=sha256:2d21d1cc5a017bd0559e36150c21c830ab1dc304dedd1b7ea85d20f45ef3edd7 \ + --hash=sha256:a4f57ab599e6a2e3016d7595cfd72eb4661a5106e787a95bcc90c7105b831efc + # via keyring jaraco-classes==3.4.0 \ --hash=sha256:47a024b51d0239c0dd8c8540c6c7f484be3b8fcf0b2d85c13825780d3b3f3acd \ --hash=sha256:f662826b6bed8cace05e7ff873ce0f9283b5c924470fe664fff1c2f00f581790 # via keyring -jaraco-context==6.0.1 \ - --hash=sha256:9bae4ea555cf0b14938dc0aee7c9f32ed303aa20a3b73e7dc80111628792d1b3 \ - --hash=sha256:f797fc481b490edb305122c9181830a3a5b76d84ef6d1aef2fb9b47ab956f9e4 +jaraco-context==6.1.2 \ + --hash=sha256:bf8150b79a2d5d91ae48629d8b427a8f7ba0e1097dd6202a9059f29a36379535 \ + --hash=sha256:f1a6c9d391e661cc5b8d39861ff077a7dc24dc23833ccee564b234b81c82dfe3 # via keyring -jaraco-functools==4.3.0 \ - --hash=sha256:227ff8ed6f7b8f62c56deff101545fa7543cf2c8e7b82a7c2116e672f29c26e8 \ - --hash=sha256:cfd13ad0dd2c47a3600b439ef72d8615d482cedcff1632930d6f28924d92f294 +jaraco-functools==4.6.0 \ + --hash=sha256:880c577ec9720b3a052d5bc611fb9f2269b3d87902ef42440df443b88e443280 \ + --hash=sha256:99e3dc0060c5cbe8fcd1cdb36258e2a65ca40f1566b2033b12abb1bb44dd3c30 # via keyring jeepney==0.9.0 \ --hash=sha256:97e5714520c16fc0a45695e5365a2e11b81ea79bba796e26f9f1d178cb182683 \ @@ -260,73 +289,74 @@ jeepney==0.9.0 \ # via # keyring # secretstorage -keyring==25.6.0 \ - --hash=sha256:0b39998aa941431eb3d9b0d4b2460bc773b9df6fed7621c2dfb291a7e0187a66 \ - --hash=sha256:552a3f7af126ece7ed5c89753650eec89c7eaae8617d0aa4d9ad2b75111266bd +keyring==25.7.0 \ + --hash=sha256:be4a0b195f149690c166e850609a477c532ddbfbaed96a404d4e43f8d5e2689f \ + --hash=sha256:fe01bd85eb3f8fb3dd0405defdeac9a5b4f6f0439edbb3149577f244a2e8245b # via twine -markdown-it-py==4.0.0 \ - --hash=sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147 \ - --hash=sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3 +markdown-it-py==4.2.0 \ + --hash=sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49 \ + --hash=sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a # via rich mdurl==0.1.2 \ --hash=sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8 \ --hash=sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba # via markdown-it-py -more-itertools==10.8.0 \ - --hash=sha256:52d4362373dcf7c52546bc4af9a86ee7c4579df9a8dc268be0a2f949d376cc9b \ - --hash=sha256:f638ddf8a1a0d134181275fb5d58b086ead7c6a72429ad725c67503f13ba30bd +more-itertools==11.1.0 \ + --hash=sha256:48e8f4d9e7e5878571ecf6f2b4e57634f93cd474cc8cfbd2376f2d11b396e30d \ + --hash=sha256:4b65538ae22f6fed0ce4874efd317463a7489796a0939fa66824dd542125a192 # via # jaraco-classes # jaraco-functools -nh3==0.3.0 \ - --hash=sha256:0649464ac8eee018644aacbc103874ccbfac80e3035643c3acaab4287e36e7f5 \ - --hash=sha256:16f8670201f7e8e0e05ed1a590eb84bfa51b01a69dd5caf1d3ea57733de6a52f \ - --hash=sha256:1adeb1062a1c2974bc75b8d1ecb014c5fd4daf2df646bbe2831f7c23659793f9 \ - --hash=sha256:37d3003d98dedca6cd762bf88f2e70b67f05100f6b949ffe540e189cc06887f9 \ - --hash=sha256:389d93d59b8214d51c400fb5b07866c2a4f79e4e14b071ad66c92184fec3a392 \ - --hash=sha256:3f1b4f8a264a0c86ea01da0d0c390fe295ea0bcacc52c2103aca286f6884f518 \ - --hash=sha256:423201bbdf3164a9e09aa01e540adbb94c9962cc177d5b1cbb385f5e1e79216e \ - --hash=sha256:634e34e6162e0408e14fb61d5e69dbaea32f59e847cfcfa41b66100a6b796f62 \ - --hash=sha256:6d68fa277b4a3cf04e5c4b84dd0c6149ff7d56c12b3e3fab304c525b850f613d \ - --hash=sha256:7275fdffaab10cc5801bf026e3c089d8de40a997afc9e41b981f7ac48c5aa7d5 \ - --hash=sha256:7852f038a054e0096dac12b8141191e02e93e0b4608c4b993ec7d4ffafea4e49 \ - --hash=sha256:7c915060a2c8131bef6a29f78debc29ba40859b6dbe2362ef9e5fd44f11487c2 \ - --hash=sha256:80fe20171c6da69c7978ecba33b638e951b85fb92059259edd285ff108b82a6d \ - --hash=sha256:a537ece1bf513e5a88d8cff8a872e12fe8d0f42ef71dd15a5e7520fecd191bbb \ - --hash=sha256:af5aa8127f62bbf03d68f67a956627b1bd0469703a35b3dad28d0c1195e6c7fb \ - --hash=sha256:b0612ccf5de8a480cf08f047b08f9d3fecc12e63d2ee91769cb19d7290614c23 \ - --hash=sha256:ba0caa8aa184196daa6e574d997a33867d6d10234018012d35f86d46024a2a95 \ - --hash=sha256:bae63772408fd63ad836ec569a7c8f444dd32863d0c67f6e0b25ebbd606afa95 \ - --hash=sha256:c7a32a7f0d89f7d30cb8f4a84bdbd56d1eb88b78a2434534f62c71dac538c450 \ - --hash=sha256:ce5e7185599f89b0e391e2f29cc12dc2e206167380cea49b33beda4891be2fe1 \ - --hash=sha256:d8ba24cb31525492ea71b6aac11a4adac91d828aadeff7c4586541bf5dc34d2f \ - --hash=sha256:d97d3efd61404af7e5721a0e74d81cdbfc6e5f97e11e731bb6d090e30a7b62b2 \ - --hash=sha256:e90883f9f85288f423c77b3f5a6f4486375636f25f793165112679a7b6363b35 \ - --hash=sha256:e9e6a7e4d38f7e8dda9edd1433af5170c597336c1a74b4693c5cb75ab2b30f2a \ - --hash=sha256:ec6cfdd2e0399cb79ba4dcffb2332b94d9696c52272ff9d48a630c5dca5e325a \ - --hash=sha256:f416c35efee3e6a6c9ab7716d9e57aa0a49981be915963a82697952cba1353e1 +nh3==0.3.6 \ + --hash=sha256:082675ff87b9385ec430ffe6d5847ba7456cc39b73720cd4add472f9f4cffd56 \ + --hash=sha256:2411e8c3cee81a1ddd62c2a5d50585c28aa5566d373ad1db92536b95ddb24ef2 \ + --hash=sha256:25c733bee928530556b1db0ea46c52cf5aa686146e38e60a6fc7cb801ef91cec \ + --hash=sha256:2f90d9a0cfdbee218994fdaaeeb5a0fde62d08f35e4eef0378ec1e2200172fd0 \ + --hash=sha256:34d2b0d934156b87ee114f599a3ba9b8b9e17b5d79652ba3a13fa50903de965e \ + --hash=sha256:36d06341bd501240d320f5942481ed5e6846136b666e1ba4faf802b78ebc875f \ + --hash=sha256:43bc1ed3fa0716295fabee29ba42b2667e4a51d140b0a68e092170a765474fa6 \ + --hash=sha256:44673b27010051ab5a5e438a86ec31bbda61d4a77d7e900af6b7be3037c1abae \ + --hash=sha256:455469a29951edc92bc48b47ac2281c3f2609e6c4f6a047056449f8c2c23facf \ + --hash=sha256:4713502748f564fee0633b37b3403783ce0a3af3a3d148ad91025a5bdadb7bc6 \ + --hash=sha256:5276ef17bdba9ad8040575c74072008b13aae429436e9d0429e718bb5f90f4da \ + --hash=sha256:597a8e843bea00b2eb5520658dc24a9bb032e7fc9e7c2c0c4cd29420220c9796 \ + --hash=sha256:69bbb92865a693d909db3a700d3c01537533844d0948c1e9323561ce06ecda41 \ + --hash=sha256:69f365963f63a1e9bff53bdbb3c542c7c2efed3e163c9d5d83a772a2ac468c21 \ + --hash=sha256:82ca5bf427ad1b216b65ede1a2e2d87dc49bec417ceba0f297213107d3cd9d78 \ + --hash=sha256:889932a97fb4abb6f95fef1914c0d269ebfb60011e67121c1163059b9449dbb4 \ + --hash=sha256:905f877dc66dd7aea4a76e54bcb26acb5ff8216f720c0017ccf63e0e6035698e \ + --hash=sha256:a43ebd7543555c3ac1bc353023d0794e75cb76f6f18f19c32e95441496c0cc25 \ + --hash=sha256:d14bf7982e7a77c0c775634c29c07ce08b38a046df73e1c1f139b3e82f18a38e \ + --hash=sha256:e196fa70c2ff2eb4de7d3df3108f8f358c1d69dff20d45b11f20a5aa227ffb6d \ + --hash=sha256:e1b160831c9cdb06a6c79c2f9cdb11386602938f9af260d1c457a85add4f6f69 \ + --hash=sha256:e6b7beece07525dc6e6b0fc2f104442de2ba328360ad00e50cbe2e1fd620447d \ + --hash=sha256:edb2b4a1a27523e6cc7c417f8d21ce3d005243548b93e56b762b66b0c7f589f9 \ + --hash=sha256:f2f14b7ae1fca99c4a66c981aac3974e7fbc1ca30a12673d223ae1df76680917 \ + --hash=sha256:f338ac7d594c067679f1e99b4f5ec3906842979560f9d8f15d6bdfa39a353b10 \ + --hash=sha256:f3736c9dd3d1856f80cd031715b84ca75cda2bbb1ac802c3da26bfce590838d7 \ + --hash=sha256:f5ed5fe84aee7f39db95c214a7421bf0499fbf500fec6d86a4e29bfc37971438 # via readme-renderer -pkginfo==1.12.1.2 \ - --hash=sha256:5cd957824ac36f140260964eba3c6be6442a8359b8c48f4adf90210f33a04b7b \ - --hash=sha256:c783ac885519cab2c34927ccfa6bf64b5a704d7c69afaea583dd9b7afe969343 +packaging==26.3 \ + --hash=sha256:94edc256424af38762eb31306eed28beb9f0efc50a8837492c9d6fd6004aed79 \ + --hash=sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c # via twine -pycparser==2.23 \ - --hash=sha256:78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2 \ - --hash=sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934 +pycparser==3.0 \ + --hash=sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29 \ + --hash=sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992 # via cffi -pygments==2.19.2 \ - --hash=sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887 \ - --hash=sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b +pygments==2.20.0 \ + --hash=sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f \ + --hash=sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176 # via # readme-renderer # rich -readme-renderer==44.0 \ - --hash=sha256:2fbca89b81a08526aadf1357a8c2ae889ec05fb03f5da67f9769c9a592166151 \ - --hash=sha256:8712034eabbfa6805cacf1402b4eeb2a73028f72d1166d6f5cb7f9c047c5d1e1 +readme-renderer==45.0 \ + --hash=sha256:030a8fac74904f8fba11ad1bb6964e3f76e896dc7e5e71f16af190c9056696d1 \ + --hash=sha256:3385ed220117104a2bceb4a9dac8c5fdf6d1f96890d7ea2a9c7174fd5c84091f # via twine -requests==2.33.0 \ - --hash=sha256:3324635456fa185245e24865e810cecec7b4caf933d7eb133dcde67d48cee69b \ - --hash=sha256:c7ebc5e8b0f21837386ad0e1c8fe8b829fa5f544d8df3b2253bff14ef29d7652 +requests==2.34.2 \ + --hash=sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0 \ + --hash=sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed # via # requests-toolbelt # twine @@ -338,25 +368,26 @@ rfc3986==2.0.0 \ --hash=sha256:50b1502b60e289cb37883f3dfd34532b8873c7de9f49bb546641ce9cbd256ebd \ --hash=sha256:97aacf9dbd4bfd829baad6e6309fa6573aaf1be3f6fa735c8ab05e46cecb261c # via twine -rich==14.1.0 \ - --hash=sha256:536f5f1785986d6dbdea3c75205c473f970777b4a0d6c6dd1b696aa05a3fa04f \ - --hash=sha256:e497a48b844b0320d45007cdebfeaeed8db2a4f4bcf49f15e455cfc4af11eaa8 +rich==15.0.0 \ + --hash=sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb \ + --hash=sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36 # via twine -secretstorage==3.3.3 \ - --hash=sha256:2403533ef369eca6d2ba81718576c5e0f564d5cca1b58f73a8b23e7d4eeebd77 \ - --hash=sha256:f356e6628222568e3af06f2eba8df495efa13b3b63081dafd4f7d9a7b7bc9f99 +secretstorage==3.5.0 \ + --hash=sha256:0ce65888c0725fcb2c5bc0fdb8e5438eece02c523557ea40ce0703c266248137 \ + --hash=sha256:f04b8e4689cbce351744d5537bf6b1329c6fc68f91fa666f60a380edddcd11be # via keyring -twine==5.1.1 \ - --hash=sha256:215dbe7b4b94c2c50a7315c0275d2258399280fbb7d04182c7e55e24b5f93997 \ - --hash=sha256:9aa0825139c02b3434d913545c7b847a21c835e11597f5255842d457da2322db +twine==7.0.0 \ + --hash=sha256:85cdb29c518efef867360ae4acd4b0dfd61c8654a22fca08e6f8539f05022177 \ + --hash=sha256:b854164df26db268af05f49aa5c0344b10e27a494343ff05b1e0bad3b135f5a7 # via -r tools/publish/requirements.in -urllib3==2.6.3 \ - --hash=sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed \ - --hash=sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4 +urllib3==2.7.0 \ + --hash=sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c \ + --hash=sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897 # via + # id # requests # twine -zipp==3.23.0 \ - --hash=sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e \ - --hash=sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166 +zipp==4.1.0 \ + --hash=sha256:25ad4e16390cd314347dd8f1de67a2ac538ae658ed4ab9db16029c07c188e97f \ + --hash=sha256:4cb57381f544315db7688e976e922a2b18cdb513d21cc194eb42232ba2a3e602 # via importlib-metadata diff --git a/tools/publish/requirements_universal.txt b/tools/publish/requirements_universal.txt index 4dbcc92abc..5446aff052 100644 --- a/tools/publish/requirements_universal.txt +++ b/tools/publish/requirements_universal.txt @@ -2,318 +2,365 @@ # bazel run //tools/publish:requirements_universal.update --index-url https://pypi.org/simple -backports-tarfile==1.2.0 ; python_full_version < '3.12' \ +backports-tarfile==1.2.0 ; python_full_version < '3.12' and platform_machine != 'ppc64le' and platform_machine != 's390x' \ --hash=sha256:77e284d754527b01fb1e6fa8a1afe577858ebe4e9dad8919e34c862cb399bc34 \ --hash=sha256:d75e02c268746e1b8144c278978b6e98e85de6ad16f8e4b0844a154557eca991 # via jaraco-context -certifi==2025.10.5 \ - --hash=sha256:0f212c2744a9bb6de0c56639a6f68afe01ecd92d91f14ae897c4fe7bbeeef0de \ - --hash=sha256:47c09d31ccf2acf0be3f701ea53595ee7e0b8fa08801c6624be771df09ae7b43 +certifi==2026.7.22 \ + --hash=sha256:62f22742b58a1a33014a2b6b706588a8d7e2a88ae7bd1a6ebe8c992928483775 \ + --hash=sha256:741e2c3b351ddf169a738da9f2c048608ff7f2c5cc02f1ebc6b118bb090d5d55 # via requests -cffi==2.0.0 ; platform_python_implementation != 'PyPy' and sys_platform == 'linux' \ - --hash=sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8 \ - --hash=sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2 \ - --hash=sha256:0e2b1fac190ae3ebfe37b979cc1ce69c81f4e4fe5746bb401dca63a9062cdaf1 \ - --hash=sha256:0f048dcf80db46f0098ccac01132761580d28e28bc0f78ae0d58048063317e15 \ - --hash=sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36 \ - --hash=sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824 \ - --hash=sha256:1d599671f396c4723d016dbddb72fe8e0397082b0a77a4fab8028923bec050e8 \ - --hash=sha256:28b16024becceed8c6dfbc75629e27788d8a3f9030691a1dbf9821a128b22c36 \ - --hash=sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17 \ - --hash=sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf \ - --hash=sha256:31000ec67d4221a71bd3f67df918b1f88f676f1c3b535a7eb473255fdc0b83fc \ - --hash=sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3 \ - --hash=sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed \ - --hash=sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702 \ - --hash=sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1 \ - --hash=sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8 \ - --hash=sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903 \ - --hash=sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6 \ - --hash=sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d \ - --hash=sha256:636062ea65bd0195bc012fea9321aca499c0504409f413dc88af450b57ffd03b \ - --hash=sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e \ - --hash=sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be \ - --hash=sha256:6f17be4345073b0a7b8ea599688f692ac3ef23ce28e5df79c04de519dbc4912c \ - --hash=sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683 \ - --hash=sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9 \ - --hash=sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c \ - --hash=sha256:7596d6620d3fa590f677e9ee430df2958d2d6d6de2feeae5b20e82c00b76fbf8 \ - --hash=sha256:78122be759c3f8a014ce010908ae03364d00a1f81ab5c7f4a7a5120607ea56e1 \ - --hash=sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4 \ - --hash=sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655 \ - --hash=sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67 \ - --hash=sha256:9755e4345d1ec879e3849e62222a18c7174d65a6a92d5b346b1863912168b595 \ - --hash=sha256:98e3969bcff97cae1b2def8ba499ea3d6f31ddfdb7635374834cf89a1a08ecf0 \ - --hash=sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65 \ - --hash=sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41 \ - --hash=sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6 \ - --hash=sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401 \ - --hash=sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6 \ - --hash=sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3 \ - --hash=sha256:b2ab587605f4ba0bf81dc0cb08a41bd1c0a5906bd59243d56bad7668a6fc6c16 \ - --hash=sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93 \ - --hash=sha256:c03e868a0b3bc35839ba98e74211ed2b05d2119be4e8a0f224fba9384f1fe02e \ - --hash=sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4 \ - --hash=sha256:c7eac2ef9b63c79431bc4b25f1cd649d7f061a28808cbc6c47b534bd789ef964 \ - --hash=sha256:c9c3d058ebabb74db66e431095118094d06abf53284d9c81f27300d0e0d8bc7c \ - --hash=sha256:ca74b8dbe6e8e8263c0ffd60277de77dcee6c837a3d0881d8c1ead7268c9e576 \ - --hash=sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0 \ - --hash=sha256:cdf5ce3acdfd1661132f2a9c19cac174758dc2352bfe37d98aa7512c6b7178b3 \ - --hash=sha256:d016c76bdd850f3c626af19b0542c9677ba156e4ee4fccfdd7848803533ef662 \ - --hash=sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3 \ - --hash=sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff \ - --hash=sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5 \ - --hash=sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd \ - --hash=sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f \ - --hash=sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5 \ - --hash=sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14 \ - --hash=sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d \ - --hash=sha256:e221cf152cff04059d011ee126477f0d9588303eb57e88923578ace7baad17f9 \ - --hash=sha256:e31ae45bc2e29f6b2abd0de1cc3b9d5205aa847cafaecb8af1476a609a2f6eb7 \ - --hash=sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382 \ - --hash=sha256:f1e22e8c4419538cb197e4dd60acc919d7696e5ef98ee4da4e01d3f8cfa4cc5a \ - --hash=sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e \ - --hash=sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a \ - --hash=sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4 \ - --hash=sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99 \ - --hash=sha256:f7f5baafcc48261359e14bcd6d9bff6d4b28d9103847c9e136694cb0501aef87 \ - --hash=sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b +cffi==2.1.1 ; platform_machine != 'ppc64le' and platform_machine != 's390x' and platform_python_implementation != 'PyPy' and sys_platform == 'linux' \ + --hash=sha256:046bfc24911b37851ee1b51aab8bffe713d89c68c6a057b09484ce9fd5f69b4e \ + --hash=sha256:06c72bb76605a4b0cd0aad6930b69d4baf7dd5d806cfc409b824191099700e66 \ + --hash=sha256:0beceaabe56af686895136a2de78db54ecd8e4046b236b8fd6d6cb61389e9bf2 \ + --hash=sha256:154852545011f779917b11c78db2358d095da62a9a172b78ad0a583ee5adc0d0 \ + --hash=sha256:194cffa889098ced9976c3fc6340305e43f6303657d298da55366907c05c22d6 \ + --hash=sha256:19ee6127ee34de7d83ce3d371ebc5ed91addbdcc39f9ab15ce4eb35a4e534971 \ + --hash=sha256:1a18a57b58cfb21fc28d72e876acf10eaed67a1ed96226f92af4df681d571c4c \ + --hash=sha256:1aa5645c30469b09530c4ebca77ebf8f17618293c58f8549cb1a543a50236e7d \ + --hash=sha256:1dea0e4d7d4f11f619fe8c1d76caf49e24405b4b5743c0e3be16a500ecd930c9 \ + --hash=sha256:208f941bb9d18e768138677f0a6d2ce01f590df56043dda1df1535ac57c88517 \ + --hash=sha256:210019b6c7cf07f081b4c54635c8cf744377001350e29cc0f81c4377b4797735 \ + --hash=sha256:246fa40ce8645a614ff682e0b70f37134e460eaf93a775e0cbe3cca585a67a80 \ + --hash=sha256:25792eac27877609e7bb06d42ff88278a6624fff2ba9bbb523c09616b117e80f \ + --hash=sha256:27350daa11d4f10c540e6e89dada4c54feb7256ad03e9a4dc075ebad7ba360d1 \ + --hash=sha256:28907ab9bfb6aa13184cfc17c6b8e1023c5ab6fd7076d8c20a35e59fe04f8f29 \ + --hash=sha256:2ae64be792b8966f2c69538199728b290e34726562896df1e5dc8ffd8d8188e8 \ + --hash=sha256:31348097ff5bbe827ccc41795d4dd099d9f0625e7def00ee653c137a490c2a6c \ + --hash=sha256:3143d81e29e1e20a9ce10901ec369012947876596f75a222235965f2b7ae832e \ + --hash=sha256:3222ba5d678f80a030e6afbcc33dc1ae5cb45facabb61cee2c7016b8432fde48 \ + --hash=sha256:3311ed60d36f83378794e1009ac6258bafbf81f7888b4caa7b35a521e3f95813 \ + --hash=sha256:334644fbac4eff73d985a17a91226df55d0f394160c4cfb880e084c8f7161cac \ + --hash=sha256:34e261f78cb6ceaaa36f42f2613f4380d94d9c759a9c73c769ee6e0247364632 \ + --hash=sha256:363e05fa78e15116c3c32c210ee36884fd6b9afa6d440e47112c3bd511d64cb6 \ + --hash=sha256:398aff33cee2767e3e781d2554c54bd0dff386bb437581e0d8011fde1a942ec1 \ + --hash=sha256:3d22a20b1fb1632cc72c22f95f7b0d2961c3e1c235f245ba4c606c4771035659 \ + --hash=sha256:42a494cee34437f05546455144f2b5d9ac09b1face62bcfce597d2e521066688 \ + --hash=sha256:42e2f76b9455f5a9a844f770bf3e200ed3da0e15f5df3db9c31fe80b04b3d004 \ + --hash=sha256:42f6930c31dc7f50732c9ae793c2786c7b6b044195967bbdde40bb9be81c4cc0 \ + --hash=sha256:456a61fa52d579ebf9df2e9552ead5129855dbaff6c1e5a9b1bc408809bdc062 \ + --hash=sha256:471cee653ae88de62096552e6d24ccb4a5adb8c8c9f10b5054d0122c15bf2779 \ + --hash=sha256:49cbc70e6542d4ccccb936558d1064a8012541e78f821f955cff24e357776c94 \ + --hash=sha256:4a7c934f7360e8cd64fe9efadcbd10c7c6364f531e432b9a4bf5ccbc9e0e8b50 \ + --hash=sha256:4be96343e422f2dfcd12ab5c9f5aebe03f82f737c6bffeca6830b3875cb44aab \ + --hash=sha256:4f42141fc14250de6dde5ee7ea4432be017252d91f19c5ad043c084cea629cac \ + --hash=sha256:507a24c282e0f42f8ed737cf048572cbf580468da5555764a8331735e9c736b6 \ + --hash=sha256:51b31d1c98274844cfd7838ce00bfc27c7423a4dc00fc0772fc3331c2cc90676 \ + --hash=sha256:58acb8ab8e295e6c5ea12f888cbb13cf21511ef2a3303a23f4325c29d17fe5c1 \ + --hash=sha256:5a59cc1c4442bc3d5c703bf720b51138d0bfc173618807c9ee2490a7541dd3d9 \ + --hash=sha256:5bb4e7ea95dcd6a014a6fef62e62467d67d8e582326443f3d68e71d6320a9fcf \ + --hash=sha256:5c58fe613dc5e5336357eff555824a314d8e43282600435c8d1cb6a7a2fedd13 \ + --hash=sha256:5e7cecbaadb83884793e05828cee59b210b24583b9c7425d0ba6a754fe22eb4e \ + --hash=sha256:616f097f2fe415bc92a247f02e11f634e1f9e9a83d327e3c915c15089c87869e \ + --hash=sha256:63bbfd5ded17c4840ac07cd8f1c21ba9d9708141f840b324f422f41b207e3973 \ + --hash=sha256:64faea20f4e2613363a1a9b9c7dd73058f3ecd00133a511e72ad7c511658f527 \ + --hash=sha256:661c298b4821edebead0c91edd2b00374d67ad7c5a1f7a91d4442633b79d6a72 \ + --hash=sha256:68e62fe11f30d5ca8289242866f0a5291402d8529ca2178ab8afc5c9694ae890 \ + --hash=sha256:6a8dddef476fab96d066d578fc88526767b836ab5ab21754e1d5bf3879c31c7c \ + --hash=sha256:6e192623c49c94421616a5778fba35cf0d5a8d000650c1967ef4448ee5cdd990 \ + --hash=sha256:7225e4514edb64eb6740324353e0da0711954fd8d7da4576755b1c6e09b697cd \ + --hash=sha256:75f80557d1389eddbd0de2681f6a390a0c5338c31ddaa821381c203fc3fd50d9 \ + --hash=sha256:770de9db11e84213beec501cfcaa013b019820ca881e03344dea5844f7876d94 \ + --hash=sha256:7750c6449dff7864bb9bb27ddfb0267756189201a3afc911d82b3caacd70dfc3 \ + --hash=sha256:7bde5e4cc5c10140859842b9d383af292b22639a4dffb725314baf45968cef80 \ + --hash=sha256:7ce713ace7c0e4520535b42b77eaa742c16dab813978064913e5a3cf82973b41 \ + --hash=sha256:7da0c5eff80f0197f3b3d1232ec5a682a9325f4ae9016a78f5f5ca35f9ced1f5 \ + --hash=sha256:7dbb61fe3a7699468030f71bbe5f8a0e326a151daa91beb11a6fc1f980c55e1c \ + --hash=sha256:811bd1e21d32de12efca32393a0ab3f5133b54fce9bd44b8bd77ab07da14bf6a \ + --hash=sha256:8ef53b2de9bcb9197d31854256575d59dbac0cba72ac627bb291ef5eceb74be4 \ + --hash=sha256:937c0052c05a31ca1daf18de3158eed4dbfcb9cc107adbea227728d647be701e \ + --hash=sha256:9d2055050ea716bd38b7f7f1579c275386646b4894c155a3e2f3cd62ed41b7c6 \ + --hash=sha256:9f8d177621de5cb38ee3e731eda45d421db093ec0739f46a5594babda7987a98 \ + --hash=sha256:a2d7755bef5a12ed488f4ef1f1b69ee9191d7396083b755a5d2295f6edb4768b \ + --hash=sha256:a48d62ab9d6f4f98c983223a547af44be6ca3691074c31cecced6facd3ba2dc1 \ + --hash=sha256:a4f00aa42f75d6e4595e8866e748cc1705adc0cddfeb2ca86d0d03993d63ba03 \ + --hash=sha256:a6e721d4b0e45d5b65e87534470e67b18dcd092c83f68fba09f152b9cbc061af \ + --hash=sha256:a730a083190634c65cca36ba5f489531576ebd79bcd5c8e172130f6453127231 \ + --hash=sha256:a931079504ecc49efed7744c476a5c343a92fabf66dec2db95edb1b2fdc770e2 \ + --hash=sha256:aa9511c62d14da7aacc9b4bf51f3f697a621e83b2d6919008243c3aad168eea3 \ + --hash=sha256:ab36d55f9ed2d067327667c2fea18dda018eb628dd6347aa01dda6cf1f5d3836 \ + --hash=sha256:ad2c86c495b899d862ea0f4b42891b8713a3bd45dd4105c7fd51c2a72f39f3a5 \ + --hash=sha256:aeae0e330c9f6acd681f647d46cefd30c29f93e3392882e792e82080c9691399 \ + --hash=sha256:b0431303acaea1089ad4b3e9ce4e6518193def1118d4073ca848635ee4ea2e96 \ + --hash=sha256:b5bdfd1c873d4e093aabc0ca84c4ca6dbc4f752afb5c86f146d9742580c9da2e \ + --hash=sha256:baed1e86cc735622097354b9d1281406caf42ff42a886d29faa8e8d1630333be \ + --hash=sha256:c1453022f490d2459a11819d83ad1d586e9ff65a12ac3e705ffebd46d3685dcf \ + --hash=sha256:c26608d2222fb1e94487e4a387d85f13eb55d5ed725cb25a0c589ac4ee60e7bc \ + --hash=sha256:c7659f22557c5a0bc4855cd635f55edec690cc008a40768527762cb9fb263455 \ + --hash=sha256:c8c69575568085ba0b1b10c0249d779a214aea6f6522e949a0fc9fb0fcb449d0 \ + --hash=sha256:c8d2c9fd1f2d16f780d15127abb050d13d1a76c03a4bd87d7e4980e45e511e12 \ + --hash=sha256:ca82be1a1d406ecfe1d25dc16cb33488e5a16bf4438c9fb590484ea29d92478b \ + --hash=sha256:cc572dace3f60ef98d7b12ff411d20f5362feb31a0439eab0085bbfd349982d7 \ + --hash=sha256:d18e5ac0f2f03f4f518d3e23db0f0cad7faa1da8620e9c09461d443bbf6e6692 \ + --hash=sha256:d28630f5854ab07ab1fd4aba756de52326c82e6be15d414b12793f1975048b54 \ + --hash=sha256:d9c275eaacd24aa73f94ffd6de08fc3f932424d8b6c376f4bed7cde376fe7bc3 \ + --hash=sha256:da0e573f9f97159390c89d9f1a9e41908b66d408cc5b58d08cf3847d844c531b \ + --hash=sha256:dd31f52ea1086513bb9df30f8fcee9b8918323ae067a3d5b78bc826a000712be \ + --hash=sha256:dddad92b554513a31f272570678ba307fb9f618f05e3d4a5eacafff9eae03e1d \ + --hash=sha256:df423d40ee8654634421812bc3b196da3f9bd7d32929da813f8394c4348a5358 \ + --hash=sha256:df913725b79db7bcf03448f36b7bf8815363417d5b58deecf9305e3e30f0f21a \ + --hash=sha256:e0bcb7e0f677f543555d2adff3bf19c05f66cdb4796e5ff602442ab2fe3c4ef7 \ + --hash=sha256:e2d65b31f36619cda3999b78b2aa9632e76b78448e7a56fc4240824200e7c4fc \ + --hash=sha256:e6e8cff14d6fb0be70a09c0bdc58096f501952d04624ebf867e0e56da2df8960 \ + --hash=sha256:f16c709686a78c727bbbf059f92b0bf41c6fc60deec706d2dc19f529175a6125 \ + --hash=sha256:f24fb43132a4c6b4cb4eb029492919b2db645be6808d738f244fd146c03c32cb \ + --hash=sha256:f53e442b08449d42821fa4a4fba000095af9f62742a500f978a9f557ec44339a \ + --hash=sha256:f5cfbc5fe74540d335175b656c725d74d90e3730c626d92575eea35029d9afaa \ + --hash=sha256:f81b3b8f3d4e343550fa4baa0e479bba9f2d29ce9c2e9b51d1ce1718d7442fcf \ + --hash=sha256:f8ec5e643a9a937f64e1999eb9f75d072263751912dc5cd06d3c85f8f44be7c3 \ + --hash=sha256:fb92203a88b3d3053034db775110081c49d28be6551923805e039924093761e4 \ + --hash=sha256:fcd22650c908d7b7da162bbfaab594a1227a15d1643a98c68b122ac642fa2264 # via cryptography -charset-normalizer==3.4.3 \ - --hash=sha256:00237675befef519d9af72169d8604a067d92755e84fe76492fef5441db05b91 \ - --hash=sha256:02425242e96bcf29a49711b0ca9f37e451da7c70562bc10e8ed992a5a7a25cc0 \ - --hash=sha256:027b776c26d38b7f15b26a5da1044f376455fb3766df8fc38563b4efbc515154 \ - --hash=sha256:07a0eae9e2787b586e129fdcbe1af6997f8d0e5abaa0bc98c0e20e124d67e601 \ - --hash=sha256:0cacf8f7297b0c4fcb74227692ca46b4a5852f8f4f24b3c766dd94a1075c4884 \ - --hash=sha256:0e78314bdc32fa80696f72fa16dc61168fda4d6a0c014e0380f9d02f0e5d8a07 \ - --hash=sha256:0f2be7e0cf7754b9a30eb01f4295cc3d4358a479843b31f328afd210e2c7598c \ - --hash=sha256:13faeacfe61784e2559e690fc53fa4c5ae97c6fcedb8eb6fb8d0a15b475d2c64 \ - --hash=sha256:14c2a87c65b351109f6abfc424cab3927b3bdece6f706e4d12faaf3d52ee5efe \ - --hash=sha256:1606f4a55c0fd363d754049cdf400175ee96c992b1f8018b993941f221221c5f \ - --hash=sha256:16a8770207946ac75703458e2c743631c79c59c5890c80011d536248f8eaa432 \ - --hash=sha256:18343b2d246dc6761a249ba1fb13f9ee9a2bcd95decc767319506056ea4ad4dc \ - --hash=sha256:18b97b8404387b96cdbd30ad660f6407799126d26a39ca65729162fd810a99aa \ - --hash=sha256:1bb60174149316da1c35fa5233681f7c0f9f514509b8e399ab70fea5f17e45c9 \ - --hash=sha256:1e8ac75d72fa3775e0b7cb7e4629cec13b7514d928d15ef8ea06bca03ef01cae \ - --hash=sha256:1ef99f0456d3d46a50945c98de1774da86f8e992ab5c77865ea8b8195341fc19 \ - --hash=sha256:2001a39612b241dae17b4687898843f254f8748b796a2e16f1051a17078d991d \ - --hash=sha256:23b6b24d74478dc833444cbd927c338349d6ae852ba53a0d02a2de1fce45b96e \ - --hash=sha256:252098c8c7a873e17dd696ed98bbe91dbacd571da4b87df3736768efa7a792e4 \ - --hash=sha256:257f26fed7d7ff59921b78244f3cd93ed2af1800ff048c33f624c87475819dd7 \ - --hash=sha256:2c322db9c8c89009a990ef07c3bcc9f011a3269bc06782f916cd3d9eed7c9312 \ - --hash=sha256:30a96e1e1f865f78b030d65241c1ee850cdf422d869e9028e2fc1d5e4db73b92 \ - --hash=sha256:30d006f98569de3459c2fc1f2acde170b7b2bd265dc1943e87e1a4efe1b67c31 \ - --hash=sha256:31a9a6f775f9bcd865d88ee350f0ffb0e25936a7f930ca98995c05abf1faf21c \ - --hash=sha256:320e8e66157cc4e247d9ddca8e21f427efc7a04bbd0ac8a9faf56583fa543f9f \ - --hash=sha256:34a7f768e3f985abdb42841e20e17b330ad3aaf4bb7e7aeeb73db2e70f077b99 \ - --hash=sha256:3653fad4fe3ed447a596ae8638b437f827234f01a8cd801842e43f3d0a6b281b \ - --hash=sha256:3cd35b7e8aedeb9e34c41385fda4f73ba609e561faedfae0a9e75e44ac558a15 \ - --hash=sha256:3cfb2aad70f2c6debfbcb717f23b7eb55febc0bb23dcffc0f076009da10c6392 \ - --hash=sha256:416175faf02e4b0810f1f38bcb54682878a4af94059a1cd63b8747244420801f \ - --hash=sha256:41d1fc408ff5fdfb910200ec0e74abc40387bccb3252f3f27c0676731df2b2c8 \ - --hash=sha256:42e5088973e56e31e4fa58eb6bd709e42fc03799c11c42929592889a2e54c491 \ - --hash=sha256:4ca4c094de7771a98d7fbd67d9e5dbf1eb73efa4f744a730437d8a3a5cf994f0 \ - --hash=sha256:511729f456829ef86ac41ca78c63a5cb55240ed23b4b737faca0eb1abb1c41bc \ - --hash=sha256:53cd68b185d98dde4ad8990e56a58dea83a4162161b1ea9272e5c9182ce415e0 \ - --hash=sha256:585f3b2a80fbd26b048a0be90c5aae8f06605d3c92615911c3a2b03a8a3b796f \ - --hash=sha256:5b413b0b1bfd94dbf4023ad6945889f374cd24e3f62de58d6bb102c4d9ae534a \ - --hash=sha256:5d8d01eac18c423815ed4f4a2ec3b439d654e55ee4ad610e153cf02faf67ea40 \ - --hash=sha256:6aab0f181c486f973bc7262a97f5aca3ee7e1437011ef0c2ec04b5a11d16c927 \ - --hash=sha256:6cf8fd4c04756b6b60146d98cd8a77d0cdae0e1ca20329da2ac85eed779b6849 \ - --hash=sha256:6fb70de56f1859a3f71261cbe41005f56a7842cc348d3aeb26237560bfa5e0ce \ - --hash=sha256:6fce4b8500244f6fcb71465d4a4930d132ba9ab8e71a7859e6a5d59851068d14 \ - --hash=sha256:70bfc5f2c318afece2f5838ea5e4c3febada0be750fcf4775641052bbba14d05 \ - --hash=sha256:73dc19b562516fc9bcf6e5d6e596df0b4eb98d87e4f79f3ae71840e6ed21361c \ - --hash=sha256:74d77e25adda8581ffc1c720f1c81ca082921329452eba58b16233ab1842141c \ - --hash=sha256:78deba4d8f9590fe4dae384aeff04082510a709957e968753ff3c48399f6f92a \ - --hash=sha256:86df271bf921c2ee3818f0522e9a5b8092ca2ad8b065ece5d7d9d0e9f4849bcc \ - --hash=sha256:88ab34806dea0671532d3f82d82b85e8fc23d7b2dd12fa837978dad9bb392a34 \ - --hash=sha256:8999f965f922ae054125286faf9f11bc6932184b93011d138925a1773830bbe9 \ - --hash=sha256:8dcfc373f888e4fb39a7bc57e93e3b845e7f462dacc008d9749568b1c4ece096 \ - --hash=sha256:939578d9d8fd4299220161fdd76e86c6a251987476f5243e8864a7844476ba14 \ - --hash=sha256:96b2b3d1a83ad55310de8c7b4a2d04d9277d5591f40761274856635acc5fcb30 \ - --hash=sha256:a2d08ac246bb48479170408d6c19f6385fa743e7157d716e144cad849b2dd94b \ - --hash=sha256:b256ee2e749283ef3ddcff51a675ff43798d92d746d1a6e4631bf8c707d22d0b \ - --hash=sha256:b5e3b2d152e74e100a9e9573837aba24aab611d39428ded46f4e4022ea7d1942 \ - --hash=sha256:b89bc04de1d83006373429975f8ef9e7932534b8cc9ca582e4db7d20d91816db \ - --hash=sha256:bd28b817ea8c70215401f657edef3a8aa83c29d447fb0b622c35403780ba11d5 \ - --hash=sha256:c60e092517a73c632ec38e290eba714e9627abe9d301c8c8a12ec32c314a2a4b \ - --hash=sha256:c6dbd0ccdda3a2ba7c2ecd9d77b37f3b5831687d8dc1b6ca5f56a4880cc7b7ce \ - --hash=sha256:c6e490913a46fa054e03699c70019ab869e990270597018cef1d8562132c2669 \ - --hash=sha256:c6f162aabe9a91a309510d74eeb6507fab5fff92337a15acbe77753d88d9dcf0 \ - --hash=sha256:c6fd51128a41297f5409deab284fecbe5305ebd7e5a1f959bee1c054622b7018 \ - --hash=sha256:cc34f233c9e71701040d772aa7490318673aa7164a0efe3172b2981218c26d93 \ - --hash=sha256:cc9370a2da1ac13f0153780040f465839e6cccb4a1e44810124b4e22483c93fe \ - --hash=sha256:ccf600859c183d70eb47e05a44cd80a4ce77394d1ac0f79dbd2dd90a69a3a049 \ - --hash=sha256:ce571ab16d890d23b5c278547ba694193a45011ff86a9162a71307ed9f86759a \ - --hash=sha256:cf1ebb7d78e1ad8ec2a8c4732c7be2e736f6e5123a4146c5b89c9d1f585f8cef \ - --hash=sha256:d0e909868420b7049dafd3a31d45125b31143eec59235311fc4c57ea26a4acd2 \ - --hash=sha256:d22dbedd33326a4a5190dd4fe9e9e693ef12160c77382d9e87919bce54f3d4ca \ - --hash=sha256:d716a916938e03231e86e43782ca7878fb602a125a91e7acb8b5112e2e96ac16 \ - --hash=sha256:d79c198e27580c8e958906f803e63cddb77653731be08851c7df0b1a14a8fc0f \ - --hash=sha256:d95bfb53c211b57198bb91c46dd5a2d8018b3af446583aab40074bf7988401cb \ - --hash=sha256:e28e334d3ff134e88989d90ba04b47d84382a828c061d0d1027b1b12a62b39b1 \ - --hash=sha256:ec557499516fc90fd374bf2e32349a2887a876fbf162c160e3c01b6849eaf557 \ - --hash=sha256:fb6fecfd65564f208cbf0fba07f107fb661bcd1a7c389edbced3f7a493f70e37 \ - --hash=sha256:fb731e5deb0c7ef82d698b0f4c5bb724633ee2a489401594c5c88b02e6cb15f7 \ - --hash=sha256:fb7f67a1bfa6e40b438170ebdc8158b78dc465a5a67b6dde178a46987b244a72 \ - --hash=sha256:fd10de089bcdcd1be95a2f73dbe6254798ec1bda9f450d5828c96f93e2536b9c \ - --hash=sha256:fdabf8315679312cfa71302f9bd509ded4f2f263fb5b765cf1433b39106c3cc9 +charset-normalizer==3.4.9 \ + --hash=sha256:0327fcd59a935777d83410750c50600ee9571af2846f71ce40f25b13da1ef380 \ + --hash=sha256:03d07803992c6c7bbc976327f34b18b6160327fc81cb82c9d504720ac0be3b62 \ + --hash=sha256:04ce310cb89c15df659582aee80a0603788732a5e017d5bd5c81158106ce249c \ + --hash=sha256:0d861473f743244d349b50f850d10eb87aeb22bbdcc8e64f79273c94af5a8226 \ + --hash=sha256:0e94703ec9684807f20cfb5eed95c70f67f2a8f21ad620146d7b5a13677b93e5 \ + --hash=sha256:0fa1aec2d32bcc03c8fa0f6f1712caad1adc38509f31142112e5c9daf5b9c833 \ + --hash=sha256:16b65ea0f2465b6fb52aa22de5eca612aa964ddfec00a912e26f4656cbef890b \ + --hash=sha256:16d10d789dd9bcca1173c95af82c58433122564b7bc39385124be735a35cbe99 \ + --hash=sha256:19ac87f93086ce37b86e098888555c4b4bc48102279bae3350098c0ed664b501 \ + --hash=sha256:1d22856ffbe153a602df38e4a5464f0b748a54002e0d69ac6d2ad0a197cc99ec \ + --hash=sha256:21e764fd1e70b6a3e205a0e46f3051701f98a8cb3fad66eeb80e48bb502f8698 \ + --hash=sha256:231ddcbb35e2ff8973e1365db41fe0572662893b99a05deb183b68ad4c0c8bd4 \ + --hash=sha256:253a4a220747e8b5faf57ec320c4f5efb0cef05f647420bf267143ec15dba10a \ + --hash=sha256:280081916dc341820640489a66e4696049401ef1cf6dd672f672e70ad915aca3 \ + --hash=sha256:2a441ea71902098ffe78c5abe6c494f44160b4af614ed16c3d9a3b1d17fd8ee2 \ + --hash=sha256:304b13570067b2547562e308af560b3963857b1fa90bd6afd978130130fe2d6a \ + --hash=sha256:32286a2c8d167e897177b673176c1e3e00d4057caf5d2b64eef9a3666b03018e \ + --hash=sha256:33bdcc2a32c0a0e861f60841a512c8acc658c87c2ac59d89e3a46dacf7d866e4 \ + --hash=sha256:375b83ed0aecfce76c16d198fbc21f3b11b337d68662bea0a995046682a11419 \ + --hash=sha256:3c09a49d6cde137258beb3d551994a2927fd35ad5cf96aed573f61bbd67c5f84 \ + --hash=sha256:3d92613ec25e43b05f042302531ec0f00b8445190e43325880cbd6ab7c2581da \ + --hash=sha256:40a126142a56b2dfc0aacbad1de8310cbf60da7656db0e6b16eebd48e3e93519 \ + --hash=sha256:416c229f77e5ea25b3dfd4b582f8d73d7e43c22320302b9ab128a2d3a0b38efe \ + --hash=sha256:432786d3561e69aeeae6c7e8648964ce0ad05736120135601f87ac26b9c83381 \ + --hash=sha256:43b9e366a31fdd1c87d0eb08f579b4a82b723ea54338f040d6b4e518a026ea29 \ + --hash=sha256:440eede837960000d74978f0eba527be106b5b9aee0daf779d395276ed0b0614 \ + --hash=sha256:45b0cc4e3556cd875e09102988d1ab8356c998b596c9fced84547c8138b487a0 \ + --hash=sha256:476743fe6dfe14a2da12e3ac79125dc84a3b2cf8094369a47a1529b0cd8549fe \ + --hash=sha256:4773092f8019072343a7447203308b176e10199920eb02d6195e81bbb3274c29 \ + --hash=sha256:4b3dac63058cc36820b0dd072f89898604e2d39686fe05321729d00d8ac185a0 \ + --hash=sha256:4d1c96a7a18b9690a4d46df09e3e3382406ae3213727cd1019ebade1c4a81917 \ + --hash=sha256:51307f5c71007673a2bf8232ad973483d281e74cb99c8c5a990af1eefa6277d9 \ + --hash=sha256:51447e9aa2684679af07ca5021c3db526e0284347ebf4ffcec1154c3350cfe32 \ + --hash=sha256:58150c9f9b9a552505912d182ccdf26f6396fb6094816ceebcbb20eecabaed94 \ + --hash=sha256:5b10cd92fc5c498b35a8635df6d5a100207f88b63a4dc1de7ef9a548e1e2cd63 \ + --hash=sha256:5e226f6218febc71f6c1fc2fafb91c226f75bdc1d8fb12d66823716e891608fd \ + --hash=sha256:609b3ba8fcc0fb5ab7af00719d0fb6ad0cb518e48e7712d12fd68f1327951198 \ + --hash=sha256:60f44ade2cf573dad7a277e6f8ca9a51a21dda572b13bd7d8539bb3cd5dbedde \ + --hash=sha256:611057cc5d5c0afc743ba8be6bd828c17e0aaa8643f9d0a9b9bb7dea80eb8012 \ + --hash=sha256:6366a16e1a25018694d6a5d784d09b046edc9eac40ea2b54065c3052672516a1 \ + --hash=sha256:65a7ff3f705e57d392f7261b6d0550fe137c3019477431f1c355e0db0a7d3e15 \ + --hash=sha256:673611bbd43f0810bec0b0f028ddeaaa501190339cac411f347ac76917c3ae7b \ + --hash=sha256:67830fc78e67501f47bb950471b2dcb9b35b140084429318e862895a8e89c993 \ + --hash=sha256:68ce9f4d6b26d5ccbf7fd4459bf75f74a0a146677ebba80597df60cbdb20e6f4 \ + --hash=sha256:68e5f26a1ad57ded6d1cfb85331d1c1a195314756471d97758c48498bb4dcdf5 \ + --hash=sha256:69b157c5d3292bcd443faca052f3096f637f1e074b98212a933c074ae23dc3b8 \ + --hash=sha256:75286256590a6320cf106a0d28970d3560aad9ee09aa7b34fb40524792436d35 \ + --hash=sha256:78841cccf1af7b40f6f716338d50c0902dbe88d9f800b3c973b7a9a0a693a642 \ + --hash=sha256:78fa18e436a1a0e58dbd7e02fc4473f3f32cceb12df9dfca542d075961c307d2 \ + --hash=sha256:79580094b00d1789d1f93ea55bc43cb2f611910c72235b7657f3482ddcc1b22d \ + --hash=sha256:7b86a2b16095d250c6f58b3d9b2eee6f4147754344f3dab0922f7c9bf7d226c9 \ + --hash=sha256:83aed2c10721ddd90f68140685391b50811a880af20654c59af6b6c66c40513c \ + --hash=sha256:84fd18bcc17526fc2b3c1af7d2b9217d32c9c04448c16ec693b9b4f1985c3d33 \ + --hash=sha256:871ff67ea1aad4dfd91736464934d56b32dac49f9fbe16cddba36198a7b3a0db \ + --hash=sha256:898f0e9068ca27d37f8e83a5b962821df851532e6c4a7d615c1c033f9da6eedf \ + --hash=sha256:8a79d9f4d8001473a30c163556b3c3bfebec837495a412dde78b51672f6134f9 \ + --hash=sha256:8c041122946b7ba21bb32c45b1aa57b1be35527690aeb3c5c234521085632eee \ + --hash=sha256:90c44bc373b7687f6948b693cceaea1348ae0975d7474746559494468e3c1d84 \ + --hash=sha256:9104ed0bd76a429d46f9ec0dbc9b08ad1d2dcdf2b00a5a0daa1c145329b35b44 \ + --hash=sha256:920079c3f7456fa213e0829ed2073aaa727fd39d889ead5b4f35d0de5460d04f \ + --hash=sha256:93d59d504b230e83c7a843251681959a0b6a9cd76f6e146ce1b8a80eb8739af9 \ + --hash=sha256:9b2aff1c7b3884512b9512c3eaadd9bab39fb45042ffaaa1dd08ff2b9f8109d9 \ + --hash=sha256:9b8e0f3107e2200b76f6054de99016eac3ee6762713587b36baaa7e4bd2ae177 \ + --hash=sha256:9bb41182d93ea91f60b4bc8fbf4c820c69ef8a12ab2d917f3f1834f1acad07e8 \ + --hash=sha256:9cdef90ae47919cae358d8ab15797a800ed41da7aba5d72419fb510729e2ed4b \ + --hash=sha256:a1786910334ed46ab1dd73222f2cd1e05c2c3bb39f6dddb4f8b36fc382058a39 \ + --hash=sha256:a4cfde78a9f2880208d16a93b795726a3017d5977e08d1e162a7a31322479c41 \ + --hash=sha256:a4fbdde9dd4a9ce5fd52c2b3a347bb50cc89483ef783f1cb00d408c13f7a96c0 \ + --hash=sha256:aa99adc8f081b475a12843953db36831eaf83ec33eb46a90629ca6a5de45a616 \ + --hash=sha256:ac351b3b8014eead140e77e9717e2992c6bbe30b63bc3422422eb84865412e3d \ + --hash=sha256:ad41ba96094304aa090f5a30cb6e4fb3b3f1c264c523394b4c39bbacc4dc92ba \ + --hash=sha256:b5314963fce9b0b12743891de876e724997864ee22aa496f903f426c7e2fa5b2 \ + --hash=sha256:bcf74c1df76758a395bf0af608c04c82257523f55c9868b334f06270d0f2112b \ + --hash=sha256:bd47ba7fc3ca94896759ea0109775132d3e7ab921fbf54038e1bab2e46c313c9 \ + --hash=sha256:c0323c9daef75ef2e5083624b4585018a0c9d5e3b40f607eed81a311270b934b \ + --hash=sha256:c1225416b463483160e4af85d5fc3a9690ccb53fd4b1865a6437825f5ede3209 \ + --hash=sha256:c1c948747b03be832dceed96ca815cef7360de9aa19d37c730f8e3f6101aca48 \ + --hash=sha256:c25fe15c70c59eb7c5ce8c06a1f3fa1da0ecc5ea1e7a5922c40fd2fa9b0d5046 \ + --hash=sha256:cc1b0fff8ead343dae06305f954eb8468ba0ec1a97881f42489d198e4ce3c632 \ + --hash=sha256:cd6280cf040f233bd7d3407b743b4b4c74f70e8e1c4199cb112a62c941c0772a \ + --hash=sha256:cd6c3d4b783c556fa00bf540854e42f135e2f256abd29669fcd0da0f2dec79c2 \ + --hash=sha256:d4d6fcde76f94f5cb9e43e9e9a61f16dacefd228cbbf6f1a09bd9b219a92f1a1 \ + --hash=sha256:ddf4af30b417d9fe16481e9b81c27ab2a7cde1ff7ba3e85653b02db7d145dc7b \ + --hash=sha256:df115d4d83168fdf2cae48ef1ff6d1cb4c466364e30861b37121de0f3bf1b990 \ + --hash=sha256:df7276909358e5635ae203673ab7e509ddd224225a8d6b0790bf13eb2bde1cc5 \ + --hash=sha256:e4fd89cc178bced6ad29cb3e6dd4aa63fa5017c3524dbd0b25998fb64a87cc8b \ + --hash=sha256:e9701d0049d92c16703a42771b98d560b95248949f23f8cf7b4eddd201814fb9 \ + --hash=sha256:ee2f2a527e3c1a6e6411eb4209642e138b544a2d72fe5d0d76daf77b24063534 \ + --hash=sha256:f7fb7d750cfa0a070d2c24e831fd3481019a60dd317ea2b39acbcebc08b6ed81 \ + --hash=sha256:f840ed6d8ecba8255df8c42b87fadeda98ddfc6eeec05e2dc66e26d46dd6f58a \ + --hash=sha256:f86c6358749bd4fda175388691e3ba8c46e24c5347d0afd20f9b7edfc9faf07d \ + --hash=sha256:fa36ec09ef71d158186bc79e359ff5fdd6e7996fe8ab638f00d6b93139ba4fcf \ + --hash=sha256:fe2c7201c642b7c308f1675355ad7ff7b66acfe3541625efe5a3ad38f29d6115 # via requests -cryptography==46.0.7 ; sys_platform == 'linux' \ - --hash=sha256:04959522f938493042d595a736e7dbdff6eb6cc2339c11465b3ff89343b65f65 \ - --hash=sha256:128c5edfe5e5938b86b03941e94fac9ee793a94452ad1365c9fc3f4f62216832 \ - --hash=sha256:1d25aee46d0c6f1a501adcddb2d2fee4b979381346a78558ed13e50aa8a59067 \ - --hash=sha256:24402210aa54baae71d99441d15bb5a1919c195398a87b563df84468160a65de \ - --hash=sha256:258514877e15963bd43b558917bc9f54cf7cf866c38aa576ebf47a77ddbc43a4 \ - --hash=sha256:35719dc79d4730d30f1c2b6474bd6acda36ae2dfae1e3c16f2051f215df33ce0 \ - --hash=sha256:397655da831414d165029da9bc483bed2fe0e75dde6a1523ec2fe63f3c46046b \ - --hash=sha256:3986ac1dee6def53797289999eabe84798ad7817f3e97779b5061a95b0ee4968 \ - --hash=sha256:420b1e4109cc95f0e5700eed79908cef9268265c773d3a66f7af1eef53d409ef \ - --hash=sha256:42a1e5f98abb6391717978baf9f90dc28a743b7d9be7f0751a6f56a75d14065b \ - --hash=sha256:462ad5cb1c148a22b2e3bcc5ad52504dff325d17daf5df8d88c17dda1f75f2a4 \ - --hash=sha256:506c4ff91eff4f82bdac7633318a526b1d1309fc07ca76a3ad182cb5b686d6d3 \ - --hash=sha256:5ad9ef796328c5e3c4ceed237a183f5d41d21150f972455a9d926593a1dcb308 \ - --hash=sha256:5d1c02a14ceb9148cc7816249f64f623fbfee39e8c03b3650d842ad3f34d637e \ - --hash=sha256:5e51be372b26ef4ba3de3c167cd3d1022934bc838ae9eaad7e644986d2a3d163 \ - --hash=sha256:60627cf07e0d9274338521205899337c5d18249db56865f943cbe753aa96f40f \ - --hash=sha256:65814c60f8cc400c63131584e3e1fad01235edba2614b61fbfbfa954082db0ee \ - --hash=sha256:73510b83623e080a2c35c62c15298096e2a5dc8d51c3b4e1740211839d0dea77 \ - --hash=sha256:7bbc6ccf49d05ac8f7d7b5e2e2c33830d4fe2061def88210a126d130d7f71a85 \ - --hash=sha256:80406c3065e2c55d7f49a9550fe0c49b3f12e5bfff5dedb727e319e1afb9bf99 \ - --hash=sha256:84d4cced91f0f159a7ddacad249cc077e63195c36aac40b4150e7a57e84fffe7 \ - --hash=sha256:8a469028a86f12eb7d2fe97162d0634026d92a21f3ae0ac87ed1c4a447886c83 \ - --hash=sha256:91bbcb08347344f810cbe49065914fe048949648f6bd5c2519f34619142bbe85 \ - --hash=sha256:935ce7e3cfdb53e3536119a542b839bb94ec1ad081013e9ab9b7cfd478b05006 \ - --hash=sha256:9694078c5d44c157ef3162e3bf3946510b857df5a3955458381d1c7cfc143ddb \ - --hash=sha256:a1529d614f44b863a7b480c6d000fe93b59acee9c82ffa027cfadc77521a9f5e \ - --hash=sha256:abad9dac36cbf55de6eb49badd4016806b3165d396f64925bf2999bcb67837ba \ - --hash=sha256:b36a4695e29fe69215d75960b22577197aca3f7a25b9cf9d165dcfe9d80bc325 \ - --hash=sha256:b7b412817be92117ec5ed95f880defe9cf18a832e8cafacf0a22337dc1981b4d \ - --hash=sha256:c5b1ccd1239f48b7151a65bc6dd54bcfcc15e028c8ac126d3fada09db0e07ef1 \ - --hash=sha256:cbd5fb06b62bd0721e1170273d3f4d5a277044c47ca27ee257025146c34cbdd1 \ - --hash=sha256:cdf1a610ef82abb396451862739e3fc93b071c844399e15b90726ef7470eeaf2 \ - --hash=sha256:cdfbe22376065ffcf8be74dc9a909f032df19bc58a699456a21712d6e5eabfd0 \ - --hash=sha256:d02c738dacda7dc2a74d1b2b3177042009d5cab7c7079db74afc19e56ca1b455 \ - --hash=sha256:d151173275e1728cf7839aaa80c34fe550c04ddb27b34f48c232193df8db5842 \ - --hash=sha256:d23c8ca48e44ee015cd0a54aeccdf9f09004eba9fc96f38c911011d9ff1bd457 \ - --hash=sha256:d3b99c535a9de0adced13d159c5a9cf65c325601aa30f4be08afd680643e9c15 \ - --hash=sha256:d5f7520159cd9c2154eb61eb67548ca05c5774d39e9c2c4339fd793fe7d097b2 \ - --hash=sha256:db0f493b9181c7820c8134437eb8b0b4792085d37dbb24da050476ccb664e59c \ - --hash=sha256:e06acf3c99be55aa3b516397fe42f5855597f430add9c17fa46bf2e0fb34c9bb \ - --hash=sha256:e4cfd68c5f3e0bfdad0d38e023239b96a2fe84146481852dffbcca442c245aa5 \ - --hash=sha256:ea42cbe97209df307fdc3b155f1b6fa2577c0defa8f1f7d3be7d31d189108ad4 \ - --hash=sha256:ebd6daf519b9f189f85c479427bbd6e9c9037862cf8fe89ee35503bd209ed902 \ - --hash=sha256:f247c8c1a1fb45e12586afbb436ef21ff1e80670b2861a90353d9b025583d246 \ - --hash=sha256:fbfd0e5f273877695cb93baf14b185f4878128b250cc9f8e617ea0c025dfb022 \ - --hash=sha256:fc9ab8856ae6cf7c9358430e49b368f3108f050031442eaeb6b9d87e4dcf4e4f \ - --hash=sha256:fcd8eac50d9138c1d7fc53a653ba60a2bee81a505f9f8850b6b2888555a45d0e \ - --hash=sha256:fdd1736fed309b4300346f88f74cd120c27c56852c3838cab416e7a166f67298 \ - --hash=sha256:ffca7aa1d00cf7d6469b988c581598f2259e46215e0140af408966a24cf086ce +cryptography==50.0.0 ; platform_machine != 'ppc64le' and platform_machine != 's390x' and sys_platform == 'linux' \ + --hash=sha256:031e2d5dd4bb9caa3ca9c82e5a197fd8ae680232cee62603d1a813f3f07e3d03 \ + --hash=sha256:06a32a980526a6ab9a4b9bf8f7385800791e2bb960903cb6b530e4817509a3b7 \ + --hash=sha256:07479a1cb08219ab719147e742e76090c9c773321959bb94946fffdd397a6437 \ + --hash=sha256:07949c449a1abcf60d1ee6e88956d89404c7df3c8258f46589e912988e551987 \ + --hash=sha256:105110f43a471dbd0060b9c9516cb8a6a79233631a04cc2ba16f28323ac6e025 \ + --hash=sha256:11b74db56cdbe3cdee6e3f6982ecb70334fa10dce99ed58bf7894aaaa3b2a037 \ + --hash=sha256:12b9c6996425c76ea6c457ace4f3073e715b8c545add07cd1a8f3a4f90691269 \ + --hash=sha256:1489e263a8048bb8b6a8bac662eb2d402ea5d2b7b4699b72f385f1e2772db105 \ + --hash=sha256:19736989797678c6af1e55cd49055cdbcb55d8f6b5583ac5335f933aba9101dc \ + --hash=sha256:1b4a266766514614f8aa60416e71f2fc6e575d36e7bdc90f644fadb2f4b75b95 \ + --hash=sha256:2a8183b489dc1f7f80f135780fadc1108f14b31b8a40411c7a5b17425f65f28b \ + --hash=sha256:37fdb0d0111f1e2ff07139dfb79f1b49531f8e213c46f1163dd7642979b58c47 \ + --hash=sha256:3f5735ffe4996d28b809371756219f5354864902a3b9e7c0b9ee87041209fc9c \ + --hash=sha256:49e7d93abdbd2990caced757e5fade25302f719c3c8fb6e6fff2dde98999fc41 \ + --hash=sha256:5e34edd123674534acd70147f0ca331eaa2c74e6325fb2028c886aa26ba0b68c \ + --hash=sha256:62598a8a57f815db4c6259a4e97d857dab56697e7de8e8ab02352ab74da1995d \ + --hash=sha256:65c2c3add92b45fd0709db8594536aea39c2a67af0e27ffcf049c498501140b7 \ + --hash=sha256:6ba6a53445bd3cfa809ef3ef5f1589aa6ba08784a1d962bf47d0940e871dab1c \ + --hash=sha256:6e7d61120573a7f2cd94cc095f9e81f6967c61ccdf194285aa143ecec8e0b708 \ + --hash=sha256:7cec5b856506da6defb290f30c9ee687d5f5e8cb0bd3f6459dde43b0b4fa40ef \ + --hash=sha256:80b63928fa35083b33966ce1efb70e5b9607181e49dcd1c22c8c005e319f667f \ + --hash=sha256:82148ec5bddac30b51a5b3c1945075f896fa022cb93f8e4a01e9f6ee95292c5f \ + --hash=sha256:828743d939e9629bc267b8e2d08d8bb67cd4319c771a33d4b18b22dd8fb7440a \ + --hash=sha256:8d89f3976b10b4ce31118de72329025f70d2c6ead14a8217c5514dd2c6d5a78f \ + --hash=sha256:8eb5e1172eb569ea8a872796576e6a67c276351728b6455d5beb01242b027c6a \ + --hash=sha256:900131fafd8aead39ac7dd3a7e833be754c17a95cfd91221636949fe4eb0aa8a \ + --hash=sha256:910d11e1a385c654bf738bf3e6b8e6ed5de0f5610fcae2be9e5b398d8081d20e \ + --hash=sha256:910e1d2668e7de9648f2bcee30e180db2a6b15c30f887d7c4c93ddf96e3992e3 \ + --hash=sha256:9aa87839c383bdbab6ef865787a1fb877af8dd03464c4400322726feaaadfc6d \ + --hash=sha256:a1b30560f2acc95aa8b2e06e716a13dbfc97314747b80d9707e307f77b40d6b3 \ + --hash=sha256:a91296cb61e8df6f86d0c19cc4068228da256bf59bf86049fbd821084565327f \ + --hash=sha256:b42a28c1844fd9de8f3f7d540e36b66f3a9c83fceac7170ebc7a6a19edd9dcae \ + --hash=sha256:bd1c592e4d5974f0d08d4888e432157adba757c66da0246918e43677fafa2d30 \ + --hash=sha256:c87f62a3d3b9888ed0fdde100ec06aa61ca9cd44bad9057d1dff9a516b5f5bb9 \ + --hash=sha256:c99c003e088647b8a5b7c145d6f78c335f6348332b62e142d411c4b63d1460b9 \ + --hash=sha256:ccdc4a71a4dabae05de219404f9f4abc38e3b58422177ff93d0da05967dafa07 \ + --hash=sha256:d24fead1d4d076e1bfb006dcec392074a3cd8d7b4fc8a595aa64073b2b7a96ba \ + --hash=sha256:d58c3db7cd6eed54e6c06744db55456b65ebd7492ddeae9c1e93cfca7aa857d3 \ + --hash=sha256:d764dcf130c428ef66786f866dd750f53182bc608813489915e9fc106bb0c82f \ + --hash=sha256:df2a58a472f332225671c35b0a830208b86d004f82baa8530fa3782c85646533 \ + --hash=sha256:e722f16708d854fe924790e051061f6704a472c3bac347b6fd88033ea8dd0dc5 \ + --hash=sha256:ecfed7367f965a0328cfbdd70da860f15441f002f613185668c6e6ebf5a0ac11 \ + --hash=sha256:eeac2acb5a20ed25e0ad6d1df9891a520b78b404266b6d11778f25d5d691a6c9 \ + --hash=sha256:f59e38625469987d7ef6d495323c55e7db6c212eaf6112267e0d3b565a2e9c9f \ + --hash=sha256:f89831ef99dd7dd169ab06d63a831adb9e20a87aac6d380266bbda5823349169 \ + --hash=sha256:fd9192b7b70c573d7f214eb1ae35e00d359f6f5e4b27c7e21e30de1fc6204645 # via secretstorage -docutils==0.22.2 \ - --hash=sha256:9fdb771707c8784c8f2728b67cb2c691305933d68137ef95a75db5f4dfbc213d \ - --hash=sha256:b0e98d679283fc3bb0ead8a5da7f501baa632654e7056e9c5846842213d674d8 +docutils==0.23 \ + --hash=sha256:25d013af9bf23bc1c7b2b093dff4208166c53a94786c9e447808335ef1185fea \ + --hash=sha256:746f5060322511280a1e50eb76846ed6bf2342984b2ac04dc42caa1a8d78799e # via readme-renderer -idna==3.10 \ - --hash=sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9 \ - --hash=sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3 +id==1.6.1 \ + --hash=sha256:d0732d624fb46fd4e7bc4e5152f00214450953b9e772c182c1c22964def1a069 \ + --hash=sha256:f5ec41ed2629a508f5d0988eda142e190c9c6da971100612c4de9ad9f9b237ca + # via twine +idna==3.18 \ + --hash=sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2 \ + --hash=sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848 # via requests -importlib-metadata==8.7.0 \ - --hash=sha256:d13b81ad223b890aa16c5471f2ac3056cf76c5f10f82d6f9292f0b415f389000 \ - --hash=sha256:e5dd1551894c77868a30651cef00984d50e1002d06942a7101d34870c5f02afd - # via - # keyring - # twine -jaraco-classes==3.4.0 \ +importlib-metadata==9.0.0 ; python_full_version < '3.12' and platform_machine != 'ppc64le' and platform_machine != 's390x' \ + --hash=sha256:2d21d1cc5a017bd0559e36150c21c830ab1dc304dedd1b7ea85d20f45ef3edd7 \ + --hash=sha256:a4f57ab599e6a2e3016d7595cfd72eb4661a5106e787a95bcc90c7105b831efc + # via keyring +jaraco-classes==3.4.0 ; platform_machine != 'ppc64le' and platform_machine != 's390x' \ --hash=sha256:47a024b51d0239c0dd8c8540c6c7f484be3b8fcf0b2d85c13825780d3b3f3acd \ --hash=sha256:f662826b6bed8cace05e7ff873ce0f9283b5c924470fe664fff1c2f00f581790 # via keyring -jaraco-context==6.0.1 \ - --hash=sha256:9bae4ea555cf0b14938dc0aee7c9f32ed303aa20a3b73e7dc80111628792d1b3 \ - --hash=sha256:f797fc481b490edb305122c9181830a3a5b76d84ef6d1aef2fb9b47ab956f9e4 +jaraco-context==6.1.2 ; platform_machine != 'ppc64le' and platform_machine != 's390x' \ + --hash=sha256:bf8150b79a2d5d91ae48629d8b427a8f7ba0e1097dd6202a9059f29a36379535 \ + --hash=sha256:f1a6c9d391e661cc5b8d39861ff077a7dc24dc23833ccee564b234b81c82dfe3 # via keyring -jaraco-functools==4.3.0 \ - --hash=sha256:227ff8ed6f7b8f62c56deff101545fa7543cf2c8e7b82a7c2116e672f29c26e8 \ - --hash=sha256:cfd13ad0dd2c47a3600b439ef72d8615d482cedcff1632930d6f28924d92f294 +jaraco-functools==4.6.0 ; platform_machine != 'ppc64le' and platform_machine != 's390x' \ + --hash=sha256:880c577ec9720b3a052d5bc611fb9f2269b3d87902ef42440df443b88e443280 \ + --hash=sha256:99e3dc0060c5cbe8fcd1cdb36258e2a65ca40f1566b2033b12abb1bb44dd3c30 # via keyring -jeepney==0.9.0 ; sys_platform == 'linux' \ +jeepney==0.9.0 ; platform_machine != 'ppc64le' and platform_machine != 's390x' and sys_platform == 'linux' \ --hash=sha256:97e5714520c16fc0a45695e5365a2e11b81ea79bba796e26f9f1d178cb182683 \ --hash=sha256:cf0e9e845622b81e4a28df94c40345400256ec608d0e55bb8a3feaa9163f5732 # via # keyring # secretstorage -keyring==25.6.0 \ - --hash=sha256:0b39998aa941431eb3d9b0d4b2460bc773b9df6fed7621c2dfb291a7e0187a66 \ - --hash=sha256:552a3f7af126ece7ed5c89753650eec89c7eaae8617d0aa4d9ad2b75111266bd +keyring==25.7.0 ; platform_machine != 'ppc64le' and platform_machine != 's390x' \ + --hash=sha256:be4a0b195f149690c166e850609a477c532ddbfbaed96a404d4e43f8d5e2689f \ + --hash=sha256:fe01bd85eb3f8fb3dd0405defdeac9a5b4f6f0439edbb3149577f244a2e8245b # via twine -markdown-it-py==4.0.0 \ - --hash=sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147 \ - --hash=sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3 +markdown-it-py==4.2.0 \ + --hash=sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49 \ + --hash=sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a # via rich mdurl==0.1.2 \ --hash=sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8 \ --hash=sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba # via markdown-it-py -more-itertools==10.8.0 \ - --hash=sha256:52d4362373dcf7c52546bc4af9a86ee7c4579df9a8dc268be0a2f949d376cc9b \ - --hash=sha256:f638ddf8a1a0d134181275fb5d58b086ead7c6a72429ad725c67503f13ba30bd +more-itertools==11.1.0 ; platform_machine != 'ppc64le' and platform_machine != 's390x' \ + --hash=sha256:48e8f4d9e7e5878571ecf6f2b4e57634f93cd474cc8cfbd2376f2d11b396e30d \ + --hash=sha256:4b65538ae22f6fed0ce4874efd317463a7489796a0939fa66824dd542125a192 # via # jaraco-classes # jaraco-functools -nh3==0.3.0 \ - --hash=sha256:0649464ac8eee018644aacbc103874ccbfac80e3035643c3acaab4287e36e7f5 \ - --hash=sha256:16f8670201f7e8e0e05ed1a590eb84bfa51b01a69dd5caf1d3ea57733de6a52f \ - --hash=sha256:1adeb1062a1c2974bc75b8d1ecb014c5fd4daf2df646bbe2831f7c23659793f9 \ - --hash=sha256:37d3003d98dedca6cd762bf88f2e70b67f05100f6b949ffe540e189cc06887f9 \ - --hash=sha256:389d93d59b8214d51c400fb5b07866c2a4f79e4e14b071ad66c92184fec3a392 \ - --hash=sha256:3f1b4f8a264a0c86ea01da0d0c390fe295ea0bcacc52c2103aca286f6884f518 \ - --hash=sha256:423201bbdf3164a9e09aa01e540adbb94c9962cc177d5b1cbb385f5e1e79216e \ - --hash=sha256:634e34e6162e0408e14fb61d5e69dbaea32f59e847cfcfa41b66100a6b796f62 \ - --hash=sha256:6d68fa277b4a3cf04e5c4b84dd0c6149ff7d56c12b3e3fab304c525b850f613d \ - --hash=sha256:7275fdffaab10cc5801bf026e3c089d8de40a997afc9e41b981f7ac48c5aa7d5 \ - --hash=sha256:7852f038a054e0096dac12b8141191e02e93e0b4608c4b993ec7d4ffafea4e49 \ - --hash=sha256:7c915060a2c8131bef6a29f78debc29ba40859b6dbe2362ef9e5fd44f11487c2 \ - --hash=sha256:80fe20171c6da69c7978ecba33b638e951b85fb92059259edd285ff108b82a6d \ - --hash=sha256:a537ece1bf513e5a88d8cff8a872e12fe8d0f42ef71dd15a5e7520fecd191bbb \ - --hash=sha256:af5aa8127f62bbf03d68f67a956627b1bd0469703a35b3dad28d0c1195e6c7fb \ - --hash=sha256:b0612ccf5de8a480cf08f047b08f9d3fecc12e63d2ee91769cb19d7290614c23 \ - --hash=sha256:ba0caa8aa184196daa6e574d997a33867d6d10234018012d35f86d46024a2a95 \ - --hash=sha256:bae63772408fd63ad836ec569a7c8f444dd32863d0c67f6e0b25ebbd606afa95 \ - --hash=sha256:c7a32a7f0d89f7d30cb8f4a84bdbd56d1eb88b78a2434534f62c71dac538c450 \ - --hash=sha256:ce5e7185599f89b0e391e2f29cc12dc2e206167380cea49b33beda4891be2fe1 \ - --hash=sha256:d8ba24cb31525492ea71b6aac11a4adac91d828aadeff7c4586541bf5dc34d2f \ - --hash=sha256:d97d3efd61404af7e5721a0e74d81cdbfc6e5f97e11e731bb6d090e30a7b62b2 \ - --hash=sha256:e90883f9f85288f423c77b3f5a6f4486375636f25f793165112679a7b6363b35 \ - --hash=sha256:e9e6a7e4d38f7e8dda9edd1433af5170c597336c1a74b4693c5cb75ab2b30f2a \ - --hash=sha256:ec6cfdd2e0399cb79ba4dcffb2332b94d9696c52272ff9d48a630c5dca5e325a \ - --hash=sha256:f416c35efee3e6a6c9ab7716d9e57aa0a49981be915963a82697952cba1353e1 +nh3==0.3.6 \ + --hash=sha256:082675ff87b9385ec430ffe6d5847ba7456cc39b73720cd4add472f9f4cffd56 \ + --hash=sha256:2411e8c3cee81a1ddd62c2a5d50585c28aa5566d373ad1db92536b95ddb24ef2 \ + --hash=sha256:25c733bee928530556b1db0ea46c52cf5aa686146e38e60a6fc7cb801ef91cec \ + --hash=sha256:2f90d9a0cfdbee218994fdaaeeb5a0fde62d08f35e4eef0378ec1e2200172fd0 \ + --hash=sha256:34d2b0d934156b87ee114f599a3ba9b8b9e17b5d79652ba3a13fa50903de965e \ + --hash=sha256:36d06341bd501240d320f5942481ed5e6846136b666e1ba4faf802b78ebc875f \ + --hash=sha256:43bc1ed3fa0716295fabee29ba42b2667e4a51d140b0a68e092170a765474fa6 \ + --hash=sha256:44673b27010051ab5a5e438a86ec31bbda61d4a77d7e900af6b7be3037c1abae \ + --hash=sha256:455469a29951edc92bc48b47ac2281c3f2609e6c4f6a047056449f8c2c23facf \ + --hash=sha256:4713502748f564fee0633b37b3403783ce0a3af3a3d148ad91025a5bdadb7bc6 \ + --hash=sha256:5276ef17bdba9ad8040575c74072008b13aae429436e9d0429e718bb5f90f4da \ + --hash=sha256:597a8e843bea00b2eb5520658dc24a9bb032e7fc9e7c2c0c4cd29420220c9796 \ + --hash=sha256:69bbb92865a693d909db3a700d3c01537533844d0948c1e9323561ce06ecda41 \ + --hash=sha256:69f365963f63a1e9bff53bdbb3c542c7c2efed3e163c9d5d83a772a2ac468c21 \ + --hash=sha256:82ca5bf427ad1b216b65ede1a2e2d87dc49bec417ceba0f297213107d3cd9d78 \ + --hash=sha256:889932a97fb4abb6f95fef1914c0d269ebfb60011e67121c1163059b9449dbb4 \ + --hash=sha256:905f877dc66dd7aea4a76e54bcb26acb5ff8216f720c0017ccf63e0e6035698e \ + --hash=sha256:a43ebd7543555c3ac1bc353023d0794e75cb76f6f18f19c32e95441496c0cc25 \ + --hash=sha256:d14bf7982e7a77c0c775634c29c07ce08b38a046df73e1c1f139b3e82f18a38e \ + --hash=sha256:e196fa70c2ff2eb4de7d3df3108f8f358c1d69dff20d45b11f20a5aa227ffb6d \ + --hash=sha256:e1b160831c9cdb06a6c79c2f9cdb11386602938f9af260d1c457a85add4f6f69 \ + --hash=sha256:e6b7beece07525dc6e6b0fc2f104442de2ba328360ad00e50cbe2e1fd620447d \ + --hash=sha256:edb2b4a1a27523e6cc7c417f8d21ce3d005243548b93e56b762b66b0c7f589f9 \ + --hash=sha256:f2f14b7ae1fca99c4a66c981aac3974e7fbc1ca30a12673d223ae1df76680917 \ + --hash=sha256:f338ac7d594c067679f1e99b4f5ec3906842979560f9d8f15d6bdfa39a353b10 \ + --hash=sha256:f3736c9dd3d1856f80cd031715b84ca75cda2bbb1ac802c3da26bfce590838d7 \ + --hash=sha256:f5ed5fe84aee7f39db95c214a7421bf0499fbf500fec6d86a4e29bfc37971438 # via readme-renderer -pkginfo==1.12.1.2 \ - --hash=sha256:5cd957824ac36f140260964eba3c6be6442a8359b8c48f4adf90210f33a04b7b \ - --hash=sha256:c783ac885519cab2c34927ccfa6bf64b5a704d7c69afaea583dd9b7afe969343 +packaging==26.3 \ + --hash=sha256:94edc256424af38762eb31306eed28beb9f0efc50a8837492c9d6fd6004aed79 \ + --hash=sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c # via twine -pycparser==2.23 ; platform_python_implementation != 'PyPy' and sys_platform == 'linux' \ - --hash=sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6 \ - --hash=sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc +pycparser==3.0 ; implementation_name != 'PyPy' and platform_machine != 'ppc64le' and platform_machine != 's390x' and platform_python_implementation != 'PyPy' and sys_platform == 'linux' \ + --hash=sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29 \ + --hash=sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992 # via cffi -pygments==2.19.2 \ - --hash=sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887 \ - --hash=sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b +pygments==2.20.0 \ + --hash=sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f \ + --hash=sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176 # via # readme-renderer # rich -pywin32-ctypes==0.2.3 ; sys_platform == 'win32' \ +pywin32-ctypes==0.2.3 ; platform_machine != 'ppc64le' and platform_machine != 's390x' and sys_platform == 'win32' \ --hash=sha256:8a1513379d709975552d202d942d9837758905c8d01eb82b8bcc30918929e7b8 \ --hash=sha256:d162dc04946d704503b2edc4d55f3dba5c1d539ead017afa00142c38b9885755 # via keyring -readme-renderer==44.0 \ - --hash=sha256:2fbca89b81a08526aadf1357a8c2ae889ec05fb03f5da67f9769c9a592166151 \ - --hash=sha256:8712034eabbfa6805cacf1402b4eeb2a73028f72d1166d6f5cb7f9c047c5d1e1 +readme-renderer==45.0 \ + --hash=sha256:030a8fac74904f8fba11ad1bb6964e3f76e896dc7e5e71f16af190c9056696d1 \ + --hash=sha256:3385ed220117104a2bceb4a9dac8c5fdf6d1f96890d7ea2a9c7174fd5c84091f # via twine -requests==2.33.0 \ - --hash=sha256:3324635456fa185245e24865e810cecec7b4caf933d7eb133dcde67d48cee69b \ - --hash=sha256:c7ebc5e8b0f21837386ad0e1c8fe8b829fa5f544d8df3b2253bff14ef29d7652 +requests==2.34.2 \ + --hash=sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0 \ + --hash=sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed # via # requests-toolbelt # twine @@ -325,25 +372,26 @@ rfc3986==2.0.0 \ --hash=sha256:50b1502b60e289cb37883f3dfd34532b8873c7de9f49bb546641ce9cbd256ebd \ --hash=sha256:97aacf9dbd4bfd829baad6e6309fa6573aaf1be3f6fa735c8ab05e46cecb261c # via twine -rich==14.1.0 \ - --hash=sha256:536f5f1785986d6dbdea3c75205c473f970777b4a0d6c6dd1b696aa05a3fa04f \ - --hash=sha256:e497a48b844b0320d45007cdebfeaeed8db2a4f4bcf49f15e455cfc4af11eaa8 +rich==15.0.0 \ + --hash=sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb \ + --hash=sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36 # via twine -secretstorage==3.3.3 ; sys_platform == 'linux' \ - --hash=sha256:2403533ef369eca6d2ba81718576c5e0f564d5cca1b58f73a8b23e7d4eeebd77 \ - --hash=sha256:f356e6628222568e3af06f2eba8df495efa13b3b63081dafd4f7d9a7b7bc9f99 +secretstorage==3.5.0 ; platform_machine != 'ppc64le' and platform_machine != 's390x' and sys_platform == 'linux' \ + --hash=sha256:0ce65888c0725fcb2c5bc0fdb8e5438eece02c523557ea40ce0703c266248137 \ + --hash=sha256:f04b8e4689cbce351744d5537bf6b1329c6fc68f91fa666f60a380edddcd11be # via keyring -twine==5.1.1 \ - --hash=sha256:215dbe7b4b94c2c50a7315c0275d2258399280fbb7d04182c7e55e24b5f93997 \ - --hash=sha256:9aa0825139c02b3434d913545c7b847a21c835e11597f5255842d457da2322db +twine==7.0.0 \ + --hash=sha256:85cdb29c518efef867360ae4acd4b0dfd61c8654a22fca08e6f8539f05022177 \ + --hash=sha256:b854164df26db268af05f49aa5c0344b10e27a494343ff05b1e0bad3b135f5a7 # via -r tools/publish/requirements.in -urllib3==2.6.3 \ - --hash=sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed \ - --hash=sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4 +urllib3==2.7.0 \ + --hash=sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c \ + --hash=sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897 # via + # id # requests # twine -zipp==3.23.0 \ - --hash=sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e \ - --hash=sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166 +zipp==4.1.0 ; python_full_version < '3.12' and platform_machine != 'ppc64le' and platform_machine != 's390x' \ + --hash=sha256:25ad4e16390cd314347dd8f1de67a2ac538ae658ed4ab9db16029c07c188e97f \ + --hash=sha256:4cb57381f544315db7688e976e922a2b18cdb513d21cc194eb42232ba2a3e602 # via importlib-metadata diff --git a/tools/publish/requirements_windows.txt b/tools/publish/requirements_windows.txt index 029c6bef4d..a1e3faa22f 100644 --- a/tools/publish/requirements_windows.txt +++ b/tools/publish/requirements_windows.txt @@ -6,170 +6,187 @@ backports-tarfile==1.2.0 \ --hash=sha256:77e284d754527b01fb1e6fa8a1afe577858ebe4e9dad8919e34c862cb399bc34 \ --hash=sha256:d75e02c268746e1b8144c278978b6e98e85de6ad16f8e4b0844a154557eca991 # via jaraco-context -certifi==2025.10.5 \ - --hash=sha256:0f212c2744a9bb6de0c56639a6f68afe01ecd92d91f14ae897c4fe7bbeeef0de \ - --hash=sha256:47c09d31ccf2acf0be3f701ea53595ee7e0b8fa08801c6624be771df09ae7b43 +certifi==2026.7.22 \ + --hash=sha256:62f22742b58a1a33014a2b6b706588a8d7e2a88ae7bd1a6ebe8c992928483775 \ + --hash=sha256:741e2c3b351ddf169a738da9f2c048608ff7f2c5cc02f1ebc6b118bb090d5d55 # via requests -charset-normalizer==3.4.3 \ - --hash=sha256:00237675befef519d9af72169d8604a067d92755e84fe76492fef5441db05b91 \ - --hash=sha256:02425242e96bcf29a49711b0ca9f37e451da7c70562bc10e8ed992a5a7a25cc0 \ - --hash=sha256:027b776c26d38b7f15b26a5da1044f376455fb3766df8fc38563b4efbc515154 \ - --hash=sha256:07a0eae9e2787b586e129fdcbe1af6997f8d0e5abaa0bc98c0e20e124d67e601 \ - --hash=sha256:0cacf8f7297b0c4fcb74227692ca46b4a5852f8f4f24b3c766dd94a1075c4884 \ - --hash=sha256:0e78314bdc32fa80696f72fa16dc61168fda4d6a0c014e0380f9d02f0e5d8a07 \ - --hash=sha256:0f2be7e0cf7754b9a30eb01f4295cc3d4358a479843b31f328afd210e2c7598c \ - --hash=sha256:13faeacfe61784e2559e690fc53fa4c5ae97c6fcedb8eb6fb8d0a15b475d2c64 \ - --hash=sha256:14c2a87c65b351109f6abfc424cab3927b3bdece6f706e4d12faaf3d52ee5efe \ - --hash=sha256:1606f4a55c0fd363d754049cdf400175ee96c992b1f8018b993941f221221c5f \ - --hash=sha256:16a8770207946ac75703458e2c743631c79c59c5890c80011d536248f8eaa432 \ - --hash=sha256:18343b2d246dc6761a249ba1fb13f9ee9a2bcd95decc767319506056ea4ad4dc \ - --hash=sha256:18b97b8404387b96cdbd30ad660f6407799126d26a39ca65729162fd810a99aa \ - --hash=sha256:1bb60174149316da1c35fa5233681f7c0f9f514509b8e399ab70fea5f17e45c9 \ - --hash=sha256:1e8ac75d72fa3775e0b7cb7e4629cec13b7514d928d15ef8ea06bca03ef01cae \ - --hash=sha256:1ef99f0456d3d46a50945c98de1774da86f8e992ab5c77865ea8b8195341fc19 \ - --hash=sha256:2001a39612b241dae17b4687898843f254f8748b796a2e16f1051a17078d991d \ - --hash=sha256:23b6b24d74478dc833444cbd927c338349d6ae852ba53a0d02a2de1fce45b96e \ - --hash=sha256:252098c8c7a873e17dd696ed98bbe91dbacd571da4b87df3736768efa7a792e4 \ - --hash=sha256:257f26fed7d7ff59921b78244f3cd93ed2af1800ff048c33f624c87475819dd7 \ - --hash=sha256:2c322db9c8c89009a990ef07c3bcc9f011a3269bc06782f916cd3d9eed7c9312 \ - --hash=sha256:30a96e1e1f865f78b030d65241c1ee850cdf422d869e9028e2fc1d5e4db73b92 \ - --hash=sha256:30d006f98569de3459c2fc1f2acde170b7b2bd265dc1943e87e1a4efe1b67c31 \ - --hash=sha256:31a9a6f775f9bcd865d88ee350f0ffb0e25936a7f930ca98995c05abf1faf21c \ - --hash=sha256:320e8e66157cc4e247d9ddca8e21f427efc7a04bbd0ac8a9faf56583fa543f9f \ - --hash=sha256:34a7f768e3f985abdb42841e20e17b330ad3aaf4bb7e7aeeb73db2e70f077b99 \ - --hash=sha256:3653fad4fe3ed447a596ae8638b437f827234f01a8cd801842e43f3d0a6b281b \ - --hash=sha256:3cd35b7e8aedeb9e34c41385fda4f73ba609e561faedfae0a9e75e44ac558a15 \ - --hash=sha256:3cfb2aad70f2c6debfbcb717f23b7eb55febc0bb23dcffc0f076009da10c6392 \ - --hash=sha256:416175faf02e4b0810f1f38bcb54682878a4af94059a1cd63b8747244420801f \ - --hash=sha256:41d1fc408ff5fdfb910200ec0e74abc40387bccb3252f3f27c0676731df2b2c8 \ - --hash=sha256:42e5088973e56e31e4fa58eb6bd709e42fc03799c11c42929592889a2e54c491 \ - --hash=sha256:4ca4c094de7771a98d7fbd67d9e5dbf1eb73efa4f744a730437d8a3a5cf994f0 \ - --hash=sha256:511729f456829ef86ac41ca78c63a5cb55240ed23b4b737faca0eb1abb1c41bc \ - --hash=sha256:53cd68b185d98dde4ad8990e56a58dea83a4162161b1ea9272e5c9182ce415e0 \ - --hash=sha256:585f3b2a80fbd26b048a0be90c5aae8f06605d3c92615911c3a2b03a8a3b796f \ - --hash=sha256:5b413b0b1bfd94dbf4023ad6945889f374cd24e3f62de58d6bb102c4d9ae534a \ - --hash=sha256:5d8d01eac18c423815ed4f4a2ec3b439d654e55ee4ad610e153cf02faf67ea40 \ - --hash=sha256:6aab0f181c486f973bc7262a97f5aca3ee7e1437011ef0c2ec04b5a11d16c927 \ - --hash=sha256:6cf8fd4c04756b6b60146d98cd8a77d0cdae0e1ca20329da2ac85eed779b6849 \ - --hash=sha256:6fb70de56f1859a3f71261cbe41005f56a7842cc348d3aeb26237560bfa5e0ce \ - --hash=sha256:6fce4b8500244f6fcb71465d4a4930d132ba9ab8e71a7859e6a5d59851068d14 \ - --hash=sha256:70bfc5f2c318afece2f5838ea5e4c3febada0be750fcf4775641052bbba14d05 \ - --hash=sha256:73dc19b562516fc9bcf6e5d6e596df0b4eb98d87e4f79f3ae71840e6ed21361c \ - --hash=sha256:74d77e25adda8581ffc1c720f1c81ca082921329452eba58b16233ab1842141c \ - --hash=sha256:78deba4d8f9590fe4dae384aeff04082510a709957e968753ff3c48399f6f92a \ - --hash=sha256:86df271bf921c2ee3818f0522e9a5b8092ca2ad8b065ece5d7d9d0e9f4849bcc \ - --hash=sha256:88ab34806dea0671532d3f82d82b85e8fc23d7b2dd12fa837978dad9bb392a34 \ - --hash=sha256:8999f965f922ae054125286faf9f11bc6932184b93011d138925a1773830bbe9 \ - --hash=sha256:8dcfc373f888e4fb39a7bc57e93e3b845e7f462dacc008d9749568b1c4ece096 \ - --hash=sha256:939578d9d8fd4299220161fdd76e86c6a251987476f5243e8864a7844476ba14 \ - --hash=sha256:96b2b3d1a83ad55310de8c7b4a2d04d9277d5591f40761274856635acc5fcb30 \ - --hash=sha256:a2d08ac246bb48479170408d6c19f6385fa743e7157d716e144cad849b2dd94b \ - --hash=sha256:b256ee2e749283ef3ddcff51a675ff43798d92d746d1a6e4631bf8c707d22d0b \ - --hash=sha256:b5e3b2d152e74e100a9e9573837aba24aab611d39428ded46f4e4022ea7d1942 \ - --hash=sha256:b89bc04de1d83006373429975f8ef9e7932534b8cc9ca582e4db7d20d91816db \ - --hash=sha256:bd28b817ea8c70215401f657edef3a8aa83c29d447fb0b622c35403780ba11d5 \ - --hash=sha256:c60e092517a73c632ec38e290eba714e9627abe9d301c8c8a12ec32c314a2a4b \ - --hash=sha256:c6dbd0ccdda3a2ba7c2ecd9d77b37f3b5831687d8dc1b6ca5f56a4880cc7b7ce \ - --hash=sha256:c6e490913a46fa054e03699c70019ab869e990270597018cef1d8562132c2669 \ - --hash=sha256:c6f162aabe9a91a309510d74eeb6507fab5fff92337a15acbe77753d88d9dcf0 \ - --hash=sha256:c6fd51128a41297f5409deab284fecbe5305ebd7e5a1f959bee1c054622b7018 \ - --hash=sha256:cc34f233c9e71701040d772aa7490318673aa7164a0efe3172b2981218c26d93 \ - --hash=sha256:cc9370a2da1ac13f0153780040f465839e6cccb4a1e44810124b4e22483c93fe \ - --hash=sha256:ccf600859c183d70eb47e05a44cd80a4ce77394d1ac0f79dbd2dd90a69a3a049 \ - --hash=sha256:ce571ab16d890d23b5c278547ba694193a45011ff86a9162a71307ed9f86759a \ - --hash=sha256:cf1ebb7d78e1ad8ec2a8c4732c7be2e736f6e5123a4146c5b89c9d1f585f8cef \ - --hash=sha256:d0e909868420b7049dafd3a31d45125b31143eec59235311fc4c57ea26a4acd2 \ - --hash=sha256:d22dbedd33326a4a5190dd4fe9e9e693ef12160c77382d9e87919bce54f3d4ca \ - --hash=sha256:d716a916938e03231e86e43782ca7878fb602a125a91e7acb8b5112e2e96ac16 \ - --hash=sha256:d79c198e27580c8e958906f803e63cddb77653731be08851c7df0b1a14a8fc0f \ - --hash=sha256:d95bfb53c211b57198bb91c46dd5a2d8018b3af446583aab40074bf7988401cb \ - --hash=sha256:e28e334d3ff134e88989d90ba04b47d84382a828c061d0d1027b1b12a62b39b1 \ - --hash=sha256:ec557499516fc90fd374bf2e32349a2887a876fbf162c160e3c01b6849eaf557 \ - --hash=sha256:fb6fecfd65564f208cbf0fba07f107fb661bcd1a7c389edbced3f7a493f70e37 \ - --hash=sha256:fb731e5deb0c7ef82d698b0f4c5bb724633ee2a489401594c5c88b02e6cb15f7 \ - --hash=sha256:fb7f67a1bfa6e40b438170ebdc8158b78dc465a5a67b6dde178a46987b244a72 \ - --hash=sha256:fd10de089bcdcd1be95a2f73dbe6254798ec1bda9f450d5828c96f93e2536b9c \ - --hash=sha256:fdabf8315679312cfa71302f9bd509ded4f2f263fb5b765cf1433b39106c3cc9 +charset-normalizer==3.4.9 \ + --hash=sha256:0327fcd59a935777d83410750c50600ee9571af2846f71ce40f25b13da1ef380 \ + --hash=sha256:03d07803992c6c7bbc976327f34b18b6160327fc81cb82c9d504720ac0be3b62 \ + --hash=sha256:04ce310cb89c15df659582aee80a0603788732a5e017d5bd5c81158106ce249c \ + --hash=sha256:0d861473f743244d349b50f850d10eb87aeb22bbdcc8e64f79273c94af5a8226 \ + --hash=sha256:0e94703ec9684807f20cfb5eed95c70f67f2a8f21ad620146d7b5a13677b93e5 \ + --hash=sha256:0fa1aec2d32bcc03c8fa0f6f1712caad1adc38509f31142112e5c9daf5b9c833 \ + --hash=sha256:16b65ea0f2465b6fb52aa22de5eca612aa964ddfec00a912e26f4656cbef890b \ + --hash=sha256:16d10d789dd9bcca1173c95af82c58433122564b7bc39385124be735a35cbe99 \ + --hash=sha256:19ac87f93086ce37b86e098888555c4b4bc48102279bae3350098c0ed664b501 \ + --hash=sha256:1d22856ffbe153a602df38e4a5464f0b748a54002e0d69ac6d2ad0a197cc99ec \ + --hash=sha256:21e764fd1e70b6a3e205a0e46f3051701f98a8cb3fad66eeb80e48bb502f8698 \ + --hash=sha256:231ddcbb35e2ff8973e1365db41fe0572662893b99a05deb183b68ad4c0c8bd4 \ + --hash=sha256:253a4a220747e8b5faf57ec320c4f5efb0cef05f647420bf267143ec15dba10a \ + --hash=sha256:280081916dc341820640489a66e4696049401ef1cf6dd672f672e70ad915aca3 \ + --hash=sha256:2a441ea71902098ffe78c5abe6c494f44160b4af614ed16c3d9a3b1d17fd8ee2 \ + --hash=sha256:304b13570067b2547562e308af560b3963857b1fa90bd6afd978130130fe2d6a \ + --hash=sha256:32286a2c8d167e897177b673176c1e3e00d4057caf5d2b64eef9a3666b03018e \ + --hash=sha256:33bdcc2a32c0a0e861f60841a512c8acc658c87c2ac59d89e3a46dacf7d866e4 \ + --hash=sha256:375b83ed0aecfce76c16d198fbc21f3b11b337d68662bea0a995046682a11419 \ + --hash=sha256:3c09a49d6cde137258beb3d551994a2927fd35ad5cf96aed573f61bbd67c5f84 \ + --hash=sha256:3d92613ec25e43b05f042302531ec0f00b8445190e43325880cbd6ab7c2581da \ + --hash=sha256:40a126142a56b2dfc0aacbad1de8310cbf60da7656db0e6b16eebd48e3e93519 \ + --hash=sha256:416c229f77e5ea25b3dfd4b582f8d73d7e43c22320302b9ab128a2d3a0b38efe \ + --hash=sha256:432786d3561e69aeeae6c7e8648964ce0ad05736120135601f87ac26b9c83381 \ + --hash=sha256:43b9e366a31fdd1c87d0eb08f579b4a82b723ea54338f040d6b4e518a026ea29 \ + --hash=sha256:440eede837960000d74978f0eba527be106b5b9aee0daf779d395276ed0b0614 \ + --hash=sha256:45b0cc4e3556cd875e09102988d1ab8356c998b596c9fced84547c8138b487a0 \ + --hash=sha256:476743fe6dfe14a2da12e3ac79125dc84a3b2cf8094369a47a1529b0cd8549fe \ + --hash=sha256:4773092f8019072343a7447203308b176e10199920eb02d6195e81bbb3274c29 \ + --hash=sha256:4b3dac63058cc36820b0dd072f89898604e2d39686fe05321729d00d8ac185a0 \ + --hash=sha256:4d1c96a7a18b9690a4d46df09e3e3382406ae3213727cd1019ebade1c4a81917 \ + --hash=sha256:51307f5c71007673a2bf8232ad973483d281e74cb99c8c5a990af1eefa6277d9 \ + --hash=sha256:51447e9aa2684679af07ca5021c3db526e0284347ebf4ffcec1154c3350cfe32 \ + --hash=sha256:58150c9f9b9a552505912d182ccdf26f6396fb6094816ceebcbb20eecabaed94 \ + --hash=sha256:5b10cd92fc5c498b35a8635df6d5a100207f88b63a4dc1de7ef9a548e1e2cd63 \ + --hash=sha256:5e226f6218febc71f6c1fc2fafb91c226f75bdc1d8fb12d66823716e891608fd \ + --hash=sha256:609b3ba8fcc0fb5ab7af00719d0fb6ad0cb518e48e7712d12fd68f1327951198 \ + --hash=sha256:60f44ade2cf573dad7a277e6f8ca9a51a21dda572b13bd7d8539bb3cd5dbedde \ + --hash=sha256:611057cc5d5c0afc743ba8be6bd828c17e0aaa8643f9d0a9b9bb7dea80eb8012 \ + --hash=sha256:6366a16e1a25018694d6a5d784d09b046edc9eac40ea2b54065c3052672516a1 \ + --hash=sha256:65a7ff3f705e57d392f7261b6d0550fe137c3019477431f1c355e0db0a7d3e15 \ + --hash=sha256:673611bbd43f0810bec0b0f028ddeaaa501190339cac411f347ac76917c3ae7b \ + --hash=sha256:67830fc78e67501f47bb950471b2dcb9b35b140084429318e862895a8e89c993 \ + --hash=sha256:68ce9f4d6b26d5ccbf7fd4459bf75f74a0a146677ebba80597df60cbdb20e6f4 \ + --hash=sha256:68e5f26a1ad57ded6d1cfb85331d1c1a195314756471d97758c48498bb4dcdf5 \ + --hash=sha256:69b157c5d3292bcd443faca052f3096f637f1e074b98212a933c074ae23dc3b8 \ + --hash=sha256:75286256590a6320cf106a0d28970d3560aad9ee09aa7b34fb40524792436d35 \ + --hash=sha256:78841cccf1af7b40f6f716338d50c0902dbe88d9f800b3c973b7a9a0a693a642 \ + --hash=sha256:78fa18e436a1a0e58dbd7e02fc4473f3f32cceb12df9dfca542d075961c307d2 \ + --hash=sha256:79580094b00d1789d1f93ea55bc43cb2f611910c72235b7657f3482ddcc1b22d \ + --hash=sha256:7b86a2b16095d250c6f58b3d9b2eee6f4147754344f3dab0922f7c9bf7d226c9 \ + --hash=sha256:83aed2c10721ddd90f68140685391b50811a880af20654c59af6b6c66c40513c \ + --hash=sha256:84fd18bcc17526fc2b3c1af7d2b9217d32c9c04448c16ec693b9b4f1985c3d33 \ + --hash=sha256:871ff67ea1aad4dfd91736464934d56b32dac49f9fbe16cddba36198a7b3a0db \ + --hash=sha256:898f0e9068ca27d37f8e83a5b962821df851532e6c4a7d615c1c033f9da6eedf \ + --hash=sha256:8a79d9f4d8001473a30c163556b3c3bfebec837495a412dde78b51672f6134f9 \ + --hash=sha256:8c041122946b7ba21bb32c45b1aa57b1be35527690aeb3c5c234521085632eee \ + --hash=sha256:90c44bc373b7687f6948b693cceaea1348ae0975d7474746559494468e3c1d84 \ + --hash=sha256:9104ed0bd76a429d46f9ec0dbc9b08ad1d2dcdf2b00a5a0daa1c145329b35b44 \ + --hash=sha256:920079c3f7456fa213e0829ed2073aaa727fd39d889ead5b4f35d0de5460d04f \ + --hash=sha256:93d59d504b230e83c7a843251681959a0b6a9cd76f6e146ce1b8a80eb8739af9 \ + --hash=sha256:9b2aff1c7b3884512b9512c3eaadd9bab39fb45042ffaaa1dd08ff2b9f8109d9 \ + --hash=sha256:9b8e0f3107e2200b76f6054de99016eac3ee6762713587b36baaa7e4bd2ae177 \ + --hash=sha256:9bb41182d93ea91f60b4bc8fbf4c820c69ef8a12ab2d917f3f1834f1acad07e8 \ + --hash=sha256:9cdef90ae47919cae358d8ab15797a800ed41da7aba5d72419fb510729e2ed4b \ + --hash=sha256:a1786910334ed46ab1dd73222f2cd1e05c2c3bb39f6dddb4f8b36fc382058a39 \ + --hash=sha256:a4cfde78a9f2880208d16a93b795726a3017d5977e08d1e162a7a31322479c41 \ + --hash=sha256:a4fbdde9dd4a9ce5fd52c2b3a347bb50cc89483ef783f1cb00d408c13f7a96c0 \ + --hash=sha256:aa99adc8f081b475a12843953db36831eaf83ec33eb46a90629ca6a5de45a616 \ + --hash=sha256:ac351b3b8014eead140e77e9717e2992c6bbe30b63bc3422422eb84865412e3d \ + --hash=sha256:ad41ba96094304aa090f5a30cb6e4fb3b3f1c264c523394b4c39bbacc4dc92ba \ + --hash=sha256:b5314963fce9b0b12743891de876e724997864ee22aa496f903f426c7e2fa5b2 \ + --hash=sha256:bcf74c1df76758a395bf0af608c04c82257523f55c9868b334f06270d0f2112b \ + --hash=sha256:bd47ba7fc3ca94896759ea0109775132d3e7ab921fbf54038e1bab2e46c313c9 \ + --hash=sha256:c0323c9daef75ef2e5083624b4585018a0c9d5e3b40f607eed81a311270b934b \ + --hash=sha256:c1225416b463483160e4af85d5fc3a9690ccb53fd4b1865a6437825f5ede3209 \ + --hash=sha256:c1c948747b03be832dceed96ca815cef7360de9aa19d37c730f8e3f6101aca48 \ + --hash=sha256:c25fe15c70c59eb7c5ce8c06a1f3fa1da0ecc5ea1e7a5922c40fd2fa9b0d5046 \ + --hash=sha256:cc1b0fff8ead343dae06305f954eb8468ba0ec1a97881f42489d198e4ce3c632 \ + --hash=sha256:cd6280cf040f233bd7d3407b743b4b4c74f70e8e1c4199cb112a62c941c0772a \ + --hash=sha256:cd6c3d4b783c556fa00bf540854e42f135e2f256abd29669fcd0da0f2dec79c2 \ + --hash=sha256:d4d6fcde76f94f5cb9e43e9e9a61f16dacefd228cbbf6f1a09bd9b219a92f1a1 \ + --hash=sha256:ddf4af30b417d9fe16481e9b81c27ab2a7cde1ff7ba3e85653b02db7d145dc7b \ + --hash=sha256:df115d4d83168fdf2cae48ef1ff6d1cb4c466364e30861b37121de0f3bf1b990 \ + --hash=sha256:df7276909358e5635ae203673ab7e509ddd224225a8d6b0790bf13eb2bde1cc5 \ + --hash=sha256:e4fd89cc178bced6ad29cb3e6dd4aa63fa5017c3524dbd0b25998fb64a87cc8b \ + --hash=sha256:e9701d0049d92c16703a42771b98d560b95248949f23f8cf7b4eddd201814fb9 \ + --hash=sha256:ee2f2a527e3c1a6e6411eb4209642e138b544a2d72fe5d0d76daf77b24063534 \ + --hash=sha256:f7fb7d750cfa0a070d2c24e831fd3481019a60dd317ea2b39acbcebc08b6ed81 \ + --hash=sha256:f840ed6d8ecba8255df8c42b87fadeda98ddfc6eeec05e2dc66e26d46dd6f58a \ + --hash=sha256:f86c6358749bd4fda175388691e3ba8c46e24c5347d0afd20f9b7edfc9faf07d \ + --hash=sha256:fa36ec09ef71d158186bc79e359ff5fdd6e7996fe8ab638f00d6b93139ba4fcf \ + --hash=sha256:fe2c7201c642b7c308f1675355ad7ff7b66acfe3541625efe5a3ad38f29d6115 # via requests -docutils==0.22.2 \ - --hash=sha256:9fdb771707c8784c8f2728b67cb2c691305933d68137ef95a75db5f4dfbc213d \ - --hash=sha256:b0e98d679283fc3bb0ead8a5da7f501baa632654e7056e9c5846842213d674d8 +docutils==0.23 \ + --hash=sha256:25d013af9bf23bc1c7b2b093dff4208166c53a94786c9e447808335ef1185fea \ + --hash=sha256:746f5060322511280a1e50eb76846ed6bf2342984b2ac04dc42caa1a8d78799e # via readme-renderer -idna==3.10 \ - --hash=sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9 \ - --hash=sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3 +id==1.6.1 \ + --hash=sha256:d0732d624fb46fd4e7bc4e5152f00214450953b9e772c182c1c22964def1a069 \ + --hash=sha256:f5ec41ed2629a508f5d0988eda142e190c9c6da971100612c4de9ad9f9b237ca + # via twine +idna==3.18 \ + --hash=sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2 \ + --hash=sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848 # via requests -importlib-metadata==8.7.0 \ - --hash=sha256:d13b81ad223b890aa16c5471f2ac3056cf76c5f10f82d6f9292f0b415f389000 \ - --hash=sha256:e5dd1551894c77868a30651cef00984d50e1002d06942a7101d34870c5f02afd - # via - # keyring - # twine +importlib-metadata==9.0.0 \ + --hash=sha256:2d21d1cc5a017bd0559e36150c21c830ab1dc304dedd1b7ea85d20f45ef3edd7 \ + --hash=sha256:a4f57ab599e6a2e3016d7595cfd72eb4661a5106e787a95bcc90c7105b831efc + # via keyring jaraco-classes==3.4.0 \ --hash=sha256:47a024b51d0239c0dd8c8540c6c7f484be3b8fcf0b2d85c13825780d3b3f3acd \ --hash=sha256:f662826b6bed8cace05e7ff873ce0f9283b5c924470fe664fff1c2f00f581790 # via keyring -jaraco-context==6.0.1 \ - --hash=sha256:9bae4ea555cf0b14938dc0aee7c9f32ed303aa20a3b73e7dc80111628792d1b3 \ - --hash=sha256:f797fc481b490edb305122c9181830a3a5b76d84ef6d1aef2fb9b47ab956f9e4 +jaraco-context==6.1.2 \ + --hash=sha256:bf8150b79a2d5d91ae48629d8b427a8f7ba0e1097dd6202a9059f29a36379535 \ + --hash=sha256:f1a6c9d391e661cc5b8d39861ff077a7dc24dc23833ccee564b234b81c82dfe3 # via keyring -jaraco-functools==4.3.0 \ - --hash=sha256:227ff8ed6f7b8f62c56deff101545fa7543cf2c8e7b82a7c2116e672f29c26e8 \ - --hash=sha256:cfd13ad0dd2c47a3600b439ef72d8615d482cedcff1632930d6f28924d92f294 +jaraco-functools==4.6.0 \ + --hash=sha256:880c577ec9720b3a052d5bc611fb9f2269b3d87902ef42440df443b88e443280 \ + --hash=sha256:99e3dc0060c5cbe8fcd1cdb36258e2a65ca40f1566b2033b12abb1bb44dd3c30 # via keyring -keyring==25.6.0 \ - --hash=sha256:0b39998aa941431eb3d9b0d4b2460bc773b9df6fed7621c2dfb291a7e0187a66 \ - --hash=sha256:552a3f7af126ece7ed5c89753650eec89c7eaae8617d0aa4d9ad2b75111266bd +keyring==25.7.0 \ + --hash=sha256:be4a0b195f149690c166e850609a477c532ddbfbaed96a404d4e43f8d5e2689f \ + --hash=sha256:fe01bd85eb3f8fb3dd0405defdeac9a5b4f6f0439edbb3149577f244a2e8245b # via twine -markdown-it-py==4.0.0 \ - --hash=sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147 \ - --hash=sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3 +markdown-it-py==4.2.0 \ + --hash=sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49 \ + --hash=sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a # via rich mdurl==0.1.2 \ --hash=sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8 \ --hash=sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba # via markdown-it-py -more-itertools==10.8.0 \ - --hash=sha256:52d4362373dcf7c52546bc4af9a86ee7c4579df9a8dc268be0a2f949d376cc9b \ - --hash=sha256:f638ddf8a1a0d134181275fb5d58b086ead7c6a72429ad725c67503f13ba30bd +more-itertools==11.1.0 \ + --hash=sha256:48e8f4d9e7e5878571ecf6f2b4e57634f93cd474cc8cfbd2376f2d11b396e30d \ + --hash=sha256:4b65538ae22f6fed0ce4874efd317463a7489796a0939fa66824dd542125a192 # via # jaraco-classes # jaraco-functools -nh3==0.3.0 \ - --hash=sha256:0649464ac8eee018644aacbc103874ccbfac80e3035643c3acaab4287e36e7f5 \ - --hash=sha256:16f8670201f7e8e0e05ed1a590eb84bfa51b01a69dd5caf1d3ea57733de6a52f \ - --hash=sha256:1adeb1062a1c2974bc75b8d1ecb014c5fd4daf2df646bbe2831f7c23659793f9 \ - --hash=sha256:37d3003d98dedca6cd762bf88f2e70b67f05100f6b949ffe540e189cc06887f9 \ - --hash=sha256:389d93d59b8214d51c400fb5b07866c2a4f79e4e14b071ad66c92184fec3a392 \ - --hash=sha256:3f1b4f8a264a0c86ea01da0d0c390fe295ea0bcacc52c2103aca286f6884f518 \ - --hash=sha256:423201bbdf3164a9e09aa01e540adbb94c9962cc177d5b1cbb385f5e1e79216e \ - --hash=sha256:634e34e6162e0408e14fb61d5e69dbaea32f59e847cfcfa41b66100a6b796f62 \ - --hash=sha256:6d68fa277b4a3cf04e5c4b84dd0c6149ff7d56c12b3e3fab304c525b850f613d \ - --hash=sha256:7275fdffaab10cc5801bf026e3c089d8de40a997afc9e41b981f7ac48c5aa7d5 \ - --hash=sha256:7852f038a054e0096dac12b8141191e02e93e0b4608c4b993ec7d4ffafea4e49 \ - --hash=sha256:7c915060a2c8131bef6a29f78debc29ba40859b6dbe2362ef9e5fd44f11487c2 \ - --hash=sha256:80fe20171c6da69c7978ecba33b638e951b85fb92059259edd285ff108b82a6d \ - --hash=sha256:a537ece1bf513e5a88d8cff8a872e12fe8d0f42ef71dd15a5e7520fecd191bbb \ - --hash=sha256:af5aa8127f62bbf03d68f67a956627b1bd0469703a35b3dad28d0c1195e6c7fb \ - --hash=sha256:b0612ccf5de8a480cf08f047b08f9d3fecc12e63d2ee91769cb19d7290614c23 \ - --hash=sha256:ba0caa8aa184196daa6e574d997a33867d6d10234018012d35f86d46024a2a95 \ - --hash=sha256:bae63772408fd63ad836ec569a7c8f444dd32863d0c67f6e0b25ebbd606afa95 \ - --hash=sha256:c7a32a7f0d89f7d30cb8f4a84bdbd56d1eb88b78a2434534f62c71dac538c450 \ - --hash=sha256:ce5e7185599f89b0e391e2f29cc12dc2e206167380cea49b33beda4891be2fe1 \ - --hash=sha256:d8ba24cb31525492ea71b6aac11a4adac91d828aadeff7c4586541bf5dc34d2f \ - --hash=sha256:d97d3efd61404af7e5721a0e74d81cdbfc6e5f97e11e731bb6d090e30a7b62b2 \ - --hash=sha256:e90883f9f85288f423c77b3f5a6f4486375636f25f793165112679a7b6363b35 \ - --hash=sha256:e9e6a7e4d38f7e8dda9edd1433af5170c597336c1a74b4693c5cb75ab2b30f2a \ - --hash=sha256:ec6cfdd2e0399cb79ba4dcffb2332b94d9696c52272ff9d48a630c5dca5e325a \ - --hash=sha256:f416c35efee3e6a6c9ab7716d9e57aa0a49981be915963a82697952cba1353e1 +nh3==0.3.6 \ + --hash=sha256:082675ff87b9385ec430ffe6d5847ba7456cc39b73720cd4add472f9f4cffd56 \ + --hash=sha256:2411e8c3cee81a1ddd62c2a5d50585c28aa5566d373ad1db92536b95ddb24ef2 \ + --hash=sha256:25c733bee928530556b1db0ea46c52cf5aa686146e38e60a6fc7cb801ef91cec \ + --hash=sha256:2f90d9a0cfdbee218994fdaaeeb5a0fde62d08f35e4eef0378ec1e2200172fd0 \ + --hash=sha256:34d2b0d934156b87ee114f599a3ba9b8b9e17b5d79652ba3a13fa50903de965e \ + --hash=sha256:36d06341bd501240d320f5942481ed5e6846136b666e1ba4faf802b78ebc875f \ + --hash=sha256:43bc1ed3fa0716295fabee29ba42b2667e4a51d140b0a68e092170a765474fa6 \ + --hash=sha256:44673b27010051ab5a5e438a86ec31bbda61d4a77d7e900af6b7be3037c1abae \ + --hash=sha256:455469a29951edc92bc48b47ac2281c3f2609e6c4f6a047056449f8c2c23facf \ + --hash=sha256:4713502748f564fee0633b37b3403783ce0a3af3a3d148ad91025a5bdadb7bc6 \ + --hash=sha256:5276ef17bdba9ad8040575c74072008b13aae429436e9d0429e718bb5f90f4da \ + --hash=sha256:597a8e843bea00b2eb5520658dc24a9bb032e7fc9e7c2c0c4cd29420220c9796 \ + --hash=sha256:69bbb92865a693d909db3a700d3c01537533844d0948c1e9323561ce06ecda41 \ + --hash=sha256:69f365963f63a1e9bff53bdbb3c542c7c2efed3e163c9d5d83a772a2ac468c21 \ + --hash=sha256:82ca5bf427ad1b216b65ede1a2e2d87dc49bec417ceba0f297213107d3cd9d78 \ + --hash=sha256:889932a97fb4abb6f95fef1914c0d269ebfb60011e67121c1163059b9449dbb4 \ + --hash=sha256:905f877dc66dd7aea4a76e54bcb26acb5ff8216f720c0017ccf63e0e6035698e \ + --hash=sha256:a43ebd7543555c3ac1bc353023d0794e75cb76f6f18f19c32e95441496c0cc25 \ + --hash=sha256:d14bf7982e7a77c0c775634c29c07ce08b38a046df73e1c1f139b3e82f18a38e \ + --hash=sha256:e196fa70c2ff2eb4de7d3df3108f8f358c1d69dff20d45b11f20a5aa227ffb6d \ + --hash=sha256:e1b160831c9cdb06a6c79c2f9cdb11386602938f9af260d1c457a85add4f6f69 \ + --hash=sha256:e6b7beece07525dc6e6b0fc2f104442de2ba328360ad00e50cbe2e1fd620447d \ + --hash=sha256:edb2b4a1a27523e6cc7c417f8d21ce3d005243548b93e56b762b66b0c7f589f9 \ + --hash=sha256:f2f14b7ae1fca99c4a66c981aac3974e7fbc1ca30a12673d223ae1df76680917 \ + --hash=sha256:f338ac7d594c067679f1e99b4f5ec3906842979560f9d8f15d6bdfa39a353b10 \ + --hash=sha256:f3736c9dd3d1856f80cd031715b84ca75cda2bbb1ac802c3da26bfce590838d7 \ + --hash=sha256:f5ed5fe84aee7f39db95c214a7421bf0499fbf500fec6d86a4e29bfc37971438 # via readme-renderer -pkginfo==1.12.1.2 \ - --hash=sha256:5cd957824ac36f140260964eba3c6be6442a8359b8c48f4adf90210f33a04b7b \ - --hash=sha256:c783ac885519cab2c34927ccfa6bf64b5a704d7c69afaea583dd9b7afe969343 +packaging==26.3 \ + --hash=sha256:94edc256424af38762eb31306eed28beb9f0efc50a8837492c9d6fd6004aed79 \ + --hash=sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c # via twine -pygments==2.19.2 \ - --hash=sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887 \ - --hash=sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b +pygments==2.20.0 \ + --hash=sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f \ + --hash=sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176 # via # readme-renderer # rich @@ -177,13 +194,13 @@ pywin32-ctypes==0.2.3 \ --hash=sha256:8a1513379d709975552d202d942d9837758905c8d01eb82b8bcc30918929e7b8 \ --hash=sha256:d162dc04946d704503b2edc4d55f3dba5c1d539ead017afa00142c38b9885755 # via keyring -readme-renderer==44.0 \ - --hash=sha256:2fbca89b81a08526aadf1357a8c2ae889ec05fb03f5da67f9769c9a592166151 \ - --hash=sha256:8712034eabbfa6805cacf1402b4eeb2a73028f72d1166d6f5cb7f9c047c5d1e1 +readme-renderer==45.0 \ + --hash=sha256:030a8fac74904f8fba11ad1bb6964e3f76e896dc7e5e71f16af190c9056696d1 \ + --hash=sha256:3385ed220117104a2bceb4a9dac8c5fdf6d1f96890d7ea2a9c7174fd5c84091f # via twine -requests==2.33.0 \ - --hash=sha256:3324635456fa185245e24865e810cecec7b4caf933d7eb133dcde67d48cee69b \ - --hash=sha256:c7ebc5e8b0f21837386ad0e1c8fe8b829fa5f544d8df3b2253bff14ef29d7652 +requests==2.34.2 \ + --hash=sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0 \ + --hash=sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed # via # requests-toolbelt # twine @@ -195,21 +212,22 @@ rfc3986==2.0.0 \ --hash=sha256:50b1502b60e289cb37883f3dfd34532b8873c7de9f49bb546641ce9cbd256ebd \ --hash=sha256:97aacf9dbd4bfd829baad6e6309fa6573aaf1be3f6fa735c8ab05e46cecb261c # via twine -rich==14.1.0 \ - --hash=sha256:536f5f1785986d6dbdea3c75205c473f970777b4a0d6c6dd1b696aa05a3fa04f \ - --hash=sha256:e497a48b844b0320d45007cdebfeaeed8db2a4f4bcf49f15e455cfc4af11eaa8 +rich==15.0.0 \ + --hash=sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb \ + --hash=sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36 # via twine -twine==5.1.1 \ - --hash=sha256:215dbe7b4b94c2c50a7315c0275d2258399280fbb7d04182c7e55e24b5f93997 \ - --hash=sha256:9aa0825139c02b3434d913545c7b847a21c835e11597f5255842d457da2322db +twine==7.0.0 \ + --hash=sha256:85cdb29c518efef867360ae4acd4b0dfd61c8654a22fca08e6f8539f05022177 \ + --hash=sha256:b854164df26db268af05f49aa5c0344b10e27a494343ff05b1e0bad3b135f5a7 # via -r tools/publish/requirements.in -urllib3==2.6.3 \ - --hash=sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed \ - --hash=sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4 +urllib3==2.7.0 \ + --hash=sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c \ + --hash=sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897 # via + # id # requests # twine -zipp==3.23.0 \ - --hash=sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e \ - --hash=sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166 +zipp==4.1.0 \ + --hash=sha256:25ad4e16390cd314347dd8f1de67a2ac538ae658ed4ab9db16029c07c188e97f \ + --hash=sha256:4cb57381f544315db7688e976e922a2b18cdb513d21cc194eb42232ba2a3e602 # via importlib-metadata 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. """ diff --git a/workspace_bazel9.bzl b/workspace_bazel9.bzl new file mode 100644 index 0000000000..dad59378a9 --- /dev/null +++ b/workspace_bazel9.bzl @@ -0,0 +1,10 @@ +"""Workaround for Bazel 9 duplicate name issue in Gazelle.""" + +def bazel_9_workaround(name = "bazel_9_workaround"): + # Necessary so that Bazel 9 recognizes this as rules_python and doesn't try + # to load the version Bazel itself uses by default. + # We hide this from Gazelle's WORKSPACE parser by putting it in a macro. + native.local_repository( + name = "rules_python", + path = ".", + )