Skip to content

Commit 605a805

Browse files
committed
Check existing source by normalized name before add and remove (astral-sh#8359)
Resolves astral-sh#8328 Resolves astral-sh#8330
1 parent f5ae2b3 commit 605a805

File tree

2 files changed

+187
-1
lines changed

2 files changed

+187
-1
lines changed

crates/uv-workspace/src/pyproject_mut.rs

+18-1
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,7 @@ impl PyProjectTomlMut {
131131
};
132132
Ok(doc)
133133
}
134+
134135
/// Adds a dependency to `project.dependencies`.
135136
///
136137
/// Returns `true` if the dependency was added, `false` if it was updated.
@@ -431,7 +432,11 @@ impl PyProjectTomlMut {
431432
.as_table_mut()
432433
.ok_or(Error::MalformedSources)?;
433434

435+
if let Some(key) = find_source(name, sources) {
436+
sources.remove(&key);
437+
}
434438
add_source(name, source, sources)?;
439+
435440
Ok(())
436441
}
437442

@@ -532,7 +537,9 @@ impl PyProjectTomlMut {
532537
.map(|sources| sources.as_table_mut().ok_or(Error::MalformedSources))
533538
.transpose()?
534539
{
535-
sources.remove(name.as_ref());
540+
if let Some(key) = find_source(name, sources) {
541+
sources.remove(&key);
542+
}
536543
}
537544

538545
Ok(())
@@ -766,6 +773,16 @@ fn find_dependencies(
766773
to_replace
767774
}
768775

776+
/// Returns the key in `tool.uv.sources` that matches the given package name.
777+
fn find_source(name: &PackageName, sources: &Table) -> Option<String> {
778+
for (key, _) in sources {
779+
if PackageName::from_str(key).is_ok_and(|ref key| key == name) {
780+
return Some(key.to_string());
781+
}
782+
}
783+
None
784+
}
785+
769786
// Add a source to `tool.uv.sources`.
770787
fn add_source(req: &PackageName, source: &Source, sources: &mut Table) -> Result<(), Error> {
771788
// Serialize as an inline table.

crates/uv/tests/it/edit.rs

+169
Original file line numberDiff line numberDiff line change
@@ -2718,6 +2718,175 @@ fn update_source_replace_url() -> Result<()> {
27182718
);
27192719
});
27202720

2721+
// Change the source again. The existing source should be replaced.
2722+
uv_snapshot!(context.filters(), context.add().arg("requests @ git+https://github.com/psf/requests").arg("--tag=v2.32.2"), @r###"
2723+
success: true
2724+
exit_code: 0
2725+
----- stdout -----
2726+
2727+
----- stderr -----
2728+
Resolved 6 packages in [TIME]
2729+
Prepared 2 packages in [TIME]
2730+
Uninstalled 2 packages in [TIME]
2731+
Installed 2 packages in [TIME]
2732+
~ project==0.1.0 (from file://[TEMP_DIR]/)
2733+
- requests==2.32.3 (from git+https://github.com/psf/requests@0e322af87745eff34caffe4df68456ebc20d9068)
2734+
+ requests==2.32.2 (from git+https://github.com/psf/requests@88dce9d854797c05d0ff296b70e0430535ef8aaf)
2735+
"###);
2736+
2737+
let pyproject_toml = context.read("pyproject.toml");
2738+
2739+
insta::with_settings!({
2740+
filters => context.filters(),
2741+
}, {
2742+
assert_snapshot!(
2743+
pyproject_toml, @r###"
2744+
[project]
2745+
name = "project"
2746+
version = "0.1.0"
2747+
requires-python = ">=3.12"
2748+
dependencies = [
2749+
"requests[security]",
2750+
]
2751+
2752+
[build-system]
2753+
requires = ["setuptools>=42"]
2754+
build-backend = "setuptools.build_meta"
2755+
2756+
[tool.uv.sources]
2757+
requests = { git = "https://github.com/psf/requests", tag = "v2.32.2" }
2758+
"###
2759+
);
2760+
});
2761+
2762+
Ok(())
2763+
}
2764+
2765+
/// If a source defined in `tool.uv.sources` but its name is not normalized, `uv add` should not
2766+
/// add the same source again.
2767+
#[test]
2768+
#[cfg(feature = "git")]
2769+
fn add_non_normalized_source() -> Result<()> {
2770+
let context = TestContext::new("3.12");
2771+
2772+
let pyproject_toml = context.temp_dir.child("pyproject.toml");
2773+
pyproject_toml.write_str(indoc! {r#"
2774+
[project]
2775+
name = "project"
2776+
version = "0.1.0"
2777+
requires-python = ">=3.12"
2778+
dependencies = [
2779+
"uv-public-pypackage"
2780+
]
2781+
2782+
[build-system]
2783+
requires = ["setuptools>=42"]
2784+
build-backend = "setuptools.build_meta"
2785+
2786+
[tool.uv.sources]
2787+
uv_public_pypackage = { git = "https://github.com/astral-test/uv-public-pypackage", tag = "0.0.1" }
2788+
"#})?;
2789+
2790+
uv_snapshot!(context.filters(), context.add().arg("uv-public-pypackage @ git+https://github.com/astral-test/[email protected]"), @r###"
2791+
success: true
2792+
exit_code: 0
2793+
----- stdout -----
2794+
2795+
----- stderr -----
2796+
Resolved 2 packages in [TIME]
2797+
Prepared 2 packages in [TIME]
2798+
Installed 2 packages in [TIME]
2799+
+ project==0.1.0 (from file://[TEMP_DIR]/)
2800+
+ uv-public-pypackage==0.1.0 (from git+https://github.com/astral-test/uv-public-pypackage@0dacfd662c64cb4ceb16e6cf65a157a8b715b979)
2801+
"###);
2802+
2803+
let pyproject_toml = context.read("pyproject.toml");
2804+
2805+
insta::with_settings!({
2806+
filters => context.filters(),
2807+
}, {
2808+
assert_snapshot!(
2809+
pyproject_toml, @r###"
2810+
[project]
2811+
name = "project"
2812+
version = "0.1.0"
2813+
requires-python = ">=3.12"
2814+
dependencies = [
2815+
"uv-public-pypackage",
2816+
]
2817+
2818+
[build-system]
2819+
requires = ["setuptools>=42"]
2820+
build-backend = "setuptools.build_meta"
2821+
2822+
[tool.uv.sources]
2823+
uv-public-pypackage = { git = "https://github.com/astral-test/uv-public-pypackage", rev = "0.0.1" }
2824+
"###
2825+
);
2826+
});
2827+
2828+
Ok(())
2829+
}
2830+
2831+
/// If a source defined in `tool.uv.sources` but its name is not normalized, `uv remove` should
2832+
/// remove the source.
2833+
#[test]
2834+
#[cfg(feature = "git")]
2835+
fn remove_non_normalized_source() -> Result<()> {
2836+
let context = TestContext::new("3.12");
2837+
2838+
let pyproject_toml = context.temp_dir.child("pyproject.toml");
2839+
pyproject_toml.write_str(indoc! {r#"
2840+
[project]
2841+
name = "project"
2842+
version = "0.1.0"
2843+
requires-python = ">=3.12"
2844+
dependencies = [
2845+
"uv-public-pypackage"
2846+
]
2847+
2848+
[build-system]
2849+
requires = ["setuptools>=42"]
2850+
build-backend = "setuptools.build_meta"
2851+
2852+
[tool.uv.sources]
2853+
uv_public_pypackage = { git = "https://github.com/astral-test/uv-public-pypackage", tag = "0.0.1" }
2854+
"#})?;
2855+
2856+
uv_snapshot!(context.filters(), context.remove().arg("uv-public-pypackage"), @r###"
2857+
success: true
2858+
exit_code: 0
2859+
----- stdout -----
2860+
2861+
----- stderr -----
2862+
Resolved 1 package in [TIME]
2863+
Prepared 1 package in [TIME]
2864+
Installed 1 package in [TIME]
2865+
+ project==0.1.0 (from file://[TEMP_DIR]/)
2866+
"###);
2867+
2868+
let pyproject_toml = context.read("pyproject.toml");
2869+
2870+
insta::with_settings!({
2871+
filters => context.filters(),
2872+
}, {
2873+
assert_snapshot!(
2874+
pyproject_toml, @r###"
2875+
[project]
2876+
name = "project"
2877+
version = "0.1.0"
2878+
requires-python = ">=3.12"
2879+
dependencies = []
2880+
2881+
[build-system]
2882+
requires = ["setuptools>=42"]
2883+
build-backend = "setuptools.build_meta"
2884+
2885+
[tool.uv.sources]
2886+
"###
2887+
);
2888+
});
2889+
27212890
Ok(())
27222891
}
27232892

0 commit comments

Comments
 (0)