List pip updates without giving Python network access (#5401) - #5405
Conversation
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.
There was a problem hiding this comment.
🟡 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.
There was a problem hiding this comment.
🟡 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
ParseReleaseaccepts 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.*; insteadIsInterpreterAllowedrejects 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.releaselevelandserial, so a prerelease interpreter such as Python 3.14.0rc2 is recorded as final 3.14.0. Requires-Python clauses such as==3.14.0rc2or!=3.14.0rc2will 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
pippath, this falls back topip --version, whose banner normally reports only major/minor (for example,python 3.11).ParseInterpreterVersionaccepts that as an exact version, so Python 3.11.9 is treated as 3.11.0: a file requiring>=3.11.4is incorrectly skipped, while one requiring<=3.11.2can be reported even though pip will reject it. This is the default non-Windows path because_loadManagerExecutableFilepreferspip3/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.WhenAllthen 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 aVersionResolutionand release the semaphore only when acquisition succeeded.
- Files reviewed: 8/8 changed files
- Comments generated: 1
- Review effort level: Balanced
There was a problem hiding this comment.
🔵 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,RunPipCommanddiscards nonzero exit status, so manypip config listfailures 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-Pythonvalue 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
There was a problem hiding this comment.
🤖 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]"
}
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 listsupplies 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.