diff --git a/crates/uv-python/src/lib.rs b/crates/uv-python/src/lib.rs index 982878ff3d56b..cca022a1afbd0 100644 --- a/crates/uv-python/src/lib.rs +++ b/crates/uv-python/src/lib.rs @@ -20,7 +20,10 @@ pub use crate::version_files::{ DiscoveryOptions as VersionFileDiscoveryOptions, FilePreference as VersionFilePreference, PythonVersionFile, PYTHON_VERSIONS_FILENAME, PYTHON_VERSION_FILENAME, }; -pub use crate::virtualenv::{Error as VirtualEnvError, PyVenvConfiguration, VirtualEnvironment}; +pub use crate::virtualenv::{ + virtualenv_from_env, virtualenv_from_working_dir, Error as VirtualEnvError, + PyVenvConfiguration, VirtualEnvironment, +}; mod cpuinfo; mod discovery; diff --git a/crates/uv-python/src/virtualenv.rs b/crates/uv-python/src/virtualenv.rs index 5fdfc094f0291..fe48b1b858b8b 100644 --- a/crates/uv-python/src/virtualenv.rs +++ b/crates/uv-python/src/virtualenv.rs @@ -49,7 +49,7 @@ pub enum Error { /// Locate an active virtual environment by inspecting environment variables. /// /// Supports `VIRTUAL_ENV`. -pub(crate) fn virtualenv_from_env() -> Option { +pub fn virtualenv_from_env() -> Option { if let Some(dir) = env::var_os(EnvVars::VIRTUAL_ENV).filter(|value| !value.is_empty()) { return Some(PathBuf::from(dir)); } @@ -114,7 +114,7 @@ pub(crate) fn conda_environment_from_env(kind: CondaEnvironmentKind) -> Option

Result, Error> { +pub fn virtualenv_from_working_dir() -> Result, Error> { let current_dir = crate::current_dir()?; for dir in current_dir.ancestors() { diff --git a/crates/uv-settings/src/lib.rs b/crates/uv-settings/src/lib.rs index 7d655bda3cf79..39c952431acd7 100644 --- a/crates/uv-settings/src/lib.rs +++ b/crates/uv-settings/src/lib.rs @@ -5,6 +5,7 @@ use std::path::{Path, PathBuf}; use etcetera::BaseStrategy; use uv_fs::Simplified; +use uv_python::{virtualenv_from_env, virtualenv_from_working_dir}; use uv_static::EnvVars; use uv_warnings::warn_user; @@ -55,6 +56,14 @@ impl FilesystemOptions { } } + pub fn venv() -> Result, Error> { + let Some(file) = venv_config_file() else { + return Ok(None); + }; + tracing::debug!("Found venv configuration in: `{}`", file.display()); + Ok(Some(Self(read_file(&file)?))) + } + pub fn system() -> Result, Error> { let Some(file) = system_config_file() else { return Ok(None); @@ -241,6 +250,26 @@ fn system_config_file() -> Option { } } +/// Returns the path to a venv-specific configuration file if one exists. +fn venv_config_file() -> Option { + // First check VIRTUAL_ENV environment variable + if let Some(venv_path) = virtualenv_from_env() { + let venv_config = PathBuf::from(venv_path).join("uv.toml"); + if venv_config.is_file() { + return Some(venv_config); + } + } + + // Then look for .venv in current or parent directories + if let Ok(Some(venv_path)) = virtualenv_from_working_dir() { + let venv_config = venv_path.join("uv.toml"); + if venv_config.is_file() { + return Some(venv_config); + } + } + + None +} /// Load [`Options`] from a `uv.toml` file. fn read_file(path: &Path) -> Result { let content = fs_err::read_to_string(path)?; diff --git a/crates/uv/src/lib.rs b/crates/uv/src/lib.rs index 0ac73dc4f3018..786075ab4c8e6 100644 --- a/crates/uv/src/lib.rs +++ b/crates/uv/src/lib.rs @@ -124,15 +124,19 @@ async fn run(mut cli: Cli) -> Result { } else if let Ok(workspace) = Workspace::discover(&project_dir, &DiscoveryOptions::default()).await { + // Combine in order of precedence: project -> venv -> user -> system let project = FilesystemOptions::find(workspace.install_path())?; let system = FilesystemOptions::system()?; + let venv = FilesystemOptions::venv()?; let user = FilesystemOptions::user()?; - project.combine(user).combine(system) + project.combine(venv).combine(user).combine(system) } else { + // Same precedence for non-workspace projects let project = FilesystemOptions::find(&project_dir)?; let system = FilesystemOptions::system()?; + let venv = FilesystemOptions::venv()?; let user = FilesystemOptions::user()?; - project.combine(user).combine(system) + project.combine(venv).combine(user).combine(system) }; // Parse the external command, if necessary. diff --git a/docs/configuration/files.md b/docs/configuration/files.md index 05dcbdb35f094..b5bac200f0602 100644 --- a/docs/configuration/files.md +++ b/docs/configuration/files.md @@ -43,21 +43,23 @@ default = true `[tool.uv]` section in the accompanying `pyproject.toml` will be ignored. uv will also discover user-level configuration at `~/.config/uv/uv.toml` (or -`$XDG_CONFIG_HOME/uv/uv.toml`) on macOS and Linux, or `%APPDATA%\uv\uv.toml` on Windows; and -system-level configuration at `/etc/uv/uv.toml` (or `$XDG_CONFIG_DIRS/uv/uv.toml`) on macOS and -Linux, or `%SYSTEMDRIVE%\ProgramData\uv\uv.toml` on Windows. - -User-and system-level configuration must use the `uv.toml` format, rather than the `pyproject.toml` -format, as a `pyproject.toml` is intended to define a Python _project_. - -If project-, user-, and system-level configuration files are found, the settings will be merged, -with project-level configuration taking precedence over the user-level configuration, and user-level -configuration taking precedence over the system-level configuration. (If multiple system-level +`$XDG_CONFIG_HOME/uv/uv.toml`) on macOS and Linux, or `%APPDATA%\uv\uv.toml` on Windows; +venv-level configuration at within the virtual environment directory; and system-level +configuration at `/etc/uv/uv.toml` (or `$XDG_CONFIG_DIRS/uv/uv.toml`) on macOS and Linux, or +`%SYSTEMDRIVE%\ProgramData\uv\uv.toml` on Windows. + +User-, venv-, and system-level configuration must use the `uv.toml` format, rather than the +`pyproject.toml` format, as a `pyproject.toml` is intended to define a Python _project_. + +If project-, venv-, user-, and system-level configuration files are found, the settings will be +merged, with project-level configuration taking precedence over venv-level configuration, +venv-level configuration taking precedence over user-level configuration, and user-level +configuration taking precedence over system-level configuration. (If multiple system-level configuration files are found, e.g., at both `/etc/uv/uv.toml` and `$XDG_CONFIG_DIRS/uv/uv.toml`, only the first-discovered file will be used, with XDG taking priority.) -For example, if a string, number, or boolean is present in both the project- and user-level -configuration tables, the project-level value will be used, and the user-level value will be +For example, if a string, number, or boolean is present in both the project- and venv-level +configuration tables, the project-level value will be used, and the venv-level value will be ignored. If an array is present in both tables, the arrays will be concatenated, with the project-level settings appearing earlier in the merged array.