Skip to content

Commit

Permalink
Use sitecustomize.py to implement environment layering
Browse files Browse the repository at this point in the history
  • Loading branch information
charliermarsh committed Jul 25, 2024
1 parent 6f45403 commit 4bb10fb
Show file tree
Hide file tree
Showing 4 changed files with 86 additions and 42 deletions.
49 changes: 23 additions & 26 deletions crates/uv/src/commands/project/run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use std::ffi::OsString;
use std::fmt::Write;
use std::path::{Path, PathBuf};

use anyhow::{bail, Context, Result};
use anyhow::{anyhow, bail, Context, Result};
use itertools::Itertools;
use owo_colors::OwoColorize;
use tokio::process::Command;
Expand All @@ -14,7 +14,7 @@ use uv_cache::Cache;
use uv_cli::ExternalCommand;
use uv_client::{BaseClientBuilder, Connectivity};
use uv_configuration::{Concurrency, ExtrasSpecification, PreviewMode};
use uv_fs::Simplified;
use uv_fs::{PythonExt, Simplified};
use uv_installer::{SatisfiesResult, SitePackages};
use uv_normalize::PackageName;
use uv_python::{
Expand Down Expand Up @@ -412,6 +412,27 @@ pub(crate) async fn run(
}
};

// If we're running in an ephemeral environment, add a `sitecustomize.py` to enable loading of
// the base environment's site packages. Setting `PYTHONPATH` is insufficient, as it doesn't
// resolve `.pth` files in the base environment.
if let Some(ephemeral_env) = ephemeral_env.as_ref() {
if let Some(base_interpreter) = base_interpreter.as_ref() {
let ephemeral_site_packages = ephemeral_env
.site_packages()
.next()
.ok_or_else(|| anyhow!("Ephemeral environment has no site packages directory"))?;
let base_site_packages = base_interpreter
.site_packages()
.next()
.ok_or_else(|| anyhow!("Base environment has no site packages directory"))?;

fs_err::write(
ephemeral_site_packages.join("sitecustomize.py"),
format!("import site; site.addsitedir(\"{}\")", base_site_packages.escape_for_python()),
)?;
}
}

debug!("Running `{command}`");
let mut process = Command::from(&command);

Expand All @@ -437,30 +458,6 @@ pub(crate) async fn run(
)?;
process.env("PATH", new_path);

// Construct the `PYTHONPATH` environment variable.
let new_python_path = std::env::join_paths(
ephemeral_env
.as_ref()
.map(PythonEnvironment::site_packages)
.into_iter()
.flatten()
.chain(
base_interpreter
.as_ref()
.map(Interpreter::site_packages)
.into_iter()
.flatten(),
)
.map(PathBuf::from)
.chain(
std::env::var_os("PYTHONPATH")
.as_ref()
.iter()
.flat_map(std::env::split_paths),
),
)?;
process.env("PYTHONPATH", new_python_path);

// Spawn and wait for completion
// Standard input, output, and error streams are all inherited
// TODO(zanieb): Throw a nicer error message if the command is not found
Expand Down
11 changes: 0 additions & 11 deletions crates/uv/src/commands/tool/run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -123,17 +123,6 @@ pub(crate) async fn run(
)?;
process.env("PATH", new_path);

// Construct the `PYTHONPATH` environment variable.
let new_python_path = std::env::join_paths(
environment.site_packages().map(PathBuf::from).chain(
std::env::var_os("PYTHONPATH")
.as_ref()
.iter()
.flat_map(std::env::split_paths),
),
)?;
process.env("PYTHONPATH", new_python_path);

// Spawn and wait for completion
// Standard input, output, and error streams are all inherited
// TODO(zanieb): Throw a nicer error message if the command is not found
Expand Down
54 changes: 54 additions & 0 deletions crates/uv/tests/run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -769,3 +769,57 @@ fn run_requirements_txt_arguments() -> Result<()> {

Ok(())
}

/// Ensure that we can import from the root project when layering `--with` requirements.
#[test]
fn run_editable() -> Result<()> {
let context = TestContext::new("3.12");

let pyproject_toml = context.temp_dir.child("pyproject.toml");
pyproject_toml.write_str(indoc! { r#"
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[project]
name = "foo"
version = "1.0.0"
requires-python = ">=3.8"
dependencies = []
"#
})?;

let src = context.temp_dir.child("src").child("foo");
src.create_dir_all()?;

let init = src.child("__init__.py");
init.touch()?;

let main = context.temp_dir.child("main.py");
main.write_str(indoc! { r"
import foo
print('Hello, world!')
"
})?;

// We treat arguments before the command as uv arguments
uv_snapshot!(context.filters(), context.run().arg("--with").arg("iniconfig").arg("main.py"), @r###"
success: true
exit_code: 0
----- stdout -----
Hello, world!
----- stderr -----
warning: `uv run` is experimental and may change without warning
Resolved 1 package in [TIME]
Prepared 1 package in [TIME]
Installed 1 package in [TIME]
+ foo==1.0.0 (from file://[TEMP_DIR]/)
Resolved 1 package in [TIME]
Prepared 1 package in [TIME]
Installed 1 package in [TIME]
+ iniconfig==2.0.0
"###);

Ok(())
}
14 changes: 9 additions & 5 deletions crates/uv/tests/sync.rs
Original file line number Diff line number Diff line change
Expand Up @@ -177,15 +177,19 @@ fn empty() -> Result<()> {

// Running `uv sync` should generate an empty lockfile.
uv_snapshot!(context.filters(), context.sync(), @r###"
success: true
exit_code: 0
success: false
exit_code: 2
----- stdout -----
----- stderr -----
warning: `uv sync` is experimental and may change without warning
warning: No `requires-python` value found in the workspace. Defaulting to `>=3.12`.
Resolved 0 packages in [TIME]
Audited 0 packages in [TIME]
error: Failed to parse: `pyproject.toml`
Caused by: TOML parse error at line 2, column 9
|
2 | [project]
| ^^^^^^^^^
missing field `name`
"###);

assert!(context.temp_dir.child("uv.lock").exists());
Expand Down

0 comments on commit 4bb10fb

Please sign in to comment.