Skip to content

Commit

Permalink
feat: allow passing extra config k,v pairs for pyvenv.cfg when creati…
Browse files Browse the repository at this point in the history
…ng a venv (#1852)

<!--
Thank you for contributing to uv! To help us out with reviewing, please
consider the following:

- Does this pull request include a summary of the change? (See below.)
- Does this pull request include a descriptive title?
- Does this pull request include references to any relevant issues?
-->

## Summary

This modifies `gourgeist` to allow passing additional k,v pairs to add
to the `pyvenv.cfg` file as proposed in #1697.
I made it allow an arbitrary set of pairs (to decouple from `uv` since
this is mainly a change to `gourgeist`) , but I can slim it down to just
allow just a name and version strings if that's desired.

The `pyvenv.cfg` will also have a `uv = <uv-crate-version>` when a venv
is created via `uv venv` ~~and `uv-build = <uv-build-crate-version>`
when it's created via `SourceBuild::setup`~~.

Example below via `uv venv`:

```ini
home = ...
implementation = CPython
version_info = 3.12
include-system-site-packages = false
base-prefix = ...
base-exec-prefix = ...
base-executable = ...
uv = 0.1.6
prompt = uv
```

Open to any suggestions, thanks!

Closes #1697 

## Test Plan

Added new test in `tests/venv.rs` called `verify_pyvenv_cfg` to verify
that it contains the right uv version string. I didn't see tests
configured in `gourgeist` itself, so I didn't add any there.
  • Loading branch information
samypr100 authored Feb 22, 2024
1 parent f0b39a3 commit 2fa67ea
Show file tree
Hide file tree
Showing 6 changed files with 79 additions and 31 deletions.
75 changes: 48 additions & 27 deletions crates/gourgeist/src/bare.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ use uv_fs::Normalized;

use uv_interpreter::Interpreter;

use crate::Prompt;
use crate::{Error, Prompt};

/// The bash activate scripts with the venv dependent paths patches out
const ACTIVATE_TEMPLATES: &[(&str, &str)] = &[
Expand All @@ -33,17 +33,10 @@ const ACTIVATE_TEMPLATES: &[(&str, &str)] = &[
const VIRTUALENV_PATCH: &str = include_str!("_virtualenv.py");

/// Very basic `.cfg` file format writer.
fn write_cfg(
f: &mut impl Write,
data: &[(&str, String); 8],
prompt: Option<String>,
) -> io::Result<()> {
fn write_cfg(f: &mut impl Write, data: &[(String, String)]) -> io::Result<()> {
for (key, value) in data {
writeln!(f, "{key} = {value}")?;
}
if let Some(prompt) = prompt {
writeln!(f, "prompt = {prompt}")?;
}
Ok(())
}

Expand Down Expand Up @@ -73,7 +66,8 @@ pub fn create_bare_venv(
location: &Utf8Path,
interpreter: &Interpreter,
prompt: Prompt,
) -> io::Result<VenvPaths> {
extra_cfg: Vec<(String, String)>,
) -> Result<VenvPaths, Error> {
// We have to canonicalize the interpreter path, otherwise the home is set to the venv dir instead of the real root.
// This would make python-build-standalone fail with the encodings module not being found because its home is wrong.
let base_python: Utf8PathBuf = fs_err::canonicalize(interpreter.sys_executable())?
Expand All @@ -84,10 +78,10 @@ pub fn create_bare_venv(
match location.metadata() {
Ok(metadata) => {
if metadata.is_file() {
return Err(io::Error::new(
return Err(Error::IO(io::Error::new(
io::ErrorKind::AlreadyExists,
format!("File exists at `{location}`"),
));
)));
} else if metadata.is_dir() {
if location.join("pyvenv.cfg").is_file() {
info!("Removing existing directory");
Expand All @@ -99,17 +93,17 @@ pub fn create_bare_venv(
{
info!("Ignoring empty directory");
} else {
return Err(io::Error::new(
return Err(Error::IO(io::Error::new(
io::ErrorKind::AlreadyExists,
format!("The directory `{location}` exists, but it's not a virtualenv"),
));
)));
}
}
}
Err(err) if err.kind() == io::ErrorKind::NotFound => {
fs::create_dir_all(location)?;
}
Err(err) => return Err(err),
Err(err) => return Err(Error::IO(err)),
}

// TODO(konstin): I bet on windows we'll have to strip the prefix again
Expand Down Expand Up @@ -226,31 +220,58 @@ pub fn create_bare_venv(
} else {
unimplemented!("Only Windows and Unix are supported")
};
let pyvenv_cfg_data = &[
("home", python_home),

// Validate extra_cfg
let reserved_keys = [
"home",
"implementation",
"version_info",
"include-system-site-packages",
"base-prefix",
"base-exec-prefix",
"base-executable",
"prompt",
];
for (key, _) in &extra_cfg {
if reserved_keys.contains(&key.as_str()) {
return Err(Error::ReservedConfigKey(key.to_string()));
}
}

let mut pyvenv_cfg_data: Vec<(String, String)> = vec![
("home".to_string(), python_home),
(
"implementation",
"implementation".to_string(),
interpreter.markers().platform_python_implementation.clone(),
),
(
"version_info",
"version_info".to_string(),
interpreter.markers().python_version.string.clone(),
),
("gourgeist", env!("CARGO_PKG_VERSION").to_string()),
// I wouldn't allow this option anyway
("include-system-site-packages", "false".to_string()),
(
"base-prefix",
"include-system-site-packages".to_string(),
"false".to_string(),
),
(
"base-prefix".to_string(),
interpreter.base_prefix().to_string_lossy().to_string(),
),
(
"base-exec-prefix",
"base-exec-prefix".to_string(),
interpreter.base_exec_prefix().to_string_lossy().to_string(),
),
("base-executable", base_python.to_string()),
];
("base-executable".to_string(), base_python.to_string()),
]
.into_iter()
.chain(extra_cfg)
.collect();

if let Some(prompt) = prompt {
pyvenv_cfg_data.push(("prompt".to_string(), prompt));
}

let mut pyvenv_cfg = BufWriter::new(File::create(location.join("pyvenv.cfg"))?);
write_cfg(&mut pyvenv_cfg, pyvenv_cfg_data, prompt)?;
write_cfg(&mut pyvenv_cfg, &pyvenv_cfg_data)?;
drop(pyvenv_cfg);

let site_packages = if cfg!(unix) {
Expand Down
5 changes: 4 additions & 1 deletion crates/gourgeist/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ pub enum Error {
InvalidPythonInterpreter(#[source] Box<dyn std::error::Error + Sync + Send>),
#[error(transparent)]
Platform(#[from] PlatformError),
#[error("Reserved key used for pyvenv.cfg: {0}")]
ReservedConfigKey(String),
}

/// The value to use for the shell prompt when inside a virtual environment.
Expand Down Expand Up @@ -51,11 +53,12 @@ pub fn create_venv(
location: &Path,
interpreter: Interpreter,
prompt: Prompt,
extra_cfg: Vec<(String, String)>,
) -> Result<Virtualenv, Error> {
let location: &Utf8Path = location
.try_into()
.map_err(|err: FromPathError| err.into_io_error())?;
let paths = create_bare_venv(location, &interpreter, prompt)?;
let paths = create_bare_venv(location, &interpreter, prompt, extra_cfg)?;
Ok(Virtualenv::from_interpreter(
interpreter,
paths.root.as_std_path(),
Expand Down
2 changes: 1 addition & 1 deletion crates/gourgeist/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ fn run() -> Result<(), gourgeist::Error> {
Cache::from_path(".gourgeist_cache")?
};
let info = Interpreter::query(python.as_std_path(), &platform, &cache).unwrap();
create_bare_venv(&location, &info, Prompt::from_args(cli.prompt))?;
create_bare_venv(&location, &info, Prompt::from_args(cli.prompt), Vec::new())?;
Ok(())
}

Expand Down
1 change: 1 addition & 0 deletions crates/uv-build/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -329,6 +329,7 @@ impl SourceBuild {
&temp_dir.path().join(".venv"),
interpreter.clone(),
gourgeist::Prompt::None,
Vec::new(),
)?;

// Setup the build environment.
Expand Down
6 changes: 5 additions & 1 deletion crates/uv/src/commands/venv.rs
Original file line number Diff line number Diff line change
Expand Up @@ -119,8 +119,12 @@ async fn venv_impl(
)
.into_diagnostic()?;

// Extra cfg for pyvenv.cfg to specify uv version
let extra_cfg = vec![("uv".to_string(), env!("CARGO_PKG_VERSION").to_string())];

// Create the virtual environment.
let venv = gourgeist::create_venv(path, interpreter, prompt).map_err(VenvError::Creation)?;
let venv = gourgeist::create_venv(path, interpreter, prompt, extra_cfg)
.map_err(VenvError::Creation)?;

// Install seed packages.
if seed {
Expand Down
21 changes: 20 additions & 1 deletion crates/uv/tests/venv.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ use assert_fs::prelude::*;

use uv_fs::Normalized;

use crate::common::{create_bin_with_executables, get_bin, uv_snapshot, EXCLUDE_NEWER};
use crate::common::{
create_bin_with_executables, get_bin, uv_snapshot, TestContext, EXCLUDE_NEWER,
};

mod common;

Expand Down Expand Up @@ -644,3 +646,20 @@ fn virtualenv_compatibility() -> Result<()> {

Ok(())
}

#[test]
fn verify_pyvenv_cfg() {
let context = TestContext::new("3.12");
let venv = context.temp_dir.child(".venv");
let pyvenv_cfg = venv.child("pyvenv.cfg");

venv.assert(predicates::path::is_dir());

// Check pyvenv.cfg exists
pyvenv_cfg.assert(predicates::path::is_file());

// Check if "uv = version" is present in the file
let version = env!("CARGO_PKG_VERSION").to_string();
let search_string = format!("uv = {version}");
pyvenv_cfg.assert(predicates::str::contains(search_string));
}

0 comments on commit 2fa67ea

Please sign in to comment.