use std::borrow::Cow;
use std::sync::Arc;
use std::time::Duration;
use percent_encoding::{AsciiSet, NON_ALPHANUMERIC, utf8_percent_encode};
use reqwest::header::{AUTHORIZATION, HeaderMap, HeaderValue, USER_AGENT};
use tracing::debug;
use url::Url;
use crate::constants;
use crate::error::{HFError, HFResult, HttpErrorContext, NotFoundContext};
use crate::retry::{self, RetryConfig};
pub(crate) fn append_path_segments(url: &mut Url, path: &str) -> HFResult<()> {
let url_str = url.as_str().to_string();
let mut segments = url
.path_segments_mut()
.map_err(|()| HFError::InvalidParameter(format!("unexpected non-base URL: {url_str}")))?;
for seg in path.split('/').filter(|s| !s.is_empty()) {
segments.push(seg);
}
Ok(())
}
const REF_ENCODE_SET: &AsciiSet = &NON_ALPHANUMERIC.remove(b'-').remove(b'.').remove(b'_').remove(b'~');
pub(crate) fn encode_ref(revision: &str) -> Cow<'_, str> {
utf8_percent_encode(revision, REF_ENCODE_SET).into()
}
pub fn split_id(id: &str) -> (&str, &str) {
match id.split_once('/') {
Some((owner, name)) => (owner, name),
None => ("", id),
}
}
fn is_relative_location(location: &str) -> bool {
Url::parse(location) == Err(url::ParseError::RelativeUrlWithoutBase) && !location.starts_with("//")
}
pub struct HFClient {
pub(crate) inner: Arc<HFClientInner>,
}
impl Clone for HFClient {
fn clone(&self) -> Self {
Self {
inner: Arc::clone(&self.inner),
}
}
}
impl std::fmt::Debug for HFClient {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mut s = f.debug_struct("HFClient");
s.field("endpoint", &self.inner.endpoint);
s.field("token_set", &self.inner.token.is_some());
s.field("cache_enabled", &self.inner.cache_enabled);
if self.inner.cache_enabled {
s.field("cache_dir", &self.inner.cache_dir);
}
s.finish()
}
}
pub(crate) struct HFClientInner {
pub(crate) client: reqwest::Client,
#[cfg(not(target_family = "wasm"))]
pub(crate) no_redirect_client: reqwest::Client,
pub(crate) retry_config: RetryConfig,
pub(crate) endpoint: String,
pub(crate) token: Option<String>,
pub(crate) cache_dir: std::path::PathBuf,
pub(crate) cache_enabled: bool,
pub(crate) xet_state: std::sync::Mutex<crate::xet::XetState>,
}
pub struct HFClientBuilder {
endpoint: Option<String>,
token: Option<String>,
user_agent: Option<String>,
headers: Option<HeaderMap>,
client: Option<reqwest::Client>,
cache_dir: Option<std::path::PathBuf>,
cache_enabled: Option<bool>,
retry_max_attempts: Option<usize>,
retry_base_delay: Option<Duration>,
}
impl HFClientBuilder {
pub fn new() -> Self {
Self {
endpoint: None,
token: None,
user_agent: None,
headers: None,
client: None,
cache_dir: None,
cache_enabled: None,
retry_max_attempts: None,
retry_base_delay: None,
}
}
pub fn endpoint(mut self, endpoint: impl Into<String>) -> Self {
self.endpoint = Some(endpoint.into());
self
}
pub fn token(mut self, token: impl Into<String>) -> Self {
self.token = Some(token.into());
self
}
pub fn user_agent(mut self, user_agent: impl Into<String>) -> Self {
self.user_agent = Some(user_agent.into());
self
}
pub fn headers(mut self, headers: HeaderMap) -> Self {
self.headers = Some(headers);
self
}
pub fn client(mut self, client: reqwest::Client) -> Self {
self.client = Some(client);
self
}
pub fn cache_dir(mut self, path: impl Into<std::path::PathBuf>) -> Self {
self.cache_dir = Some(path.into());
self
}
pub fn cache_enabled(mut self, enabled: bool) -> Self {
self.cache_enabled = Some(enabled);
self
}
pub fn retry_max_attempts(mut self, n: usize) -> Self {
self.retry_max_attempts = Some(n);
self
}
pub fn retry_base_delay(mut self, delay: Duration) -> Self {
self.retry_base_delay = Some(delay);
self
}
pub fn build(self) -> HFResult<HFClient> {
let endpoint = self
.endpoint
.or_else(|| std::env::var(constants::HF_ENDPOINT).ok())
.unwrap_or_else(|| constants::DEFAULT_HF_ENDPOINT.to_string());
let _ = url::Url::parse(&endpoint)?;
let token = self.token.or_else(resolve_token);
let cache_dir = self.cache_dir.unwrap_or_else(constants::resolve_cache_dir);
let mut default_headers = self.headers.unwrap_or_default();
let user_agent = self.user_agent.unwrap_or_else(|| {
let ua_origin = std::env::var(constants::HF_HUB_USER_AGENT_ORIGIN).ok();
match ua_origin {
Some(origin) => format!("hf-hub/{}; {origin}", env!("CARGO_PKG_VERSION")),
None => format!("hf-hub/{}", env!("CARGO_PKG_VERSION")),
}
});
default_headers.insert(
USER_AGENT,
HeaderValue::from_str(&user_agent)
.map_err(|e| HFError::InvalidParameter(format!("invalid user agent {user_agent:?}: {e}")))?,
);
let client = match self.client {
Some(c) => c,
None => reqwest::Client::builder().default_headers(default_headers.clone()).build()?,
};
#[cfg(not(target_family = "wasm"))]
let no_redirect_client = reqwest::Client::builder()
.default_headers(default_headers)
.redirect(reqwest::redirect::Policy::none())
.build()?;
let retry_config = RetryConfig {
max_attempts: self.retry_max_attempts.unwrap_or(retry::DEFAULT_MAX_ATTEMPTS),
base_delay: self.retry_base_delay.unwrap_or(retry::DEFAULT_BASE_DELAY),
};
Ok(HFClient {
inner: Arc::new(HFClientInner {
client,
#[cfg(not(target_family = "wasm"))]
no_redirect_client,
retry_config,
endpoint: endpoint.trim_end_matches('/').to_string(),
token,
cache_dir,
cache_enabled: self.cache_enabled.unwrap_or(true),
xet_state: std::sync::Mutex::new(crate::xet::XetState::default()),
}),
})
}
#[cfg(feature = "blocking")]
#[cfg_attr(docsrs, doc(cfg(feature = "blocking")))]
pub fn build_sync(self) -> HFResult<crate::blocking::HFClientSync> {
let async_client = self.build()?;
let client = crate::blocking::HFClientSync::from_inner(async_client)?;
Ok(client)
}
}
impl Default for HFClientBuilder {
fn default() -> Self {
Self::new()
}
}
impl HFClient {
pub fn new() -> HFResult<Self> {
HFClientBuilder::new().build()
}
pub fn builder() -> HFClientBuilder {
HFClientBuilder::new()
}
pub(crate) fn http_client(&self) -> &reqwest::Client {
&self.inner.client
}
#[cfg(not(target_family = "wasm"))]
pub(crate) fn no_redirect_client(&self) -> &reqwest::Client {
&self.inner.no_redirect_client
}
pub(crate) fn retry_config(&self) -> &RetryConfig {
&self.inner.retry_config
}
pub fn endpoint(&self) -> &str {
&self.inner.endpoint
}
pub fn cache_dir(&self) -> &std::path::Path {
&self.inner.cache_dir
}
pub fn cache_enabled(&self) -> bool {
self.inner.cache_enabled
}
pub(crate) fn auth_headers(&self) -> HeaderMap {
let mut headers = HeaderMap::new();
if let Some(ref token) = self.inner.token
&& let Ok(val) = HeaderValue::from_str(&format!("Bearer {token}"))
{
headers.insert(AUTHORIZATION, val);
}
headers
}
pub(crate) fn api_url(&self, api_segment: &str, repo_id: &str) -> String {
format!("{}/api/{}/{}", self.endpoint(), api_segment, repo_id)
}
pub(crate) fn download_url(
&self,
url_prefix: &str,
repo_id: &str,
revision: &str,
filename: &str,
) -> HFResult<String> {
let base = format!("{}/{}{}/resolve/{}", self.endpoint(), url_prefix, repo_id, encode_ref(revision));
let mut url = Url::parse(&base)?;
append_path_segments(&mut url, filename)?;
Ok(url.into())
}
pub(crate) fn bucket_api_url(&self, bucket_id: &str) -> String {
format!("{}/api/buckets/{}", self.endpoint(), bucket_id)
}
#[cfg(not(target_family = "wasm"))]
pub(crate) async fn head_with_relative_redirects(
&self,
url: &str,
headers: &HeaderMap,
) -> HFResult<reqwest::Response> {
const MAX_RELATIVE_REDIRECTS: usize = 10;
let mut url = Url::parse(url)?;
for _ in 0..=MAX_RELATIVE_REDIRECTS {
let response = retry::retry(self.retry_config(), || {
self.no_redirect_client().head(url.clone()).headers(headers.clone()).send()
})
.await?;
if response.status().is_redirection()
&& let Some(location) = response.headers().get(reqwest::header::LOCATION)
&& let Ok(location) = location.to_str()
&& is_relative_location(location)
{
url = url.join(location)?;
continue;
}
return Ok(response);
}
Err(HFError::malformed_response_at("too many relative redirects", url.to_string()))
}
pub(crate) async fn check_response(
&self,
response: reqwest::Response,
repo_id: Option<&str>,
not_found_ctx: NotFoundContext,
) -> HFResult<reqwest::Response> {
let status = response.status();
if status.is_success() {
return Ok(response);
}
let retry_after = if status == reqwest::StatusCode::TOO_MANY_REQUESTS {
crate::error::parse_retry_after(response.headers())
} else {
None
};
let context = Box::new(HttpErrorContext::from_response(response).await);
let repo_id_str = repo_id.unwrap_or("").to_string();
match status.as_u16() {
401 => Err(HFError::AuthRequired { context }),
403 => Err(HFError::Forbidden { context }),
404 => match not_found_ctx {
NotFoundContext::Repo => Err(HFError::RepoNotFound {
repo_id: repo_id_str,
context: Some(context),
}),
NotFoundContext::Bucket => Err(HFError::BucketNotFound {
bucket_id: repo_id_str,
context: Some(context),
}),
NotFoundContext::Entry { path } => Err(HFError::EntryNotFound {
path,
repo_id: repo_id_str,
context: Some(context),
}),
NotFoundContext::Revision { revision } => Err(HFError::RevisionNotFound {
revision,
repo_id: repo_id_str,
context: Some(context),
}),
NotFoundContext::Generic => Err(HFError::Http { context }),
},
409 => Err(HFError::Conflict { context }),
429 => Err(HFError::RateLimited { retry_after, context }),
_ => Err(HFError::Http { context }),
}
}
pub(crate) fn xet_session(&self) -> HFResult<(xet::xet_session::XetSession, u64)> {
let mut guard = self
.inner
.xet_state
.lock()
.map_err(|e| HFError::Other(format!("xet session mutex poisoned: {e}")))?;
if let Some(ref session) = guard.session {
return Ok((session.clone(), guard.generation));
}
#[cfg(not(target_family = "wasm"))]
let builder = xet::xet_session::XetSessionBuilder::new();
#[cfg(target_family = "wasm")]
let builder = {
let mut config = xet::xet_session::XetConfig::new();
config.client.ac_max_upload_concurrency = 8;
config.client.ac_max_download_concurrency = 8;
xet::xet_session::XetSessionBuilder::new_with_config(config)
};
let session = builder
.build()
.map_err(|e| HFError::xet(crate::error::XetOperation::Session, e))?;
guard.session = Some(session.clone());
guard.generation += 1;
Ok((session, guard.generation))
}
pub(crate) fn replace_xet_session(&self, generation: u64, err: &xet::error::XetError) {
tracing::warn!(error = %err, generation, "replacing cached XetSession");
let Ok(mut guard) = self.inner.xet_state.lock() else {
return;
};
if guard.generation == generation {
guard.session = None;
}
}
}
fn resolve_token() -> Option<String> {
if let Ok(val) = std::env::var(constants::HF_HUB_DISABLE_IMPLICIT_TOKEN)
&& !val.is_empty()
{
debug!("implicit token disabled via HF_HUB_DISABLE_IMPLICIT_TOKEN");
return None;
}
if let Ok(token) = std::env::var(constants::HF_TOKEN)
&& !token.is_empty()
{
debug!("resolved token from HF_TOKEN env var");
return Some(token);
}
if let Ok(path) = std::env::var(constants::HF_TOKEN_PATH)
&& let Ok(token) = std::fs::read_to_string(&path)
{
let token = token.trim().to_string();
if !token.is_empty() {
debug!("resolved token from HF_TOKEN_PATH file");
return Some(token);
}
}
let hf_home = constants::hf_home();
let token_path = hf_home.join(constants::TOKEN_FILENAME);
if let Ok(token) = std::fs::read_to_string(&token_path) {
let token = token.trim().to_string();
if !token.is_empty() {
debug!("resolved token from stored token file");
return Some(token);
}
}
debug!("no token found");
None
}
#[cfg(test)]
mod tests {
use serial_test::serial;
use url::Url;
use super::{HFClientBuilder, append_path_segments, encode_ref, is_relative_location, split_id};
#[test]
fn append_path_segments_encodes_special_chars() {
let mut url = Url::parse("https://example.com/base").unwrap();
append_path_segments(&mut url, "with space/and#hash?and+plus").unwrap();
assert_eq!(url.as_str(), "https://example.com/base/with%20space/and%23hash%3Fand+plus");
}
#[test]
fn append_path_segments_handles_leading_trailing_and_repeated_slashes() {
let mut url = Url::parse("https://example.com/base").unwrap();
append_path_segments(&mut url, "/a//b/c/").unwrap();
assert_eq!(url.as_str(), "https://example.com/base/a/b/c");
}
#[test]
fn append_path_segments_encodes_percent_literals() {
let mut url = Url::parse("https://example.com/base").unwrap();
append_path_segments(&mut url, "a%2Fb/c").unwrap();
assert_eq!(url.as_str(), "https://example.com/base/a%252Fb/c");
}
#[test]
fn append_path_segments_handles_unicode() {
let mut url = Url::parse("https://example.com/base").unwrap();
append_path_segments(&mut url, "ümlaut/файл.txt").unwrap();
assert_eq!(url.as_str(), "https://example.com/base/%C3%BCmlaut/%D1%84%D0%B0%D0%B9%D0%BB.txt");
}
#[test]
fn append_path_segments_empty_path_is_noop() {
let mut url = Url::parse("https://example.com/base").unwrap();
append_path_segments(&mut url, "").unwrap();
assert_eq!(url.as_str(), "https://example.com/base");
}
#[test]
fn encode_ref_encodes_slash_and_reserved_chars() {
assert_eq!(encode_ref("feature/x"), "feature%2Fx");
assert_eq!(encode_ref("fix#123"), "fix%23123");
assert_eq!(encode_ref("wip?draft"), "wip%3Fdraft");
assert_eq!(encode_ref("v1.0+beta"), "v1.0%2Bbeta");
assert_eq!(encode_ref("with space"), "with%20space");
assert_eq!(encode_ref("100%done"), "100%25done");
}
#[test]
fn encode_ref_preserves_unreserved_chars() {
assert_eq!(encode_ref("main"), "main");
assert_eq!(encode_ref("v1.0-rc.2_final~ok"), "v1.0-rc.2_final~ok");
assert_eq!(encode_ref("abc123def456"), "abc123def456");
}
#[test]
fn encode_ref_keeps_compare_dots_literal() {
assert_eq!(encode_ref("main..feature/x"), "main..feature%2Fx");
assert_eq!(encode_ref("v1.0...v2.0"), "v1.0...v2.0");
}
#[test]
fn encode_ref_encodes_unicode() {
assert_eq!(encode_ref("ветка"), "%D0%B2%D0%B5%D1%82%D0%BA%D0%B0");
}
#[test]
fn is_relative_location_classification() {
assert!(is_relative_location("/Snowflake/repo/resolve/main/config.json"));
assert!(is_relative_location("relative/path"));
assert!(!is_relative_location("https://cdn-lfs.huggingface.co/repos/x"));
assert!(!is_relative_location("//cdn-lfs.huggingface.co/repos/x"));
}
#[test]
fn download_url_encodes_revision() {
let client = HFClientBuilder::new().endpoint("https://huggingface.co").build().unwrap();
let url = client.download_url("", "owner/repo", "feature/x", "file.bin").unwrap();
assert_eq!(url, "https://huggingface.co/owner/repo/resolve/feature%2Fx/file.bin");
let url = client.download_url("", "owner/repo", "fix#123", "file.bin").unwrap();
assert_eq!(url, "https://huggingface.co/owner/repo/resolve/fix%23123/file.bin");
}
#[test]
fn download_url_encodes_filename_segments() {
let client = HFClientBuilder::new().endpoint("https://huggingface.co").build().unwrap();
let url = client
.download_url("", "owner/repo", "main", "subdir/file name #1.bin")
.unwrap();
assert_eq!(url, "https://huggingface.co/owner/repo/resolve/main/subdir/file%20name%20%231.bin");
}
#[test]
fn download_url_dataset_prefix() {
let client = HFClientBuilder::new().endpoint("https://huggingface.co").build().unwrap();
let url = client.download_url("datasets/", "owner/ds", "v1.0", "data/train.csv").unwrap();
assert_eq!(url, "https://huggingface.co/datasets/owner/ds/resolve/v1.0/data/train.csv");
}
#[test]
fn split_id_splits_owner_and_name() {
assert_eq!(split_id("openai-community/gpt2"), ("openai-community", "gpt2"));
assert_eq!(split_id("HuggingFaceFW/fineweb"), ("HuggingFaceFW", "fineweb"));
}
#[test]
fn split_id_ownerless_short_form() {
assert_eq!(split_id("gpt2"), ("", "gpt2"));
assert_eq!(split_id(""), ("", ""));
}
#[test]
fn split_id_only_splits_first_slash() {
assert_eq!(split_id("owner/sub/name"), ("owner", "sub/name"));
}
#[test]
fn split_id_empty_owner_or_name() {
assert_eq!(split_id("/name"), ("", "name"));
assert_eq!(split_id("owner/"), ("owner", ""));
}
#[test]
fn test_builder_cache_dir_explicit() {
let client = HFClientBuilder::new().cache_dir("/tmp/my-cache").build().unwrap();
assert_eq!(client.cache_dir(), std::path::Path::new("/tmp/my-cache"));
}
#[test]
#[serial]
fn test_builder_cache_dir_default() {
let client = HFClientBuilder::new().build().unwrap();
let path_str = client.cache_dir().to_string_lossy();
assert!(path_str.contains("huggingface") && path_str.ends_with("hub"));
}
#[test]
fn test_xet_session_lazy_creation() {
let client = HFClientBuilder::new().build().unwrap();
assert!(client.inner.xet_state.lock().unwrap().session.is_none());
let (_s1, _gen) = client.xet_session().unwrap();
assert!(client.inner.xet_state.lock().unwrap().session.is_some());
}
#[test]
fn test_xet_session_shared_across_clones() {
let client = HFClientBuilder::new().build().unwrap();
let clone = client.clone();
let (_s1, _gen) = client.xet_session().unwrap();
assert!(clone.inner.xet_state.lock().unwrap().session.is_some());
}
#[test]
fn test_xet_session_recovers_after_abort() {
let client = HFClientBuilder::new().build().unwrap();
let (session, generation) = client.xet_session().unwrap();
session.abort().unwrap();
match session.new_file_download_group() {
Ok(_) => panic!("expected error after abort"),
Err(e) => client.replace_xet_session(generation, &e),
}
let (recovered, _) = client.xet_session().unwrap();
assert!(recovered.new_file_download_group().is_ok());
}
#[test]
fn test_xet_session_recovers_after_sigint_abort() {
let client = HFClientBuilder::new().build().unwrap();
let (session, generation) = client.xet_session().unwrap();
session.sigint_abort().unwrap();
client.replace_xet_session(generation, &xet::error::XetError::KeyboardInterrupt);
let (recovered, _) = client.xet_session().unwrap();
assert!(recovered.new_file_download_group().is_ok());
}
#[test]
fn test_replace_and_retry_after_abort() {
let client = HFClientBuilder::new().build().unwrap();
let (session, generation) = client.xet_session().unwrap();
assert!(session.new_file_download_group().is_ok());
session.abort().unwrap();
let group = match session.new_file_download_group() {
Ok(b) => b,
Err(e) => {
client.replace_xet_session(generation, &e);
client
.xet_session()
.unwrap()
.0
.new_file_download_group()
.expect("fresh session factory call should succeed")
},
};
drop(group);
}
#[test]
fn test_replace_with_stale_generation_is_noop() {
let client = HFClientBuilder::new().build().unwrap();
let (session, gen1) = client.xet_session().unwrap();
session.abort().unwrap();
client.replace_xet_session(gen1, &xet::error::XetError::KeyboardInterrupt);
let (_fresh, gen2) = client.xet_session().unwrap();
assert_ne!(gen1, gen2);
client.replace_xet_session(gen1, &xet::error::XetError::KeyboardInterrupt);
let (still_fresh, gen3) = client.xet_session().unwrap();
assert_eq!(gen2, gen3);
assert!(still_fresh.new_file_download_group().is_ok());
}
#[test]
fn test_xet_session_reuse_without_replacement() {
let client = HFClientBuilder::new().build().unwrap();
let (s1, g1) = client.xet_session().unwrap();
let (s2, g2) = client.xet_session().unwrap();
assert_eq!(g1, g2);
assert!(s1.new_file_download_group().is_ok());
assert!(s2.new_file_download_group().is_ok());
}
mod token_precedence {
use std::io::Write;
use serial_test::serial;
use tempfile::TempDir;
use super::HFClientBuilder;
use crate::constants::{HF_HOME, HF_HUB_DISABLE_IMPLICIT_TOKEN, HF_TOKEN, HF_TOKEN_PATH, TOKEN_FILENAME};
struct EnvGuard {
saved: Vec<(&'static str, Option<String>)>,
_hf_home: TempDir,
}
impl EnvGuard {
fn new() -> Self {
let hf_home = tempfile::tempdir().expect("tempdir for HF_HOME");
let keys = [HF_TOKEN, HF_TOKEN_PATH, HF_HOME, HF_HUB_DISABLE_IMPLICIT_TOKEN];
let saved = keys.iter().map(|k| (*k, std::env::var(*k).ok())).collect();
for k in keys {
unsafe { std::env::remove_var(k) };
}
unsafe { std::env::set_var(HF_HOME, hf_home.path()) };
Self {
saved,
_hf_home: hf_home,
}
}
}
impl Drop for EnvGuard {
fn drop(&mut self) {
for (k, v) in &self.saved {
match v {
Some(val) => unsafe { std::env::set_var(k, val) },
None => unsafe { std::env::remove_var(k) },
}
}
}
}
fn write_token_file(dir: &std::path::Path, name: &str, contents: &str) -> std::path::PathBuf {
let path = dir.join(name);
let mut f = std::fs::File::create(&path).unwrap();
f.write_all(contents.as_bytes()).unwrap();
path
}
#[test]
#[serial]
fn test_explicit_token_overrides_env() {
let _g = EnvGuard::new();
unsafe { std::env::set_var(HF_TOKEN, "env-token") };
let client = HFClientBuilder::new().token("explicit-token").build().unwrap();
assert_eq!(client.inner.token.as_deref(), Some("explicit-token"));
}
#[test]
#[serial]
fn test_env_token_used_when_no_explicit() {
let _g = EnvGuard::new();
unsafe { std::env::set_var(HF_TOKEN, "env-token") };
let client = HFClientBuilder::new().build().unwrap();
assert_eq!(client.inner.token.as_deref(), Some("env-token"));
}
#[test]
#[serial]
fn test_env_token_overrides_token_path_file() {
let g = EnvGuard::new();
let dir = tempfile::tempdir().unwrap();
let token_file = write_token_file(dir.path(), "tok", "file-token");
unsafe { std::env::set_var(HF_TOKEN, "env-token") };
unsafe { std::env::set_var(HF_TOKEN_PATH, &token_file) };
let client = HFClientBuilder::new().build().unwrap();
assert_eq!(client.inner.token.as_deref(), Some("env-token"));
drop(g);
}
#[test]
#[serial]
fn test_token_path_file_used_when_no_env() {
let _g = EnvGuard::new();
let dir = tempfile::tempdir().unwrap();
let token_file = write_token_file(dir.path(), "tok", "file-token\n");
unsafe { std::env::set_var(HF_TOKEN_PATH, &token_file) };
let client = HFClientBuilder::new().build().unwrap();
assert_eq!(client.inner.token.as_deref(), Some("file-token"));
}
#[test]
#[serial]
fn test_token_from_hf_home_file() {
let g = EnvGuard::new();
let hf_home = std::env::var(HF_HOME).unwrap();
write_token_file(std::path::Path::new(&hf_home), TOKEN_FILENAME, "home-token");
let client = HFClientBuilder::new().build().unwrap();
assert_eq!(client.inner.token.as_deref(), Some("home-token"));
drop(g);
}
#[test]
#[serial]
fn test_disable_implicit_token_returns_none() {
let _g = EnvGuard::new();
unsafe { std::env::set_var(HF_TOKEN, "env-token") };
unsafe { std::env::set_var(HF_HUB_DISABLE_IMPLICIT_TOKEN, "1") };
let client = HFClientBuilder::new().build().unwrap();
assert!(client.inner.token.is_none());
}
#[test]
#[serial]
fn test_disable_implicit_token_does_not_block_explicit() {
let _g = EnvGuard::new();
unsafe { std::env::set_var(HF_HUB_DISABLE_IMPLICIT_TOKEN, "1") };
let client = HFClientBuilder::new().token("explicit-token").build().unwrap();
assert_eq!(client.inner.token.as_deref(), Some("explicit-token"));
}
#[test]
#[serial]
fn test_no_token_anywhere_is_none() {
let _g = EnvGuard::new();
let client = HFClientBuilder::new().build().unwrap();
assert!(client.inner.token.is_none());
}
}
}