Search beyond workspace root when discovering configuration (#5931)

## Summary

Previously, we wouldn't respect configuration files in directories
_above_ a workspace root. But this is somewhat problematic, because any
`pyproject.toml` will define a workspace root...

Instead, I think we should _start_ the search at the workspace root, but
go above it if necessary.

Closes: #5929.

See: https://github.com/astral-sh/uv/pull/4295.
This commit is contained in:
Charlie Marsh 2024-08-08 17:05:02 -04:00 committed by GitHub
parent cbc3274848
commit 88ece8b791
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 284 additions and 10 deletions

View file

@ -40,9 +40,12 @@ impl FilesystemOptions {
let root = dir.join("uv");
let file = root.join("uv.toml");
debug!("Loading user configuration from: `{}`", file.display());
debug!("Searching for user configuration in: `{}`", file.display());
match read_file(&file) {
Ok(options) => Ok(Some(Self(options))),
Ok(options) => {
debug!("Found user configuration in: `{}`", file.display());
Ok(Some(Self(options)))
}
Err(Error::Io(err)) if err.kind() == std::io::ErrorKind::NotFound => Ok(None),
Err(_) if !dir.is_dir() => {
// Ex) `XDG_CONFIG_HOME=/dev/null`

View file

@ -103,12 +103,10 @@ async fn run(cli: Cli) -> Result<ExitStatus> {
// Load configuration from the filesystem, prioritizing (in order):
// 1. The configuration file specified on the command-line.
// 2. The configuration file in the current workspace (i.e., the `pyproject.toml` or `uv.toml`
// file in the workspace root directory). If found, this file is combined with the user
// configuration file.
// 3. The nearest `uv.toml` file in the directory tree, starting from the current directory. If
// found, this file is combined with the user configuration file. In this case, we don't
// search for `pyproject.toml` files, since we're not in a workspace.
// 2. The nearest configuration file (`uv.toml` or `pyproject.toml`) above the workspace root.
// If found, this file is combined with the user configuration file.
// 3. The nearest configuration file (`uv.toml` or `pyproject.toml`) in the directory tree,
// starting from the current directory.
let filesystem = if let Some(config_file) = cli.config_file.as_ref() {
if config_file
.file_name()
@ -122,8 +120,8 @@ async fn run(cli: Cli) -> Result<ExitStatus> {
} else if matches!(&*cli.command, Commands::Tool(_)) {
// For commands that operate at the user-level, ignore local configuration.
FilesystemOptions::user()?
} else if let Ok(project) = Workspace::discover(&CWD, &DiscoveryOptions::default()).await {
let project = FilesystemOptions::from_directory(project.install_path())?;
} else if let Ok(workspace) = Workspace::discover(&CWD, &DiscoveryOptions::default()).await {
let project = FilesystemOptions::find(workspace.install_path())?;
let user = FilesystemOptions::user()?;
project.combine(user)
} else {

View file

@ -3035,3 +3035,272 @@ fn resolve_config_file() -> anyhow::Result<()> {
Ok(())
}
/// Ignore empty `pyproject.toml` files when discovering configuration.
#[test]
#[cfg_attr(
windows,
ignore = "Configuration tests are not yet supported on Windows"
)]
fn resolve_skip_empty() -> anyhow::Result<()> {
let context = TestContext::new("3.12");
// Set `lowest-direct` in a `uv.toml`.
let config = context.temp_dir.child("uv.toml");
config.write_str(indoc::indoc! {r#"
[pip]
resolution = "lowest-direct"
"#})?;
let child = context.temp_dir.child("child");
fs_err::create_dir(&child)?;
// Create an empty in a `pyproject.toml`.
let pyproject = child.child("pyproject.toml");
pyproject.write_str(indoc::indoc! {r#"
[project]
name = "child"
dependencies = [
"httpx",
]
"#})?;
// Resolution in `child` should use lowest-direct, skipping the `pyproject.toml`, which lacks a
// `tool.uv`.
uv_snapshot!(context.filters(), add_shared_args(context.pip_compile())
.arg("--show-settings")
.arg("requirements.in")
.current_dir(&child), @r###"
success: true
exit_code: 0
----- stdout -----
GlobalSettings {
quiet: false,
verbose: 0,
color: Auto,
native_tls: false,
connectivity: Online,
show_settings: true,
preview: Disabled,
python_preference: OnlySystem,
python_fetch: Automatic,
no_progress: false,
}
CacheSettings {
no_cache: false,
cache_dir: Some(
"[CACHE_DIR]/",
),
}
PipCompileSettings {
src_file: [
"requirements.in",
],
constraint: [],
override: [],
constraints_from_workspace: [],
overrides_from_workspace: [],
build_constraint: [],
refresh: None(
Timestamp(
SystemTime {
tv_sec: [TIME],
tv_nsec: [TIME],
},
),
),
settings: PipSettings {
index_locations: IndexLocations {
index: None,
extra_index: [],
flat_index: [],
no_index: false,
},
python: None,
system: false,
extras: None,
break_system_packages: false,
target: None,
prefix: None,
index_strategy: FirstIndex,
keyring_provider: Disabled,
no_build_isolation: false,
no_build_isolation_package: [],
build_options: BuildOptions {
no_binary: None,
no_build: None,
},
allow_empty_requirements: false,
strict: false,
dependency_mode: Transitive,
resolution: LowestDirect,
prerelease: IfNecessaryOrExplicit,
output_file: None,
no_strip_extras: false,
no_strip_markers: false,
no_annotate: false,
no_header: false,
custom_compile_command: None,
generate_hashes: false,
setup_py: Pep517,
config_setting: ConfigSettings(
{},
),
python_version: None,
python_platform: None,
universal: false,
exclude_newer: Some(
ExcludeNewer(
2024-03-25T00:00:00Z,
),
),
no_emit_package: [],
emit_index_url: false,
emit_find_links: false,
emit_build_options: false,
emit_marker_expression: false,
emit_index_annotation: false,
annotation_style: Split,
link_mode: Clone,
compile_bytecode: false,
sources: Enabled,
hash_checking: None,
upgrade: None,
reinstall: None,
concurrency: Concurrency {
downloads: 50,
builds: 16,
installs: 8,
},
},
}
----- stderr -----
"###
);
// Adding a `tool.uv` section should cause us to ignore the `uv.toml`.
pyproject.write_str(indoc::indoc! {r#"
[project]
name = "child"
dependencies = [
"httpx",
]
[tool.uv]
"#})?;
uv_snapshot!(context.filters(), add_shared_args(context.pip_compile())
.arg("--show-settings")
.arg("requirements.in")
.current_dir(&child), @r###"
success: true
exit_code: 0
----- stdout -----
GlobalSettings {
quiet: false,
verbose: 0,
color: Auto,
native_tls: false,
connectivity: Online,
show_settings: true,
preview: Disabled,
python_preference: OnlySystem,
python_fetch: Automatic,
no_progress: false,
}
CacheSettings {
no_cache: false,
cache_dir: Some(
"[CACHE_DIR]/",
),
}
PipCompileSettings {
src_file: [
"requirements.in",
],
constraint: [],
override: [],
constraints_from_workspace: [],
overrides_from_workspace: [],
build_constraint: [],
refresh: None(
Timestamp(
SystemTime {
tv_sec: [TIME],
tv_nsec: [TIME],
},
),
),
settings: PipSettings {
index_locations: IndexLocations {
index: None,
extra_index: [],
flat_index: [],
no_index: false,
},
python: None,
system: false,
extras: None,
break_system_packages: false,
target: None,
prefix: None,
index_strategy: FirstIndex,
keyring_provider: Disabled,
no_build_isolation: false,
no_build_isolation_package: [],
build_options: BuildOptions {
no_binary: None,
no_build: None,
},
allow_empty_requirements: false,
strict: false,
dependency_mode: Transitive,
resolution: Highest,
prerelease: IfNecessaryOrExplicit,
output_file: None,
no_strip_extras: false,
no_strip_markers: false,
no_annotate: false,
no_header: false,
custom_compile_command: None,
generate_hashes: false,
setup_py: Pep517,
config_setting: ConfigSettings(
{},
),
python_version: None,
python_platform: None,
universal: false,
exclude_newer: Some(
ExcludeNewer(
2024-03-25T00:00:00Z,
),
),
no_emit_package: [],
emit_index_url: false,
emit_find_links: false,
emit_build_options: false,
emit_marker_expression: false,
emit_index_annotation: false,
annotation_style: Split,
link_mode: Clone,
compile_bytecode: false,
sources: Enabled,
hash_checking: None,
upgrade: None,
reinstall: None,
concurrency: Concurrency {
downloads: 50,
builds: 16,
installs: 8,
},
},
}
----- stderr -----
"###
);
Ok(())
}

View file

@ -11,6 +11,10 @@ in the nearest parent directory.
files will be ignored. Instead, uv will exclusively read from user-level configuration
(e.g., `~/.config/uv/uv.toml`).
In workspaces, uv will begin its search at the workspace root, ignoring any configuration defined in
workspace members. Since the workspace is locked as a single unit, configuration is shared across
all members.
If a `pyproject.toml` file is found, uv will read configuration from the `[tool.uv.pip]` table. For
example, to set a persistent index URL, add the following to a `pyproject.toml`: