Attach subcommand to User-Agent string (#16837)

This commit is contained in:
Zsolt Dollenstein 2025-12-01 15:29:54 +00:00 committed by GitHub
parent 5773b12fa9
commit 6b00d6522c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 130 additions and 34 deletions

View File

@ -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<Client>,
/// uv subcommand in which this client is being used
subcommand: Option<Vec<String>>,
}
/// 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<String>) -> 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}");
}

View File

@ -12,6 +12,7 @@ use uv_version::version;
pub struct Installer {
pub name: Option<String>,
pub version: Option<String>,
pub subcommand: Option<Vec<String>>,
}
#[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<Vec<String>>,
) -> 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 {

View File

@ -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

View File

@ -630,7 +630,7 @@ async fn run(mut cli: Cli) -> Result<ExitStatus> {
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<ExitStatus> {
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<ExitStatus> {
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<ExitStatus> {
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<ExitStatus> {
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<ExitStatus> {
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<ExitStatus> {
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<ExitStatus> {
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<ExitStatus> {
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<ExitStatus> {
.map(RequirementsSource::from_constraints_txt)
.collect::<Result<Vec<_>, _>>()?;
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<ExitStatus> {
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<ExitStatus> {
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<ExitStatus> {
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<ExitStatus> {
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<ExitStatus> {
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<ExitStatus> {
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<ExitStatus> {
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<ExitStatus> {
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<ExitStatus> {
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,