') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); })(); feat(pypi): first check index contents before downloading metadata about distributions by aignas · Pull Request #3657 · bazel-contrib/rules_python · GitHub
Skip to content

feat(pypi): first check index contents before downloading metadata about distributions - #3657

Merged
aignas merged 15 commits into
bazel-contrib:mainfrom
aignas:aignas.feat.check_first
Apr 2, 2026
Merged

feat(pypi): first check index contents before downloading metadata about distributions#3657
aignas merged 15 commits into
bazel-contrib:mainfrom
aignas:aignas.feat.check_first

Conversation

@aignas

@aignasaignas commented Mar 10, 2026

Copy link
Copy Markdown
Collaborator

The overall changes to the architecture are:

  • First check which packages are on which index.
  • Then write these details as facts for future reuse.
  • Then use the index_url_overrides to ensure that we are pulling things from the right index for the packages.
  • Then download everything.

Notes on implementation:

  • This will pull index contents at most once for each index.
  • This will make the initial download times longer, but because we have MODULE.bazel.lock file and the facts written there, it should be OK.
  • This allows us to just parse the index_url and extra_index_urls from the lock files and use that without printing any warning messages. If we don't see any regressions in testing, I think this code path could be enabled for everyone by default. So experimental_index_url will no longer be experimental. I think this might have been the last thing holding us from flipping the switch.

Fixes#2632
Fixes#3260

@aignasaignas added help wanted do not merge Tag that prevents merging labels Mar 10, 2026
@gemini-code-assist

Copy link
Copy Markdown
Contributor

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This experimental pull request introduces a significant change to how Python packages are resolved and downloaded from PyPI. Instead of directly attempting downloads, the system now first queries the PyPI index contents to identify available packages and their precise download URLs. This approach aims to improve reliability, enable cross-compilation by fetching platform-specific wheels, and lay the groundwork for more sophisticated package resolution, especially in multi-index or private index scenarios. The changes also refine the handling of "yanked" packages by allowing a more descriptive status.

Highlights

  • Enhanced Yanked Package Handling: The yanked field in package metadata now supports None (not yanked) or a string (yank reason), providing more granular status than a simple boolean.
  • Optimized HTML Parsing: The parse_simpleapi_html function has been significantly refactored for improved performance and now supports parsing the main PyPI index page to discover package URLs.
  • Pre-download Index Content Check: A new mechanism (_get_index_overrides) was introduced to query PyPI indexes for available packages and their specific URLs before attempting to download, allowing for more intelligent and targeted downloads.
  • Refined Download Failure Handling: The allow_fail=True parameter was removed from ctx.download calls, indicating that download failures are now expected to be handled by the pre-check logic rather than being silently allowed.

🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Changelog
  • python/private/pypi/parse_requirements.bzl
    • Updated the yanked field's default value from False to None in _package_srcs and _add_dists functions.
    • Adjusted logic to check for yanked != None instead of yanked or not yanked to correctly interpret the new None state.
  • python/private/pypi/parse_simpleapi_html.bzl
    • Modified parse_simpleapi_html to accept a parse_index boolean parameter, enabling it to parse the main PyPI index page for package URLs.
    • Refactored HTML parsing logic for improved efficiency, replacing content.split with content.find for iteration.
    • Introduced _parse_attrs helper function to efficiently extract HTML attributes.
    • Added _unescape_pypi_html utility to decode HTML entities in attribute values, particularly for data-yanked reasons.
    • Updated the yanked field in returned structs to be str | None instead of bool.
  • python/private/pypi/simpleapi_download.bzl
    • Added a new _get_index_overrides function to pre-fetch package availability and URLs from all configured indexes.
    • Modified the simpleapi_download function to utilize _get_index_overrides to determine the correct index URL for each package.
    • Removed allow_fail = True from ctx.download calls, relying on the pre-check logic to prevent unnecessary download attempts.
    • Updated the docstring to reflect the new pre-check and targeted download strategy.
    • Propagated the parse_index parameter to _read_simpleapi and _read_index_result.
  • tests/pypi/hub_builder/hub_builder_tests.bzl
    • Updated expected yanked values in test structs from False to None to align with the new yanked field type.
  • tests/pypi/parse_requirements/parse_requirements_tests.bzl
    • Updated expected yanked values in test structs from False to None to align with the new yanked field type.
  • tests/pypi/parse_simpleapi_html/parse_simpleapi_html_tests.bzl
    • Added new test cases to verify correct parsing of data-yanked attributes, including empty reasons and reasons with HTML entities.
    • Updated assertions to expect yanked as str | None instead of bool.
Activity
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

FeatureCommandDescription
Code Review/gemini reviewPerforms a code review for the current pull request in its current state.
Pull Request Summary/gemini summaryProvides a summary of the current pull request in its current state.
Comment@gemini-code-assistResponds in comments when explicitly tagged, both in pull request comments and review comments.
Help/gemini helpDisplays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@gemini-code-assistgemini-code-assistBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request significantly improves the handling of multiple PyPI indexes by introducing a more efficient method for querying index roots and refactoring the HTML parser to support modern features like yanked packages. A security audit found no medium, high, or critical vulnerabilities, confirming adherence to Bazel's security model and absence of injection or data leakage issues. However, a critical error handling placeholder (fail("TODO")) for unavailable indexes still needs to be addressed.

Comment threadpython/private/pypi/simpleapi_download.bzl Outdated
Comment threadpython/private/pypi/simpleapi_download.bzl Outdated
@aignas
aignasforce-pushed the aignas.feat.check_first branch from 60b4cd6 to 9e940ccCompareMarch 20, 2026 01:39
wip
@aignas
aignasforce-pushed the aignas.feat.check_first branch from 9e940cc to d4cea67CompareMarch 20, 2026 01:40
@aignasaignas changed the title exp: use allow_fail True for downloads after checking the index contentsfeat(pypi): first check index contents before downloading metadata about distributionsMar 20, 2026
Comment threadpython/private/pypi/pypi_cache.bzl Outdated
Comment threadpython/private/pypi/parse_simpleapi_html.bzl
Comment threadpython/private/pypi/simpleapi_download.bzl Outdated
@aignasaignas removed the do not merge Tag that prevents merging label Mar 21, 2026
@aignas
aignas marked this pull request as ready for review March 21, 2026 16:12
@aignas

Copy link
Copy Markdown
CollaboratorAuthor

OK, done - PTAL.

@aignas

Copy link
Copy Markdown
CollaboratorAuthor

OK, PTAL, I am changing the behaviour in a way that will make it easier to default index_url to pypi.org and parse/override params from the lock files as is asked in #2951. In the next PR I plan to just flip the switch by: defaulting the index-url internally to pypi.org and then parsing the requirements files to get the values. I think it is going to be a good thing for 2.0 and if it is breaking, it should be minimally so. I think we should also rename the experimental_index_url_overrides to index_url_overrides.

Given that there are usages of experimental_index_url in the bazel-central-registry, I think it is best to just make the parameter a noop. All of the values are actually using pypi.org/simple.

@aignas
aignas added this pull request to the merge queueApr 2, 2026
Merged via the queue into bazel-contrib:main with commit 900d557Apr 2, 2026
4 checks passed
@aignas
aignas deleted the aignas.feat.check_first branch April 2, 2026 07:35
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

SimpleAPI download failures are cached and require bazel clean pypi downloads blocked by experimental_downloader_config fail silently

2 participants

@aignas@rickeylev