Skip to content

Maintenance/code cleanup 2026-05 Phase V (performance and pure python resolver prework) - #6668

Open
matteius wants to merge 48 commits into
maintenance/code-cleanup-phase4-deferred-2026-05from
maintenance/code-cleanup-phase5-perf-2026-06
Open

Maintenance/code cleanup 2026-05 Phase V (performance and pure python resolver prework)#6668
matteius wants to merge 48 commits into
maintenance/code-cleanup-phase4-deferred-2026-05from
maintenance/code-cleanup-phase5-perf-2026-06

Conversation

@matteius

@matteius matteius commented May 12, 2026

Copy link
Copy Markdown
Member

Summary

Phase V of the 2026-05 modernization track. Two intertwined workstreams:

  1. 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 ceiling
    is pip's per-package sequential index revalidation and Link.from_json
    loop, which pipenv can't move from outside pip.

  2. 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 zero
    pip._internal.* symbols. Phases 1 + 2 of a four-phase plan ship here;
    Phase 3 (the full pure-python backend replacing pip's PackageFinder)
    and Phase 4 (promote to default) are explicitly future work.

Companion design + plan docs in docs/dev/initiative-g-pure-python-design.md
and initiative-g-phase1-2-plan.md. No user-visible API change, no new
runtime 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_lock always iterated ["default", "develop", …] even when a section
    was empty, paying ~5-6 s of subprocess + Resolver/Session setup to produce
    an empty lockfile section. Skip the call when packages is an empty
    mapping. 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 module
    load. 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_constraints called pip's PackageFinder.find_best_candidate(name, specifier) once
    per resolved package solely to read candidate.link.requires_python. The resolved tree already
    carries that link (pip's resolvelib stores the chosen candidate on every InstallRequirement it
    returns). 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 3d16ca04 documents this explicitly): users who flip
    finder._ignore_compatibility = True in the resolve pipeline (cross-platform locking workflows,
    the patched-pip flag) will see cross-compat packages now CARRY their advertised requires-python
    markers in the lockfile. Pre-fix, 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
    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 docstring
    on resolve_constraints document 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 semantic
    regression: the change silently froze wildcard versions across
    pipenv lock runs, breaking the historical contract that pipenv lock
    picks 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):

stat before after delta
lock-warm 21.295 s 19.250 s −9.6 %
lock-cold 25.389 s 22.695 s −10.6 %
install-warm 19.951 s 18.648 s −6.5 %
install-cold 43.191 s 41.796 s −3.2 %
add-package 30.740 s 27.405 s −10.8 %
import (full) 73.263 s 69.889 s −4.6 %

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 (zero pip._internal.* imports, enforced
by a pre-commit grep gate scoped to pipenv/resolver/):

  • pipenv/resolver/candidate.pyCandidate dataclass + Hash
    namedtuple + Candidate.from_filename helper. Frozen, slotted, pure
    data; wheel tags derived once at parse time via pipenv.vendor.packaging.tags.
  • pipenv/resolver/pep691_types.pySimplePageResponse and
    FetchError typed result envelopes.
  • pipenv/resolver/pep691.py_parse_pep691_json (PEP 691 JSON),
    _parse_pep503_html (PEP 503 HTML fallback), and PEP691Client class.
    Threads per-request verify / cert into 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.pyParsedManifestCache with
    TTL, atomic write, schema versioning, and (per FU1, landed here)
    peek_etag for stale-cache If-None-Match short-circuits.
  • pipenv/resolver/fetcher.pyParallelFetcher with capped 16-worker
    ThreadPoolExecutor. Sends If-None-Match for stale cache entries
    (FU1); option-a TTL refresh on 304 Not Modified.
  • pipenv/resolver/auth.py — netrc / URL-embedded basic-auth /
    PIP_CLIENT_CERT helpers. Keyring deferred to Phase 3.

Phase-2 integration (opt-in, off by default):

  • New setting [pipenv] prefetch_index_manifests + env-var override
    PIPENV_PREFETCH_INDEX_MANIFESTS=1. When enabled,
    do_lock calls _prefetch_index_manifests_if_enabled to fan out
    per-source parallel pre-fetches (FU2, per-verify_ssl policy)
    through pip's own PipSession — so pip's SafeFileCache is warmed
    as 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:

  • FU1 (91c1e4e9) — ParsedManifestCache.peek_etag() + fetcher
    integration. 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 entries
    short-circuit to a 304 instead of re-downloading.
  • FU2 (4a0ff8a1) — per-source verify_ssl fan-out in
    _prefetch_index_manifests_if_enabled. Original T19 cut routed every
    target 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 ParallelFetcher per unique
    verify_ssl value. Single-policy projects (common case) unchanged.
  • FU3 (0047a2e3) — PEP691Client.fetch threads verify / cert
    into the per-request session.request(...) call. Pre-fix these
    kwargs were stored on self but never reached the request — so
    FU2'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 in test_import_requirements.py
    that monkey-patched pipenv.utils.dependencies.unpack_url (no longer
    a 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-path
    mismatch: T17's _clear_parsed_manifest_cache wiped
    <PIPENV_CACHE_DIR>/manifests-v1/ while T19's prefetch wrote to
    <PIPENV_CACHE_DIR>/pipenv-manifests/manifests-v1/. Aligned both on
    the namespaced path.

Test plan

  • Full unit suite green locally: 1263 tests passing (+341 since
    the design-doc commit a6832ce3; +307 from Initiative G alone).
  • Targeted resolver-module coverage (T17's CI gate, narrow scope):
    99.68 % across the six new modules (603 statements, 2 missed
    — the Windows-only _netrc branch in auth.py, reaches 100 % on
    Windows CI).
  • Phase-1 acceptance (T10): byte-equivalent parity vs pip's
    Link.from_json / Link.from_element across the T2 fixture
    suite (six, django, cryptography, tablib, yanked-pkg, missing-hash
    × JSON + HTML, including a 3496-file cryptography snapshot).
    Zero semantic divergences; representation diffs documented in
    tests/unit/test_pep691_parity_known_diffs.md.
  • Phase-2 integration (T20): 5 scenarios passing, 1 skipped
    (self-signed-cert fixture not available — gap documented in
    docstring for a future Phase-3 follow-up).
  • FU1+FU2+FU3 each carry their own dedicated tests; coverage
    maintained at 100 % on candidate.py, fetcher.py,
    manifest_cache.py, pep691.py, pep691_types.py.
  • CI: Lint, build, unit, resolver-module-coverage gate, benchmark.

Test fixtures added

tests/unit/fixtures/pep691/*.json and tests/unit/fixtures/pep503/*.html
contain 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 swarm
    plan 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_manifests
    alongside the existing cool-down-period block.

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:

  • All Initiative G modules are new code under pipenv/resolver/. No
    existing imports break; the modules are reachable via
    from pipenv.resolver import PEP691Client, ParsedManifestCache, ParallelFetcher, Candidate, Hash, FetchError, SimplePageResponse, CachedManifest if any caller wants them. Phase 1 ships them
    standalone; Phase 2's do_lock hook is gated on the opt-in setting.
  • Lazy-import changes (fileutils.py, internet.py, dependencies.py,
    project.py, environment.py, utils/resolver.py) keep all
    public 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_url fix in
    c85cd3b6 covers the four cases CI flagged. No other tests in the
    repo follow the same anti-pattern (verified by sweep).

What's NOT in this PR

  • T21 (CI bench measurement for the prefetch path) — explicitly
    deferred per maintainer scope call. The Phase-2 perf claim is
    theoretical, not measured. Design-doc §11a documents this.
  • Phase 3 — the full pure_python backend that replaces pip's
    PackageFinder.find_all_candidates via a resolvelib.Provider
    implementation. 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.
  • Self-signed-cert integration test fixture — T20's skipped
    scenario; needs work in tests/pytest-pypi/ that's out of scope here.
  • CI bench workflow changes — the existing benchmark job is
    unchanged. Adding a PIPENV_PREFETCH_INDEX_MANIFESTS=1 matrix entry
    would require T21-style statistical guards; deferred.

🤖 Generated with Claude Code

@matteius matteius changed the title Maintenance/code cleanup phase5 perf 2026 06 Maintenance/code cleanup 2026-05 Phase V (performance and pure python resolver prework) May 12, 2026
@matteius
matteius marked this pull request as ready for review May 12, 2026 23:11
@matteius
matteius requested a review from Copilot May 13, 2026 03:19

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.

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

  • REGISTRY is annotated as dict[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 like dict[str, type[Backend] | Backend] (or introduce a dedicated BackendEntry type 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.

Comment thread pipenv/resolver/core.py
@matteius
matteius force-pushed the maintenance/code-cleanup-phase5-perf-2026-06 branch from 9697a6c to 0307e31 Compare August 3, 2026 19:30
@matteius
matteius changed the base branch from maintenance/code-cleanup-phase4-resolver-followups-2026-05 to maintenance/code-cleanup-phase4-deferred-2026-05 August 3, 2026 19:30
@matteius

matteius commented Aug 3, 2026

Copy link
Copy Markdown
Member Author

Restacked onto the updated Phase IV tip and corrected this PR's base from the obsolete maintenance/code-cleanup-phase4-resolver-followups-2026-05 branch to maintenance/code-cleanup-phase4-deferred-2026-05. Range-diff confirms all 46 Phase V commits remain patch-equivalent.

Local verification: 1,283 unit tests passed, 10 platform skips; full pre-commit suite passed.

matteius and others added 19 commits August 3, 2026 15:40
``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>
…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>
matteius and others added 22 commits August 3, 2026 15:40
…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>
Folds T9 (ParallelFetcher) and T13 (PEP691Client test suite) plan
updates into a single tracking commit.  Both landed in their own
commits already: 198acbd, cc7c0f2.

Initiative G phase 1 — Wave 5 plan-tracking.

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>
…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>
@matteius
matteius force-pushed the maintenance/code-cleanup-phase5-perf-2026-06 branch from 0307e31 to eaabe12 Compare August 3, 2026 19:40
Comment thread tests/integration/test_prefetch_manifest.py Fixed
@matteius

matteius commented Aug 3, 2026

Copy link
Copy Markdown
Member Author

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
@matteius

matteius commented Aug 3, 2026

Copy link
Copy Markdown
Member Author

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
Sign up for free to 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.

4 participants