[uv-settings]: Correct behavior for relative find-links paths when run from a subdir (#10827)

## One-liner
Relative find-links configuration to local path from a pyproject.toml or
uv.toml is now relative to the config file
## Summary
### Background
One can configure find-links in a `pyproject.toml` or `uv.toml` file,
which are located from the cli arg, system directory, user directory, or
by traversing parent directories until one is encountered.

This PR addresses the following scenario:
- A project directory which includes a `pyproject.toml` or `uv.toml`
file
- The config file includes a `find-links` option. (eg under `[tool.uv]`
for `pyproject.toml`)
- The `find-links` option is configured to point to a local subdirectory
in the project: `packages/`
- There is a subdirectory called `subdir`, which is the current working
directory
- I run `uv run my_script.py`. This will locate the `pyproject.toml` in
the parent directory
### Current Behavior
- uv tries to use the path `subdir/packages/` to find packages, and
fails.
### New Behavior
- uv tries to use the path `packages/` to find the packages, and
succeeds
- Specifically, any relative local find-links path will resolve to be
relative to the configuration file.

### Why is this behavior change OK?
- I believe no one depends on the behavior that a relative find-links
when running in a subdir will refer to different directories each time
- Thus this change only allows a more common use case which didn't work
previously.

## Test Plan
- I re-created the setup mentioned above:
```
UvTest/
├── packages/
│   ├── colorama-0.4.6-py2.py3-none-any.whl
│   └── tqdm-4.67.1-py3-none-any.whl
├── subdir/
│   └── my_script.py
└── pyproject.toml
```
```toml 
# pyproject.toml
[project]
name = "uvtest"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.12"
dependencies = [
    "tqdm>=4.67.1",
]

[tool.uv]
offline = true
no-index = true
find-links = ["packages/"]
```
- With working directory under `subdir`, previously, running `uv sync
--offline` would fail resolving the tdqm package, and after the change
it succeeds.
- Additionally, one can use `uv sync --show-settings` to show the
actually-resolved settings - now having the desired path in
`flat_index.url.path`

## Alternative designs considered
- I considered modifying the `impl Deserialize for IndexUrl` to parse
ahead of time directly with a base directory by having a custom
`Deserializer` with a base dir field, but it seems to contradict the
design of the serde `Deserialize` trait - which should work with all
`Deserializer`s

## Future work
- Support for adjusting all other local-relative paths in `Options`
would be desired, but is out of scope for the current PR.

---------

Co-authored-by: Charlie Marsh <charlie.r.marsh@gmail.com>
This commit is contained in:
Ilan Godik 2025-01-22 21:44:35 +02:00 committed by GitHub
parent d6d0593b71
commit b1e4bc779c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 208 additions and 25 deletions

View file

@ -108,8 +108,9 @@ impl FilesystemOptions {
let path = dir.join("uv.toml");
match fs_err::read_to_string(&path) {
Ok(content) => {
let options: Options = toml::from_str(&content)
.map_err(|err| Error::UvToml(path.clone(), Box::new(err)))?;
let options = toml::from_str::<Options>(&content)
.map_err(|err| Error::UvToml(path.clone(), Box::new(err)))?
.relative_to(&std::path::absolute(dir)?)?;
// If the directory also contains a `[tool.uv]` table in a `pyproject.toml` file,
// warn.
@ -155,6 +156,8 @@ impl FilesystemOptions {
return Ok(None);
};
let options = options.relative_to(&std::path::absolute(dir)?)?;
tracing::debug!("Found workspace configuration at `{}`", path.display());
return Ok(Some(Self(options)));
}
@ -252,8 +255,13 @@ fn system_config_file() -> Option<PathBuf> {
/// Load [`Options`] from a `uv.toml` file.
fn read_file(path: &Path) -> Result<Options, Error> {
let content = fs_err::read_to_string(path)?;
let options: Options =
toml::from_str(&content).map_err(|err| Error::UvToml(path.to_path_buf(), Box::new(err)))?;
let options = toml::from_str::<Options>(&content)
.map_err(|err| Error::UvToml(path.to_path_buf(), Box::new(err)))?;
let options = if let Some(parent) = std::path::absolute(path)?.parent() {
options.relative_to(parent)?
} else {
options
};
Ok(options)
}
@ -294,6 +302,9 @@ pub enum Error {
#[error(transparent)]
Io(#[from] std::io::Error),
#[error(transparent)]
Index(#[from] uv_distribution_types::IndexUrlError),
#[error("Failed to parse: `{}`", _0.user_display())]
PyprojectToml(PathBuf, #[source] Box<toml::de::Error>),