Skip to content

Commit 7bba3d0

Browse files
authored
Write the path of the parent environment to an extends-environment key in the pyvenv.cfg file of an ephemeral environment (#13598)
1 parent f657359 commit 7bba3d0

File tree

4 files changed

+117
-0
lines changed

4 files changed

+117
-0
lines changed

crates/uv/src/commands/project/environment.rs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
1+
use std::path::Path;
2+
13
use tracing::debug;
24

35
use uv_cache::{Cache, CacheBucket};
46
use uv_cache_key::{cache_digest, hash_digest};
57
use uv_configuration::{Concurrency, Constraints, PreviewMode};
68
use uv_distribution_types::{Name, Resolution};
9+
use uv_fs::PythonExt;
710
use uv_python::{Interpreter, PythonEnvironment};
811

912
use crate::commands::pip::loggers::{InstallLogger, ResolveLogger};
@@ -168,6 +171,30 @@ impl CachedEnvironment {
168171
Ok(())
169172
}
170173

174+
/// Set the `extends-environment` key in the `pyvenv.cfg` file to the given path.
175+
///
176+
/// Ephemeral environments created by `uv run --with` extend a parent (virtual or system)
177+
/// environment by adding a `.pth` file to the ephemeral environment's `site-packages`
178+
/// directory. The `pth` file contains Python code to dynamically add the parent
179+
/// environment's `site-packages` directory to Python's import search paths in addition to
180+
/// the ephemeral environment's `site-packages` directory. This works well at runtime, but
181+
/// is too dynamic for static analysis tools like ty to understand. As such, we
182+
/// additionally write the `sys.prefix` of the parent environment to to the
183+
/// `extends-environment` key of the ephemeral environment's `pyvenv.cfg` file, making it
184+
/// easier for these tools to statically and reliably understand the relationship between
185+
/// the two environments.
186+
#[allow(clippy::result_large_err)]
187+
pub(crate) fn set_parent_environment(
188+
&self,
189+
parent_environment_sys_prefix: &Path,
190+
) -> Result<(), ProjectError> {
191+
self.0.set_pyvenv_cfg(
192+
"extends-environment",
193+
&parent_environment_sys_prefix.escape_for_python(),
194+
)?;
195+
Ok(())
196+
}
197+
171198
/// Return the [`Interpreter`] to use for the cached environment, based on a given
172199
/// [`Interpreter`].
173200
///

crates/uv/src/commands/project/run.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -992,6 +992,16 @@ hint: If you are running a script with `{}` in the shebang, you may need to incl
992992
site_packages.escape_for_python()
993993
))?;
994994

995+
// Write the `sys.prefix` of the parent environment to the `extends-environment` key of the `pyvenv.cfg`
996+
// file. This helps out static-analysis tools such as ty (see docs on
997+
// `CachedEnvironment::set_parent_environment`).
998+
//
999+
// Note that we do this even if the parent environment is not a virtual environment.
1000+
// For ephemeral environments created by `uv run --with`, the parent environment's
1001+
// `site-packages` directory is added to `sys.path` even if the parent environment is not
1002+
// a virtual environment and even if `--system-site-packages` was not explicitly selected.
1003+
ephemeral_env.set_parent_environment(base_interpreter.sys_prefix())?;
1004+
9951005
// If `--system-site-packages` is enabled, add the system site packages to the ephemeral
9961006
// environment.
9971007
if base_interpreter.is_virtualenv()

crates/uv/tests/it/common/mod.rs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,30 @@ impl TestContext {
243243
self
244244
}
245245

246+
/// Filtering for various keys in a `pyvenv.cfg` file that will vary
247+
/// depending on the specific machine used:
248+
/// - `home = foo/bar/baz/python3.X.X/bin`
249+
/// - `uv = X.Y.Z`
250+
/// - `extends-environment = <path/to/parent/venv>`
251+
#[must_use]
252+
pub fn with_pyvenv_cfg_filters(mut self) -> Self {
253+
let added_filters = [
254+
(r"home = .+".to_string(), "home = [PYTHON_HOME]".to_string()),
255+
(
256+
r"uv = \d+\.\d+\.\d+".to_string(),
257+
"uv = [UV_VERSION]".to_string(),
258+
),
259+
(
260+
r"extends-environment = .+".to_string(),
261+
"extends-environment = [PARENT_VENV]".to_string(),
262+
),
263+
];
264+
for filter in added_filters {
265+
self.filters.insert(0, filter);
266+
}
267+
self
268+
}
269+
246270
/// Add extra filtering for ` -> <PATH>` symlink display for Python versions in the test
247271
/// context, e.g., for use in `uv python list`.
248272
#[must_use]

crates/uv/tests/it/run.rs

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1264,6 +1264,62 @@ fn run_with() -> Result<()> {
12641264
Ok(())
12651265
}
12661266

1267+
/// Test that an ephemeral environment writes the path of its parent environment to the `extends-environment` key
1268+
/// of its `pyvenv.cfg` file. This feature makes it easier for static-analysis tools like ty to resolve which import
1269+
/// search paths are available in these ephemeral environments.
1270+
#[test]
1271+
fn run_with_pyvenv_cfg_file() -> Result<()> {
1272+
let context = TestContext::new("3.12").with_pyvenv_cfg_filters();
1273+
1274+
let pyproject_toml = context.temp_dir.child("pyproject.toml");
1275+
pyproject_toml.write_str(indoc! { r#"
1276+
[project]
1277+
name = "foo"
1278+
version = "1.0.0"
1279+
requires-python = ">=3.8"
1280+
1281+
[build-system]
1282+
requires = ["setuptools>=42"]
1283+
build-backend = "setuptools.build_meta"
1284+
"#
1285+
})?;
1286+
1287+
let test_script = context.temp_dir.child("main.py");
1288+
test_script.write_str(indoc! { r#"
1289+
import os
1290+
1291+
with open(f'{os.getenv("VIRTUAL_ENV")}/pyvenv.cfg') as f:
1292+
print(f.read())
1293+
"#
1294+
})?;
1295+
1296+
uv_snapshot!(context.filters(), context.run().arg("--with").arg("iniconfig").arg("main.py"), @r"
1297+
success: true
1298+
exit_code: 0
1299+
----- stdout -----
1300+
home = [PYTHON_HOME]
1301+
implementation = CPython
1302+
uv = [UV_VERSION]
1303+
version_info = 3.12.[X]
1304+
include-system-site-packages = false
1305+
relocatable = true
1306+
extends-environment = [PARENT_VENV]
1307+
1308+
1309+
----- stderr -----
1310+
Resolved 1 package in [TIME]
1311+
Prepared 1 package in [TIME]
1312+
Installed 1 package in [TIME]
1313+
+ foo==1.0.0 (from file://[TEMP_DIR]/)
1314+
Resolved 1 package in [TIME]
1315+
Prepared 1 package in [TIME]
1316+
Installed 1 package in [TIME]
1317+
+ iniconfig==2.0.0
1318+
");
1319+
1320+
Ok(())
1321+
}
1322+
12671323
#[test]
12681324
fn run_with_build_constraints() -> Result<()> {
12691325
let context = TestContext::new("3.8");

0 commit comments

Comments
 (0)