Skip to content

List pip updates without giving Python network access (#5401) - #5405

Merged
Gabriel Dufresne (GabrielDuf) merged 3 commits into
mainfrom
fix/pip-update-check-without-python
Sep 18, 2026
Merged

Gabriel Dufresne (GabrielDuf) merged 3 commits into
mainfrom
fix/pip-update-check-without-python

Conversation

@GabrielDuf

Copy link
Copy Markdown
Contributor

The hourly update check ran python.exe -m pip list --outdated, so every check made python.exe open a connection to PyPI. On a firewall that approves traffic per executable, that means either blanket-approving the interpreter that runs arbitrary code, blocking pip entirely, or answering a prompt every hour.

Routing through Scripts\pip.exe does not help: it is a distlib shebang launcher that CreateProcessW's the interpreter, so python.exe still owns the socket.

Instead, resolve available updates in-process. pip list supplies the installed set without touching the index, and the newest compatible version comes from PyPI's PEP 691 simple API over UniGetUI's own HttpClient. Python now reaches the network only for what the user asked for: an install, an update, or a version listing.

Resolution filters each file's requires-python against the running interpreter and skips yanked files and pre-releases. Without that filter the check would report releases pip then refuses to install, which is the phantom-update class of bug seen in #5326. Wheel platform tags are still not considered, so a project publishing no sdist and no wheel for the platform can be over-reported.

The pip CLI stays in charge when its answer cannot be matched: a configured custom index (PIP_INDEX_URL, PIP_EXTRA_INDEX_URL or pip config), an interpreter version that cannot be determined, or DisablePipHttpUpdateCheck.

Lookups are bounded by a 45 s budget inside the 60 s listing timeout, cached by ETag so later checks mostly return 304, and fall back to the last known version on a transient failure. A check that loses more than a quarter of its lookups fails loudly rather than reporting a truncated update list.

PIP_DISABLE_PIP_VERSION_CHECK joins PIP_REQUIRE_VIRTUALENV. It removes no background traffic, since pip only self-checks on commands that already open a session, but it keeps the [notice] lines out of parsed output.

Verified against a real environment: both paths return the same 14 updates, and a proxy interposed on the manager's child processes records 42 connections from python.exe on the CLI path and none on the new one.

The hourly update check ran `python.exe -m pip list --outdated`, so every check
made python.exe open a connection to PyPI. On a firewall that approves traffic
per executable, that means either blanket-approving the interpreter that runs
arbitrary code, blocking pip entirely, or answering a prompt every hour.

Routing through Scripts\pip.exe does not help: it is a distlib shebang launcher
that CreateProcessW's the interpreter, so python.exe still owns the socket.

Instead, resolve available updates in-process. `pip list` supplies the installed
set without touching the index, and the newest compatible version comes from
PyPI's PEP 691 simple API over UniGetUI's own HttpClient. Python now reaches the
network only for what the user asked for: an install, an update, or a version
listing.

Resolution filters each file's requires-python against the running interpreter
and skips yanked files and pre-releases. Without that filter the check would
report releases pip then refuses to install, which is the phantom-update class of
bug seen in #5326. Wheel platform tags are still not considered, so a project
publishing no sdist and no wheel for the platform can be over-reported.

The pip CLI stays in charge when its answer cannot be matched: a configured
custom index (PIP_INDEX_URL, PIP_EXTRA_INDEX_URL or pip config), an interpreter
version that cannot be determined, or DisablePipHttpUpdateCheck.

Lookups are bounded by a 45 s budget inside the 60 s listing timeout, cached by
ETag so later checks mostly return 304, and fall back to the last known version
on a transient failure. A check that loses more than a quarter of its lookups
fails loudly rather than reporting a truncated update list.

PIP_DISABLE_PIP_VERSION_CHECK joins PIP_REQUIRE_VIRTUALENV. It removes no
background traffic, since pip only self-checks on commands that already open a
session, but it keeps the [notice] lines out of parsed output.

Verified against a real environment: both paths return the same 14 updates, and
a proxy interposed on the manager's child processes records 42 connections from
python.exe on the CLI path and none on the new one.

Copilot AI 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.

🟡 Changes recommended

Interpreter reloads, additional pip source controls, and valid PEP 440 constraints are not handled correctly.

Get a fresh assessment by requesting another Copilot review.

Pull request overview

Moves pip update discovery from Python networking to UniGetUI’s HTTP client.

Changes:

  • Resolves compatible PyPI versions using PEP 691/440.
  • Adds caching, time limits, and CLI fallback.
  • Adds update-resolution and version-specifier tests.
File summaries
File Description
src/UniGetUI.PackageEngine.Managers.Pip/Pip.cs Implements HTTP update resolution and fallback.
src/UniGetUI.Core.Tools/PythonVersionSpecifier.cs Parses Python version constraints.
src/UniGetUI.Core.Tools/PythonVersion.cs Exposes release components.
src/UniGetUI.Core.Settings/SettingsEngine_Names.cs Adds the HTTP-check disable setting.
src/UniGetUI.PackageEngine.Tests/PythonVersionSpecifierTests.cs Tests specifier parsing.
src/UniGetUI.PackageEngine.Tests/PipManagerTests.cs Tests PyPI resolution behavior.
src/UniGetUI.PackageEngine.Tests/Fixtures/Pip/simple-sample-project.json Provides compatibility fixtures.
src/UniGetUI.PackageEngine.Tests/Fixtures/Pip/simple-zope-interface.json Provides filename-normalization fixtures.
Review details
  • Files reviewed: 8/8 changed files
  • Comments generated: 3
  • Review effort level: Balanced

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread src/UniGetUI.Core.Tools/PythonVersionSpecifier.cs
Comment thread src/UniGetUI.PackageEngine.Managers.Pip/Pip.cs
Comment thread src/UniGetUI.PackageEngine.Managers.Pip/Pip.cs Outdated

Copilot AI 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.

🟡 Changes recommended

Interpreter detection, cache invalidation, and cancellation handling can produce incorrect or failed update checks.

Get a fresh assessment by requesting another Copilot review.

Review details

Suppressed comments (4)

Previously missed (4) — in code that hasn't changed since the last review.

src/UniGetUI.Core.Tools/PythonVersionSpecifier.cs:84

  • Wildcard prefix specifiers may include an epoch, but ParseRelease accepts only digits and dots, so valid clauses such as !=1!3.* and ==0!3.* are rejected. In the exclusion case the current epoch-0 interpreter should satisfy !=1!3.*; instead IsInterpreterAllowed rejects the entire file and can hide an update. Parse and compare the epoch as part of wildcard prefixes.
    src/UniGetUI.PackageEngine.Managers.Pip/Pip.cs:423
  • This query drops sys.version_info.releaselevel and serial, so a prerelease interpreter such as Python 3.14.0rc2 is recorded as final 3.14.0. Requires-Python clauses such as ==3.14.0rc2 or !=3.14.0rc2 will then produce the opposite compatibility result from pip. Print the complete PEP 440 interpreter version instead.
    src/UniGetUI.PackageEngine.Managers.Pip/Pip.cs:446
  • On the standalone pip path, this falls back to pip --version, whose banner normally reports only major/minor (for example, python 3.11). ParseInterpreterVersion accepts that as an exact version, so Python 3.11.9 is treated as 3.11.0: a file requiring >=3.11.4 is incorrectly skipped, while one requiring <=3.11.2 can be reported even though pip will reject it. This is the default non-Windows path because _loadManagerExecutableFile prefers pip3/pip; query the launcher's full interpreter version, or treat an abbreviated banner as undetermined and use the CLI fallback.
    src/UniGetUI.PackageEngine.Managers.Pip/Pip.cs:468
  • The cancellation-aware semaphore wait is outside the try, so expiration of the 45-second budget while a lookup is queued faults that task directly. Task.WhenAll then throws before the code can apply cached values or the one-quarter failure threshold; with more than six cached packages and stalled requests, even queued entries that have a last-known version can fail the entire update check. Handle cancellation as a VersionResolution and release the semaphore only when acquisition succeeded.
  • Files reviewed: 8/8 changed files
  • Comments generated: 1
  • Review effort level: Balanced

Comment thread src/UniGetUI.PackageEngine.Managers.Pip/Pip.cs Outdated

Copilot AI 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.

🔵 Needs a closer look

Yanked-file handling, malformed metadata, and failed configuration probes can produce results inconsistent with pip.

Review details

Suppressed comments (3)

Previously missed (3) — in code that hasn't changed since the last review.

src/UniGetUI.PackageEngine.Managers.Pip/Pip.cs:399

  • A failed configuration probe currently falls through to false, which enables public-PyPI resolution even though the code could not establish whether pip uses a custom source. Moreover, RunPipCommand discards nonzero exit status, so many pip config list failures never reach this catch. Preserve the command status and conservatively select the CLI path whenever configuration cannot be read.
    src/UniGetUI.PackageEngine.Managers.Pip/Pip.cs:576
  • pip deliberately ignores an invalid Requires-Python value and considers the link usable, but this returns false (and the new test currently codifies that mismatch). A release with malformed metadata can therefore disappear from the update list even though pip would install it. Treat an unparseable constraint as allowed; valid constraints still need to be evaluated normally.
    src/UniGetUI.PackageEngine.Managers.Pip/Pip.cs:586
  • In the Simple API, any string value means the file is yanked; an empty string represents “yanked with no reason.” Treating "" as not yanked can select a release that pip excludes, recreating a phantom update. Return true for every string value.
  • Files reviewed: 8/8 changed files
  • Comments generated: 0 new
  • Review effort level: Balanced

@randy-but-a-ro randy-but-a-ro Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🤖 Pull request was approved automatically: the AI review is complete and all its review threads are resolved. 🎉

Integration Details
{
	"deliveryId": "fa77b280-b39a-11f1-990e-87841c249d56",
	"headSha": "1979a9cceb46e9ff67f359af306416afbecc3cd4",
	"reviewer": "copilot-pull-request-reviewer[bot]"
}

@GabrielDuf
Gabriel Dufresne (GabrielDuf) merged commit 5e8b14e into main Sep 18, 2026
6 checks passed
@GabrielDuf
Gabriel Dufresne (GabrielDuf) deleted the fix/pip-update-check-without-python branch September 18, 2026 20:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

[BUG] Pip management design is a problem

2 participants