Skip to content

Commit 8b8bded

Browse files
j178MtkN1
authored andcommitted
Check existing source by normalized name before add and remove (astral-sh#8359)
Resolves astral-sh#8328 Resolves astral-sh#8330
1 parent a82c528 commit 8b8bded

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
@@ -2717,6 +2717,175 @@ fn update_source_replace_url() -> Result<()> {
27172717
);
27182718
});
27192719

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

0 commit comments

Comments
 (0)