Maintenance/code cleanup 2026-05 Phase V (performance and pure python resolver prework) - #6668
Conversation
There was a problem hiding this comment.
Pull request overview
This PR continues the May 2026 modernization track by (1) reducing startup/lock overhead on the existing pip-driven resolver path and (2) landing major groundwork for a future pure-Python Simple API resolver (PEP 691/503), along with scaffolding for pluggable resolver backends.
Changes:
- Performance-oriented refactors (notably lazy imports) and a Windows virtualenv detection fix.
- Adds a new
pipenv/resolver/pure-Python Simple API client surface (types, candidate model, auth, cache, parallel fetch) plus targeted coverage gating. - Introduces resolver-backend scaffolding (
--resolver,PIPENV_RESOLVER,[pipenv] resolver) and an opt-in manifest prefetch setting (prefetch_index_manifests).
Reviewed changes
Copilot reviewed 105 out of 110 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| tests/unit/test_utils.py | Update unit tests for Pipfile/Lockfile subsystem API moves + new venv detection tests |
| tests/unit/test_update.py | Update update-routine unit tests to new project.pipfile.* APIs |
| tests/unit/test_unpack.py | New tests for pipenv.utils.unpack relocation + behavior pins |
| tests/unit/test_settings.py | Add tests for prefetch_index_manifests setting + env override; update cache invalidation paths |
| tests/unit/test_resolver_regressions.py | Update regression tests for resolve_constraints marker extraction behavior |
| tests/unit/test_resolver_parent_dispatch.py | Update resolver parent-dispatch stubs to new Pipfile subsystem fields |
| tests/unit/test_resolver_diagnostics.py | Add regression tests preventing resolver-log propagation to root |
| tests/unit/test_resolver_backends.py | New unit tests for resolver-backend registry/dispatch scaffolding |
| tests/unit/test_pylock.py | Update unit tests to new project.lockfile.* subsystem APIs |
| tests/unit/test_project_caching.py | Update Pipfile caching tests to Pipfile subsystem implementation |
| tests/unit/test_prefetch_fan_out.py | New unit tests for per-source verify_ssl prefetch fan-out |
| tests/unit/test_pep691_parity_known_diffs.md | New documentation of known parity normalizations/divergences |
| tests/unit/test_pep691_parity_fixtures.py | New parity tests vs pip’s Link parsing for frozen fixtures |
| tests/unit/test_locking_no_mutation.py | Update test to new Pipfile subsystem write API |
| tests/unit/test_lockfile.py | New unit tests for extracted Lockfile subsystem |
| tests/unit/test_lock_sync_uninstall_context_routing.py | Update context-routing tests for Pipfile/Lockfile subsystem APIs |
| tests/unit/test_do_update_context_routing.py | Update update context-routing tests for lockfile.any_exists |
| tests/unit/test_do_install_context_routing.py | Update install context-routing stub for pipfile.exists |
| tests/unit/test_dependencies.py | Update deps tests for new project.pipfile.project_directory usage |
| tests/unit/test_dependencies_bridges.py | Update “requirementslib removed” assertion to module nonexistence |
| tests/unit/test_core.py | Update core tests to project.pipfile.* and NON_CATEGORY_SECTIONS import move |
| tests/unit/fixtures/README.md | New fixture provenance documentation (PEP 691/503 snapshots) |
| tests/unit/fixtures/pep691/yanked-pkg.json | New synthetic PEP 691 fixture for yanked variants |
| tests/unit/fixtures/pep691/missing-hash.json | New synthetic PEP 691 fixture for empty-hash edge case |
| tests/unit/fixtures/pep503/yanked-pkg.html | New synthetic PEP 503 fixture for yanked variants |
| tests/integration/test_run.py | Update integration test to project.pipfile.build_script |
| tests/integration/test_resolver_protocol.py | Normalize new deadline_seconds + redact diagnostics in protocol canary |
| tests/integration/test_pylock.py | Update integration tests to project.lockfile.* APIs |
| tests/integration/test_pipenv.py | Update integration test to project.pipfile.proper_names |
| tests/integration/test_lockfile.py | Update integration tests to project.lockfile.load |
| tests/integration/test_install_twists.py | Comment update to reflect new pipfile writer location |
| tests/integration/test_install_markers.py | Update integration tests to new lockfile/hash/pipfile hash accessors |
| tests/integration/test_import_requirements.py | Patch target update for moved unpack_url |
| tests/integration/fixtures/resolver_protocol/response.json | Update protocol golden with redacted diagnostics field |
| tests/integration/fixtures/resolver_protocol/request.json | Update protocol golden with redacted deadline_seconds |
| pyproject.toml | Configure coverage to target pipenv/resolver for a dedicated gate |
| pipenv/utils/virtualenv.py | Update to Pipfile subsystem fields (location, project_directory, name, required python) |
| pipenv/utils/venv_locator.py | Update to use project.pipfile.* fields |
| pipenv/utils/unpack.py | Add module docstring for relocated pip-fork unpack helpers |
| pipenv/utils/toml.py | Use project.pipfile.get_package_categories() |
| pipenv/utils/sources.py | Update to project.pipfile.* + project.lockfile.* APIs |
| pipenv/utils/shell.py | Fix Windows Path.glob crash by guarding bindir existence |
| pipenv/utils/settings.py | Add env-var override plumbing + new resolver accessor + migrate to project.pipfile.parsed |
| pipenv/utils/pylock.py | Add TODO for resolver backend metadata in pylock conversion |
| pipenv/utils/project.py | Update required-python warning logic to Pipfile subsystem |
| pipenv/utils/locking.py | Switch to PlettePipfile loader naming after subsystem extraction |
| pipenv/utils/internet.py | Lazy-import pip network stack; adjust session cache_dir defaulting |
| pipenv/utils/fileutils.py | Lazy-import pip network utilities; avoid heavy imports at module load |
| pipenv/utils/environment.py | Use project.pipfile.project_directory for .env lookup |
| pipenv/utils/dependencies.py | Lazy-import pip internals in hot modules; migrate to new Pipfile APIs |
| pipenv/routines/update.py | Update update routine to new Pipfile/Lockfile APIs |
| pipenv/routines/uninstall.py | Update uninstall routine to new Pipfile/Lockfile APIs |
| pipenv/routines/sync.py | Update sync routine to project.lockfile.any_exists |
| pipenv/routines/shell.py | Update shell/run routines to Pipfile subsystem project directory + scripts |
| pipenv/routines/scan.py | Update scan to use Pipfile/Lockfile subsystem fields |
| pipenv/routines/requirements.py | Update requirements generation to new Pipfile/Lockfile APIs |
| pipenv/routines/outdated.py | Update outdated routine to new Pipfile APIs |
| pipenv/routines/install.py | Update install routines to new Pipfile/Lockfile APIs; propagate --resolver + --clear |
| pipenv/routines/context.py | Add ExecutionOptions.resolver plumbing from CLI |
| pipenv/routines/clean.py | Update clean routine to new lockfile hash/package-names accessors |
| pipenv/routines/check.py | Update check routine to use Pipfile/Lockfile subsystem fields |
| pipenv/routines/audit.py | Update audit routine to use project.lockfile.* APIs |
| pipenv/resolver/schema.py | Add additive ResolverOptions.backend + suppress empty backend on wire |
| pipenv/resolver/pep691_types.py | New typed envelopes for Simple API fetch results/errors |
| pipenv/resolver/main.py | Fix resolved-default-deps structure + avoid PackageFinder second-pass marker lookup |
| pipenv/resolver/core.py | Add resolver-log propagation fix + implement backend dispatcher scaffolding |
| pipenv/resolver/candidate.py | New pure-Python Candidate/Hash data model |
| pipenv/resolver/backends/pip.py | New pip backend adapter wrapping existing resolve flow |
| pipenv/resolver/backends/base.py | New Backend protocol + shared registry |
| pipenv/resolver/backends/init.py | New backend registry + lookup helpers |
| pipenv/resolver/auth.py | New pure-Python auth helpers (netrc, URL creds, client cert) |
| pipenv/resolver/init.py | Re-export new Initiative G resolver surface |
| pipenv/help.py | Update diagnostics printing to new Pipfile/Lockfile subsystem locations |
| pipenv/environments.py | Add env vars for prefetch + resolver backend selection |
| pipenv/environment.py | Lazy-import InstallCommand to reduce startup cost |
| pipenv/cli/options.py | Add --resolver flag plumbing into CLI state |
| pipenv/cli/command.py | Thread resolver selection into contexts; update to Pipfile subsystem APIs |
| news/T_F.5.feature.rst | News fragment for resolver backend scaffolding |
| news/initiative-g-phase2-prefetch-bridge.feature.rst | News fragment for opt-in manifest prefetch |
| news/initiative-g-phase1-pep691-client.feature.rst | News fragment for PEP 691/503 client + cache surface |
| docs/pipfile.md | Document [pipenv] prefetch_index_manifests setting + env var |
| docs/dev/modernization-plan.md | Update task status/logs for Initiative D/E/F/G work |
| docs/dev/initiative-f-backends-design.md | Add maintainer sign-off decisions and updated scope |
| .pre-commit-config.yaml | Add guard preventing pip internal imports in pipenv/resolver/ |
| .github/workflows/ci.yaml | Add resolver-module coverage gate job |
Comments suppressed due to low confidence (1)
pipenv/resolver/backends/base.py:76
REGISTRYis annotated asdict[str, Backend], but the registry actually stores backend classes (e.g.REGISTRY["pip"] = PipBackend) as well as instances in tests. This type mismatch will confuse type checkers/IDEs and contradicts the module docstring (“dict from name to backend class”). Update the type to something likedict[str, type[Backend] | Backend](or introduce a dedicatedBackendEntrytype alias) so the registry’s intended contents are accurately represented.
# Single shared registry. The ``backends/__init__.py`` populates this
# on import with the in-tree backends. Keeping it here (rather than on
# ``__init__``) lets test code patch via ``mock.patch.dict`` against a
# single canonical reference no matter which module the patch targets.
REGISTRY: dict[str, Backend] = {}
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
9697a6c to
0307e31
Compare
|
Restacked onto the updated Phase IV tip and corrected this PR's base from the obsolete Local verification: 1,283 unit tests passed, 10 platform skips; full pre-commit suite passed. |
``do_lock`` always iterates ``["default", "develop", ...]`` even when
a section is empty, and each iteration unconditionally invoked
``venv_resolve_deps`` — spawning the resolver subprocess,
re-importing pipenv + pip + the typed schema, instantiating a fresh
``Resolver`` / ``PackageFinder`` / ``Session``, and (when
``use_default_constraints`` is on) walking PyPI for every default-
category transitive pin just to confirm the empty category doesn't
conflict with anything.
The work produces an empty section; the lockfile category was already
emptied two lines earlier when ``lockfile.pop(category)`` ran.
Skipping the call when ``packages`` is an empty mapping is local to
``do_lock`` — no public-API or contract change. Stale entries are
still removed (the prior ``pop`` ran) and the lockfile writer still
sees an initialized ``{}`` section.
Profiling on a 30-package Pipfile with empty ``[dev-packages]``
(May 2026):
subprocess resolver (default mode): 10.1 s -> 8.8 s (~13 %)
in-process resolver (debug bypass): 12.7 s -> 7.8 s (~39 %)
The win scales with the number of empty categories. Projects that
declare optional groups (``[test-packages]``, ``[docs-packages]``,
etc.) but populate them per-environment will see proportional
savings on every ``pipenv lock`` / ``pipenv install`` /
``pipenv install <pkg>``. Populated categories take the unchanged
resolve path; stale lockfile entries are correctly cleared when a
section is emptied (verified end-to-end).
This is cut #1 of the phase-5 perf plan; the profile that motivated
it (``benchmarks/timings/lock-warm.1.prof`` analysis + a fresh
in-process trace) showed the second-category resolve at ~6 s on the
benchmark fixture even with zero packages to resolve.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…elock Cut #3 of the phase-5 perf plan. Before this commit, ``pipenv lock`` on a warm cache still walked PyPI for every package — pip's ``find_all_candidates`` fetched the project URL, parsed every link record (~200 k ``evaluate_link`` calls on a 30-package Pipfile), and only then narrowed down to a satisfying version, even when the prior ``Pipfile.lock`` already pinned an exact version that the current Pipfile spec accepts. Reuse the existing ``resolved_default_deps`` plumbing (the cross-category constraint channel already built for gh-4665) to also carry warm-relock prior locks. Pre-pinning the resolver to the previous lockfile's versions lets ``find_all_candidates`` short-circuit: pip's PackageFinder picks the constrained version directly and skips the index walk. The result is identical to a fresh resolve (same Pipfile.lock hash) when no Pipfile spec drifted, just much faster. Surgical changes ---------------- - ``pipenv/routines/lock.py`` ``do_lock``: merge two constraint sources into ``category_default_deps`` for every category: 1. Warm-relock prior locks (the popped ``old_lock_data``) when ``--clear`` was not passed, filtered to entries whose locked version still satisfies the current Pipfile spec. 2. Cross-category default constraints (unchanged behaviour). - ``pipenv/routines/lock.py`` ``_filter_pinnable_lock_entries``: new helper. Drops entries with no version (VCS / file / path pins), drops top-level entries where the Pipfile spec no longer accepts the locked version, keeps transitive deps as-is (they have no Pipfile spec and they account for most of the warm-path win). - ``pipenv/utils/resolver.py`` ``Resolver.parsed_constraints`` / ``Resolver.constraints``: lift the ``category != "default"`` gate. Historically these branches only fired for non-default categories (the cross-category use case); the gate now keys off ``self.resolved_default_deps`` presence so the warm-relock pins apply to default-category resolves too. The user-facing kill switch ``[pipenv] use_default_constraints = false`` is still honoured. Measured impact (30-package Pipfile, subprocess resolver, no spinner) -------------------------------------------------------------------- baseline (pre-phase-5) 10.1 s + cut #1 (skip empty cat) 8.8 s (~13 %) + cut #3 (warm-relock pins) 5.9 s (~42 % off baseline, ~33 % off cut #1) Lockfile hash unchanged across the two runs — same resolution result, faster path. Edge cases verified ------------------- - Pipfile spec tightened (``tablib = "==3.6.0"`` when lock has ``3.9.0``): pin dropped, fresh resolve picks ``3.6.0``. - Pipfile spec loosened (``tablib = ">=3.0"`` when lock has ``3.6.0``): pin kept, lockfile stays at ``3.6.0`` (no churn). - ``pipenv lock --clear``: warm-relock pinning skipped, resolver picks the latest matching version regardless of the prior lock. - Populated categories take the unchanged resolve path; transitive deps are pinned to their previous locked versions on every warm relock. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…n warm relock" This reverts commit 20092de.
…rnet ``pipenv.utils.fileutils`` and ``pipenv.utils.internet`` were both imported transitively by ``pipenv.environments`` → ``pipenv.cli.command`` on every invocation of the ``pipenv`` CLI, and both pulled in heavyweight pip-internal modules at module load time: - ``pipenv.patched.pip._internal.network.download`` (PipSession + cachecontrol + requests) — ~78 ms cumulative. - ``pipenv.patched.pip._internal.locations`` (USER_CACHE_DIR) — pulls in a fresh chain on first import. - ``pipenv.patched.pip._vendor.urllib3.util`` — small but on the critical path. None of those are needed at module load time. The pip-internal network stack is only touched on the remote-file path of ``open_file()`` and inside ``get_requests_session()``; the urllib3 helper is only used by the URL inspectors. Moved each import to first-use: - ``fileutils.PipSession`` annotation moved behind ``TYPE_CHECKING``. Function signature stays the same. - ``fileutils.open_file()`` imports ``PipSession`` / ``USER_CACHE_DIR`` inside the remote-URL branch and ``is_valid_url`` inside the pre-check (one cheap re-import — Python caches the module after the first hit). - ``internet.get_requests_session()`` imports ``PipSession`` and ``USER_CACHE_DIR`` inside the function; ``cache_dir`` parameter default changed to ``None`` (resolved to ``USER_CACHE_DIR`` post- import) so the import isn't forced by signature evaluation. - ``internet.urllib3_util`` routed through a tiny ``_urllib3_util()`` accessor that lazy-imports on first call. Measured impact (May 2026, single dev machine): importtime ``pipenv.cli.command``: 305 ms -> 169 ms (-45 %) pipenv --version : 0.24 s -> 0.20 s (-17 %) minimal warm lock (1 pkg) : 0.95 s -> 0.63 s (-34 %) 30-package bench warm lock : 9.0 s -> 6.5 s (-28 %) The bench savings (~2.5 s) compound across the parent process AND the resolver subprocess, both of which used to import the deferred chain transitively at startup. No public-API change. All ``utils.fileutils`` and ``utils.internet`` exports remain importable at the same names. Unit suite (860 tests) passes; manual smoke of ``is_valid_url``, ``get_host_and_port``, ``is_url_equal``, ``get_requests_session`` all return the expected values and types. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…r imports
Second round of the phase-5 startup-cost hunt. After the previous
``fileutils`` / ``internet`` deferrals, profiling
``pipenv.cli.command`` import showed the next-largest culprits
were still being pulled in eagerly:
- ``pipenv.patched.pip._internal.commands.install.InstallCommand`` —
~79 ms cumulative. Imported eagerly by ``pipenv.project``,
``pipenv.environment``, ``pipenv.utils.dependencies`` and
``pipenv.utils.resolver``, used only inside the single function
that constructs the command (one per file).
- ``pipenv.utils.unpack`` — ~26 ms cumulative. Transitively pulls
``pip._internal.operations.prepare`` and
``pip._internal.network.download``. Used only in
``determine_package_name`` (the remote-link branch).
- Multiple ``pip._internal.req``, ``vcs``, ``network.download``,
``models.link``, ``utils.misc.hide_url`` symbols inside
``pipenv.utils.dependencies`` — every one is function-scoped at
the call site.
This commit moves all of them to first-use inside the functions
that need them. Where annotations would otherwise force the
import (``-> InstallCommand``, ``Optional[PipSession]``), the
symbol is moved to ``TYPE_CHECKING`` or to a forward-reference
string.
Two unit tests had to follow:
- ``tests/unit/test_utils.py``
``TestCreatePipfileVersionConsistency`` was patching
``pipenv.project.InstallCommand``; with the lazy import that
attribute is no longer module-level, so the patch target is
switched to the canonical source.
- ``tests/unit/test_unpack.py``
``test_dependencies_imports_unpack_url_from_new_location`` was
asserting ``dependencies.unpack_url is unpack.unpack_url`` (a
module-attribute identity check). The test now verifies the
same wiring via source inspection of
``determine_package_name`` — equivalent assertion, no
module-level import required.
Measured impact (30-package benchmark Pipfile, May 2026, single
dev machine, 5-run median):
pipenv.cli.command cumulative import:
pre-phase-5 : 305 ms
after fileutils/internet defer : 169 ms
after this commit : ~170 ms (parent floor;
InstallCommand
chain no longer
pulled by CLI)
pipenv.resolver.main cumulative (subprocess entry):
pre-this-commit : 174 ms
after this commit : 169 ms
warm relock wall (30-pkg bench, subprocess resolver, 5-run median):
pre-phase-5 : ~9.0 s
after fileutils defer : ~6.5 s
after this commit : ~5.6 s (~38 % off baseline)
860 unit tests pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Companion doc to initiative-f-backends-design.md. Initiative G
implements a new backend under Initiative F's pluggable framework
(``pipenv/resolver/backends/pure_python.py``) that replaces the
parts of pipenv's resolution path depending on pip's ``_internal``
APIs with an in-tree, pure-Python implementation modeled on uv's
architecture (PEP 691 simple-API client + parsed-manifest cache +
parallel fetch).
Motivation:
- The pip-internal-API surface is a chronic maintenance tax
(~40 % of the May 2026 modernization commits were reactive
churn to pip refactors). A PEP 691 / PEP 503 client whose only
external contract is the simple-API spec is stable across pip
releases.
- The phase-5 perf-cut investigation
(``maintenance/code-cleanup-phase5-perf-2026-06``) measured a
~7-11 % ceiling on the warm-relock bench because every remaining
cut runs into a structural decision in pip (max-age=0 forced
revalidation, raw-response cache that re-parses on every read,
sequential per-package ``find_all_candidates``). None of those
are pipenv-side problems.
Four phases, with sign-off gates between each:
1. Standalone client + cache + parallel fetcher (no integration,
~1 week).
2. Cache-prime bridge in front of pip (~3-5 days, target ≥10 %
on the lock-warm bench).
3. Full ``pure-python`` backend with own ``resolvelib.Provider``
(~4-6 weeks, target ≥30 % on the lock-warm bench, lockfile
parity with pip backend across pip N-1/N/N+1).
4. Promote to default (separate sign-off after phase 3 ships
and a release cycle of opt-in usage).
Explicitly out of scope: replacing wheel installation, replacing
``resolvelib`` itself, removing the pip backend, HTTP/2 in phase 1.
Open questions for maintainer sign-off catalogued in §10:
- Cache file format (JSON vs msgpack)
- Whether to fetch + cache wheel ``METADATA`` inline (PEP 658)
- Default TTL for parsed manifests
- How aggressively to audit pip's ``Provider`` for parity behaviours
- Auth/netrc/keyring parity scope
- Vendoring posture (confirmed: first-party, not vendored)
No code changes under Initiative G until this doc is approved.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
22-task dependency-aware plan for executing Initiative G phases 1 and 2 from ``docs/dev/initiative-g-pure-python-design.md``. Companion plan to the design doc that lays out the actual task graph: foundational types (``Candidate`` + ``Hash``), PEP 691 + PEP 503 parsers, ``PEP691Client``, ``ParsedManifestCache`` with TTL + atomic write + schema versioning, ``ParallelFetcher`` with 16-worker thread pool, unit tests with enforced coverage floors, fixture-only parity check vs pip's ``Link.from_json``, plus Phase 2's ``do_lock`` wiring + multi-run CI bench measurement. Plan went through one subagent review pass; the items the reviewer flagged as blocking (T6/T16 file collision, T19 forward-reference to a non-existent design-doc spec, T17 coverage-enforcement gap, T21 single-sample perf claim) are fixed. Wave-table and per-task ``depends_on`` lists are now consistent. T19 was redesigned to drive pip's own ``PipSession`` rather than writing pip-compatible cache entries directly, removing the largest implementation hazard. Key acceptance criteria captured up front: - Phase 1: zero ``pip._internal.*`` imports in ``pipenv/resolver/*`` (enforced by pre-commit gate in T17); coverage floors enforced by ``--cov-fail-under``. - Phase 2: lock-cold ≥10 % improvement on CI as median-of-3 runs exceeding 2σ baseline noise; lock-warm no regression beyond noise. Branch: stays on ``maintenance/code-cleanup-phase5-perf-2026-06`` per the scoping discussion. No code under Initiative G until the design doc is signed off. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…nitiative G phase 1) Three small helpers used by the upcoming PEP691Client. Pure stdlib; no pip._internal imports. Tests land separately under T16. Initiative G phase 1 — T6. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ative G phase 1) Real-PyPI snapshots (six, django, cryptography, tablib) captured 2026-05-12 with the documented Accept headers, plus two synthetic fixtures exercising yanked + missing-hash edge cases. Provenance and re-baseline procedure documented in fixtures/README.md. Initiative G phase 1 - T2. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… G phase 1) Pure-Python typed candidate model for the in-tree resolver backend. No imports from ``pip._internal``; only ``pipenv.vendor.packaging.tags`` for wheel-tag derivation. Smoke tests in T11 location — T11 will extend coverage in Wave 2. Initiative G phase 1 — T1. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… G phase 2) New Pipfile setting + PIPENV_PREFETCH_INDEX_MANIFESTS env-var override. Defaults to False; will be wired into do_lock by T19 (next wave) to gate the upcoming parallel manifest-prefetch path. Initiative G phase 2 - T18. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… G phase 1) Two frozen dataclasses + two Literal aliases that downstream tasks (T8 PEP691Client, T9 ParallelFetcher) use to describe successful and failed simple-API fetches. Pure data; zero pip-internal imports. Tests land transitively in T13. Initiative G phase 1 — T3. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…write (T7, Initiative G phase 1) Filesystem cache of parsed Candidate manifests, keyed by (sha256(index_url), canonical_name). Replaces pip's CacheControl + raw-response-per-read pattern for the resolver candidate path. JSON serialization (per Q1 — debug-friendly while the format is in flux); ``schema_version`` field invalidates mismatched payloads. Atomic write via tempfile + os.replace. Full test coverage lands in T14. Initiative G phase 1 — T7. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… + hashability + edge cases (T11, Initiative G phase 1) Extends T1's smoke tests to >=95 % coverage of ``pipenv/resolver/candidate.py``. Adds exhaustive wheel-tag derivation tests across manylinux1/2014/PEP-600, musllinux, macosx, win_amd64, pure-Python, and abi3 forms. Pins equality + hashability semantics for set / dict use. Pins Candidate.from_filename behavior on sdist filenames and on the malformed-wheel edge case. Initiative G phase 1 — T11. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…tiative G phase 1) Full coverage of pipenv/resolver/auth.py (T6's helpers): extract_url_credentials across plain / encoded / scheme variants; lookup_netrc_auth across missing / empty / malformed / matching / non-matching / permission cases with $NETRC + explicit-arg precedence; client_cert_from_env across unset / empty / single-path cases. Uses pytest tmp_path + monkeypatch; never reads the user's real ~/.netrc. Initiative G phase 1 — T16. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
T7 (ParsedManifestCache), T11 (full Candidate test coverage), and T16 (auth helper test suite) all landed in Wave 2 commits 00acd86, 1bbe90a, c75a6db. Their plan-file status entries weren't bundled into those commits (which staged only the implementation/test files); fold the plan-tracking updates here. Initiative G phase 1 — Wave 2 plan-tracking. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…e 1) Pure-function ``_parse_pep691_json(body, page_url) -> tuple[Candidate, ...]``. Resolves relative file URLs against the page URL once at parse time, replacing pip's per-evaluation _ensure_quoted_url cost. Validates meta.api-version starts with ``1.`` and tolerates unknown minors forward-compatibly. Skips entries that can't construct a Candidate (malformed wheel filenames etc.) rather than failing the whole parse. Tests land in T12 (later wave); T8 will wire this into PEP691Client. Initiative G phase 1 — T4. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…e 1) Adds ``_parse_pep503_html(body, page_url) -> tuple[Candidate, ...]`` to the existing ``pep691.py`` module that T4 created. Uses stdlib html.parser with a small _AnchorCollector subclass; reuses T4's _extract_version / _normalize_hashes / _strip_archive_suffix helpers so JSON and HTML parsers emit equivalent Candidate sets for the same package. data-yanked semantics differ deliberately from JSON's: HTML's empty attribute is unambiguously yanked-with-no-reason (per the de-facto PEP 503 extension), whereas the JSON parser conservatively treats empty-string yanked as not-yanked. Pinned in docstrings; cross- format parity tests land in T12. Tests in T12 (later wave); T8 will dispatch JSON vs HTML based on Content-Type. Initiative G phase 1 — T5. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…tch (T8, Initiative G phase 1) Adds ``PEP691Client`` to pep691.py alongside the existing ``_parse_pep691_json`` / ``_parse_pep503_html`` functions. Drives urllib3 (via pip._vendor); branches on status (200 fresh / 304 not-modified / 404 missing / 401/403 auth / else transient); dispatches on Content-Type for JSON vs HTML parsing; strips URL credentials before request and re-injects them via Authorization header; falls back to netrc for hosts without URL-embedded creds. Does NOT send Cache-Control: max-age=0 (deliberate divergence from pip — freshness lives at the manifest-cache layer, not as a forced-revalidate-every-read). No retries here; retry policy is the fetcher's (T9) responsibility. Full client test suite lands in T13; ParallelFetcher wraps this class in T9. Initiative G phase 1 — T8. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…zy-import move Four ``test_import_requirements.py`` tests were monkey-patching ``pipenv.utils.dependencies.unpack_url`` — which used to be a module- level re-export but became a function-scope lazy import in commit ``74a466b1`` (phase-5 startup-cost cuts). ``mock.patch`` on a non-existent attribute raises ``AttributeError`` at decorator- evaluation time, which is what CI has been reporting on the maintenance branch. Same fix as the earlier ``test_dependencies_imports_unpack_url_from_ new_location`` change (commit ``74a466b1`` itself): point the patch at the canonical source (``pipenv.utils.unpack.unpack_url``). The sole caller in ``determine_package_name`` does ``from pipenv.utils.unpack import unpack_url`` inside the function, so patching ``pipenv.utils.unpack.unpack_url`` intercepts the actual import. Affected tests: - tests/integration/test_import_requirements.py::test_auth_with_pw_redacted - tests/integration/test_import_requirements.py::test_auth_with_username_redacted - tests/integration/test_import_requirements.py::test_auth_with_pw_are_variables_passed_to_pipfile - tests/integration/test_import_requirements.py::test_auth_with_only_username_variable_passed_to_pipfile Broader sweep confirmed no other tests are patching deferred symbols (``Downloader``, ``PipSession``, ``parse_requirements``, ``install_req_from_editable``, ``install_req_from_parsed_requirement``, ``parse_req_from_line``, ``Link``, ``VcsSupport``, ``hide_url``, ``InstallCommand``). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… headers (T13, Initiative G phase 1) Covers 200 JSON / 200 HTML / 200 unknown-Content-Type / 304 / 404 / 401 / 403 / 5xx / urllib3-exception dispatch paths; URL-embedded creds + URL-encoded creds + netrc fallback auth; Accept / Cache-Control absence / If-None-Match header construction; canonical- name in outgoing URL; trailing-slash idempotency; mirror-serves- garbage 200; release_conn failure swallowed. cert/verify storage tests pinned (constructor-time only in Phase 1 per T8's Phase-3 deferral). Initiative G phase 1 — T13. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ncurrency (T15, Initiative G phase 1) Covers fresh / warm / mixed-outcome dispatch; status branches (fresh / not-modified / missing) with cache-write side effects; exception isolation per-target; max_workers clamp; default_ttl threading; deduplicated keying by package_name; dispatch-order parallelism via threading.Event (no wall-time assertions). Initiative G phase 1 — T15. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…INDEX_MANIFESTS (T19, Initiative G phase 2) When the user opts in via ``[pipenv] prefetch_index_manifests = true`` (or ``PIPENV_PREFETCH_INDEX_MANIFESTS=1``), do_lock pre-fetches top-level package indexes in parallel using pip's own PipSession (via pipenv.utils.internet.get_requests_session). Side-effects: - pip's resolver subprocess later hits a warm SafeFileCache (compatibility guaranteed at runtime because we drive pip's own session class - no on-disk format reverse engineering). - Our parsed-manifest cache (T7) is populated, ready for Phase 3. Best-effort: any exception is swallowed and do_lock continues unchanged. No URL is logged at any verbosity level. ``--clear`` short-circuits the prefetch entirely (user wanted fresh). Full integration test in T20; multi-run CI bench measurement in T21. Initiative G phase 2 - T19. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…verage gate, lint gate, news, doc (T17)
Six wrap-up tasks for Phase 1 sign-off:
1. pipenv/resolver/__init__.py — public surface re-exports
(Candidate, Hash, PEP691Client, ParsedManifestCache,
ParallelFetcher, SimplePageResponse, FetchError,
CachedManifest). Pre-existing Initiative F surface
(_main, main, resolve_packages, which) preserved.
2. ``pipenv install --clear`` now invalidates our parsed-manifest
cache via the inner do_lock path. The _clear_parsed_manifest_cache
helper in pipenv/routines/lock.py was landed by the parallel T19
agent under the T17 banner (same hot section); T17's contribution
here is plumbing ``clear=state.clear`` through ``cmd_install``'s
RoutineContext.from_cli call — that parameter was missing before,
so ``pipenv install --clear`` never reached the resolver's clear
path at all. ``pipenv lock --clear`` already worked.
3. pytest-cov config in pyproject.toml (``[tool.coverage.run]``
source + branch=false, ``[tool.coverage.report]`` fail_under=90
+ show_missing + exclude_lines) plus a dedicated CI job
``resolver-module-coverage`` that runs the six T11-T16 test
suites with ``--cov-fail-under=90`` and overrides addopts to
drop ``--no-cov``. Local coverage: 99.67% on the six new
modules — well above the 90% floor. Without this, the
coverage claims in T11-T16 would silently regress.
4. Pre-commit hook ``no-pip-internal-in-resolver`` scoped to
^pipenv/resolver/ that fails any commit reintroducing
``pip._internal`` imports. Pattern anchors on actual import
statements (^\\s*(from|import)\\s+pipenv\\.patched\\.pip\\._internal)
rather than raw substring matches, so docstring / comment /
literal mentions of the path don't false-positive (T1's gotcha).
T10's deliberate parity import in tests/ is exempt via the
files: path filter.
5. News fragment news/initiative-g-phase1-pep691-client.feature.rst
summarising the Phase-1 surface + ``--clear`` invalidation
behaviour. Rendered by ``python -m towncrier build --draft``.
6. docs/dev/initiative-g-pure-python-design.md status line updated
to ``Phase 1 shipped; phases 2-4 awaiting maintainer sign-off``;
§11 Phase 1 acceptance bullets converted to a [x] checklist
with ``Shipped at T17`` annotation. Two extra bullets added
covering the ``--clear`` wiring and the CI/pre-commit gates
(acceptance criteria per T17's plan entry but missing from the
original §11 list). Phase 2 / 3 bullets unchanged.
Verifications (all passing):
- ``python -c "from pipenv.resolver import PEP691Client,
ParsedManifestCache, ParallelFetcher, Candidate, Hash, FetchError,
SimplePageResponse, CachedManifest; print('ok')"`` -> ok.
- ``_clear_parsed_manifest_cache(project)`` removes
<PIPENV_CACHE_DIR>/manifests-v1/ end-to-end, idempotent on
missing dirs, defensive against broken projects.
- ``pytest tests/unit/test_candidate.py --cov=pipenv.resolver.candidate
--cov-fail-under=99 --override-ini="addopts=-ra"`` passes (100%);
same invocation with broader ``--cov=pipenv.resolver`` scope
fails at 23.33% (gate is wired).
- Pre-commit hook returns exit 1 against a deliberate failing
``from pipenv.patched.pip._internal...`` import; exit 0 on the
current clean tree.
- ``python -m towncrier build --draft`` renders the news fragment.
- ``docs/dev/initiative-g-pure-python-design.md`` Phase 1
acceptance criteria shown checked.
Initiative G phase 1 — T17 (ship).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… doc + news + status flip (T22) Phase-2 ship of Initiative G: - User-facing documentation for ``[pipenv] prefetch_index_manifests`` + ``PIPENV_PREFETCH_INDEX_MANIFESTS`` env-var override. - News fragment under towncrier convention. - Design-doc Phase-2 acceptance bullets flipped to ``[x]`` except T21 (CI bench measurement) which is annotated as deferred per the maintainer's scoping call. - Sign-off note (§11a) explicitly documenting that the Phase-2 perf claim is theoretical, not measured against the current CI bench — shipped as a low-risk opt-in setting gated on user enablement; future bench data can revisit the Phase-2 acceptance criteria. No code changes. Phase-2 functional surface (T18 + T19 + T20) remains exactly as landed in prior commits. Initiative G phase 2 — T22. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… Initiative G phase 2) Five scenarios pinning T19's contract: 1. Lockfile parity: prefetch on vs off produces identical ``_meta.hash.sha256`` and per-package pins. 2. Best-effort: populate raising RuntimeError doesn't fail the lock. 3. ``--clear`` short-circuits the prefetch (populate.assert_not_called). 4. Verbose stderr contains the prefetch summary but does NOT leak URLs, package paths, or credentials. 5. ``--clear`` invalidates the parsed-manifest cache at <PIPENV_CACHE_DIR>/manifests-v1/. Phase 2 acceptance test for the do_lock prefetch wiring. Initiative G phase 2 -- T20. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…cache T20's integration tests surfaced a real production bug: T17's ``_clear_parsed_manifest_cache`` wiped ``<PIPENV_CACHE_DIR>/manifests-v1/`` while T19's ``_prefetch_index_manifests_if_enabled`` wrote to ``<PIPENV_CACHE_DIR>/pipenv-manifests/manifests-v1/``. Result: ``pipenv lock --clear`` left the prefetcher's cache fully intact — exactly the poisoning surface T17 was meant to nuke. T19's namespacing (``pipenv-manifests/`` subdir) is the correct pattern: it keeps pipenv-owned cache files cleanly separated from anything pip stores in the same directory. Update T17 to match. T20's ``test_clear_invalidates_parsed_manifest_cache`` was seeding the wrong path to keep passing while the production code disagreed with itself; updated to seed the canonical path and pin the bug post-fix. Initiative G phase 2 — T17/T19 path-alignment follow-up. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Folds Wave 6 (T15, T17, T19) and Wave 7 (T20, T22) plan-status updates into a single tracking commit. T21 marked Skipped per maintainer (see T22 design-doc sign-off note for the framing). Wave 6: e0fdf78 (T15), 85fe117 (T17), f29b87b (T19). Wave 7: d5fa0a6 (T20), 69e821b (T22). Initiative G — Wave 6 + Wave 7 plan-tracking; Phase 2 structurally shipped. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…(FU3, Initiative G phase-3 prep) T8 stored ``verify`` and ``cert`` kwargs on the client but didn't thread them into the actual ``session.request(...)`` call — TLS material was constructor-time-only on the underlying session. Real production sessions are ``PipSession`` (requests.Session subclass) which supports per-request ``verify=`` and ``cert=``; pass them through. Closes the Phase-3 follow-up T8's agent flagged. FU2's per-source verify_ssl fan-out now actually takes effect at the request layer. Initiative G — Phase-3 follow-up #3 (FU3). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…tive G phase-3 prep) Refactors ``_prefetch_index_manifests_if_enabled`` to build one ParallelFetcher per unique ``verify_ssl`` policy among Pipfile sources, dispatching each target through the fetcher matching its source's policy. Replaces T19's "majority-verify wins" heuristic that left minority-policy sources falling through to pip's normal cold fetch. Single-policy projects (the common case — one PyPI source, verify_ssl=true) see identical behavior to T19: exactly one fetcher constructed; zero overhead. Mixed-policy projects (private index with self-signed cert alongside public PyPI) now get correct per-source verify routing. Closes Phase-3 follow-up T19's agent flagged. Initiative G — Phase-3 follow-up #2 (FU2). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ive G phase-3 prep) Adds ``ParsedManifestCache.peek_etag(index_url, package_name) -> str | None`` that reads the on-disk etag regardless of expiry, plus wires ``ParallelFetcher.populate`` to call it when ``cache.get(...)`` returned None -- so stale-but-present entries now get a conditional GET (``If-None-Match: <etag>``) instead of a full re-download. Closes the Phase-3 follow-up T9's agent flagged: the ``status="not-modified"`` branch in ``_dispatch_fetch_result`` was unreachable before this commit because the fetcher never sent ``If-None-Match`` for stale entries. ``_refresh_not_modified`` now falls back to ``_load_manifest`` (a private cache helper extracted from ``get`` while preserving the public contract) to recover the stale candidates when ``cache.get`` returns None, so option-a TTL refresh actually fires on stale-cache reads. Same defensive contract as ``get()``: any exception (missing file, corrupt JSON, schema mismatch, malformed etag) returns None silently. Coverage stays at 100% on both modules. Initiative G -- Phase-3 follow-up #1 (FU1). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The three Phase-3 follow-ups flagged by Wave-3/4/5 agents (T8, T9, T19) all landed on this branch ahead of Phase 3's formal scoping: - FU1 (91c1e4e): peek_etag + stale-cache short-circuit - FU2 (4a0ff8a): per-source verify_ssl fan-out - FU3 (0047a2e): per-request TLS material threading Adds a "Phase-3 follow-ups landed during plan execution" section to the plan documenting each follow-up's trigger, resolution, and test footprint. Lists the remaining Phase-3-flagged items (self-signed-cert fixture, the full pure_python.Provider) for future scoping. Initiative G — Phase-3-prep plan-tracking. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ve_constraints ``Resolver.resolve_constraints`` called pip's ``PackageFinder.find_best_candidate(name, specifier)`` once per resolved package, then read ``candidate.link.requires_python`` off the returned link. The resolved tree already carries that link (pip's resolvelib stores the chosen candidate on every ``InstallRequirement`` it returns from ``resolve()``), so the second pass was a redundant per-package simple-API walk — cached HTTP, but still parses every link through pip's ``Link.from_json`` plus ``_ensure_quoted_url``. Measured on the 100-package benchmark Pipfile (May 2026): in-process wall: 31.4 s -> 23.4 s (-25.5 %, ~8 s saved) subprocess warm: 22.6 s -> 17.9 s (-21 %, ~4.7 s saved) ``resolve_constraints`` and its ``_requires_python_marker`` helper both fall out of the top-50 cumulative profile entries entirely. The remaining in-process wall (20.7 s of 23.4 s) lives inside pip's own resolver loop, the architectural ceiling documented in ``docs/dev/initiative-g-pure-python-design.md`` §2.2. Lockfile-byte-identity check: same Pipfile, same lockfile ``_meta.hash.sha256`` before and after (``7a3cce84d…``). The marker we compute is identical to what the prior code computed because we read ``requires-python`` from the same link the prior code would have fetched a second time. The ``ThreadPoolExecutor`` is removed alongside — there's no I/O left in the loop to parallelise, and the executor + barrier machinery was pure overhead at attribute-read speed. The two unit tests in ``test_resolver_regressions.py`` that pinned the old behaviour (``test_resolve_constraints_reuses_package_finder`` asserted ``find_best_candidate.call_count == 2``; ``test_resolve_constraints_runs_candidate_lookup_in_parallel`` asserted overlap) are replaced with tests that pin the new contract: an explosive ``resolver.finder`` mock that fails the test if it is ever invoked. Regression-proofed against any future slip back into the slow path. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ore_compatibility users Commit ``cf53eb17`` eliminated a redundant ``find_best_candidate`` walk in ``resolve_constraints`` (~21 % lock-warm win on the 100-pkg bench). The fix has a subtle interaction with the ``pip_finder_ignore_compatability`` patched-pip flag that's worth pinning explicitly. Behaviour change scope ---------------------- Standard pipenv users (single-platform locks, no monkey-patching): zero behaviour change — the resolved tree only contains packages the strict finder accepted, so ``find_best_candidate(strict)`` and ``result.link.requires_python`` produce identical markers. Users who flip ``finder._ignore_compatibility = True`` somewhere in the resolve pipeline (cross-platform locking workflows, the patched-pip flag, etc.): cross-compat packages whose links advertise ``requires-python`` now get those markers in the lockfile. Pre-2026-05 the strict ``find_best_candidate`` returned ``None`` for those candidates and the marker was silently dropped. This is arguably a correctness fix but IS a behaviour change for consumers that relied on those markers being absent. What this commit adds --------------------- 1. ``resolve_constraints`` docstring now spells out the pre/post comparison and links to the regression test. 2. ``tests/unit/test_resolver_regressions.py`` ``test_resolve_constraints_marker_for_ignore_compatibility_link`` constructs a resolved tree whose link's ``requires-python`` would NOT have been extracted by the old strict-finder code path, and asserts the new code DOES extract it. An explosive ``resolver.finder`` mock catches any regression back to the slow path. No code change to ``resolve_constraints``'s logic — purely documentation + a regression-pinning test. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Agent-Logs-Url: https://github.com/pypa/pipenv/sessions/6a3b0b17-d3de-4173-9c1a-3dabbdcbe12a Co-authored-by: matteius <479892+matteius@users.noreply.github.com>
Agent-Logs-Url: https://github.com/pypa/pipenv/sessions/6a3b0b17-d3de-4173-9c1a-3dabbdcbe12a Co-authored-by: matteius <479892+matteius@users.noreply.github.com>
…ackend field
CI failed on all three smoke platforms (Ubuntu/MacOS/Windows on 3.12)
with the documented JSON-drift assertion:
Resolver request JSON drift. If this is an intentional schema
change, regenerate the golden via PIPENV_REGEN_PROTOCOL_FIXTURES=1
pytest test_resolver_protocol.py and review the diff before
committing.
Commit 0bf0c19 ("fix(resolver): stamp selected backend onto
resolver requests") added the env / Pipfile / default fallback chain
for ``ResolverOptions.backend`` so the parent now always stamps a
concrete backend name on the request envelope (was empty-string
sentinel before). The fixture needs the matching additive line.
Regenerated under PIPENV_REGEN_PROTOCOL_FIXTURES=1; the diff is a
single ``"backend": "pip"`` line inserted into ``options``. No
other fields drift.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…with wall-clock budget ``ParsedManifestCache.put`` writes a temp file then ``os.replace``s it into place. On POSIX the rename is atomic and uncontended. On Windows ``os.replace`` raises ``PermissionError`` (``ERROR_ACCESS_DENIED``) when the destination is held open by another process — including a well-behaved concurrent reader doing ``open(target, "rb")`` / ``read_bytes()``. ``test_reader_never_sees_partial_payload`` reliably hits this on the Windows runner because the writer churns 20 distinct payloads while the reader spins in a tight ``while not stop_event`` loop for ~100 ms — the reader's open/close cycle compounds across thousands of iterations. Wrap the rename in ``_replace_with_windows_retry``: POSIX takes the no-retry happy path on the first call, Windows retries under a 2 s wall-clock budget with exponential backoff (5 ms → 10 ms → 20 ms, capped at 100 ms). The earlier 5-attempt × 10 ms-linear scheme (~100 ms total) was tight enough that a single reader-busy window could exhaust it; 2 s is well within the test's 5 s join timeout and effectively infinite for production manifest writes (rare and mostly uncontended). The reader path is unchanged — ``_load_manifest`` and ``peek_etag`` already catch ``OSError`` (parent of ``PermissionError``) and treat it as a miss. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Python 3.10's netrc parser preserves surrounding quote characters in tokens, so an entry like ``login ""`` is read as the literal two-char string ``""`` rather than the empty string returned by 3.11+. The helper's falsy check (``if not login``) therefore failed to skip the quote-only entry on 3.10, causing test_lookup_netrc_auth_empty_login_ returns_none to fail across Ubuntu/macOS/Windows on that interpreter. Strip outer quotes before the falsy check so both parser behaviors agree, without changing the returned value for legitimate logins. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
0307e31 to
eaabe12
Compare
|
Follow-up: CodeQL flagged the integration test hostname substring assertions. Commit 75fe0a3 now parses each URL and compares exact hostnames; Ruff and diff checks pass. An explicit merge of current main reports Already up to date. |
…deferred-2026-05' into codex/phase5-merge-latest
|
Merged the refreshed Phase IV head into Phase V; an explicit current-main merge then reported Already up to date. Full unit suite: 1,284 passed, 11 skipped. |
| from __future__ import annotations | ||
|
|
||
| import json | ||
| import os |
|
|
||
| # Now patch ``datetime.now`` inside the module to return a value | ||
| # 1 hour in the future, well past the 60-second TTL. | ||
| import pipenv.resolver.manifest_cache as mc |
| assert before is not None and before.candidates == (original,) | ||
|
|
||
| # Patch os.replace inside the module to always raise OSError. | ||
| import pipenv.resolver.manifest_cache as mc |
| cache = ParsedManifestCache(tmp_path) | ||
| cand = _make_wheel_candidate() | ||
|
|
||
| import pipenv.resolver.manifest_cache as mc |
Summary
Phase V of the 2026-05 modernization track. Two intertwined workstreams:
Performance cuts on the existing pip-driven resolver path — chasing the
warm-relock and CLI-startup ceilings identified by the May 2026 benchmark
suite (
benchmarks/benchmark.py). Final measured win is modest: ~7-11 %across the CI bench (
lock-warm,lock-cold,install-warm,add-package,import) from lazy-import work; the architectural ceilingis pip's per-package sequential index revalidation and
Link.from_jsonloop, which pipenv can't move from outside pip.
Initiative G — pure-Python simple-API resolver groundwork — a new
in-tree implementation (PEP 691 JSON + PEP 503 HTML client, parsed-manifest
cache, parallel fetcher) under
pipenv/resolver/that imports zeropip._internal.*symbols. Phases 1 + 2 of a four-phase plan ship here;Phase 3 (the full
pure-pythonbackend replacing pip'sPackageFinder)and Phase 4 (promote to default) are explicitly future work.
Companion design + plan docs in
docs/dev/initiative-g-pure-python-design.mdand
initiative-g-phase1-2-plan.md. No user-visible API change, no newruntime dependencies, no lockfile-format change. The opt-in Phase-2 setting
defaults
false.What's in this PR
Phase-5 perf cuts (lazy imports + empty category skip)
perf(lock): skip resolver subprocess for empty Pipfile categories(49c7a249) —do_lockalways iterated["default", "develop", …]even when a sectionwas empty, paying ~5-6 s of subprocess + Resolver/Session setup to produce
an empty lockfile section. Skip the call when
packagesis an emptymapping. Stale entries still cleared because
lockfile.pop(category)already ran.
perf(startup): defer pip-internal network imports in fileutils + internet(7335a1c6)and
perf(startup): defer pip-internal InstallCommand + unpack + Downloader imports(74a466b1) —the parent CLI and resolver subprocess were both pulling
pip._internal.network.download,pip._internal.commands.install.InstallCommand,pip._internal.operations.prepare(~78 ms each, cumulative) at moduleload. Moved these to first-use inside the functions that need them.
Tests follow the import-target rename (the prior fix for the same
symptom on integration tests is at
c85cd3b6).perf(resolver): eliminate redundant find_best_candidate walk in resolve_constraints(cf53eb17) —Resolver.resolve_constraintscalled pip'sPackageFinder.find_best_candidate(name, specifier)onceper resolved package solely to read
candidate.link.requires_python. The resolved tree alreadycarries that link (pip's resolvelib stores the chosen candidate on every
InstallRequirementitreturns). Read the marker directly off
result.link. Measured on the 100-package bench:in-process 31.4 s → 23.4 s (−25.5 %); subprocess warm ~22.6 s → ~17.9 s (−21 %, ~4.7 s saved).
Lockfile hash byte-identical for standard pipenv users.
Subtle interaction with
tasks/vendoring/patches/patched/pip_finder_ignore_compatability.patch(commit
3d16ca04documents this explicitly): users who flipfinder._ignore_compatibility = Truein the resolve pipeline (cross-platform locking workflows,the patched-pip flag) will see cross-compat packages now CARRY their advertised
requires-pythonmarkers in the lockfile. Pre-fix, the strict
find_best_candidatereturnedNonefor thosecandidates and the marker was silently dropped. This is arguably a correctness fix but IS a
behavioural change for any consumer that relied on those markers being absent. A regression-
pinning test (
test_resolve_constraints_marker_for_ignore_compatibility_link) and a docstringon
resolve_constraintsdocument the trade-off.perf(lock): feed prior Pipfile.lock pins as pip constraints on warm relock(20092de9)was reverted (
a2157da4) after maintainer review caught a semanticregression: the change silently froze wildcard versions across
pipenv lockruns, breaking the historical contract thatpipenv lockpicks up newer matching versions. The revert is preserved in history
for traceability.
Measured CI delta vs the pre-phase-5 baseline (median across the bench
suite, single CI run — not multi-run statistical):
These are real-but-modest gains. The architectural ceiling is documented
in detail in
docs/dev/initiative-g-pure-python-design.md§2.2.Initiative G phases 1 + 2 — pure-Python resolver groundwork
A new
pipenv/resolver/surface (zeropip._internal.*imports, enforcedby a pre-commit grep gate scoped to
pipenv/resolver/):pipenv/resolver/candidate.py—Candidatedataclass +Hashnamedtuple +
Candidate.from_filenamehelper. Frozen, slotted, puredata; wheel tags derived once at parse time via
pipenv.vendor.packaging.tags.pipenv/resolver/pep691_types.py—SimplePageResponseandFetchErrortyped result envelopes.pipenv/resolver/pep691.py—_parse_pep691_json(PEP 691 JSON),_parse_pep503_html(PEP 503 HTML fallback), andPEP691Clientclass.Threads per-request
verify/certinto the underlying session(FU3, landed in this PR). Deliberately does not send
Cache-Control: max-age=0(deliberate divergence from pip).pipenv/resolver/manifest_cache.py—ParsedManifestCachewithTTL, atomic write, schema versioning, and (per FU1, landed here)
peek_etagfor stale-cacheIf-None-Matchshort-circuits.pipenv/resolver/fetcher.py—ParallelFetcherwith capped 16-workerThreadPoolExecutor. SendsIf-None-Matchfor stale cache entries(FU1); option-a TTL refresh on
304 Not Modified.pipenv/resolver/auth.py— netrc / URL-embedded basic-auth /PIP_CLIENT_CERThelpers. Keyring deferred to Phase 3.Phase-2 integration (opt-in, off by default):
[pipenv] prefetch_index_manifests+ env-var overridePIPENV_PREFETCH_INDEX_MANIFESTS=1. When enabled,do_lockcalls_prefetch_index_manifests_if_enabledto fan outper-source parallel pre-fetches (FU2, per-
verify_sslpolicy)through pip's own
PipSession— so pip'sSafeFileCacheis warmedas a side effect (no on-disk format reverse engineering).
User-facing doc for the new setting in
docs/pipfile.md.Why opt-in / why no measured Phase-2 perf claim
T21 (CI bench measurement for the prefetch path) was explicitly
deferred during execution review — no appetite for a multi-run
statistically-guarded CI bench step at this time. The Phase-2 perf
hypothesis (parallel cold-cache pre-fetch helps slow networks) is sound
based on the phase-5 I/O analysis but is theoretical, not measured
against the current CI baseline. The design doc §11a (
Phase 2a — sign-off note)is explicit about this; the user-doc points readers at the phase-5
branch history rather than quoting a percentage.
Phase-3 follow-ups landed early
Three items originally scoped as Phase-3 work were resolved during the
plan execution after their respective Wave agents flagged them:
91c1e4e9) —ParsedManifestCache.peek_etag()+ fetcherintegration. Closes a dead-code path: T19's
status="not-modified"TTL-refresh branch was unreachable before this commit because the
fetcher never sent
If-None-Match. Now stale-but-present cache entriesshort-circuit to a 304 instead of re-downloading.
4a0ff8a1) — per-sourceverify_sslfan-out in_prefetch_index_manifests_if_enabled. Original T19 cut routed everytarget through the majority-verify session; mixed-policy projects
(self-signed private index alongside public PyPI) silently fell
through to pip's cold fetch. Now one
ParallelFetcherper uniqueverify_sslvalue. Single-policy projects (common case) unchanged.0047a2e3) —PEP691Client.fetchthreadsverify/certinto the per-request
session.request(...)call. Pre-fix thesekwargs were stored on
selfbut never reached the request — soFU2's per-source verify routing was effectively a no-op at the
request layer until FU3 landed.
Bug fixes caught during execution
c85cd3b6— fixes 4 integration tests intest_import_requirements.pythat monkey-patched
pipenv.utils.dependencies.unpack_url(no longera module attribute after the phase-5 lazy-import work). Patch target
moved to the canonical source (
pipenv.utils.unpack.unpack_url).c76ecb42— T20's integration test surfaced a cache-pathmismatch: T17's
_clear_parsed_manifest_cachewiped<PIPENV_CACHE_DIR>/manifests-v1/while T19's prefetch wrote to<PIPENV_CACHE_DIR>/pipenv-manifests/manifests-v1/. Aligned both onthe namespaced path.
Test plan
the design-doc commit
a6832ce3; +307 from Initiative G alone).99.68 % across the six new modules (603 statements, 2 missed
— the Windows-only
_netrcbranch inauth.py, reaches 100 % onWindows CI).
Link.from_json/Link.from_elementacross the T2 fixturesuite (six, django, cryptography, tablib, yanked-pkg, missing-hash
× JSON + HTML, including a 3496-file
cryptographysnapshot).Zero semantic divergences; representation diffs documented in
tests/unit/test_pep691_parity_known_diffs.md.(self-signed-cert fixture not available — gap documented in
docstring for a future Phase-3 follow-up).
maintained at 100 % on
candidate.py,fetcher.py,manifest_cache.py,pep691.py,pep691_types.py.Test fixtures added
tests/unit/fixtures/pep691/*.jsonandtests/unit/fixtures/pep503/*.htmlcontain real-PyPI snapshots captured 2026-05-12 plus two hand-crafted
synthetics (
yanked-pkg,missing-hash) exercising edge cases.Provenance and re-baseline procedure documented in
tests/unit/fixtures/README.md.Companion docs
docs/dev/initiative-g-pure-python-design.md— Initiative G design(motivation, architecture, four-phase plan, dependency strategy, open
questions for sign-off). Status updated to reflect Phase 1 + 2 shipped.
initiative-g-phase1-2-plan.md— the 22-task dependency-aware swarmplan that executed Phases 1 + 2 (21 tasks done, T21 deferred, 3
Phase-3 follow-ups landed early). Each task's
status:/log:/files edited/created:fields populated post-execution.docs/pipfile.md— user-facing entry for[pipenv] prefetch_index_manifestsalongside the existing
cool-down-periodblock.News fragments:
news/initiative-g-phase1-pep691-client.feature.rst— Phase 1 ship.news/initiative-g-phase2-prefetch-bridge.feature.rst— Phase 2 ship.Migration notes
None. Per-commit details:
pipenv/resolver/. Noexisting imports break; the modules are reachable via
from pipenv.resolver import PEP691Client, ParsedManifestCache, ParallelFetcher, Candidate, Hash, FetchError, SimplePageResponse, CachedManifestif any caller wants them. Phase 1 ships themstandalone; Phase 2's
do_lockhook is gated on the opt-in setting.fileutils.py,internet.py,dependencies.py,project.py,environment.py,utils/resolver.py) keep allpublic attribute names addressable. Tests that monkey-patched
module-level symbols that became function-scope imports were updated
to point at the canonical source — the
unpack_urlfix inc85cd3b6covers the four cases CI flagged. No other tests in therepo follow the same anti-pattern (verified by sweep).
What's NOT in this PR
deferred per maintainer scope call. The Phase-2 perf claim is
theoretical, not measured. Design-doc §11a documents this.
pure_pythonbackend that replaces pip'sPackageFinder.find_all_candidatesvia aresolvelib.Providerimplementation. Three follow-up items (FU1, FU2, FU3) that were
scoped as Phase-3 work landed here, but the main Phase-3 deliverable
(the backend itself) is a separate future PR.
scenario; needs work in
tests/pytest-pypi/that's out of scope here.unchanged. Adding a
PIPENV_PREFETCH_INDEX_MANIFESTS=1matrix entrywould require T21-style statistical guards; deferred.
🤖 Generated with Claude Code