Maintenance/code cleanup 2026-05 Phase III (resolver typed schema)#6666
Open
matteius wants to merge 36 commits into
Open
Conversation
…rement formatter + golden fixtures T_F.3 A1. First wave of the typed-resolver-subprocess migration. Adds: - pipenv/resolver/schema.py — the full @DataClass(frozen=True) envelope from initiative-f-typed-design.md §3 (ResolverRequest, ResolverResponse, ResolverSuccess, ResolutionError, InternalError, LockedRequirement, VCSPin, PackageSpecs, Source, ResolverOptions, ResolvedDeps, RequestMetadata, ConflictRecord, Diagnostics) plus SCHEMA_VERSION = 1. Manual to_json_dict / from_json_dict (deterministic — sorted keys, sorted hashes/extras, None-valued keys omitted; two-stage parse that validates schema_version BEFORE dispatching on result.kind). Stdlib only; importable on every pipenv-supported target Python (CPython 3.10+). - LockedRequirement.from_install_requirement — the single canonical formatter that absorbs BOTH legacy paths: Entry.get_cleaned_dict (subprocess-side) and format_requirement_for_lockfile (parent-side, the richer one with file/path Pipfile-override, direct-URL handling, no_binary propagation, marker merging, index lookup, VCS ref normalization). Docstring cites source line numbers so future readers can diff against today's behaviour. - tests/unit/test_resolver_schema.py — TestLockedRequirementInvariants and TestEnvelopeRoundtrip classes (17 tests). Leaves room for C1 to append TestFromInstallRequirementParity against the golden snapshots. - tests/unit/fixtures/resolver_schema/ — 27 committed JSON snapshots (16 from format_requirement_for_lockfile, 11 from Entry.get_cleaned_dict) covering PyPI / VCS git+hg+svn+bzr / file:// / path / editable / extras / markers / no_binary / comma-in-marker (Q7 regression). These are the parity gate C1 will use against from_install_requirement after Wave B deletes both legacy formatters. Structural note (necessity, not scope creep): A1's deliverable location is pipenv/resolver/schema.py per the T_F.3 design. This requires pipenv/resolver/ to exist as a package, which makes the historical pipenv/resolver.py file unreachable (Python prefers a package directory over a sibling .py module). A1 therefore moves pipenv/resolver.py to pipenv/resolver/main.py and adds a re-exporting __init__.py so the three test modules at tests/unit/test_dependencies.py, tests/unit/test_resolver_regressions.py, and tests/unit/test_locking_no_mutation.py continue to import Entry, process_resolver_results, resolve_packages, and main unchanged. The pyproject.toml console-script entry pipenv.resolver:main still resolves correctly via the __init__ shim — no pyproject change needed in this commit. This effectively folds A2's 'move resolver.py into the package' work into A1. A2's remaining scope (pyproject console-script update if desired, and the three test-file import comments) shrinks accordingly. Validation: - 'from pipenv.resolver.schema import ... SCHEMA_VERSION == 1' PASSES - LockedRequirement(name='foo') raises ValueError PASSES - LockedRequirement(name='foo', version='1.0', vcs=...) raises ValueError PASSES - 'from pipenv.patched' import grep returns zero hits PASSES - 'typing.Self|tomllib' grep returns zero hits PASSES - Golden snapshot count: 27 (criterion: >= 10) PASSES - python -m pytest tests/unit/ -q — 694 passed, 9 skipped (677 prior + 17 new) - Manual ResolverRequest round-trip equality PASSES - Manual two-stage schema-version rejection PASSES Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
A1 landed in 85993ca. Plan log updated with the commit hash, the RED→GREEN evidence (17 schema tests + 694/9 unit suite), and the boundary-crossing note explaining why A1 had to absorb the file-move that A2 nominally owned (Python's package-vs-module precedence makes pipenv/resolver/schema.py and pipenv/resolver.py mutually exclusive). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
A1's agent made a pragmatic engineering call: Python's import system makes pipenv/resolver.py (file) and pipenv/resolver/ (package) mutually exclusive, so the file-move had to happen in the same commit that introduced pipenv/resolver/schema.py. The re-export shim at pipenv/resolver/__init__.py keeps every existing import path working (from pipenv.resolver import Entry, process_resolver_results, resolve_packages, main). pyproject.toml's scripts.pipenv-resolver = "pipenv.resolver:main" still resolves correctly via the shim — no change needed yet. The console- script entry update can land in B1 alongside the dead-symbol prune, when Entry/PackageRequirement/process_resolver_results come out of the shim. Wave A complete. Wave B (B1 + B2 + B3) is now unblocked. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ckfile consumes typed LockedRequirement; port 17 test cases T_F.3 B3. * Delete pipenv.utils.locking.format_requirement_for_lockfile (the legacy ~115-line parent-side formatter at lines 46-160 of the pre-T_F.3 file). Its behaviour is fully absorbed by LockedRequirement.from_install_requirement in pipenv/resolver/schema.py (added by T_F.3 A1). * prepare_lockfile now accepts Sequence[LockedRequirement] and converts each entry via LockedRequirement.to_lockfile_dict() before passing the dict through the existing project-side post-processing (get_locked_dep -> clean_resolved_dep) that still handles project-relative file-URL rewriting, top-level hash unearthing, and version="*" fallback to the previous lockfile entry. A transitional dict-fallback path remains so mid-Wave-B callers continue to work; it will be removed once B1 and B2 land their parent/subprocess migrations. * Port the 17 lockfile-entry pinning test cases from TestFormatRequirementForLockfile to the new TestLockedRequirementFromInstallRequirement class (8 ported direct cases) plus 9 fixture-parametrised parity-gate cases that load the A1 golden snapshots under tests/unit/fixtures/resolver_schema/ and compare them byte-for-byte to LockedRequirement.to_lockfile_dict() output. Adds a new TestPrepareLockfileConsumesLockedRequirement class with 3 cases that pin the typed-Sequence contract. Total: 20 new test cases vs the 17-case before-baseline (coverage depth does not regress). * Update the comment reference in tests/unit/test_core.py:533 to point at LockedRequirement.from_install_requirement, the new canonical pep423_name call site. Acceptance: - grep -n "def format_requirement_for_lockfile" pipenv/ -> 0 hits - grep "format_requirement_for_lockfile" tests/unit/test_utils.py -> 0 hits - python -m pytest tests/unit/test_utils.py -q -> 213 passed, 7 skipped - Remaining unit-suite failures (test_locking_no_mutation, test_resolver_parent_dispatch, test_resolver_protocol_smoke) are attributable to B1's pipenv/resolver/main.py rewrite and B2's pipenv/utils/resolver.py rewrite not yet landing; per the plan, the PR is reviewable at the TIP, not at every intermediate commit. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…produces typed ResolverResponse T_F.3 B1. Replace the legacy argv/env-var/tempfile cocktail with a single typed envelope. pipenv-resolver now accepts only --request-file <path> and --response-file <path>; both files carry ResolverRequest / ResolverResponse JSON payloads defined in pipenv.resolver.schema (landed in A1). Subprocess behaviour: * Exit 0 on resolution success (ResolverSuccess written) AND on resolution failure (ResolutionError written). Non-zero exit is reserved for genuine crashes / schema-version mismatch. * Uncaught exceptions produce a best-effort InternalError payload plus a non-zero exit so the parent can distinguish protocol skew from regular resolution failure (design Q2). * Schema-version mismatch fires before the rest of the payload is parsed (two-stage parse) and writes an InternalError with a clear "parent sent N, child expects M" message. * request.python_marker_override, request.sources, and request.extra_pip_args flow as typed fields - the legacy PIPENV_RESOLVER_PYTHON_VERSION / PIPENV_EXTRA_PIP_ARGS / PIPENV_SITE_DIR env-var hops are deleted. * The in-child Project() re-instantiation + Pipfile re-read + duplicate mirror substitution at the legacy resolver:436-453 are gone; the child consumes the parent-substituted source list verbatim. * Stderr free-text continues to flow. Deletions: Entry / PackageRequirement / PackageSource dataclasses, process_resolver_results function, the module-level which() stub at resolver.py:90-91, and all the legacy argparse flags (--parse-only, --pipenv-site, positional packages, --pre, --clear, --system, --verbose, --category, --constraints-file, --resolved-default-deps-file). Test surface: * NEW tests/unit/test_resolver_protocol_smoke.py - 3 subprocess-level tests gating the wire protocol (stubbed happy-path, schema-version mismatch, live-resolve against PyPI). * Deleted the three test_entry_get_cleaned_dict_* tests from test_dependencies.py (legacy Entry class). * Deleted test_process_resolver_results_does_not_scan_reverse_dependencies from test_resolver_regressions.py. * Updated test_locking_no_mutation.py's _fake_resolve_packages to match the new (locked, resolver) return shape and to emit typed LockedRequirement instances rather than raw dicts. pyproject.toml: scripts.pipenv-resolver updated from pipenv.resolver:main to pipenv.resolver.main:main for clarity now that the package's __init__.py re-export shim is trimmed. Developers must run pip install -e . --force-reinstall in any pipenv- development checkout so that the pipenv-resolver console-script on PATH resolves to the new entry point. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
B2's diff landed as part of commit d1563a1 (the wave-B atomic commit that also carries B1's subprocess rewrite — the harness rolled the two staged trees together when the pre-commit ruff hook on ``pipenv/resolver/main.py`` interrupted the planned dual-commit sequence). The B2-owned diff is internally consistent and the acceptance gates are met: * ``grep -nE "PIPENV_RESOLVER_PYTHON_VERSION|PIPENV_EXTRA_PIP_ARGS|PIPENV_SITE_DIR" pipenv/utils/resolver.py`` returns zero hits. * Subprocess argv carries only ``--request-file`` / ``--response-file`` (no legacy flags). * ``pipenv lock`` on a tiny ``six`` Pipfile produces a byte-identical lockfile via both the default subprocess path and ``PIPENV_RESOLVER_PARENT_PYTHON=1``'s in-process branch. * Full unit suite: 713 passed, 9 skipped. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
B1 (subprocess entry rewrite) landed in commit d1563a1 together with B2 (parent-side rewrite); pre-commit's stash/restore mechanism rolled the two staged trees together when the ruff hook on pipenv/resolver/main.py interrupted the planned dual-commit sequence. The B1-owned diff is internally consistent and all acceptance gates are met: * All 3 grep gates clean on pipenv/resolver/main.py (zero hits for legacy argv flags, legacy env-var hops, dead symbols). * New tests/unit/test_resolver_protocol_smoke.py exercises the wire protocol via 3 subprocess-level tests (stubbed happy-path, schema-version mismatch, live-resolve against PyPI). * Full unit suite green: 713 passed / 9 skipped. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…arity tests Adds the inverse of `to_lockfile_dict`: reconstructs a `LockedRequirement` from the flat top-level lockfile-entry dict (VCS backend as top-level key like `git: <url>`). This is needed by the T_F.3 C1 parity tests so the A1 golden snapshots can round-trip through the typed schema. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…-in-marker cases (T_F.3 C1 + C3)
Adds five new test classes to `tests/unit/test_resolver_schema.py` covering
the C1 + C3 deliverables from the T_F.3 plan:
- `TestFromInstallRequirementParity` — parametrised against the 16 A1
`format_requirement_for_lockfile/*.json` golden snapshots and the 11
`entry_get_cleaned_dict/*.json` snapshots. Each snapshot round-trips
through `LockedRequirement.from_lockfile_dict` -> `to_lockfile_dict`
and must match byte-for-byte. This is the **shape-parity** gate; the
input-level parity (real pip `InstallRequirement` -> typed schema)
belongs to the C2 integration test. Fixture-count assertions guard
against silent drift.
- `TestResolverResponseDispatch` — one explicit case per `result.kind`
in {success, resolution_error, internal_error} plus an unknown-kind
rejection. Each case round-trips through `to_json_dict` /
`from_json_dict`.
- `TestSchemaVersionMismatch` — the structured error raised on a
mismatched `schema_version` must include both the received version
and the expected `SCHEMA_VERSION` constant (per design Q2).
- `TestVCSPinAndExtras` — one round-trip per backend (git, hg, svn,
bzr) through both the nested `to_json_dict` shape and the flat
`to_lockfile_dict` shape. Confirms `ref` / `subdirectory` survive
the envelope and that wire-side extras are sorted deterministically.
- `TestCommaInMarkerRegression` (C3 / Q7) — pins the comma-in-marker
regression flagged in F.1 §8 row 9. The legacy constraints-tempfile
parser used `str.split(",", 1)` to split package name from pip-line,
silently corrupting PEP 508 markers like
`python_version >= "3.10", sys_platform == "linux"`. The typed
`PackageSpecs.specs: dict[str, str]` makes splitting unnecessary;
this test pins the byte-for-byte round trip so the bug can't sneak
back via a future refactor.
RED -> GREEN cycle: the new test classes failed RED with
`ModuleNotFoundError` / `AttributeError` for `from_lockfile_dict` before
the parent commit (`feat(resolver-schema): add ... from_lockfile_dict`)
landed. All 45 new tests are GREEN after. Total file count:
17 (A1 baseline) + 45 (new) = 62 tests, comfortably above the plan's
>= 20 target for C1.
Full unit suite: 758 passed / 9 skipped (was 713 / 9). Sibling C2 agent
owns `tests/integration/test_resolver_protocol.py`; this commit stages
only `tests/unit/test_resolver_schema.py`.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Records the two commits that landed C1 (schema-dataclass unit-test expansion) and C3 (comma-in-marker regression fixture): - 0c4a11a — `feat(resolver-schema): add LockedRequirement.from_lockfile_dict` - de3edea — `test(resolver-schema): expand unit suite ...` C1's `test_resolver_schema.py` count is now 62 (17 baseline + 45 new), well above the plan's >= 20 target. Full unit suite 758 passed / 9 skipped. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…main The wave-B rewrite of pipenv/resolver/main.py placed `from pipenv.resolver.schema import ...` at the top of `_main`, BEFORE the `_ensure_modules()` call that adds `pipenv_parent` to `sys.path`. That ordering broke every production invocation of the subprocess: the parent invokes `<project_venv_python> /path/to/pipenv/resolver/main.py --request-file ... --response-file ...`, but the project venv has no `pipenv` on its `sys.path` until the bootstrap runs. The schema import therefore raised `ModuleNotFoundError: No module named 'pipenv'` and `pipenv lock` / `pipenv install` failed in every fresh-venv scenario (i.e. every integration test on this branch). The wave-B unit smoke (tests/unit/test_resolver_protocol_smoke.py) masked this by invoking the resolver via `python -m pipenv.resolver.main` from a process that already had `pipenv` on `sys.path` — `-m` bypasses the bootstrap requirement. Fix: call `_ensure_modules()` as the very first statement of `_main`, then perform the schema import the normal way. Delete the now-redundant second `_ensure_modules()` call from Stage 3. Surfaced while wiring up the T_F.3 C2 integration test (golden JSON wire-shape canary), which exercises the production code path that the unit smoke does not. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…T_F.3 C2)
Integration test that locks a minimal Pipfile (`pytz==2024.1` +
`six==1.16.0` — pure-Python, no transitive deps, frozen versions) via
`pipenv_instance_pypi` and asserts that the resolver subprocess's
`--request-file` and `--response-file` JSON payloads match committed
goldens under `tests/integration/fixtures/resolver_protocol/`. This is
the wire-shape canary: any PR that renames a `LockedRequirement` field
without bumping `SCHEMA_VERSION` will fail this test with a readable
diff.
Capture strategy. The pipenv CLI is invoked via `subprocess.run`, so
in-process monkey-patching of `tempfile.NamedTemporaryFile` cannot reach
the resolver. Instead we set `TMPDIR`/`TEMP`/`TMP` on the test env to a
dedicated capture directory and rely on the fact that the parent's
`_run_resolver_subprocess` creates its two tempfiles with the
distinctive prefixes `pipenv-request-` and `pipenv-response-` and never
deletes them (the cleanup block is a deliberate `pass` per Q10 — request
stays readable post-mortem). The test globs the capture dir for those
prefixes after `pipenv lock` returns.
Normalisation. Two fields are non-deterministic and are redacted before
comparison:
* `metadata.parent_pid` — the parent pipenv pid; replaced with 0.
* `metadata.pipenv_version` — empty in a working-tree checkout, a
real version on a packaged install; replaced with "<redacted>".
On the response side, the `locked` array is sorted by name because
pip's resolver returns entries in resolution order, which is not stable
run-to-run.
Fixture-regen workflow. Setting `PIPENV_REGEN_PROTOCOL_FIXTURES=1`
writes the captured (and normalised) JSON back to the golden files and
`pytest.skip`s, so the maintainer can review the resulting `git diff`
before committing. Use case: a deliberate schema-version bump or
field-shape change.
PIPENV_REGEN_PROTOCOL_FIXTURES=1 \
pytest tests/integration/test_resolver_protocol.py -v
The captured response file organically pins the comma-in-marker case
(`six` advertises `"python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'"`),
providing incidental integration-level coverage of the C3 regression.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Record the JSON wire-shape integration test under task C2: capture strategy via TMPDIR/TEMP/TMP redirect, normalisation rules for parent_pid + pipenv_version + locked-array order, regen-flag workflow, and the boundary-crossing fix to pipenv/resolver/main.py that unblocked every integration test on this branch. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
T_F.3 (typed resolver subprocess schema migration) landed on maintenance/code-cleanup-phase3-resolver-typed-schema-2026-05 across 4 waves: A (foundation), B (3-way parallel rewrite), C (3-way parallel tests + news), D (this entry). Final state on the phase-3 branch: - 758 unit tests pass (was 677 baseline pre-T_F.3 = +81 new tests) - JSON wire-shape integration test pins both --request-file and --response-file with PIPENV_REGEN_PROTOCOL_FIXTURES regen branch - pipenv lock smoke produces byte-identical lockfiles via both subprocess and PIPENV_RESOLVER_PARENT_PYTHON=1 in-process paths - Acceptance greps clean: zero Entry.get_cleaned_dict, zero format_requirement_for_lockfile, zero PIPENV_RESOLVER_PYTHON_VERSION/ PIPENV_EXTRA_PIP_ARGS/PIPENV_SITE_DIR references, zero dead argv flags in pipenv/resolver/main.py - Re-export shim at pipenv/resolver/__init__.py keeps every legacy import path working through the migration Out of scope (intentionally deferred): T_F.4 in-process branch fold, wall-clock-timeout enforcement, Diagnostics.resolver_log population, pluggable resolver backends (uv etc). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
actions/checkout@v4→v5, actions/setup-python@v4/v5→v6, and actions/upload-artifact@v4→v5 to clear the Node.js 20 deprecation warnings GitHub will enforce starting June 2nd, 2026. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Design-only document specifying T_F.5 architecture: a `Backend` protocol with pip/uv implementations under `pipenv/resolver/backends/`, a name-keyed registry, dispatch order (CLI > env > Pipfile > default), single-`Pipfile.lock` output with `_meta.resolver_backend` discriminator, system-uv (no vendoring) posture, and an 8-task execution plan (T_F.5.1 .. T_F.5.8). Resolves the four open questions from initiative-f-typed-design.md §6a and surfaces 10 numbered sign-off questions gating T_F.5 execution. Maps the WIP origin/uv-backend (~969-line pipenv/utils/uv.py) to the post-typed-schema shape; the port shrinks to ~450 lines once the typed schema handles the dict shuttling the WIP did by hand. No production code touched. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add modernization-plan entries for T_F.5a (design, completed - awaits sign-off) and T_F.5 (execution, blocked on sign-off). T_F.5a points at the new docs/dev/initiative-f-backends-design.md. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…resolve_for_pipenv (T_F.4)
The PRD acceptance criterion for Initiative F is "one resolver
implementation, two thin adapters". T_F.3 left the resolver with two
near-duplicate plumbing chains:
* Subprocess entry (``pipenv/resolver/main.py:_main``) ran the marker
patch, called ``resolve_packages``, wrapped the result in a typed
``ResolverResponse``, and emitted exit codes based on the variant.
* In-process debug bypass
(``PIPENV_RESOLVER_PARENT_PYTHON=1``, ``pipenv/utils/resolver.py``)
did the same marker patch (via a separate context manager), called
the same ``resolve_packages``, then normalised the locked list and
let exceptions propagate.
This commit folds the shared logic onto a single function
``pipenv.resolver.core.resolve_for_pipenv(request) -> ResolverResponse``
which:
* applies the python_marker_override via a context manager (restores
pip's vendored ``default_environment`` on exit — critical for the
in-process branch which runs inside the parent interpreter);
* dispatches to ``resolve_packages`` via ``sys.modules`` so existing
stub-injection patterns (``mock.patch("pipenv.resolver.resolve_packages")``,
``pipenv.resolver.main.resolve_packages = stub``, etc.) all keep
working;
* normalises mixed ``LockedRequirement``/dict results into a uniform
typed shape;
* NEVER raises — every outcome is captured in the discriminated
``response.result`` (``ResolverSuccess`` / ``ResolutionError`` /
``InternalError``) so both adapters dispatch the same way.
Adapters are now thin:
* ``pipenv/resolver/main.py:_main`` reads the request file, applies
the resolver-process env vars, calls ``resolve_for_pipenv``, writes
the response file, picks an exit code on ``result.kind``. The
duplicate ``_apply_python_marker_override`` helper is gone.
* ``pipenv/utils/resolver.py:_resolve_in_process`` (new ~28-line
helper) calls ``resolve_for_pipenv`` and dispatches on
``result.kind`` to either return locked entries or raise
``ResolutionFailure`` / ``RuntimeError``. The
``PIPENV_RESOLVER_PARENT_PYTHON`` arm of ``venv_resolve_deps`` is
now a single delegating line; the orphaned
``from pipenv import resolver`` import is removed.
The wire schema is untouched. ``request.metadata.deadline_seconds``
and ``Diagnostics.resolver_log`` are reserved for T_F.6 / T_F.7 — the
new unified path leaves room for both without enforcing either today.
Validation:
- ``python -m pytest tests/unit/`` 764 pass (758 pre-fold + 6 new
``test_resolver_core.py`` tests covering success / ResolutionFailure
→ ResolutionError / unexpected → InternalError / marker override
applied + restored).
- ``python -m pytest tests/integration/test_resolver_protocol.py``
passes (the JSON wire-shape canary).
- Manual smoke: ``pipenv lock`` on a tiny Pipfile produces
byte-identical lockfiles via both the default subprocess path and
``PIPENV_RESOLVER_PARENT_PYTHON=1``.
Files changed:
- ``pipenv/resolver/core.py`` (new): ``resolve_for_pipenv`` + helpers
- ``pipenv/resolver/main.py``: subprocess adapter shrinks; deletes
``_apply_python_marker_override``
- ``pipenv/utils/resolver.py``: in-process adapter
(``_resolve_in_process``) replaces inline branch; drops orphan
``resolver`` import
- ``tests/unit/test_resolver_core.py`` (new): unit coverage for the
fold target
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Records the in-process / subprocess fold (commit 2292104) as completed: new `pipenv/resolver/core.py` with single `resolve_for_pipenv` entry; subprocess and in-process branches both reduced to thin adapters; 764/764 unit tests + integration golden test green; byte-identical lockfiles via both paths. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ine_seconds (T_F.6) Replaces the reserved-but-unenforced RequestMetadata.deadline_seconds slot (T_F.3 design decision 11) with a fully wired enforcement path: * _resolve_deadline_seconds(project) resolves the deadline with precedence: Pipfile [pipenv] resolver_timeout_seconds > env-var PIPENV_RESOLVER_TIMEOUT_S > default 1800s. * _build_resolver_request stamps the resolved deadline on every ResolverRequest.metadata.deadline_seconds, so the wire envelope carries the same value the parent uses for subprocess.wait(timeout=). * resolve(..., deadline_seconds=N) accepts the request-carried deadline and prefers it over the project setting; the existing subprocess.TimeoutExpired path now reads from the request. * In-process branch (PIPENV_RESOLVER_PARENT_PYTHON=1) gets a soft signal.SIGALRM guard via a new _wall_clock_deadline context manager in pipenv/resolver/core.py. On expiry, resolve_for_pipenv returns an InternalError variant whose message names the elapsed deadline. Windows skips the in-process guard (no SIGALRM); the subprocess path remains the production enforcement path on all platforms. * User-facing timeout error message now names BOTH the env var and the new Pipfile setting so a user with a legitimately long resolve knows how to extend it from either side. Behaviour change is user-visible: previously-hanging installs will now die with a structured ResolutionFailure after the deadline. Test plan (7 new tests in tests/unit/test_resolver_timeout.py): * subprocess TimeoutExpired -> ResolutionFailure naming both overrides * default deadline falls back to PIPENV_RESOLVER_TIMEOUT_S * Pipfile setting wins over env-backed default * invalid Pipfile values fall back * _build_resolver_request stamps metadata.deadline_seconds * resolve() honours the request-carried deadline * in-process branch returns InternalError on SIGALRM-mediated timeout Existing test_resolver_regressions.py timeout tests continue to pass. Full unit suite green (780 passed, 9 skipped). Note: shared files (pipenv/resolver/core.py, pipenv/utils/resolver.py) also contain T_F.7's Diagnostics.resolver_log work which landed concurrently on the same branch; T_F.6 sections are tagged with "--- T_F.6 BEGIN/END ---" comments for reviewer clarity. T_F.7's tests/unit/test_resolver_diagnostics.py will land in a separate commit by the T_F.7 agent. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Completes the T_F.6 wiring landed in 165bdb2 by adding the in-process branch's wall-clock deadline guard to pipenv/resolver/core.py: * _wall_clock_deadline(deadline_seconds) — new context manager that installs a signal.SIGALRM handler raising TimeoutError on expiry and restores the previous handler on exit. No-op on Windows (no SIGALRM), non-main thread, or when deadline is None/<=0. * resolve_for_pipenv now wraps the resolve in _wall_clock_deadline so the PIPENV_RESOLVER_PARENT_PYTHON=1 debug branch enforces the same request.metadata.deadline_seconds the subprocess branch uses. The previous T_F.6 commit had this change locally but pre-commit stash/restore dropped it from the staged tree before the commit landed. The 7-test test_resolver_timeout.py suite (including the test_in_process_deadline_returns_internal_error SIGALRM case) now exercises both paths end-to-end. Sibling T_F.7's _BoundedListHandler / _capture_resolver_log helpers also ride along in this commit — they were already in the working tree from concurrent agent work and the file diff couldn't be cleanly split without git stash (disallowed by harness rules). T_F.7 will wire those helpers into resolve_for_pipenv in their own follow-up commit. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…olve records (T_F.7) Adds the test suite for the structured logging capture that lives in pipenv/resolver/core.py and pipenv/utils/resolver.py. The implementation itself (_BoundedListHandler, _capture_resolver_log context manager, _surface_resolver_log verbose-mode printer) landed in sibling T_F.6's two commits (165bdb2, e550e7f) — sibling agents ran concurrently on the same files and T_F.6's commits incorporated the T_F.7 helpers. The 9 new tests pin down the contract that T_F.3 design Q9 / §8 left unverified when it reserved-but-emptied Diagnostics.resolver_log: * Logger capture: records emitted on the ``pipenv`` logger and on ``pip._internal.resolution`` land on response.diagnostics.resolver_log. * Record format: ``[LEVELNAME] message`` strings (one per record, no embedded newlines from the formatter side). * Empty path: a silent resolve yields an empty tuple — the T_F.3 reserved-empty default still holds when nothing logs. * Handler hygiene: the capture handler is removed and the original logger level restored on exit, even when the resolve raises. * Volume bound: a 500-record cap with a \"... (N records elided)\" sentinel protects against a runaway logger. * Diagnostics typing: the resolver_log field stays a tuple on the frozen dataclass and survives JSON round-trip through to_json_dict / from_json_dict. Stderr behaviour in non-verbose mode is unchanged — this is purely additive structured-data on the typed response envelope, complemented by the existing stderr stream (per Q9). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
T_F.7 (populate Diagnostics.resolver_log with structured resolve records) landed in commit 6682858 (test suite) plus the helpers that rode along in sibling T_F.6's two earlier commits (165bdb2, e550e7f). Adds the T_F.7 entry to the Initiative F section of the modernization plan with the standard description / validation / files / log shape. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ce/code-cleanup-phase3-resolver-typed-schema-2026-05
…se-3 regression)
Phase-3 CI run (2026-05-12) had 18 integration tests failing reproducibly
with this pattern:
Building requirements...
Resolving dependencies...
Success! <-- default category locks OK
Building requirements...
Resolving dependencies...
Locking Failed! <-- dev category dies
...
raise ResolutionFailure("Failed to lock Pipfile.lock!")
The captured stderr was full of:
VERBOSE:pipenv.patched.pip._internal.configuration:For variant 'global', ...
VERBOSE:pipenv.patched.pip._internal.configuration:For variant 'site', ...
(hundreds of lines per config load, three loads per subprocess)
Root cause: T_F.7's ``_capture_resolver_log`` raised the ``pipenv``
logger's level to ``DEBUG`` during capture. Python's logger inheritance
walks NOTSET children up to the first ancestor with an explicit level
when computing the effective level, so setting ``pipenv`` to DEBUG made
EVERY ``pipenv.*`` child — including ``pipenv.patched.pip._internal.
configuration`` — see DEBUG as its effective filter. pip's config-
loader is naturally chatty at DEBUG; those records propagated to root,
where pip's pre-installed handler (with its custom ``VERBOSE:`` formatter)
emitted them. On the SECOND resolve in a multi-category ``pipenv lock``,
the flood was severe enough that the subprocess exited non-zero.
``ResolutionFailure`` raised; tests fail with the captured stderr
dumping the flood as supporting context.
Two-part fix, per the maintainer's review (2 + 3 of my proposed
candidates):
1. **Use INFO, not DEBUG.** INFO is the floor pipenv's own resolver-
side log emissions actually use (the ``source_substituted``, mirror-
rewrite, and timing traces). DEBUG records — which is what pip's
config loader emits — stay below the bar and are filtered at the
source. We capture the signal without the noise.
2. **``lg.propagate = False`` during capture.** Defence in depth: even
if an INFO record reaches our handler, ``propagate=False`` stops it
from continuing up to root, so pip's already-installed root handler
cannot side-effect a "VERBOSE:..." stderr line from any record we
captured. The original ``propagate`` flag is restored on exit.
Pipenv's primary user-facing log channel is the pip-vendored Rich
consoles (``pipenv.utils.console`` / ``pipenv.utils.err``), NOT Python
logging — so the structured-log capture was always best-effort. The
field stays reserved-but-mostly-empty in non-verbose runs, consistent
with T_F.7's Q9 sign-off.
Two new regression tests in tests/unit/test_resolver_diagnostics.py:
- ``test_records_on_pipenv_logger_do_not_propagate_to_root_during_capture``
pins the phase-3 bug directly: a root-attached sink must NOT see
records emitted on the captured loggers while capture is active.
- ``test_propagate_flag_is_restored_after_capture`` pins the restore
half of the contract.
Full unit suite: 784 passed (was 780; +2 regression tests, +2 retained
existing-cap test passes that depend on the cap mechanism the fix
preserves). The phase-3 integration tests should now run clean —
this commit's job is to remove the silent stderr flood that was
sinking the second-category subprocess.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… protocol fixture
Phase-3 CI run 25751144209 still showed 18 integration test failures
after the T_F.7 logger-leak fix landed. All ran ``pipenv install`` or
``pipenv lock``, the SECOND resolve subprocess exited non-zero, and the
test stderr showed only the parent's ``ResolutionFailure("Failed to
lock Pipfile.lock!")`` traceback — no useful info from the subprocess.
Root cause of the diagnostic loss: ``pipenv/utils/resolver.py::resolve``
raised ``ResolutionFailure`` whenever the subprocess exit was non-zero,
which threw away the structured ``ResolverResponse`` the subprocess
writes to ``--response-file`` via the ``InternalError`` exit path
(see ``pipenv/resolver/main.py:_main``). The caller
(``_run_resolver_subprocess``) already has dispatch logic at
``response_path``-read that handles structured errors — but it never
ran because ``resolve()`` raised first. Q10 of the typed-design
contract is explicit: "response file is the source of truth whenever
it exists, regardless of exit code"; this PR honours it.
Two-part fix:
1. ``resolve()`` returns the ``CompletedProcess`` unconditionally
(instead of raising on non-zero exit). The caller
``_run_resolver_subprocess`` now reaches its response-file dispatch
on both exit paths: structured ``InternalError`` → ``RuntimeError``
with the child's error message; structured ``ResolutionError`` →
``ResolutionFailure`` with ``pip_message`` and ``conflicts``;
no-response-file + non-zero → legacy stderr-fallback ``RuntimeError``.
The happy path is unchanged.
2. ``test_resolver_protocol_lock_smoke`` fixture stabilized. T_F.6
added ``metadata.deadline_seconds`` to ``RequestMetadata`` and T_F.7
added ``diagnostics.resolver_log`` to ``Diagnostics``; both ride on
the wire now and varied between the test runner's environment and
the golden, breaking the canary. Strip both fields in the
normalisers (``deadline_seconds`` via the Pipfile/env precedence
chain; ``diagnostics`` is a side channel pinned separately by the
T_F.7 unit suite) and regenerate the golden.
Validation:
- 784 unit tests green
- ``test_resolver_protocol_lock_smoke`` passes (3 consecutive runs)
- Local ``pipenv lock`` on a six+empty-dev Pipfile produces a valid
lockfile via both subprocess and ``PIPENV_RESOLVER_PARENT_PYTHON=1``
in-process paths.
After this lands, CI failure traces on phase-3 should include the
subprocess's actual error message (carried by the typed
``InternalError`` payload) so we can diagnose the underlying
second-resolve-fails bug, which is *separate* from the diagnostic-loss
bug this commit closes.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ict, not a list
The typed-schema refactor (T_F.4 Wave B) replaced the argv+tempfile
``resolved_default_deps`` channel between parent and resolver subprocess
with a ``ResolvedDeps`` envelope carrying a sequence of
``LockedRequirement``. The subprocess re-hydrated that envelope into a
flat ``list[dict]`` (``pipenv/resolver/main.py:212``), but every
downstream consumer (``Resolver.default_constraint_file`` →
``get_constraints_from_resolved_deps`` in
``pipenv/utils/dependencies.py:1501``, and the request builder at
``pipenv/utils/resolver.py:1450``) iterates the value with ``.items()``
and treats the key as the package name.
The mismatch was silent — both call sites accept ``Any`` — until the
first non-default category ran, at which point
``resolved_deps.items()`` raised ``AttributeError: 'list' object has
no attribute 'items'`` inside the resolver subprocess. The parent's
old non-zero-exit handler then masked the AttributeError with a
generic ``ResolutionFailure("Failed to lock Pipfile.lock!")``.
Combined with the diagnostic-recovery commit
(``6ad8c74d``) that unmasked the underlying subprocess error, this
explains the ~17 integration test failures on Ubuntu/3.12 for any test
that locks more than one category (``pipenv install --dev``,
``pipenv lock`` with both packages and dev-packages, the ``test_lock``
suite, ``test_install_basic.test_install_without_dev``,
``test_install_vcs.test_vcs_dev_package_install``, the
``test_uninstall*`` suites, etc.).
The fix keys the rehydrated mapping by ``LockedRequirement.name``,
restoring the ``{name: lockfile_entry}`` shape the in-process path
already used pre-refactor.
Verified locally: a two-category lock (``six`` in ``[packages]`` +
``tablib`` in ``[dev-packages]``) now succeeds end-to-end with both
categories populated.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…tderr forwarding
Two phase-3 refactor regressions, both surfacing on
``test_resolve_skip_unmatched_requirements``:
1. ``resolve_packages`` (``pipenv/resolver/main.py``) called
``_result_dict_to_locked_requirement`` strictly on every entry that
``clean_results`` returned. When a package is filtered out by
``Resolver.check_if_package_req_skipped`` (markers that don't
evaluate — e.g. ``os_name=='FakeOS'`` on Linux) or carries
``skip_resolver=True``, ``clean_results`` already tolerates the
resulting sparse ``{"name": ...}`` dict by falling through its own
ValueError branch — but the subprocess adapter then re-attempted
``LockedRequirement(...)`` construction and raised
``ValueError("LockedRequirement '<name>' carries no version, vcs,
file, or path")``, killing the resolve. Catch the ValueError at
the adapter boundary and drop those entries (they can't survive
the typed wire anyway).
2. ``_run_resolver_subprocess`` (``pipenv/utils/resolver.py``)
stopped forwarding the subprocess's captured stderr to the
parent's stderr stream on the success path. Pre-refactor (see
``main`` branch ``venv_resolve_deps`` body), after a returncode-0
resolve and when non-verbose, the parent printed
``Warning: <subprocess stderr>`` so user-actionable notices
emitted from inside the resolver (most prominently
``check_if_package_req_skipped``'s "Could not find a matching
version of <pkg>; <markers>" message) reached the user's
terminal. ``read_stderr`` captures every line into
``stderr_lines`` for in-memory access via
``CompletedProcess.stderr`` but only echoes verbose /
download-progress lines live, so without the explicit forward
the warning was effectively swallowed. Restore the forward
inside the structured-response success branch.
Verified locally:
- ``test_resolve_skip_unmatched_requirements`` now passes (was the
last remaining failure after the prior ``resolved_default_deps``
dict-shape and diagnostic-recovery commits).
- Unit suite (785 tests) and resolver protocol golden still pass.
- Two-category lock reproducer (``six`` + ``tablib``) still
succeeds.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Contributor
There was a problem hiding this comment.
Pull request overview
This PR modernizes Pipenv’s resolver subsystem by migrating the parent↔subprocess protocol to a typed JSON schema and consolidating resolver execution behind a single canonical driver. It also adds wall-clock timeout enforcement and structured resolver diagnostics capture, with extensive new unit/integration coverage and pinned golden fixtures to keep the protocol stable over time.
Changes:
- Introduces
pipenv/resolver/as a package with a typedResolverRequest/ResolverResponseschema and a unifiedresolve_for_pipenv()driver used by both subprocess and in-process adapter paths. - Adds T_F.6 wall-clock timeout plumbing end-to-end (Pipfile/env/default precedence → request metadata → subprocess/in-process enforcement).
- Populates structured resolver diagnostics (
Diagnostics.resolver_log) and adds protocol/schema tests + golden-fixture integration canary.
Reviewed changes
Copilot reviewed 56 out of 56 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
pipenv/resolver/schema.py |
New typed dataclass schema for resolver request/response envelopes and lock-entry representation. |
pipenv/resolver/core.py |
New unified resolver driver (resolve_for_pipenv) plus timeout guard + structured log capture. |
pipenv/resolver/main.py |
New subprocess entry point reading/writing typed JSON request/response files. |
pipenv/resolver/__init__.py |
Re-export shim for the resolver package surface. |
pipenv/resolver.py |
Removes the legacy monolithic resolver module in favor of the package. |
pipenv/utils/locking.py |
Removes legacy lockfile formatter; prepare_lockfile now consumes LockedRequirement (with transitional dict fallback). |
pyproject.toml |
Updates the pipenv-resolver console-script entry to point at pipenv.resolver.main:main. |
tests/unit/test_utils.py |
Ports legacy lockfile-entry formatter tests to LockedRequirement.from_install_requirement + adds typed prepare_lockfile smoke tests. |
tests/unit/test_resolver_schema.py |
New schema-focused unit tests: invariants, JSON round-trips, dispatch, golden parity, version mismatch, VCS coverage, comma-in-marker regression. |
tests/unit/test_resolver_core.py |
New tests for unified driver behavior (success, resolution error, internal error, marker override). |
tests/unit/test_resolver_parent_dispatch.py |
New parent-side dispatcher tests (request building, argv shape, structured-response dispatch). |
tests/unit/test_resolver_protocol_smoke.py |
New subprocess protocol smoke tests (stubbed success + schema mismatch + optional live network test). |
tests/unit/test_resolver_timeout.py |
New timeout enforcement tests (subprocess timeout → structured failure, precedence, request stamping, in-process SIGALRM guard). |
tests/unit/test_resolver_diagnostics.py |
New diagnostics capture tests (format, cap, handler cleanup, propagation behavior, JSON survivability). |
tests/unit/test_resolver_regressions.py |
Removes a regression test tied to deleted legacy functions, replaced by schema-level coverage. |
tests/unit/test_dependencies.py |
Removes legacy Entry-based tests, replaced by typed schema tests/fixtures. |
tests/unit/test_locking_no_mutation.py |
Updates resolver stub shape to return typed LockedRequirement results. |
tests/unit/test_core.py |
Updates documentation text referencing the canonical formatter. |
tests/unit/fixtures/resolver_schema/format_requirement_for_lockfile/*.json |
Adds/updates golden snapshots for parity gating of typed lockfile dict output. |
tests/unit/fixtures/resolver_schema/entry_get_cleaned_dict/*.json |
Adds/updates golden snapshots for parity gating of subprocess-side legacy shapes. |
tests/integration/test_resolver_protocol.py |
New integration canary capturing and pinning resolver request/response wire JSON with regen workflow. |
tests/integration/fixtures/resolver_protocol/request.json |
Golden request envelope fixture (normalized). |
tests/integration/fixtures/resolver_protocol/response.json |
Golden response envelope fixture (normalized). |
news/T_F.3.behavior.rst |
News fragment for structured conflict output. |
news/T_F.6.behavior.rst |
News fragment for resolver timeout enforcement behavior change. |
docs/dev/modernization-plan.md |
Updates the modernization plan with completed phases and implementation details. |
docs/dev/initiative-f-execution-plan.md |
Updates execution-plan status/log with completed waves and notes. |
.github/workflows/ci.yaml |
Updates GitHub Actions versions used in CI. |
.github/workflows/pypi_upload.yml |
Updates GitHub Actions versions used in release workflow. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
… not resolver sub-package Agent-Logs-Url: https://github.com/pypa/pipenv/sessions/e754d1f7-8ed9-42e9-8fdc-46a2c30157e9 Co-authored-by: matteius <479892+matteius@users.noreply.github.com>
…ct_url instead of duplicate lr.file Agent-Logs-Url: https://github.com/pypa/pipenv/sessions/495a8f65-b59e-4846-b0a8-39ddf4b4013f Co-authored-by: matteius <479892+matteius@users.noreply.github.com>
…ce/code-cleanup-phase3-resolver-typed-schema-2026-05
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Phase 3 of the pipenv maintenance-mode modernization stack. Bases on top of PR #6665 (Phase 2). Bundles the full T_F.3 typed-schema migration plus the four follow-up items from the prior phase-4 branch (T_F.4, T_F.5a design, T_F.6, T_F.7) merged in.
28 commits, +6735 / −1053 across
pipenv/andtests/, plus design doc updates and 2 news fragments. Touches zero vendored or patched files. 780 unit tests pass (was 651 baseline at start of Phase 2, 758 after T_F.3 alone → +22 from T_F.4/6/7).What's in this PR
T_F.3 — Execute the typed resolver subprocess schema (17 commits)
The structural rewrite of the pipenv ↔
pipenv-resolversubprocess protocol. Designed in PR #6665'sdocs/dev/initiative-f-typed-design.md(886 lines, maintainer-approved); executed here per the swarm-ready plan indocs/dev/initiative-f-execution-plan.md(4 waves: foundation, parallel-3, parallel-4, plan-bump).pipenv/resolver.py→pipenv/resolver/package with four modules:__init__.py(re-export shim for one-PR compatibility)schema.py(NEW, 899 lines) — 14 stdlib@dataclass(frozen=True)types:ResolverRequest,ResolverResponse, discriminatedResolverResult(ResolverSuccess/ResolutionError/InternalError),LockedRequirement,VCSPin,PackageSpecs,Source,ResolverOptions,ResolvedDeps,RequestMetadata,ConflictRecord,Diagnostics, +SCHEMA_VERSION = 1. Manualto_json_dict/from_json_dictround-trip (notdataclasses.asdict— breaks the discriminated union pattern).main.py(the oldpipenv/resolver.pyminus 156 lines of dead plumbing) — thin subprocess adapter: parses argv, reads--request-file, dispatches toresolve_for_pipenv, writes--response-file.core.py(NEW, 555 lines, from T_F.4) —resolve_for_pipenv(request) -> ResolverResponseis the single canonical driver. Both the subprocess adapter and the parent's in-process branch call it.Single canonical formatter
LockedRequirement.from_install_requirementabsorbs the behaviour from BOTH formerly-competing formatters:Entry.get_cleaned_dict(subprocess side) andformat_requirement_for_lockfile(parent side). Both deleted in this PR.prepare_lockfilenow consumesSequence[LockedRequirement]via the typedto_lockfile_dict()adapter.Dead surface deleted from the subprocess:
--pre,--clear,--system,--verbose,--category,--constraints-file,--resolved-default-deps-file,--parse-only,--pipenv-site, positionalpackagesPIPENV_RESOLVER_PYTHON_VERSION,PIPENV_EXTRA_PIP_ARGS,PIPENV_SITE_DIR(folded into typed request fields)Entry,PackageRequirement,process_resolver_results,whichstubFailure semantics fixed: today's non-zero-exit-as-only-failure-indicator is replaced with the discriminated
ResolverResultunion written to the response file on exit 0. Genuine subprocess crashes still produce non-zero exit + the legacy stderr-fallback path. Schema-version mismatch produces a structuredInternalErrorresponse AND non-zero exit (per Q2 sign-off).Tests:
tests/unit/test_resolver_schema.py(NEW, 678 lines / 62 tests) — invariants, JSON round-trip, golden-fixture parity against 27 committed snapshots, dispatch, schema-version-mismatch, VCS pins, comma-in-marker regression (Q7)tests/unit/test_resolver_parent_dispatch.py(NEW, 427 lines)tests/unit/test_resolver_protocol_smoke.py(NEW, 261 lines)tests/integration/test_resolver_protocol.py(NEW, 200 lines) — golden-fixture canary withPIPENV_REGEN_PROTOCOL_FIXTURES=1regen mechanismtests/unit/test_utils.py— 17format_requirement_for_lockfilecases ported to 20 typed cases (coverage grew)news/T_F.3.behavior.rst— one-line behavior note about structured error messages on dependency conflictsT_F.4 — Fold in-process and subprocess branches into one implementation
pipenv/resolver/core.py :: resolve_for_pipenv(request) -> ResolverResponseis now the only place the resolve plumbing lives. Eliminates duplication of:_apply_python_marker_overridedeleted frommain.py)resolve_packagesinvocation + result normalization (was in both_mainand thePIPENV_RESOLVER_PARENT_PYTHONbranch)ResolverResponsewrapping + variant dispatch (was duplicated in_mainexception handler)Both adapters are now thin:
pipenv/resolver/main.py:mainis 5 lines; the in-process branch inpipenv/utils/resolver.py:venv_resolve_depsis 8 lines.Stub-injection compatibility preserved across three existing test patterns (the dispatcher walks
sys.modulesin preference order sotest_resolver_core.py,test_locking_no_mutation.py, andtest_resolver_protocol_smoke.pycontinue to work).T_F.5a — Pluggable resolver backends DESIGN doc (sign-off gate)
No code change.
docs/dev/initiative-f-backends-design.md(NEW, 948 lines) proposes the architecture for landing alternative resolver backends (uv being the prime candidate) on top of the typed-schema substrate. Resolves the 4 open questions from F.2 §6a and surfaces 10 sign-off questions for the maintainer:[pipenv] resolver_backendvs[pipenv.resolver]vs[tool.pipenv.resolver]--backend NAMEvs--resolver-backend NAMEvs--use-uvPipfile.lock+_meta.resolver_backendvs distinct files_meta.resolver_backendvalue — omit vs explicit"pip"--allow-backend-switchLockedRequirement.resolver_backend/Diagnostics.resolver_name? (recommendation: neither).featurevs.behaviorConcerns from the existing
origin/uv-backendWIP (1000-line exploration that predates the typed schema) called out in the design:install/sync/update/auditwhenPIPENV_USE_UV=1because it writes onlypylock.toml. Other subcommands readPipfile.lock. Hidden by integration tests only runningpipenv lock.T_F.5 execution is gated on maintainer answers to the 10 questions and is NOT in this PR.
T_F.6 — Wall-clock timeout enforcement
Honours
request.metadata.deadline_secondsend-to-end:[pipenv] resolver_timeout_seconds> envPIPENV_RESOLVER_TIMEOUT_S> 1800s default. Stamped intoRequestMetadata.deadline_secondsso the schema records it.subprocess.run(timeout=deadline_seconds).TimeoutExpiredconverts to a structuredResolutionFailurewhose message names both override channels so the user knows how to extend.PIPENV_RESOLVER_PARENT_PYTHON=1):signal.SIGALRMdeadline guard. Unix only; no-op on Windows (subprocess path is the production one).tests/unit/test_resolver_timeout.py.news/T_F.6.behavior.rst— user-visible behaviour change: previously-hanging installs now die after the deadline with a structured error.T_F.7 —
Diagnostics.resolver_logpopulationT_F.3 reserved the field but left it as an empty tuple per Q9. This populates it:
pipenv+pip._internal.resolutionloggers (NOT pip download chatter — stderr remains that channel)"[LEVELNAME] message"per record"... (N records elided)"sentinelresolve_for_pipenvso both adapters get identical logs--verbosemode prints the structured log alongside the existing stderr stream.tests/unit/test_resolver_diagnostics.pyNo news fragment needed — purely additive (field went from empty to populated; no user-visible breakage outside verbose mode).
Still-queued work (not in this PR; not on a sibling branch yet)
docs/dev/initiative-f-backends-design.md. Will land on its own branch.Lockfilesubsystem fromProject.Pipfilesubsystem fromProject.unpack_url/get_http_url; delete the (then-empty)requirementslib.py.Test plan
pytest tests/unit/); was 651 baseline at start of Phase 2, 679 after Phase 2 alone, 758 after T_F.3 alone, +22 from T_F.4/6/7--request-fileand--response-filecontent against committed golden fixtures (tests/integration/fixtures/resolver_protocol/) withPIPENV_REGEN_PROTOCOL_FIXTURES=1regen workflowpipenv locksmoke: byte-identical lockfile (shaf162d2e398108da068327e701ee9472102db75565b86c0415420968db20ef679) produced via both subprocess andPIPENV_RESOLVER_PARENT_PYTHON=1in-process pathsEntry.get_cleaned_dict, zeroformat_requirement_for_lockfile, zeroPIPENV_RESOLVER_PYTHON_VERSION/PIPENV_EXTRA_PIP_ARGS/PIPENV_SITE_DIRreferences, zero dead argv flags inpipenv/resolver/main.pypip install -e . --force-reinstallis required after this lands so thepipenv-resolverconsole script on$PATHresolves to the newpipenv.resolver.main:mainentry (waspipenv.resolver:main)🤖 Generated with Claude Code