diff --git a/crates/uv-client/src/base_client.rs b/crates/uv-client/src/base_client.rs index c66c4a038..50151487a 100644 --- a/crates/uv-client/src/base_client.rs +++ b/crates/uv-client/src/base_client.rs @@ -89,6 +89,8 @@ pub struct BaseClientBuilder<'a> { cross_origin_credential_policy: CrossOriginCredentialsPolicy, /// Optional custom reqwest client to use instead of creating a new one. custom_client: Option, + /// uv subcommand in which this client is being used + subcommand: Option>, } /// The policy for handling HTTP redirects. @@ -143,6 +145,7 @@ impl Default for BaseClientBuilder<'_> { redirect_policy: RedirectPolicy::default(), cross_origin_credential_policy: CrossOriginCredentialsPolicy::Secure, custom_client: None, + subcommand: None, } } } @@ -276,6 +279,12 @@ impl<'a> BaseClientBuilder<'a> { self } + #[must_use] + pub fn subcommand(mut self, subcommand: Vec) -> Self { + self.subcommand = Some(subcommand); + self + } + pub fn is_native_tls(&self) -> bool { self.native_tls } @@ -358,7 +367,7 @@ impl<'a> BaseClientBuilder<'a> { let mut user_agent_string = format!("uv/{}", version()); // Add linehaul metadata. - let linehaul = LineHaul::new(self.markers, self.platform); + let linehaul = LineHaul::new(self.markers, self.platform, self.subcommand.clone()); if let Ok(output) = serde_json::to_string(&linehaul) { let _ = write!(user_agent_string, " {output}"); } diff --git a/crates/uv-client/src/linehaul.rs b/crates/uv-client/src/linehaul.rs index 4eea4f755..705d107b3 100644 --- a/crates/uv-client/src/linehaul.rs +++ b/crates/uv-client/src/linehaul.rs @@ -12,6 +12,7 @@ use uv_version::version; pub struct Installer { pub name: Option, pub version: Option, + pub subcommand: Option>, } #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] @@ -63,7 +64,11 @@ pub struct LineHaul { impl LineHaul { /// Initializes Linehaul information based on PEP 508 markers. #[instrument(name = "linehaul", skip_all)] - pub fn new(markers: Option<&MarkerEnvironment>, platform: Option<&Platform>) -> Self { + pub fn new( + markers: Option<&MarkerEnvironment>, + platform: Option<&Platform>, + subcommand: Option>, + ) -> Self { // https://github.com/pypa/pip/blob/24.0/src/pip/_internal/network/session.py#L87 let looks_like_ci = [ EnvVars::BUILD_BUILDID, @@ -123,6 +128,7 @@ impl LineHaul { installer: Option::from(Installer { name: Some("uv".to_string()), version: Some(version().to_string()), + subcommand, }), python: markers.map(|markers| markers.python_full_version().version.to_string()), implementation: Option::from(Implementation { diff --git a/crates/uv-client/tests/it/user_agent_version.rs b/crates/uv-client/tests/it/user_agent_version.rs index 90aecef38..0b2c31bfd 100644 --- a/crates/uv-client/tests/it/user_agent_version.rs +++ b/crates/uv-client/tests/it/user_agent_version.rs @@ -57,7 +57,70 @@ async fn test_user_agent_has_version() -> Result<()> { assert_json_snapshot!(&linehaul.installer, @r#" { "name": "uv", - "version": "[VERSION]" + "version": "[VERSION]", + "subcommand": null + } + "#); + }); + + // Wait for the server task to complete, to be a good citizen. + let _ = server_task.await?; + + Ok(()) +} + +#[tokio::test] +async fn test_user_agent_has_subcommand() -> Result<()> { + // Initialize dummy http server + let (server_task, addr) = start_http_user_agent_server().await?; + + // Initialize uv-client + let cache = Cache::temp()?.init()?; + let client = RegistryClientBuilder::new( + BaseClientBuilder::default().subcommand(vec!["foo".to_owned(), "bar".to_owned()]), + cache, + ) + .build(); + + // Send request to our dummy server + let url = DisplaySafeUrl::from_str(&format!("http://{addr}"))?; + let res = client + .cached_client() + .uncached() + .for_host(&url) + .get(Url::from(url)) + .send() + .await?; + + // Check the HTTP status + assert!(res.status().is_success()); + + // Check User Agent + let body = res.text().await?; + + let (uv_version, uv_linehaul) = body + .split_once(' ') + .expect("Failed to split User-Agent header"); + + // Deserializing Linehaul + let linehaul: LineHaul = serde_json::from_str(uv_linehaul)?; + + // Assert linehaul user agent + let filters = vec![(version(), "[VERSION]")]; + with_settings!({ + filters => filters + }, { + // Assert uv version + assert_snapshot!(uv_version, @"uv/[VERSION]"); + // Assert linehaul json + assert_json_snapshot!(&linehaul.installer, @r#" + { + "name": "uv", + "version": "[VERSION]", + "subcommand": [ + "foo", + "bar" + ] } "#); }); @@ -152,11 +215,12 @@ async fn test_user_agent_has_linehaul() -> Result<()> { assert_json_snapshot!(&linehaul, { ".distro" => "[distro]", ".ci" => "[ci]" - }, @r###" + }, @r#" { "installer": { "name": "uv", - "version": "[VERSION]" + "version": "[VERSION]", + "subcommand": null }, "python": "3.12.2", "implementation": { @@ -174,7 +238,7 @@ async fn test_user_agent_has_linehaul() -> Result<()> { "rustc_version": null, "ci": "[ci]" } - "###); + "#); }); // Assert distro diff --git a/crates/uv/src/lib.rs b/crates/uv/src/lib.rs index b30c9507d..13336d50a 100644 --- a/crates/uv/src/lib.rs +++ b/crates/uv/src/lib.rs @@ -630,7 +630,7 @@ async fn run(mut cli: Cli) -> Result { args.settings.torch_backend, args.settings.dependency_metadata, args.settings.keyring_provider, - &client_builder, + &client_builder.subcommand(vec!["pip".to_owned(), "compile".to_owned()]), args.settings.config_setting, args.settings.config_settings_package, args.settings.build_isolation.clone(), @@ -707,7 +707,7 @@ async fn run(mut cli: Cli) -> Result { args.settings.torch_backend, args.settings.dependency_metadata, args.settings.keyring_provider, - &client_builder, + &client_builder.subcommand(vec!["pip".to_owned(), "sync".to_owned()]), args.settings.allow_empty_requirements, globals.installer_metadata, &args.settings.config_setting, @@ -856,7 +856,7 @@ async fn run(mut cli: Cli) -> Result { args.settings.torch_backend, args.settings.dependency_metadata, args.settings.keyring_provider, - &client_builder, + &client_builder.subcommand(vec!["pip".to_owned(), "install".to_owned()]), args.settings.reinstall, args.settings.link_mode, args.settings.compile_bytecode, @@ -919,7 +919,7 @@ async fn run(mut cli: Cli) -> Result { args.settings.prefix, cache, args.settings.keyring_provider, - &client_builder, + &client_builder.subcommand(vec!["pip".to_owned(), "uninstall".to_owned()]), args.dry_run, printer, globals.preview, @@ -968,7 +968,7 @@ async fn run(mut cli: Cli) -> Result { args.settings.index_locations, args.settings.index_strategy, args.settings.keyring_provider, - &client_builder, + &client_builder.subcommand(vec!["pip".to_owned(), "list".to_owned()]), globals.concurrency, args.settings.strict, args.settings.exclude_newer, @@ -1022,7 +1022,7 @@ async fn run(mut cli: Cli) -> Result { args.settings.index_locations, args.settings.index_strategy, args.settings.keyring_provider, - client_builder, + client_builder.subcommand(vec!["pip".to_owned(), "tree".to_owned()]), globals.concurrency, args.settings.strict, args.settings.exclude_newer, @@ -1109,7 +1109,7 @@ async fn run(mut cli: Cli) -> Result { args.python, args.install_mirrors, &args.settings, - &client_builder, + &client_builder.subcommand(vec!["build".to_owned()]), cli.top_level.no_config, globals.python_preference, globals.python_downloads, @@ -1176,7 +1176,7 @@ async fn run(mut cli: Cli) -> Result { args.settings.index_strategy, args.settings.dependency_metadata, args.settings.keyring_provider, - &client_builder, + &client_builder.subcommand(vec!["venv".to_owned()]), uv_virtualenv::Prompt::from_args(prompt), args.system_site_packages, args.seed, @@ -1216,7 +1216,16 @@ async fn run(mut cli: Cli) -> Result { token, dry_run, }), - }) => commands::self_update(target_version, token, dry_run, printer, client_builder).await, + }) => { + commands::self_update( + target_version, + token, + dry_run, + printer, + client_builder.subcommand(vec!["self".to_owned(), "update".to_owned()]), + ) + .await + } Commands::Self_(SelfNamespace { command: SelfCommand::Version { @@ -1323,6 +1332,13 @@ async fn run(mut cli: Cli) -> Result { .map(RequirementsSource::from_constraints_txt) .collect::, _>>()?; + let client_builder = match invocation_source { + ToolRunCommand::Uvx => client_builder.subcommand(vec!["uvx".to_owned()]), + ToolRunCommand::ToolRun => { + client_builder.subcommand(vec!["tool".to_owned(), "run".to_owned()]) + } + }; + Box::pin(commands::tool_run( args.command, args.from, @@ -1432,7 +1448,7 @@ async fn run(mut cli: Cli) -> Result { args.force, args.options, args.settings, - client_builder, + client_builder.subcommand(vec!["tool".to_owned(), "install".to_owned()]), globals.python_preference, globals.python_downloads, globals.installer_metadata, @@ -1481,7 +1497,7 @@ async fn run(mut cli: Cli) -> Result { args.install_mirrors, args.args, args.filesystem, - client_builder, + client_builder.subcommand(vec!["tool".to_owned(), "upgrade".to_owned()]), globals.python_preference, globals.python_downloads, globals.installer_metadata, @@ -1538,7 +1554,7 @@ async fn run(mut cli: Cli) -> Result { args.python_downloads_json_url, globals.python_preference, globals.python_downloads, - &client_builder, + &client_builder.subcommand(vec!["python".to_owned(), "list".to_owned()]), &cache, printer, globals.preview, @@ -1564,7 +1580,7 @@ async fn run(mut cli: Cli) -> Result { args.python_install_mirror, args.pypy_install_mirror, args.python_downloads_json_url, - client_builder, + client_builder.subcommand(vec!["python".to_owned(), "install".to_owned()]), args.default, globals.python_downloads, cli.top_level.no_config, @@ -1593,7 +1609,7 @@ async fn run(mut cli: Cli) -> Result { args.python_install_mirror, args.pypy_install_mirror, args.python_downloads_json_url, - client_builder, + client_builder.subcommand(vec!["python".to_owned(), "upgrade".to_owned()]), args.default, globals.python_downloads, cli.top_level.no_config, @@ -1631,7 +1647,8 @@ async fn run(mut cli: Cli) -> Result { commands::python_find_script( (&script).into(), args.show_version, - &client_builder, + // TODO(zsol): is this the right thing to do here? + &client_builder.subcommand(vec!["python".to_owned(), "find".to_owned()]), globals.python_preference, globals.python_downloads, cli.top_level.no_config, @@ -1650,7 +1667,7 @@ async fn run(mut cli: Cli) -> Result { args.system, globals.python_preference, args.python_downloads_json_url.as_deref(), - &client_builder, + &client_builder.subcommand(vec!["python".to_owned(), "find".to_owned()]), &cache, printer, globals.preview, @@ -1677,7 +1694,7 @@ async fn run(mut cli: Cli) -> Result { args.global, args.rm, args.install_mirrors, - client_builder, + client_builder.subcommand(vec!["python".to_owned(), "pin".to_owned()]), &cache, printer, globals.preview, @@ -1734,7 +1751,7 @@ async fn run(mut cli: Cli) -> Result { trusted_publishing, keyring_provider, &environment, - &client_builder, + &client_builder.subcommand(vec!["publish".to_owned()]), username, password, check_url, @@ -1877,7 +1894,7 @@ async fn run_project( args.python, args.install_mirrors, args.no_workspace, - &client_builder, + &client_builder.subcommand(vec!["init".to_owned()]), globals.python_preference, globals.python_downloads, no_config, @@ -1938,7 +1955,7 @@ async fn run_project( args.python_platform, args.install_mirrors, args.settings, - client_builder, + client_builder.subcommand(vec!["run".to_owned()]), globals.python_preference, globals.python_downloads, globals.installer_metadata, @@ -1989,7 +2006,7 @@ async fn run_project( globals.python_preference, globals.python_downloads, args.settings, - client_builder, + client_builder.subcommand(vec!["sync".to_owned()]), script, globals.installer_metadata, globals.concurrency, @@ -2035,7 +2052,7 @@ async fn run_project( args.python, args.install_mirrors, args.settings, - client_builder, + client_builder.subcommand(vec!["lock".to_owned()]), script, globals.python_preference, globals.python_downloads, @@ -2162,7 +2179,7 @@ async fn run_project( args.workspace, args.install_mirrors, args.settings, - client_builder, + client_builder.subcommand(vec!["add".to_owned()]), script, globals.python_preference, globals.python_downloads, @@ -2206,7 +2223,7 @@ async fn run_project( args.python, args.install_mirrors, args.settings, - client_builder, + client_builder.subcommand(vec!["remove".to_owned()]), script, globals.python_preference, globals.python_downloads, @@ -2247,7 +2264,7 @@ async fn run_project( args.python, args.install_mirrors, args.settings, - client_builder, + client_builder.subcommand(vec!["version".to_owned()]), globals.python_preference, globals.python_downloads, globals.installer_metadata, @@ -2292,7 +2309,7 @@ async fn run_project( args.python, args.install_mirrors, args.resolver, - &client_builder, + &client_builder.subcommand(vec!["tree".to_owned()]), script, globals.python_preference, globals.python_downloads, @@ -2339,7 +2356,7 @@ async fn run_project( args.python, args.install_mirrors, args.settings, - client_builder, + client_builder.subcommand(vec!["export".to_owned()]), globals.python_preference, globals.python_downloads, globals.concurrency, @@ -2366,7 +2383,7 @@ async fn run_project( args.diff, args.extra_args, args.version, - client_builder, + client_builder.subcommand(vec!["format".to_owned()]), cache, printer, globals.preview,