Skip to content

Commit 6e23849

Browse files
committed
Test Pipenv .env and .venv discovery end to end
Scan-level regressions for both fixes: a .env-named venv is scanned (and PIPENV_DONT_LOAD_ENV turns that off), and an explicit "not in project" setting now scans ./.venv as well as the WORKON_HOME venv. The Pipenv compatibility doc describes the new discovery rules. Assisted-by: Claude Code:claude-opus-5-5
1 parent aa605f2 commit 6e23849

2 files changed

Lines changed: 85 additions & 13 deletions

File tree

‎crates/socket-patch-cli/tests/in_process_python_envs.rs‎

Lines changed: 84 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -546,6 +546,8 @@ const PIPENV_VARS: &[&str] = &[
546546
"PIPENV_NO_VENV_IN_PROJECT",
547547
"PIPENV_CUSTOM_VENV_NAME",
548548
"PIPENV_PIPFILE",
549+
"PIPENV_DONT_LOAD_ENV",
550+
"PIPENV_DOTENV_LOCATION",
549551
];
550552

551553
/// Run `scan` with exactly `env` set among [`PIPENV_VARS`].
@@ -612,33 +614,103 @@ async fn pipenv_opt_outs_keep_activated_virtual_env_from_hijacking_scan() {
612614
}
613615
}
614616

615-
/// #334: Pipenv never uses `venv/`, and `PIPENV_VENV_IN_PROJECT=0` makes it
616-
/// ignore a `./.venv` directory, so neither may shadow Pipenv's venv.
617+
/// #334: Pipenv never uses `venv/`, so it may not shadow Pipenv's venv.
617618
#[tokio::test]
618619
#[serial]
619620
async fn pipenv_stray_venv_dirs_do_not_shadow_the_pipenv_venv() {
620-
for (stray, opt_out) in [("venv", None), (".venv", Some("0"))] {
621+
let (_tmp, project, workon) = pipenv_project();
622+
let stray_site = venv_site_packages(&project.join("venv"), "python3.12");
623+
std::fs::create_dir_all(&stray_site).unwrap();
624+
write_dist_info(&stray_site, "stray_decoy", "6.6.6");
625+
let server = MockServer::start().await;
626+
mock_batch_empty(&server).await;
627+
let env: Vec<(&str, &Path)> = vec![
628+
("WORKON_HOME", &workon),
629+
("PIPENV_CUSTOM_VENV_NAME", Path::new("proj-env")),
630+
];
631+
let code = scan_with_pipenv_env(default_args(&project, server.uri()), &env).await;
632+
assert_eq!(code, 0);
633+
let bodies = batch_bodies(&server).await;
634+
assert_discovered(&bodies, "pkg:pypi/pipenv-pkg@1.0.0");
635+
assert_not_discovered(&bodies, "pkg:pypi/stray-decoy@6.6.6");
636+
}
637+
638+
/// #645: with `PIPENV_VENV_IN_PROJECT=0` (or `PIPENV_NO_VENV_IN_PROJECT=1`)
639+
/// only Pipenv 2023.11.14+ ignores a `./.venv` directory; 2018.11 through
640+
/// 2023.10.24 still use it. Both venvs are scanned, so whichever one the
641+
/// installed Pipenv uses is patched.
642+
#[tokio::test]
643+
#[serial]
644+
async fn pipenv_explicit_not_in_project_still_scans_dot_venv() {
645+
for (name, value) in [
646+
("PIPENV_VENV_IN_PROJECT", "0"),
647+
("PIPENV_NO_VENV_IN_PROJECT", "1"),
648+
] {
621649
let (_tmp, project, workon) = pipenv_project();
622-
let stray_site = venv_site_packages(&project.join(stray), "python3.12");
623-
std::fs::create_dir_all(&stray_site).unwrap();
624-
write_dist_info(&stray_site, "stray_decoy", "6.6.6");
650+
let dot_site = venv_site_packages(&project.join(".venv"), "python3.12");
651+
std::fs::create_dir_all(&dot_site).unwrap();
652+
write_dist_info(&dot_site, "dot_venv_pkg", "1.0.0");
625653
let server = MockServer::start().await;
626654
mock_batch_empty(&server).await;
627-
let mut env: Vec<(&str, &Path)> = vec![
655+
let env: Vec<(&str, &Path)> = vec![
628656
("WORKON_HOME", &workon),
629657
("PIPENV_CUSTOM_VENV_NAME", Path::new("proj-env")),
658+
(name, Path::new(value)),
630659
];
631-
if let Some(value) = opt_out {
632-
env.push(("PIPENV_VENV_IN_PROJECT", Path::new(value)));
633-
}
634660
let code = scan_with_pipenv_env(default_args(&project, server.uri()), &env).await;
635-
assert_eq!(code, 0, "{stray}");
661+
assert_eq!(code, 0, "{name}={value}");
636662
let bodies = batch_bodies(&server).await;
637663
assert_discovered(&bodies, "pkg:pypi/pipenv-pkg@1.0.0");
638-
assert_not_discovered(&bodies, "pkg:pypi/stray-decoy@6.6.6");
664+
assert_discovered(&bodies, "pkg:pypi/dot-venv-pkg@1.0.0");
639665
}
640666
}
641667

668+
/// #546: Pipenv loads the project's `.env` before it picks the venv, so a
669+
/// `PIPENV_CUSTOM_VENV_NAME` or `WORKON_HOME` there decides which venv is
670+
/// scanned (and `PIPENV_DONT_LOAD_ENV` turns that off).
671+
#[tokio::test]
672+
#[serial]
673+
async fn pipenv_dotenv_settings_pick_the_scanned_venv() {
674+
// Name in .env, WORKON_HOME exported.
675+
let (_tmp, project, workon) = pipenv_project();
676+
std::fs::write(project.join(".env"), "PIPENV_CUSTOM_VENV_NAME=proj-env\n").unwrap();
677+
let server = MockServer::start().await;
678+
mock_batch_empty(&server).await;
679+
let code = scan_with_pipenv_env(
680+
default_args(&project, server.uri()),
681+
&[("WORKON_HOME", &workon)],
682+
)
683+
.await;
684+
assert_eq!(code, 0);
685+
assert_discovered(&batch_bodies(&server).await, "pkg:pypi/pipenv-pkg@1.0.0");
686+
687+
// Both in .env, nothing exported.
688+
std::fs::write(
689+
project.join(".env"),
690+
format!(
691+
"export WORKON_HOME=\"{}\"\nPIPENV_CUSTOM_VENV_NAME=proj-env # named\n",
692+
workon.display()
693+
),
694+
)
695+
.unwrap();
696+
let server = MockServer::start().await;
697+
mock_batch_empty(&server).await;
698+
let code = scan_with_pipenv_env(default_args(&project, server.uri()), &[]).await;
699+
assert_eq!(code, 0);
700+
assert_discovered(&batch_bodies(&server).await, "pkg:pypi/pipenv-pkg@1.0.0");
701+
702+
// PIPENV_DONT_LOAD_ENV: Pipenv ignores .env, and so does discovery.
703+
let server = MockServer::start().await;
704+
mock_batch_empty(&server).await;
705+
let code = scan_with_pipenv_env(
706+
default_args(&project, server.uri()),
707+
&[("PIPENV_DONT_LOAD_ENV", Path::new("1"))],
708+
)
709+
.await;
710+
assert_eq!(code, 0);
711+
assert_not_discovered(&batch_bodies(&server).await, "pkg:pypi/pipenv-pkg@1.0.0");
712+
}
713+
642714
// ---------------------------------------------------------------------------
643715
// Package-manager-recorded envs: PDM's saved interpreter / PEP 582, and uv's
644716
// UV_PROJECT_ENVIRONMENT, ahead of a stray `./.venv` the manager never uses

‎docs/testing/pipenv-compatibility.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ requirements.txt lanes of the same ecosystem.
1717

1818
| Input | Hosted | Vendored | Agent |
1919
|-------|--------|----------|-------|
20-
| `Pipfile.lock`, `pipfile-spec: 6` (Pipenv 7 and later) | Every category (`default`, `develop`, Pipenv 2022+ named categories) that pins the patched release becomes `{"file" \| "path": "<url>#sha256=<hex>", "hashes": ["sha256:<hex>"]}` with `markers`/`extras`/`index` kept as Pipenv wrote them and `version` dropped. `path` for Pipenv 7–11, `file` from 2018. `_meta` (the Pipfile content hash) and the Pipfile are untouched. | Every matching category refers to the committed wheel under `.socket/vendor/pypi/<uuid>/`; wheels with extras use `path` (Pipenv 2022's file-URL bug). Requires Pipenv 2018 or later (`pypi_pipenv_installer_unsupported`). | Independent of the lock: patches the installed distribution in the venv Pipenv resolves for the project — `VIRTUAL_ENV` unless `PIPENV_ACTIVE` / `PIPENV_IGNORE_VIRTUALENVS` is set, in-project `.venv` subject to `PIPENV_VENV_IN_PROJECT` and the Pipfile's `[pipenv] venv_in_project`, or Pipenv's default `$WORKON_HOME/<dir>-<hash>[-<python>]`; never `venv/` (discovered without running Pipenv). With an auto-detected `.venv` and an existing WORKON_HOME venv, both are patched, since Pipenv 2026.2+ uses the WORKON_HOME venv and older releases use `.venv`. |
20+
| `Pipfile.lock`, `pipfile-spec: 6` (Pipenv 7 and later) | Every category (`default`, `develop`, Pipenv 2022+ named categories) that pins the patched release becomes `{"file" \| "path": "<url>#sha256=<hex>", "hashes": ["sha256:<hex>"]}` with `markers`/`extras`/`index` kept as Pipenv wrote them and `version` dropped. `path` for Pipenv 7–11, `file` from 2018. `_meta` (the Pipfile content hash) and the Pipfile are untouched. | Every matching category refers to the committed wheel under `.socket/vendor/pypi/<uuid>/`; wheels with extras use `path` (Pipenv 2022's file-URL bug). Requires Pipenv 2018 or later (`pypi_pipenv_installer_unsupported`). | Independent of the lock: patches the installed distribution in the venv Pipenv resolves for the project — `VIRTUAL_ENV` unless `PIPENV_ACTIVE` / `PIPENV_IGNORE_VIRTUALENVS` is set, in-project `.venv` subject to `PIPENV_VENV_IN_PROJECT` and the Pipfile's `[pipenv] venv_in_project`, or Pipenv's default `$WORKON_HOME/<dir>-<hash>[-<python>]`; never `venv/` (discovered without running Pipenv). Settings come from the project's `.env` (or `PIPENV_DOTENV_LOCATION`, unless `PIPENV_DONT_LOAD_ENV`) layered over the environment, as Pipenv loads it first, and from the environment alone. With a `.venv` directory and an existing WORKON_HOME venv, both are patched unless the project is explicitly in-project: Pipenv 2026.2+ prefers the WORKON_HOME venv when nothing is set, 2023.11.14+ uses it when the project is explicitly not in-project, and older releases use `.venv` either way. |
2121
| `Pipfile.lock`, `pipfile-spec` < 6 (Pipenv 0–6) | Refused (`redirect_pipenv_skipped`), lock untouched. | Refused (`pypi_pipenv_spec_unsupported`). | Works. |
2222
| Lock-only checkout (nothing installed) | Discovered from the lock and redirected. | Discovered from the lock; the patched wheel or source distribution is downloaded and verified from the service without a local install. | Nothing to patch (no installed distribution); the lock's pins are listed as lockfile-only packages. |
2323

0 commit comments

Comments
 (0)