mirror of
				https://github.com/astral-sh/uv.git
				synced 2025-11-03 21:23:54 +00:00 
			
		
		
		
	Allow @ references in uv tool install --from (#6842)
				
					
				
			## Summary Closes https://github.com/astral-sh/uv/issues/6796.
This commit is contained in:
		
							parent
							
								
									3e207da3bc
								
							
						
					
					
						commit
						a1805d175e
					
				
					 4 changed files with 135 additions and 11 deletions
				
			
		| 
						 | 
					@ -117,7 +117,7 @@ pub(crate) async fn install(
 | 
				
			||||||
            .unwrap()
 | 
					            .unwrap()
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        // Ex) `ruff@0.6.0`
 | 
					        // Ex) `ruff@0.6.0`
 | 
				
			||||||
        Target::Version(name, ref version) => {
 | 
					        Target::Version(name, ref version) | Target::FromVersion(_, name, ref version) => {
 | 
				
			||||||
            if editable {
 | 
					            if editable {
 | 
				
			||||||
                bail!("`--editable` is only supported for local packages");
 | 
					                bail!("`--editable` is only supported for local packages");
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
| 
						 | 
					@ -136,7 +136,7 @@ pub(crate) async fn install(
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        // Ex) `ruff@latest`
 | 
					        // Ex) `ruff@latest`
 | 
				
			||||||
        Target::Latest(name) => {
 | 
					        Target::Latest(name) | Target::FromLatest(_, name) => {
 | 
				
			||||||
            if editable {
 | 
					            if editable {
 | 
				
			||||||
                bail!("`--editable` is only supported for local packages");
 | 
					                bail!("`--editable` is only supported for local packages");
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
| 
						 | 
					@ -153,7 +153,7 @@ pub(crate) async fn install(
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        // Ex) `ruff>=0.6.0`
 | 
					        // Ex) `ruff>=0.6.0`
 | 
				
			||||||
        Target::UserDefined(package, from) => {
 | 
					        Target::From(package, from) => {
 | 
				
			||||||
            // Parse the positional name. If the user provided more than a package name, it's an error
 | 
					            // Parse the positional name. If the user provided more than a package name, it's an error
 | 
				
			||||||
            // (e.g., `uv install foo==1.0 --from foo`).
 | 
					            // (e.g., `uv install foo==1.0 --from foo`).
 | 
				
			||||||
            let Ok(package) = PackageName::from_str(package) else {
 | 
					            let Ok(package) = PackageName::from_str(package) else {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -22,15 +22,49 @@ pub(crate) enum Target<'a> {
 | 
				
			||||||
    Version(&'a str, Version),
 | 
					    Version(&'a str, Version),
 | 
				
			||||||
    /// e.g., `ruff@latest`
 | 
					    /// e.g., `ruff@latest`
 | 
				
			||||||
    Latest(&'a str),
 | 
					    Latest(&'a str),
 | 
				
			||||||
    /// e.g., `--from ruff==0.6.0`
 | 
					    /// e.g., `ruff --from ruff>=0.6.0`
 | 
				
			||||||
    UserDefined(&'a str, &'a str),
 | 
					    From(&'a str, &'a str),
 | 
				
			||||||
 | 
					    /// e.g., `ruff --from ruff@0.6.0`
 | 
				
			||||||
 | 
					    FromVersion(&'a str, &'a str, Version),
 | 
				
			||||||
 | 
					    /// e.g., `ruff --from ruff@latest`
 | 
				
			||||||
 | 
					    FromLatest(&'a str, &'a str),
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
impl<'a> Target<'a> {
 | 
					impl<'a> Target<'a> {
 | 
				
			||||||
    /// Parse a target into a command name and a requirement.
 | 
					    /// Parse a target into a command name and a requirement.
 | 
				
			||||||
    pub(crate) fn parse(target: &'a str, from: Option<&'a str>) -> Self {
 | 
					    pub(crate) fn parse(target: &'a str, from: Option<&'a str>) -> Self {
 | 
				
			||||||
        if let Some(from) = from {
 | 
					        if let Some(from) = from {
 | 
				
			||||||
            return Self::UserDefined(target, from);
 | 
					            // e.g. `--from ruff`, no special handling
 | 
				
			||||||
 | 
					            let Some((name, version)) = from.split_once('@') else {
 | 
				
			||||||
 | 
					                return Self::From(target, from);
 | 
				
			||||||
 | 
					            };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // e.g. `--from ruff@`, warn and treat the whole thing as the command
 | 
				
			||||||
 | 
					            if version.is_empty() {
 | 
				
			||||||
 | 
					                debug!("Ignoring empty version request in `--from`");
 | 
				
			||||||
 | 
					                return Self::From(target, from);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // e.g., ignore `git+https://github.com/astral-sh/ruff.git@main`
 | 
				
			||||||
 | 
					            if PackageName::from_str(name).is_err() {
 | 
				
			||||||
 | 
					                debug!("Ignoring non-package name `{name}` in `--from`");
 | 
				
			||||||
 | 
					                return Self::From(target, from);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            match version {
 | 
				
			||||||
 | 
					                // e.g., `ruff@latest`
 | 
				
			||||||
 | 
					                "latest" => return Self::FromLatest(target, name),
 | 
				
			||||||
 | 
					                // e.g., `ruff@0.6.0`
 | 
				
			||||||
 | 
					                version => {
 | 
				
			||||||
 | 
					                    if let Ok(version) = Version::from_str(version) {
 | 
				
			||||||
 | 
					                        return Self::FromVersion(target, name, version);
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // e.g. `--from ruff@invalid`, warn and treat the whole thing as the command
 | 
				
			||||||
 | 
					            debug!("Ignoring invalid version request `{version}` in `--from`");
 | 
				
			||||||
 | 
					            return Self::From(target, from);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        // e.g. `ruff`, no special handling
 | 
					        // e.g. `ruff`, no special handling
 | 
				
			||||||
| 
						 | 
					@ -72,12 +106,14 @@ impl<'a> Target<'a> {
 | 
				
			||||||
            Self::Unspecified(name) => name,
 | 
					            Self::Unspecified(name) => name,
 | 
				
			||||||
            Self::Version(name, _) => name,
 | 
					            Self::Version(name, _) => name,
 | 
				
			||||||
            Self::Latest(name) => name,
 | 
					            Self::Latest(name) => name,
 | 
				
			||||||
            Self::UserDefined(name, _) => name,
 | 
					            Self::FromVersion(name, _, _) => name,
 | 
				
			||||||
 | 
					            Self::FromLatest(name, _) => name,
 | 
				
			||||||
 | 
					            Self::From(name, _) => name,
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /// Returns `true` if the target is `latest`.
 | 
					    /// Returns `true` if the target is `latest`.
 | 
				
			||||||
    fn is_latest(&self) -> bool {
 | 
					    fn is_latest(&self) -> bool {
 | 
				
			||||||
        matches!(self, Self::Latest(_))
 | 
					        matches!(self, Self::Latest(_) | Self::FromLatest(_, _))
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -352,7 +352,7 @@ async fn get_or_create_environment(
 | 
				
			||||||
            origin: None,
 | 
					            origin: None,
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        // Ex) `ruff@0.6.0`
 | 
					        // Ex) `ruff@0.6.0`
 | 
				
			||||||
        Target::Version(name, version) => Requirement {
 | 
					        Target::Version(name, version) | Target::FromVersion(_, name, version) => Requirement {
 | 
				
			||||||
            name: PackageName::from_str(name)?,
 | 
					            name: PackageName::from_str(name)?,
 | 
				
			||||||
            extras: vec![],
 | 
					            extras: vec![],
 | 
				
			||||||
            marker: MarkerTree::default(),
 | 
					            marker: MarkerTree::default(),
 | 
				
			||||||
| 
						 | 
					@ -365,7 +365,7 @@ async fn get_or_create_environment(
 | 
				
			||||||
            origin: None,
 | 
					            origin: None,
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        // Ex) `ruff@latest`
 | 
					        // Ex) `ruff@latest`
 | 
				
			||||||
        Target::Latest(name) => Requirement {
 | 
					        Target::Latest(name) | Target::FromLatest(_, name) => Requirement {
 | 
				
			||||||
            name: PackageName::from_str(name)?,
 | 
					            name: PackageName::from_str(name)?,
 | 
				
			||||||
            extras: vec![],
 | 
					            extras: vec![],
 | 
				
			||||||
            marker: MarkerTree::default(),
 | 
					            marker: MarkerTree::default(),
 | 
				
			||||||
| 
						 | 
					@ -376,7 +376,7 @@ async fn get_or_create_environment(
 | 
				
			||||||
            origin: None,
 | 
					            origin: None,
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        // Ex) `ruff>=0.6.0`
 | 
					        // Ex) `ruff>=0.6.0`
 | 
				
			||||||
        Target::UserDefined(_, from) => resolve_names(
 | 
					        Target::From(_, from) => resolve_names(
 | 
				
			||||||
            vec![RequirementsSpecification::parse_package(from)?],
 | 
					            vec![RequirementsSpecification::parse_package(from)?],
 | 
				
			||||||
            &interpreter,
 | 
					            &interpreter,
 | 
				
			||||||
            settings,
 | 
					            settings,
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -2657,6 +2657,94 @@ fn tool_install_at_latest() {
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Test installing a tool with `uv tool install {package} --from {package}@latest`.
 | 
				
			||||||
 | 
					#[test]
 | 
				
			||||||
 | 
					fn tool_install_from_at_latest() {
 | 
				
			||||||
 | 
					    let context = TestContext::new("3.12")
 | 
				
			||||||
 | 
					        .with_filtered_counts()
 | 
				
			||||||
 | 
					        .with_filtered_exe_suffix();
 | 
				
			||||||
 | 
					    let tool_dir = context.temp_dir.child("tools");
 | 
				
			||||||
 | 
					    let bin_dir = context.temp_dir.child("bin");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    uv_snapshot!(context.filters(), context.tool_install()
 | 
				
			||||||
 | 
					        .arg("pybabel")
 | 
				
			||||||
 | 
					        .arg("--from")
 | 
				
			||||||
 | 
					        .arg("babel@latest")
 | 
				
			||||||
 | 
					        .env("UV_TOOL_DIR", tool_dir.as_os_str())
 | 
				
			||||||
 | 
					        .env("XDG_BIN_HOME", bin_dir.as_os_str())
 | 
				
			||||||
 | 
					        .env("PATH", bin_dir.as_os_str()), @r###"
 | 
				
			||||||
 | 
					    success: true
 | 
				
			||||||
 | 
					    exit_code: 0
 | 
				
			||||||
 | 
					    ----- stdout -----
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    ----- stderr -----
 | 
				
			||||||
 | 
					    Resolved [N] packages in [TIME]
 | 
				
			||||||
 | 
					    Prepared [N] packages in [TIME]
 | 
				
			||||||
 | 
					    Installed [N] packages in [TIME]
 | 
				
			||||||
 | 
					     + babel==2.14.0
 | 
				
			||||||
 | 
					    Installed 1 executable: pybabel
 | 
				
			||||||
 | 
					    "###);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    insta::with_settings!({
 | 
				
			||||||
 | 
					        filters => context.filters(),
 | 
				
			||||||
 | 
					    }, {
 | 
				
			||||||
 | 
					        assert_snapshot!(fs_err::read_to_string(tool_dir.join("babel").join("uv-receipt.toml")).unwrap(), @r###"
 | 
				
			||||||
 | 
					        [tool]
 | 
				
			||||||
 | 
					        requirements = [{ name = "babel" }]
 | 
				
			||||||
 | 
					        entrypoints = [
 | 
				
			||||||
 | 
					            { name = "pybabel", install-path = "[TEMP_DIR]/bin/pybabel" },
 | 
				
			||||||
 | 
					        ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        [tool.options]
 | 
				
			||||||
 | 
					        exclude-newer = "2024-03-25T00:00:00Z"
 | 
				
			||||||
 | 
					        "###);
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Test installing a tool with `uv tool install {package} --from {package}@{version}`.
 | 
				
			||||||
 | 
					#[test]
 | 
				
			||||||
 | 
					fn tool_install_from_at_version() {
 | 
				
			||||||
 | 
					    let context = TestContext::new("3.12")
 | 
				
			||||||
 | 
					        .with_filtered_counts()
 | 
				
			||||||
 | 
					        .with_filtered_exe_suffix();
 | 
				
			||||||
 | 
					    let tool_dir = context.temp_dir.child("tools");
 | 
				
			||||||
 | 
					    let bin_dir = context.temp_dir.child("bin");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    uv_snapshot!(context.filters(), context.tool_install()
 | 
				
			||||||
 | 
					        .arg("pybabel")
 | 
				
			||||||
 | 
					        .arg("--from")
 | 
				
			||||||
 | 
					        .arg("babel@2.13.0")
 | 
				
			||||||
 | 
					        .env("UV_TOOL_DIR", tool_dir.as_os_str())
 | 
				
			||||||
 | 
					        .env("XDG_BIN_HOME", bin_dir.as_os_str())
 | 
				
			||||||
 | 
					        .env("PATH", bin_dir.as_os_str()), @r###"
 | 
				
			||||||
 | 
					    success: true
 | 
				
			||||||
 | 
					    exit_code: 0
 | 
				
			||||||
 | 
					    ----- stdout -----
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    ----- stderr -----
 | 
				
			||||||
 | 
					    Resolved [N] packages in [TIME]
 | 
				
			||||||
 | 
					    Prepared [N] packages in [TIME]
 | 
				
			||||||
 | 
					    Installed [N] packages in [TIME]
 | 
				
			||||||
 | 
					     + babel==2.13.0
 | 
				
			||||||
 | 
					    Installed 1 executable: pybabel
 | 
				
			||||||
 | 
					    "###);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    insta::with_settings!({
 | 
				
			||||||
 | 
					        filters => context.filters(),
 | 
				
			||||||
 | 
					    }, {
 | 
				
			||||||
 | 
					        assert_snapshot!(fs_err::read_to_string(tool_dir.join("babel").join("uv-receipt.toml")).unwrap(), @r###"
 | 
				
			||||||
 | 
					        [tool]
 | 
				
			||||||
 | 
					        requirements = [{ name = "babel", specifier = "==2.13.0" }]
 | 
				
			||||||
 | 
					        entrypoints = [
 | 
				
			||||||
 | 
					            { name = "pybabel", install-path = "[TEMP_DIR]/bin/pybabel" },
 | 
				
			||||||
 | 
					        ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        [tool.options]
 | 
				
			||||||
 | 
					        exclude-newer = "2024-03-25T00:00:00Z"
 | 
				
			||||||
 | 
					        "###);
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/// Test upgrading an already installed tool via `{package}@{latest}`.
 | 
					/// Test upgrading an already installed tool via `{package}@{latest}`.
 | 
				
			||||||
#[test]
 | 
					#[test]
 | 
				
			||||||
fn tool_install_at_latest_upgrade() {
 | 
					fn tool_install_at_latest_upgrade() {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue