Skip to content

Commit d355edb

Browse files
committed
Normalize relative paths when --project is specified
1 parent 696b64f commit d355edb

File tree

3 files changed

+215
-5
lines changed

3 files changed

+215
-5
lines changed

crates/uv-fs/src/path.rs

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -272,13 +272,16 @@ pub fn relative_to(
272272
path: impl AsRef<Path>,
273273
base: impl AsRef<Path>,
274274
) -> Result<PathBuf, std::io::Error> {
275+
// Normalize both paths, to avoid intermediate `..` components.
276+
let path = normalize_path(path.as_ref());
277+
let base = normalize_path(base.as_ref());
278+
275279
// Find the longest common prefix, and also return the path stripped from that prefix
276280
let (stripped, common_prefix) = base
277-
.as_ref()
278281
.ancestors()
279282
.find_map(|ancestor| {
280283
// Simplifying removes the UNC path prefix on windows.
281-
dunce::simplified(path.as_ref())
284+
dunce::simplified(&path)
282285
.strip_prefix(dunce::simplified(ancestor))
283286
.ok()
284287
.map(|stripped| (stripped, ancestor))
@@ -288,14 +291,14 @@ pub fn relative_to(
288291
std::io::ErrorKind::Other,
289292
format!(
290293
"Trivial strip failed: {} vs. {}",
291-
path.as_ref().simplified_display(),
292-
base.as_ref().simplified_display()
294+
path.simplified_display(),
295+
base.simplified_display()
293296
),
294297
)
295298
})?;
296299

297300
// go as many levels up as required
298-
let levels_up = base.as_ref().components().count() - common_prefix.components().count();
301+
let levels_up = base.components().count() - common_prefix.components().count();
299302
let up = std::iter::repeat("..").take(levels_up).collect::<PathBuf>();
300303

301304
Ok(up.join(stripped))

crates/uv/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,8 @@ async fn run(mut cli: Cli) -> Result<ExitStatus> {
6565
.as_deref()
6666
.map(std::path::absolute)
6767
.transpose()?
68+
.as_deref()
69+
.map(uv_fs::normalize_path)
6870
.map(Cow::Owned)
6971
.unwrap_or_else(|| Cow::Borrowed(&*CWD));
7072

crates/uv/tests/it/lock.rs

Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6347,6 +6347,110 @@ fn lock_no_workspace_source() -> Result<()> {
63476347
Ok(())
63486348
}
63496349

6350+
/// Lock a workspace with a member that's a peer to the root.
6351+
#[test]
6352+
fn lock_peer_member() -> Result<()> {
6353+
let context = TestContext::new("3.12");
6354+
6355+
context
6356+
.temp_dir
6357+
.child("project")
6358+
.child("pyproject.toml")
6359+
.write_str(
6360+
r#"
6361+
[project]
6362+
name = "project"
6363+
version = "0.1.0"
6364+
requires-python = ">=3.12"
6365+
dependencies = ["child"]
6366+
6367+
[tool.uv.workspace]
6368+
members = ["../child"]
6369+
6370+
[tool.uv.sources]
6371+
child = { workspace = true }
6372+
"#,
6373+
)?;
6374+
6375+
context
6376+
.temp_dir
6377+
.child("child")
6378+
.child("pyproject.toml")
6379+
.write_str(
6380+
r#"
6381+
[project]
6382+
name = "child"
6383+
version = "0.1.0"
6384+
requires-python = ">=3.12"
6385+
dependencies = []
6386+
6387+
[build-system]
6388+
requires = ["setuptools>=42"]
6389+
build-backend = "setuptools.build_meta"
6390+
"#,
6391+
)?;
6392+
6393+
uv_snapshot!(context.filters(), context.lock().current_dir(context.temp_dir.child("project")), @r###"
6394+
success: true
6395+
exit_code: 0
6396+
----- stdout -----
6397+
6398+
----- stderr -----
6399+
Using CPython 3.12.[X] interpreter at: [PYTHON-3.12]
6400+
Resolved 2 packages in [TIME]
6401+
"###);
6402+
6403+
let lock = fs_err::read_to_string(context.temp_dir.child("project").child("uv.lock")).unwrap();
6404+
6405+
insta::with_settings!({
6406+
filters => context.filters(),
6407+
}, {
6408+
assert_snapshot!(
6409+
lock, @r###"
6410+
version = 1
6411+
requires-python = ">=3.12"
6412+
6413+
[options]
6414+
exclude-newer = "2024-03-25T00:00:00Z"
6415+
6416+
[manifest]
6417+
members = [
6418+
"child",
6419+
"project",
6420+
]
6421+
6422+
[[package]]
6423+
name = "child"
6424+
version = "0.1.0"
6425+
source = { editable = "../child" }
6426+
6427+
[[package]]
6428+
name = "project"
6429+
version = "0.1.0"
6430+
source = { virtual = "." }
6431+
dependencies = [
6432+
{ name = "child" },
6433+
]
6434+
6435+
[package.metadata]
6436+
requires-dist = [{ name = "child", editable = "../child" }]
6437+
"###
6438+
);
6439+
});
6440+
6441+
// Re-run with `--locked`.
6442+
uv_snapshot!(context.filters(), context.lock().arg("--locked"), @r###"
6443+
success: false
6444+
exit_code: 2
6445+
----- stdout -----
6446+
6447+
----- stderr -----
6448+
error: No `pyproject.toml` found in current directory or any parent directory
6449+
"###);
6450+
6451+
Ok(())
6452+
}
6453+
63506454
/// Ensure that development dependencies are omitted for non-workspace members. Below, `bar` depends
63516455
/// on `foo`, but `bar/uv.lock` should omit `anyio`, but should include `typing-extensions`.
63526456
#[test]
@@ -18886,6 +18990,107 @@ fn mismatched_name_self_editable() -> Result<()> {
1888618990
Ok(())
1888718991
}
1888818992

18993+
#[test]
18994+
fn lock_relative_project() -> Result<()> {
18995+
let context = TestContext::new("3.12");
18996+
18997+
context
18998+
.temp_dir
18999+
.child("project")
19000+
.child("pyproject.toml")
19001+
.write_str(
19002+
r#"
19003+
[project]
19004+
name = "project"
19005+
version = "0.1.0"
19006+
requires-python = ">=3.12"
19007+
dependencies = ["typing-extensions"]
19008+
"#,
19009+
)?;
19010+
19011+
let peer = context.temp_dir.child("peer");
19012+
fs_err::create_dir_all(&peer)?;
19013+
19014+
uv_snapshot!(context.filters(), context.lock().arg("--project").arg("../project").current_dir(&peer), @r###"
19015+
success: true
19016+
exit_code: 0
19017+
----- stdout -----
19018+
19019+
----- stderr -----
19020+
Using CPython 3.12.[X] interpreter at: [PYTHON-3.12]
19021+
Resolved 2 packages in [TIME]
19022+
"###);
19023+
19024+
let lock = context.read(context.temp_dir.child("project").child("uv.lock"));
19025+
19026+
insta::with_settings!({
19027+
filters => context.filters(),
19028+
}, {
19029+
assert_snapshot!(
19030+
lock, @r###"
19031+
version = 1
19032+
requires-python = ">=3.12"
19033+
19034+
[options]
19035+
exclude-newer = "2024-03-25T00:00:00Z"
19036+
19037+
[[package]]
19038+
name = "project"
19039+
version = "0.1.0"
19040+
source = { virtual = "." }
19041+
dependencies = [
19042+
{ name = "typing-extensions" },
19043+
]
19044+
19045+
[package.metadata]
19046+
requires-dist = [{ name = "typing-extensions" }]
19047+
19048+
[[package]]
19049+
name = "typing-extensions"
19050+
version = "4.10.0"
19051+
source = { registry = "https://pypi.org/simple" }
19052+
sdist = { url = "https://files.pythonhosted.org/packages/16/3a/0d26ce356c7465a19c9ea8814b960f8a36c3b0d07c323176620b7b483e44/typing_extensions-4.10.0.tar.gz", hash = "sha256:b0abd7c89e8fb96f98db18d86106ff1d90ab692004eb746cf6eda2682f91b3cb", size = 77558 }
19053+
wheels = [
19054+
{ url = "https://files.pythonhosted.org/packages/f9/de/dc04a3ea60b22624b51c703a84bbe0184abcd1d0b9bc8074b5d6b7ab90bb/typing_extensions-4.10.0-py3-none-any.whl", hash = "sha256:69b1a937c3a517342112fb4c6df7e72fc39a38e7891a5730ed4985b5214b5475", size = 33926 },
19055+
]
19056+
"###
19057+
);
19058+
});
19059+
19060+
// Re-run with `--locked`.
19061+
uv_snapshot!(context.filters(), context.lock().arg("--locked").arg("--project").arg("../project").current_dir(&peer), @r###"
19062+
success: true
19063+
exit_code: 0
19064+
----- stdout -----
19065+
19066+
----- stderr -----
19067+
Using CPython 3.12.[X] interpreter at: [PYTHON-3.12]
19068+
Resolved 2 packages in [TIME]
19069+
"###);
19070+
19071+
// Create a virtual environment in the project directory.
19072+
context
19073+
.command()
19074+
.arg("venv")
19075+
.current_dir(context.temp_dir.child("project"))
19076+
.assert()
19077+
.success();
19078+
19079+
// Install from the lockfile.
19080+
uv_snapshot!(context.filters(), context.sync().arg("--frozen").arg("--project").arg("../project").current_dir(&peer), @r###"
19081+
success: true
19082+
exit_code: 0
19083+
----- stdout -----
19084+
19085+
----- stderr -----
19086+
Prepared 1 package in [TIME]
19087+
Installed 1 package in [TIME]
19088+
+ typing-extensions==4.10.0
19089+
"###);
19090+
19091+
Ok(())
19092+
}
19093+
1888919094
#[test]
1889019095
fn lock_recursive_extra() -> Result<()> {
1889119096
let context = TestContext::new("3.12");

0 commit comments

Comments
 (0)