Script 'mail_helper' called by obssrc
Hello community,
here is the log from the commit of package python-huggingface-hub for
openSUSE:Factory checked in at 2026-07-28 18:15:33
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-huggingface-hub (Old)
and /work/SRC/openSUSE:Factory/.python-huggingface-hub.new.2004 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-huggingface-hub"
Tue Jul 28 18:15:33 2026 rev:9 rq:1368071 version:1.25.1
Changes:
--------
---
/work/SRC/openSUSE:Factory/python-huggingface-hub/python-huggingface-hub.changes
2026-07-17 18:52:09.854701101 +0200
+++
/work/SRC/openSUSE:Factory/.python-huggingface-hub.new.2004/python-huggingface-hub.changes
2026-07-28 18:17:15.513164518 +0200
@@ -1,0 +2,12 @@
+Mon Jul 27 16:45:10 UTC 2026 - Martin Pluskal <[email protected]>
+
+- update to 1.25.1:
+ * Stop injecting the HF token into sandbox jobs as SBX_DL_TOKEN
+ * Auto-name jobs on creation from the image plus a command hash
+ * Add a timeout parameter to the safetensors metadata methods
+ * Warn on cache inconsistencies and fix the file-count progress bar
+ on completion
+ * Fix catastrophic backtracking (ReDoS) in the model card YAML
+ block regular expression
+
+-------------------------------------------------------------------
Old:
----
huggingface_hub-1.24.0.tar.gz
New:
----
huggingface_hub-1.25.1.tar.gz
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Other differences:
------------------
++++++ python-huggingface-hub.spec ++++++
--- /var/tmp/diff_new_pack.Cz5Bk4/_old 2026-07-28 18:17:18.189258069 +0200
+++ /var/tmp/diff_new_pack.Cz5Bk4/_new 2026-07-28 18:17:18.205258628 +0200
@@ -18,7 +18,7 @@
%{?sle15_python_module_pythons}
Name: python-huggingface-hub
-Version: 1.24.0
+Version: 1.25.1
Release: 0
Summary: Client library for interaction with the huggingface hub
License: Apache-2.0
++++++ huggingface_hub-1.24.0.tar.gz -> huggingface_hub-1.25.1.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/huggingface_hub-1.24.0/PKG-INFO
new/huggingface_hub-1.25.1/PKG-INFO
--- old/huggingface_hub-1.24.0/PKG-INFO 2026-07-17 11:53:43.926415700 +0200
+++ new/huggingface_hub-1.25.1/PKG-INFO 2026-07-27 11:23:51.058730000 +0200
@@ -1,6 +1,6 @@
Metadata-Version: 2.4
Name: huggingface_hub
-Version: 1.24.0
+Version: 1.25.1
Summary: Client library to download and publish models, datasets and other
repos on the huggingface.co hub
Home-page: https://github.com/huggingface/huggingface_hub
Author: Hugging Face, Inc.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/huggingface_hub-1.24.0/src/huggingface_hub/__init__.py
new/huggingface_hub-1.25.1/src/huggingface_hub/__init__.py
--- old/huggingface_hub-1.24.0/src/huggingface_hub/__init__.py 2026-07-17
11:53:37.000000000 +0200
+++ new/huggingface_hub-1.25.1/src/huggingface_hub/__init__.py 2026-07-27
11:23:46.000000000 +0200
@@ -46,7 +46,7 @@
from typing import TYPE_CHECKING
-__version__ = "1.24.0"
+__version__ = "1.25.1"
# Alphabetical order of definitions is ensured in tests
# WARNING: any comment added in this dictionary definition will be lost when
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/huggingface_hub-1.24.0/src/huggingface_hub/_commit_api.py
new/huggingface_hub-1.25.1/src/huggingface_hub/_commit_api.py
--- old/huggingface_hub-1.24.0/src/huggingface_hub/_commit_api.py
2026-07-17 11:53:37.000000000 +0200
+++ new/huggingface_hub-1.25.1/src/huggingface_hub/_commit_api.py
2026-07-27 11:23:46.000000000 +0200
@@ -16,8 +16,6 @@
from pathlib import Path, PurePosixPath
from typing import TYPE_CHECKING, Any, BinaryIO, Literal, NamedTuple, Union
-from tqdm.contrib.concurrent import thread_map
-
from . import constants
from .errors import EntryNotFoundError
from .file_download import hf_hub_url
@@ -28,6 +26,7 @@
chunk_iterable,
get_session,
hf_raise_for_status,
+ hf_thread_map,
http_backoff,
logging,
sha,
@@ -536,7 +535,7 @@
logger.debug(
f"Uploading {len(filtered_actions)} LFS files to the Hub using up
to {num_threads} threads concurrently"
)
- thread_map(
+ hf_thread_map(
_wrapped_lfs_upload,
filtered_actions,
desc=f"Upload {len(filtered_actions)} LFS files",
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/huggingface_hub-1.24.0/src/huggingface_hub/_jobs_api.py
new/huggingface_hub-1.25.1/src/huggingface_hub/_jobs_api.py
--- old/huggingface_hub-1.24.0/src/huggingface_hub/_jobs_api.py 2026-07-17
11:53:37.000000000 +0200
+++ new/huggingface_hub-1.25.1/src/huggingface_hub/_jobs_api.py 2026-07-27
11:23:46.000000000 +0200
@@ -13,6 +13,7 @@
# limitations under the License.
import hashlib
import platform
+import re
from dataclasses import dataclass
from datetime import datetime
from enum import Enum
@@ -91,6 +92,14 @@
# Stages indicating the Job has reached a terminal state and will not run
further.
TERMINAL_JOB_STAGES = (JobStage.COMPLETED, JobStage.CANCELED, JobStage.ERROR,
JobStage.DELETED)
+# URL prefixes identifying an image that points to a HF Space rather than a
Docker image.
+_SPACE_IMAGE_PREFIXES = (
+ "https://huggingface.co/spaces/",
+ "https://hf.co/spaces/",
+ "huggingface.co/spaces/",
+ "hf.co/spaces/",
+)
+
@dataclass
class JobStatus:
@@ -510,6 +519,55 @@
return f"{dirname}-{digest}"
+def _sanitize_job_name(name: str) -> str:
+ """Sanitize a string so it is a valid Job `name` label.
+
+ Job names must match `^[a-zA-Z0-9._-]*$` and are also stored as tags,
which only allow
+ alphanumerics, `-` and `_`. Every other character (including `/`, `:` and
`.`) is replaced with `-`.
+ """
+ return re.sub(r"[^a-zA-Z0-9_-]", "-", name)
+
+
+def _short_invocation_hash(parts: list[str]) -> str:
+ """Short, stable hash of a Job invocation (image/script + command line).
+
+ Appended to auto-generated names so reruns of the same command share a
name while different ones don't.
+ """
+ return hashlib.sha256("\x00".join(parts).encode()).hexdigest()[:8]
+
+
+def _default_job_name_from_image(image: str, command: list[str]) -> str:
+ """Derive a default Job name from a Docker image or Space reference and
its command.
+
+ A short hash of the full command line is appended to group reruns of the
same command.
+ e.g. python:3.12 + [foo, --truc] -> python-3-12-1a2b3c4d
+ hf.co/spaces/lhoestq/duckdb -> lhoestq-duckdb-<hash>
+ pytorch/pytorch:2.6.0-cuda12.4-... ->
pytorch-2-6-0-cuda12-4-...-<hash>
+ """
+ for prefix in _SPACE_IMAGE_PREFIXES:
+ if image.startswith(prefix):
+ base = _sanitize_job_name(image[len(prefix) :] or image)
+ break
+ else:
+ base = _sanitize_job_name(image.rstrip("/").split("/")[-1] or image)
# drop registry host and namespace
+ return f"{base}-{_short_invocation_hash([image, *command])}"
+
+
+def _default_job_name_from_script(script: str, script_args: list[str]) -> str:
+ """Derive a default Job name from a UV script path, URL, or command and
its arguments.
+
+ A short hash of the full command line is appended to group reruns of the
same command.
+ e.g. my_script.py + [--epochs, 3] -> my_script-1a2b3c4d
+ https://.../sft.py?raw=1 -> sft-<hash>
+ lighteval -> lighteval-<hash>
+ """
+ name = script.split("?", 1)[0].split("#", 1)[0].rstrip("/").split("/")[-1]
+ if name.endswith(".py"):
+ name = name[: -len(".py")]
+ base = _sanitize_job_name(name or script)
+ return f"{base}-{_short_invocation_hash([script, *script_args])}"
+
+
def _create_job_spec(
*,
image: str,
@@ -559,12 +617,7 @@
if ssh:
job_spec["ssh"] = {"enabled": True}
# input is either from docker hub or from HF spaces
- for prefix in (
- "https://huggingface.co/spaces/",
- "https://hf.co/spaces/",
- "huggingface.co/spaces/",
- "hf.co/spaces/",
- ):
+ for prefix in _SPACE_IMAGE_PREFIXES:
if image.startswith(prefix):
job_spec["spaceId"] = image[len(prefix) :]
break
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/huggingface_hub-1.24.0/src/huggingface_hub/_local_folder.py
new/huggingface_hub-1.25.1/src/huggingface_hub/_local_folder.py
--- old/huggingface_hub-1.24.0/src/huggingface_hub/_local_folder.py
2026-07-17 11:53:37.000000000 +0200
+++ new/huggingface_hub-1.25.1/src/huggingface_hub/_local_folder.py
2026-07-27 11:23:46.000000000 +0200
@@ -431,15 +431,26 @@
"""Return the path to the `.cache/huggingface` directory in a local
directory."""
# Wrap in lru_cache to avoid overwriting the .gitignore file if called
multiple times
path = local_dir / ".cache" / "huggingface"
- path.mkdir(exist_ok=True, parents=True)
+ # Without long path support enabled, Windows caps directory paths at 247
characters
+ # (MAX_PATH minus room for an 8.3 file name), so creating the
`.cache/huggingface` directory
+ # (and the bookkeeping files below) fails for a deep `local_dir`.
+ # Use the extended-length "\\?\" prefix for the filesystem operations,
matching what
+ # `get_local_download_paths`/`get_local_upload_paths` already do for the
download/upload paths.
+ # The un-prefixed `path` is still returned so callers keep
re-deriving/prefixing as before.
+ target = path
+ if os.name == "nt":
+ abs_path = os.path.abspath(path)
+ if len(abs_path) > 247 and not abs_path.startswith("\\\\?\\"):
+ target = Path("\\\\?\\" + abs_path)
+ target.mkdir(exist_ok=True, parents=True)
# Create a CACHEDIR.TAG so backup tools can skip this directory.
- _create_cachedir_tag(path)
+ _create_cachedir_tag(target)
# Create a .gitignore file in the .cache/huggingface directory if it
doesn't exist
# Should be thread-safe enough like this.
- gitignore = path / ".gitignore"
- gitignore_lock = path / ".gitignore.lock"
+ gitignore = target / ".gitignore"
+ gitignore_lock = target / ".gitignore.lock"
if not gitignore.exists():
try:
with WeakFileLock(gitignore_lock, timeout=0.1):
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/huggingface_hub-1.24.0/src/huggingface_hub/_sandbox.py
new/huggingface_hub-1.25.1/src/huggingface_hub/_sandbox.py
--- old/huggingface_hub-1.24.0/src/huggingface_hub/_sandbox.py 2026-07-17
11:53:37.000000000 +0200
+++ new/huggingface_hub-1.25.1/src/huggingface_hub/_sandbox.py 2026-07-27
11:23:46.000000000 +0200
@@ -77,15 +77,16 @@
# hf-mount path where the server bucket is mounted on every sandbox job
_SERVER_MOUNT_PATH = "/.hf-sbx-server"
-# Job startup script (needs only /bin/sh)
+# Job startup script (needs only /bin/sh). The server bucket is public, so the
download is
+# unauthenticated: no HF credential is ever placed in the job environment (see
`_derive_sandbox_token`).
_BOOTSTRAP_DOWNLOAD = """\
set -e
d=/tmp/.sbx-server
-if command -v wget >/dev/null 2>&1; then wget -q --header "Authorization:
Bearer $SBX_DL_TOKEN" -O "$d" "$SBX_SERVER_URL"
-elif command -v curl >/dev/null 2>&1; then curl -fsSL -H "Authorization:
Bearer $SBX_DL_TOKEN" -o "$d" "$SBX_SERVER_URL"
+if command -v wget >/dev/null 2>&1; then wget -q -O "$d" "$SBX_SERVER_URL"
+elif command -v curl >/dev/null 2>&1; then curl -fsSL -o "$d" "$SBX_SERVER_URL"
else cp "$SBX_SERVER_MOUNT/sbx-server" "$d"; fi
chmod +x "$d"
-unset SBX_DL_TOKEN SBX_SERVER_URL SBX_SERVER_MOUNT
+unset SBX_SERVER_URL SBX_SERVER_MOUNT
exec "$d"
"""
@@ -1685,7 +1686,6 @@
job_secrets["HF_TOKEN"] = hf_token
job_env["SBX_SERVER_URL"] =
f"{api.endpoint}/buckets/{constants.SANDBOX_SERVER_BUCKET}/resolve/sbx-server"
- job_secrets["SBX_DL_TOKEN"] = hf_token
# Always mount the server bucket as a transparent fallback for images
without wget/curl.
# It's only read (paying the ~2-3s FUSE cost) when the bootstrap script
can't download.
job_env["SBX_SERVER_MOUNT"] = _SERVER_MOUNT_PATH
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/huggingface_hub-1.24.0/src/huggingface_hub/_snapshot_download.py
new/huggingface_hub-1.25.1/src/huggingface_hub/_snapshot_download.py
--- old/huggingface_hub-1.24.0/src/huggingface_hub/_snapshot_download.py
2026-07-17 11:53:37.000000000 +0200
+++ new/huggingface_hub-1.25.1/src/huggingface_hub/_snapshot_download.py
2026-07-27 11:23:46.000000000 +0200
@@ -4,7 +4,6 @@
import httpx
from tqdm.auto import tqdm as base_tqdm
-from tqdm.contrib.concurrent import thread_map
from . import constants
from ._tree_cache import TreeCacheEntry, read_tree_cache,
tree_cache_folder_for_local_dir, write_tree_cache
@@ -28,7 +27,7 @@
_set_aggregate_rate_postfix,
_update_transfer_bar,
)
-from .utils.tqdm import _create_progress_bar
+from .utils.tqdm import _create_progress_bar, hf_thread_map
from .utils.tqdm import tqdm as hf_tqdm
@@ -512,7 +511,7 @@
)
)
- thread_map(
+ hf_thread_map(
_inner_hf_hub_download,
filtered_repo_files,
desc=tqdm_desc,
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/huggingface_hub-1.24.0/src/huggingface_hub/_upload_pipeline.py
new/huggingface_hub-1.25.1/src/huggingface_hub/_upload_pipeline.py
--- old/huggingface_hub-1.24.0/src/huggingface_hub/_upload_pipeline.py
2026-07-17 11:53:37.000000000 +0200
+++ new/huggingface_hub-1.25.1/src/huggingface_hub/_upload_pipeline.py
2026-07-27 11:23:46.000000000 +0200
@@ -622,7 +622,9 @@
# Nothing was committed (everything unchanged/ignored): mimic
`create_commit` and
# return info about the latest commit on the target revision.
logger.warning("No files have been modified since last commit.
Skipping to prevent empty commit.")
- info = self.api.repo_info(repo_id=self.repo_id,
repo_type=self.repo_type, revision=self.revision)
+ info = self.api.repo_info(
+ repo_id=self.repo_id, repo_type=self.repo_type,
revision=self.revision, token=self.token
+ )
url_prefix = self.api.endpoint
if self.repo_type != constants.REPO_TYPE_MODEL:
url_prefix = f"{url_prefix}/{self.repo_type}s"
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/huggingface_hub-1.24.0/src/huggingface_hub/cli/cache.py
new/huggingface_hub-1.25.1/src/huggingface_hub/cli/cache.py
--- old/huggingface_hub-1.24.0/src/huggingface_hub/cli/cache.py 2026-07-17
11:53:37.000000000 +0200
+++ new/huggingface_hub-1.25.1/src/huggingface_hub/cli/cache.py 2026-07-27
11:23:46.000000000 +0200
@@ -418,6 +418,12 @@
help="Limit the number of results returned. Returns only the top N
entries after sorting.",
),
] = None,
+ show_warnings: Annotated[
+ bool,
+ Option(
+ help="Show warnings about cache inconsistencies.",
+ ),
+ ] = False,
) -> None:
"""List cached repositories or revisions."""
try:
@@ -508,6 +514,15 @@
)
)
+ if len(hf_cache_info.warnings):
+ if show_warnings:
+ for warning in hf_cache_info.warnings:
+ out.warning(str(warning).rstrip(".") + ". Repo ignored.")
+ else:
+ out.warning(
+ f"Found {len(hf_cache_info.warnings)} cache inconsistencies.
Re-run with `--show-warnings` to display them."
+ )
+
incomplete_files = hf_cache_info.incomplete_files
if incomplete_files:
out.hint(
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/huggingface_hub-1.24.0/src/huggingface_hub/cli/collections.py
new/huggingface_hub-1.25.1/src/huggingface_hub/cli/collections.py
--- old/huggingface_hub-1.24.0/src/huggingface_hub/cli/collections.py
2026-07-17 11:53:37.000000000 +0200
+++ new/huggingface_hub-1.25.1/src/huggingface_hub/cli/collections.py
2026-07-27 11:23:46.000000000 +0200
@@ -107,7 +107,7 @@
] = None,
description: Annotated[
str | None,
- Option(help="A description for the collection."),
+ Option(help="A description for the collection (max 150 characters)."),
] = None,
private: Annotated[
bool,
@@ -147,7 +147,7 @@
] = None,
description: Annotated[
str | None,
- Option(help="The new description for the collection."),
+ Option(help="The new description for the collection (max 150
characters)."),
] = None,
position: Annotated[
int | None,
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/huggingface_hub-1.24.0/src/huggingface_hub/cli/jobs.py
new/huggingface_hub-1.25.1/src/huggingface_hub/cli/jobs.py
--- old/huggingface_hub-1.24.0/src/huggingface_hub/cli/jobs.py 2026-07-17
11:53:37.000000000 +0200
+++ new/huggingface_hub-1.25.1/src/huggingface_hub/cli/jobs.py 2026-07-27
11:23:46.000000000 +0200
@@ -165,7 +165,7 @@
str | None,
Option(
"--name",
- help="Name the Job. Stored as the `name` label. Names do not have to
be unique.",
+ help="Name the Job. Stored as the `name` label. Names do not have to
be unique. Defaults to the image or script name plus a short hash of the
command.",
),
]
@@ -367,8 +367,12 @@
namespace=namespace,
)
out.result("Job started", id=job.id, url=job.url)
- if name is None:
- out.hint(f"Name this Job with `hf jobs labels
{job.owner.name}/{job.id} --name NAME`.")
+ if not _has_explicit_name(name, label):
+ auto_name = (job.labels or {}).get("name")
+ out.hint(
+ f"Job auto-named '{auto_name}'. Pass `--name` or run "
+ f"`hf jobs labels {job.owner.name}/{job.id} --name NAME` to rename
it."
+ )
if isinstance(job.status.expose_urls, list):
urls = "\n".join(f" {url}" for url in job.status.expose_urls)
out.hint(f"Exposed ports are reachable at (requires an HF token with
read access to the job):\n{urls}")
@@ -950,8 +954,12 @@
namespace=namespace,
)
out.result("Job started", id=job.id, url=job.url)
- if name is None:
- out.hint(f"Name this Job with `hf jobs labels
{job.owner.name}/{job.id} --name NAME`.")
+ if not _has_explicit_name(name, label):
+ auto_name = (job.labels or {}).get("name")
+ out.hint(
+ f"Job auto-named '{auto_name}'. Pass `--name` or run "
+ f"`hf jobs labels {job.owner.name}/{job.id} --name NAME` to rename
it."
+ )
if isinstance(job.status.expose_urls, list):
urls = "\n".join(f" {url}" for url in job.status.expose_urls)
out.hint(f"Exposed ports are reachable at (requires an HF token with
read access to the job):\n{urls}")
@@ -1014,9 +1022,11 @@
namespace=namespace,
)
out.result("Scheduled Job created", id=scheduled_job.id)
- if name is None:
+ if not _has_explicit_name(name, label):
+ auto_name = (scheduled_job.job_spec.labels or {}).get("name")
out.hint(
- f"Name this scheduled Job with `hf jobs scheduled labels
{scheduled_job.owner.name}/{scheduled_job.id} --name NAME`."
+ f"Scheduled Job auto-named '{auto_name}'. Pass `--name` or run "
+ f"`hf jobs scheduled labels
{scheduled_job.owner.name}/{scheduled_job.id} --name NAME` to rename it."
)
out.hint(f"Use `hf jobs scheduled inspect
{scheduled_job.owner.name}/{scheduled_job.id}` to view its details.")
@@ -1282,14 +1292,23 @@
namespace=namespace,
)
out.result("Scheduled Job created", id=job.id)
- if name is None:
- out.hint(f"Name this scheduled Job with `hf jobs scheduled labels
{job.owner.name}/{job.id} --name NAME`.")
+ if not _has_explicit_name(name, label):
+ auto_name = (job.job_spec.labels or {}).get("name")
+ out.hint(
+ f"Scheduled Job auto-named '{auto_name}'. Pass `--name` or run "
+ f"`hf jobs scheduled labels {job.owner.name}/{job.id} --name NAME`
to rename it."
+ )
out.hint(f"Use `hf jobs scheduled inspect {job.owner.name}/{job.id}` to
view its details.")
### UTILS
+def _has_explicit_name(name: str | None, label: list[str] | None) -> bool:
+ """Whether the user explicitly named the Job (via `--name` or a `name=`
label)."""
+ return name is not None or any(item.split("=", 1)[0] == "name" for item in
label or [])
+
+
def _parse_labels_map(labels: list[str] | None, *, name: str | None = None) ->
dict[str, str] | None:
"""Parse label key-value pairs from CLI arguments.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/huggingface_hub-1.24.0/src/huggingface_hub/constants.py
new/huggingface_hub-1.25.1/src/huggingface_hub/constants.py
--- old/huggingface_hub-1.24.0/src/huggingface_hub/constants.py 2026-07-17
11:53:37.000000000 +0200
+++ new/huggingface_hub-1.25.1/src/huggingface_hub/constants.py 2026-07-27
11:23:46.000000000 +0200
@@ -240,7 +240,7 @@
HF_HUB_DISABLE_TELEMETRY = (
_is_true(os.environ.get("HF_HUB_DISABLE_TELEMETRY")) # HF-specific env
variable
or _is_true(os.environ.get("DISABLE_TELEMETRY"))
- or _is_true(os.environ.get("DO_NOT_TRACK")) #
https://consoledonottrack.com/
+ or _is_true(os.environ.get("DO_NOT_TRACK")) # https://donottrack.sh/
)
HF_TOKEN_PATH = os.path.expandvars(
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/huggingface_hub-1.24.0/src/huggingface_hub/dataclasses.py
new/huggingface_hub-1.25.1/src/huggingface_hub/dataclasses.py
--- old/huggingface_hub-1.24.0/src/huggingface_hub/dataclasses.py
2026-07-17 11:53:37.000000000 +0200
+++ new/huggingface_hub-1.25.1/src/huggingface_hub/dataclasses.py
2026-07-27 11:23:46.000000000 +0200
@@ -117,7 +117,7 @@
# List and store validators
field_validators: dict[str, list[Validator_T]] = {}
- for f in fields(cls): # type: ignore [arg-type]
+ for f in fields(cls): # type: ignore
validators = []
validators.append(_create_type_validator(f))
custom_validator = f.metadata.get("validator")
@@ -157,7 +157,7 @@
@wraps(original_init)
def __init__(self, *args, **kwargs: Any) -> None:
# Extract only the fields that are part of the dataclass
- dataclass_fields = {f.name for f in fields(cls)} # type:
ignore [arg-type]
+ dataclass_fields = {f.name for f in fields(cls)} # type:
ignore
standard_kwargs = {k: v for k, v in kwargs.items() if k in
dataclass_fields}
# User shouldn't define custom `__init__` when
`accepts_kwargs`, and instead
@@ -412,8 +412,8 @@
metadata = {}
metadata["validator"] = validator
return field( # type: ignore
- default=default, # type: ignore [arg-type]
- default_factory=default_factory, # type: ignore [arg-type]
+ default=default, # type: ignore
+ default_factory=default_factory, # type: ignore
init=init,
repr=repr,
hash=hash,
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/huggingface_hub-1.24.0/src/huggingface_hub/file_download.py
new/huggingface_hub-1.25.1/src/huggingface_hub/file_download.py
--- old/huggingface_hub-1.24.0/src/huggingface_hub/file_download.py
2026-07-17 11:53:37.000000000 +0200
+++ new/huggingface_hub-1.25.1/src/huggingface_hub/file_download.py
2026-07-27 11:23:46.000000000 +0200
@@ -271,7 +271,7 @@
raise ValueError("Invalid repo type")
if repo_type in constants.REPO_TYPES_URL_PREFIXES:
- repo_id = constants.REPO_TYPES_URL_PREFIXES[repo_type] + repo_id
+ repo_id = constants.REPO_TYPES_URL_PREFIXES[repo_type] + repo_id #
type: ignore
if revision is None:
revision = constants.DEFAULT_REVISION
@@ -1183,6 +1183,15 @@
will_download=force_download or not is_cached,
)
+ # Pointer already exists -> update the ref best-effort, then return without
+ # attempting to write to the cache (which may be mounted read-only).
+ if not force_download and os.path.exists(pointer_path):
+ try:
+ _cache_commit_hash_for_specific_revision(storage_folder, revision,
commit_hash)
+ except OSError:
+ pass
+ return pointer_path
+
os.makedirs(os.path.dirname(blob_path), exist_ok=True)
os.makedirs(os.path.dirname(pointer_path), exist_ok=True)
@@ -1219,10 +1228,6 @@
Path(lock_path).parent.mkdir(parents=True, exist_ok=True)
- # pointer already exists -> immediate return
- if not force_download and os.path.exists(pointer_path):
- return pointer_path
-
# Blob exists but pointer must be (safely) created -> take the lock
if not force_download and os.path.exists(blob_path):
with WeakFileLock(lock_path):
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/huggingface_hub-1.24.0/src/huggingface_hub/hf_api.py
new/huggingface_hub-1.25.1/src/huggingface_hub/hf_api.py
--- old/huggingface_hub-1.24.0/src/huggingface_hub/hf_api.py 2026-07-17
11:53:37.000000000 +0200
+++ new/huggingface_hub-1.25.1/src/huggingface_hub/hf_api.py 2026-07-27
11:23:46.000000000 +0200
@@ -35,7 +35,6 @@
import httpcore
import httpx
from tqdm.auto import tqdm as base_tqdm
-from tqdm.contrib.concurrent import thread_map
from . import constants
from ._buckets import (
@@ -76,6 +75,8 @@
JobStage,
ScheduledJobInfo,
_create_job_spec,
+ _default_job_name_from_image,
+ _default_job_name_from_script,
_derive_job_volume_name,
)
from ._space_api import (
@@ -130,6 +131,7 @@
get_session,
get_token,
hf_raise_for_status,
+ hf_thread_map,
http_backoff,
logging,
paginate,
@@ -422,7 +424,7 @@
# Check if repo type is known (mapping "spaces" => "space" + empty value
=> `None`)
if repo_type in constants.REPO_TYPES_MAPPING:
- repo_type = constants.REPO_TYPES_MAPPING[repo_type]
+ repo_type = constants.REPO_TYPES_MAPPING[repo_type] # type: ignore
if repo_type == "":
repo_type = None
if repo_type not in constants.REPO_TYPES_WITH_KERNEL and repo_type !=
"bucket":
@@ -4693,10 +4695,11 @@
if exist_ok and err.response.status_code == 409:
# Repo already exists and `exist_ok=True`
pass
- elif exist_ok and err.response.status_code in (401, 403):
+ elif exist_ok and err.response.status_code in (401, 402, 403):
# 401 -> if JWT token without create repo scope
+ # 402 -> if payment required (e.g. if Gradio/Docker Space and
free user)
# 403 -> if no write permission on the namespace
- # In both cases, repo might already exist
+ # In all 3 cases, repo might already exist
try:
self.repo_info(repo_id=repo_id, repo_type=repo_type,
token=token)
if repo_type is None or repo_type ==
constants.REPO_TYPE_MODEL:
@@ -6693,6 +6696,7 @@
repo_type: str | None = None,
revision: str | None = None,
token: bool | str | None = None,
+ timeout: float | None = constants.HF_HUB_DOWNLOAD_TIMEOUT,
) -> SafetensorsRepoMetadata:
"""
Parse metadata for a safetensors repo on the Hub.
@@ -6719,6 +6723,10 @@
token, which is the recommended method for authentication (see
https://huggingface.co/docs/huggingface_hub/quick-start#authentication).
To disable authentication, pass `False`.
+ timeout (`float`, *optional*, defaults to 10):
+ How many seconds to wait for the server to send data before
giving up, passed to each request that
+ fetches a safetensors file header. Set to `None` to disable
the timeout (not recommended, as a stalled
+ connection can hang the call indefinitely).
Returns:
[`SafetensorsRepoMetadata`]: information related to safetensors
repo.
@@ -6770,6 +6778,7 @@
repo_type=repo_type,
revision=revision,
token=token,
+ timeout=timeout,
)
return SafetensorsRepoMetadata(
metadata=None,
@@ -6804,10 +6813,15 @@
def _parse(filename: str) -> None:
files_metadata[filename] =
self.parse_safetensors_file_metadata(
- repo_id=repo_id, filename=filename, repo_type=repo_type,
revision=revision, token=token
+ repo_id=repo_id,
+ filename=filename,
+ repo_type=repo_type,
+ revision=revision,
+ token=token,
+ timeout=timeout,
)
- thread_map(
+ hf_thread_map(
_parse,
set(weight_map.values()),
desc="Parse safetensors files",
@@ -6834,6 +6848,7 @@
repo_type: str | None = None,
revision: str | None = None,
token: bool | str | None = None,
+ timeout: float | None = constants.HF_HUB_DOWNLOAD_TIMEOUT,
) -> SafetensorsFileMetadata:
"""
Parse metadata from a safetensors file on the Hub.
@@ -6858,6 +6873,9 @@
token, which is the recommended method for authentication (see
https://huggingface.co/docs/huggingface_hub/quick-start#authentication).
To disable authentication, pass `False`.
+ timeout (`float`, *optional*, defaults to 10):
+ How many seconds to wait for the server to send data before
giving up. Set to `None` to disable the
+ timeout (not recommended, as a stalled connection can hang the
call indefinitely).
Returns:
[`SafetensorsFileMetadata`]: information related to a safetensors
file.
@@ -6881,7 +6899,7 @@
# We assume fetching 100kb is faster than making 2 GET requests.
Therefore we always fetch the first 100kb to
# avoid the 2nd GET in most cases.
# See
https://github.com/huggingface/huggingface_hub/pull/1855#discussion_r1404286419.
- response = get_session().get(url, headers={**_headers, "range":
"bytes=0-100000"})
+ response = get_session().get(url, headers={**_headers, "range":
"bytes=0-100000"}, timeout=timeout)
hf_raise_for_status(response)
# 2. Parse and validate metadata size using shared helper
@@ -6891,7 +6909,9 @@
if metadata_size <= 100000:
metadata_as_bytes = response.content[8 : 8 + metadata_size]
else: # 3.b. Request full metadata
- response = get_session().get(url, headers={**_headers, "range":
f"bytes=8-{metadata_size + 7}"})
+ response = get_session().get(
+ url, headers={**_headers, "range": f"bytes=8-{metadata_size +
7}"}, timeout=timeout
+ )
hf_raise_for_status(response)
metadata_as_bytes = response.content
@@ -9997,7 +10017,7 @@
namespace (`str`, *optional*):
Namespace of the collection to create (username or org). Will
default to the owner name.
description (`str`, *optional*):
- Description of the collection to create.
+ Description of the collection to create. The maximum size for
a description is 150 characters.
private (`bool`, *optional*):
Whether the collection should be private or not. Defaults to
`False` (i.e. public collection).
exists_ok (`bool`, *optional*):
@@ -10068,7 +10088,7 @@
title (`str`):
Title of the collection to update.
description (`str`, *optional*):
- Description of the collection to update.
+ Description of the collection to update. The maximum size for
a description is 150 characters.
position (`int`, *optional*):
New position of the collection in the list of collections of
the user.
private (`bool`, *optional*):
@@ -11844,7 +11864,7 @@
name (`str`, *optional*):
A name for the Job. Stored as the `name` label. Cannot be
passed together with a `name` key in
- `labels`. Names do not have to be unique.
+ `labels`. Names do not have to be unique. Defaults to a name
derived from image and command (with a short hash suffix).
labels (`dict[str, str]`, *optional*):
Labels to attach to the job (key-value pairs).
@@ -11904,6 +11924,8 @@
"""
if namespace is None:
namespace = self.whoami(token=token)["name"]
+ if name is None and not (labels and "name" in labels):
+ name = _default_job_name_from_image(image, command)
job_spec = _create_job_spec(
image=image,
command=command,
@@ -12483,7 +12505,7 @@
name (`str`, *optional*):
A name for the Job. Stored as the `name` label. Cannot be
passed together with a `name` key in
- `labels`. Names do not have to be unique.
+ `labels`. Names do not have to be unique. Defaults to a name
derived from script and its arguments (with a short hash suffix).
labels (`dict[str, str]`, *optional*):
Labels to attach to the job (key-value pairs).
@@ -12555,6 +12577,9 @@
env = env or {}
secrets = secrets or {}
+ if name is None and not (labels and "name" in labels):
+ name = _default_job_name_from_script(script, script_args or [])
+
# Build command
command, env, secrets, extra_volumes =
self._create_uv_command_env_and_secrets(
script=script,
@@ -12643,7 +12668,7 @@
name (`str`, *optional*):
A name for the scheduled Job. Stored as the `name` label.
Cannot be passed together with a `name`
- key in `labels`. Names do not have to be unique.
+ key in `labels`. Names do not have to be unique. Defaults to a
name derived from image and command (with a short hash suffix).
labels (`dict[str, str]`, *optional*):
Labels to attach to the job (key-value pairs).
@@ -12693,6 +12718,8 @@
"""
if namespace is None:
namespace = self.whoami(token=token)["name"]
+ if name is None and not (labels and "name" in labels):
+ name = _default_job_name_from_image(image, command)
# prepare payload to send to HF Jobs API
job_spec = _create_job_spec(
@@ -13038,7 +13065,7 @@
name (`str`, *optional*):
A name for the scheduled Job. Stored as the `name` label.
Cannot be passed together with a `name`
- key in `labels`. Names do not have to be unique.
+ key in `labels`. Names do not have to be unique. Defaults to a
name derived from script and its arguments (with a short hash suffix).
labels (`dict[str, str]`, *optional*):
Labels to attach to the job (key-value pairs).
@@ -13091,6 +13118,9 @@
```
"""
image = image or "ghcr.io/astral-sh/uv:python3.12-bookworm"
+ if name is None and not (labels and "name" in labels):
+ name = _default_job_name_from_script(script, script_args or [])
+
# Build command
command, env, secrets, extra_volumes =
self._create_uv_command_env_and_secrets(
script=script,
@@ -13931,7 +13961,7 @@
)
all_adds.append((local_path, target_path))
- thread_map(_download_and_collect, pending_downloads,
desc="Downloading text files for copy")
+ hf_thread_map(_download_and_collect, pending_downloads,
desc="Downloading text files for copy")
# Send copies first (no upload needed), then adds (may need upload)
if all_copies:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/huggingface_hub-1.24.0/src/huggingface_hub/hf_file_system.py
new/huggingface_hub-1.25.1/src/huggingface_hub/hf_file_system.py
--- old/huggingface_hub-1.24.0/src/huggingface_hub/hf_file_system.py
2026-07-17 11:53:37.000000000 +0200
+++ new/huggingface_hub-1.25.1/src/huggingface_hub/hf_file_system.py
2026-07-27 11:23:46.000000000 +0200
@@ -1258,13 +1258,9 @@
raise ValueError(f"HfFileSystemStreamFile only supports
cache_type='none' but got {cache_type}")
if "w" in mode:
raise ValueError(f"HfFileSystemStreamFile only supports reading
but got mode='{mode}'")
- try:
- self.resolved_path = fs.resolve_path(path, revision=revision)
- except FileNotFoundError as e:
- if "w" in kwargs.get("mode", ""):
- raise FileNotFoundError(
- f"{e}.\nMake sure the repository and revision exist before
writing data."
- ) from e
+ # Let a missing repo/revision/file surface as FileNotFoundError (as
the buffered
+ # `HfFileSystemFile` does) instead of an AttributeError on
`self.resolved_path` below.
+ self.resolved_path = fs.resolve_path(path, revision=revision)
# avoid an unnecessary .info() call to instantiate .details
self.details = {"name": self.resolved_path.unresolve(), "size": None}
super().__init__(
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/huggingface_hub-1.24.0/src/huggingface_hub/repocard.py
new/huggingface_hub-1.25.1/src/huggingface_hub/repocard.py
--- old/huggingface_hub-1.24.0/src/huggingface_hub/repocard.py 2026-07-17
11:53:37.000000000 +0200
+++ new/huggingface_hub-1.25.1/src/huggingface_hub/repocard.py 2026-07-27
11:23:46.000000000 +0200
@@ -31,7 +31,7 @@
# exact same regex as in the Hub server. Please keep in sync.
# See
https://github.com/huggingface/moon-landing/blob/main/server/lib/ViewMarkdown.ts#L18
-REGEX_YAML_BLOCK =
re.compile(r"^(\s*---[\r\n]+)([\S\s]*?)([\r\n]+---(\r\n|\n|$))")
+REGEX_YAML_BLOCK =
re.compile(r"^(\s*---(?:\r\n|\r|\n))([\S\s]*?)((?:\r\n|\r|\n)---[
\t]*(\r\n|\n|$))")
class RepoCard:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/huggingface_hub-1.24.0/src/huggingface_hub/repocard_data.py
new/huggingface_hub-1.25.1/src/huggingface_hub/repocard_data.py
--- old/huggingface_hub-1.24.0/src/huggingface_hub/repocard_data.py
2026-07-17 11:53:37.000000000 +0200
+++ new/huggingface_hub-1.25.1/src/huggingface_hub/repocard_data.py
2026-07-27 11:23:46.000000000 +0200
@@ -211,9 +211,10 @@
`str`: CardData represented as a YAML block.
"""
if original_order:
+ original_order_set = set(original_order)
self.__dict__ = {
k: self.__dict__[k]
- for k in original_order + list(set(self.__dict__.keys()) -
set(original_order))
+ for k in original_order + [k for k in self.__dict__ if k not
in original_order_set]
if k in self.__dict__
}
return yaml_dump(self.to_dict(), sort_keys=False,
line_break=line_break).strip()
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/huggingface_hub-1.24.0/src/huggingface_hub/utils/__init__.py
new/huggingface_hub-1.25.1/src/huggingface_hub/utils/__init__.py
--- old/huggingface_hub-1.24.0/src/huggingface_hub/utils/__init__.py
2026-07-17 11:53:37.000000000 +0200
+++ new/huggingface_hub-1.25.1/src/huggingface_hub/utils/__init__.py
2026-07-27 11:23:46.000000000 +0200
@@ -126,6 +126,7 @@
are_progress_bars_disabled,
disable_progress_bars,
enable_progress_bars,
+ hf_thread_map,
is_tqdm_disabled,
silent_tqdm,
tqdm,
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/huggingface_hub-1.24.0/src/huggingface_hub/utils/tqdm.py
new/huggingface_hub-1.25.1/src/huggingface_hub/utils/tqdm.py
--- old/huggingface_hub-1.24.0/src/huggingface_hub/utils/tqdm.py
2026-07-17 11:53:37.000000000 +0200
+++ new/huggingface_hub-1.25.1/src/huggingface_hub/utils/tqdm.py
2026-07-27 11:23:46.000000000 +0200
@@ -84,10 +84,11 @@
import os
import threading
import warnings
-from collections.abc import Iterator
+from collections.abc import Iterable, Iterator
+from concurrent.futures import ThreadPoolExecutor, as_completed
from contextlib import contextmanager, nullcontext
from pathlib import Path
-from typing import ContextManager
+from typing import Callable, ContextManager, TypeVar, cast
from tqdm.auto import tqdm as old_tqdm
@@ -370,3 +371,79 @@
initial=initial,
desc=desc,
)
+
+
+T = TypeVar("T")
+R = TypeVar("R")
+
+
+def _make_thread_pool_executor(*, max_workers: int | None, bar_class:
type[old_tqdm]) -> ThreadPoolExecutor:
+ """Create a `ThreadPoolExecutor` that shares the tqdm lock with its worker
threads.
+
+ Like `tqdm.contrib.concurrent.thread_map`, we make sure the bar class has
a lock and pass it to the
+ worker threads, so that bars created inside the mapped function (e.g. the
per-file download bars in
+ `snapshot_download`) don't race on concurrent `update()` calls.
`get_lock()` creates the shared
+ class-level lock if needed. Classes that don't implement tqdm's locking
API (e.g. `silent_tqdm`)
+ render nothing anyway, so no lock is needed.
+ """
+ if hasattr(bar_class, "get_lock") and hasattr(bar_class, "set_lock"):
+ return ThreadPoolExecutor(
+ max_workers=max_workers, initializer=bar_class.set_lock,
initargs=(bar_class.get_lock(),)
+ )
+ return ThreadPoolExecutor(max_workers=max_workers)
+
+
+def hf_thread_map(
+ fn: Callable[[T], R],
+ iterable: Iterable[T],
+ *,
+ max_workers: int | None = None,
+ tqdm_class: type[old_tqdm] | None = None,
+ **tqdm_kwargs,
+) -> list[R]:
+ """Drop-in replacement for `tqdm.contrib.concurrent.thread_map`.
+
+ The version shipped by `tqdm` consumes results in submission order and the
progress bar only advances when the "next
+ item in input order" finishes. A single slow early item pins the bar near
zero while later items
+ complete, with everything flushed at once at the end. This helper avoids
that problem by consuming items in completion
+ order.
+
+ See https://github.com/huggingface/huggingface_hub/issues/4518 for more
details.
+
+ Args:
+ fn (`Callable`):
+ Function to apply to each item.
+ iterable (`Iterable`):
+ Items to process.
+ max_workers (`int`, *optional*):
+ Maximum number of worker threads. Defaults to tqdm's own default.
+ tqdm_class (`type`, *optional*):
+ Progress bar class to use. Defaults to `huggingface_hub`'s `tqdm`.
+ **tqdm_kwargs:
+ Additional keyword arguments forwarded to the progress bar (e.g.
`desc`).
+
+ Returns:
+ `list`: Results in the same order as `iterable`.
+ """
+ items = list(iterable)
+ if max_workers is None:
+ max_workers = min(32, (os.cpu_count() or 1) + 4)
+ tqdm_kwargs.setdefault("total", len(items))
+ bar_class = tqdm_class or tqdm
+ results: list[R | None] = [None] * len(items)
+ with (
+ bar_class(**tqdm_kwargs) as pbar,
+ _make_thread_pool_executor(max_workers=max_workers,
bar_class=bar_class) as executor,
+ ):
+ future_to_index = {executor.submit(fn, item): index for index, item in
enumerate(items)}
+ try:
+ for future in as_completed(future_to_index):
+ results[future_to_index[future]] = future.result()
+ pbar.update(1)
+ except BaseException:
+ # Cancel not-yet-started tasks so a single failure doesn't keep
the whole queue running
+ # (mirrors `tqdm.contrib.concurrent.thread_map`, which relies on
`Executor.map` for this).
+ for future in future_to_index:
+ future.cancel()
+ raise
+ return cast("list[R]", results)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/huggingface_hub-1.24.0/src/huggingface_hub.egg-info/PKG-INFO
new/huggingface_hub-1.25.1/src/huggingface_hub.egg-info/PKG-INFO
--- old/huggingface_hub-1.24.0/src/huggingface_hub.egg-info/PKG-INFO
2026-07-17 11:53:43.000000000 +0200
+++ new/huggingface_hub-1.25.1/src/huggingface_hub.egg-info/PKG-INFO
2026-07-27 11:23:51.000000000 +0200
@@ -1,6 +1,6 @@
Metadata-Version: 2.4
Name: huggingface_hub
-Version: 1.24.0
+Version: 1.25.1
Summary: Client library to download and publish models, datasets and other
repos on the huggingface.co hub
Home-page: https://github.com/huggingface/huggingface_hub
Author: Hugging Face, Inc.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/huggingface_hub-1.24.0/tests/test_hf_api.py
new/huggingface_hub-1.25.1/tests/test_hf_api.py
--- old/huggingface_hub-1.24.0/tests/test_hf_api.py 2026-07-17
11:53:37.000000000 +0200
+++ new/huggingface_hub-1.25.1/tests/test_hf_api.py 2026-07-27
11:23:46.000000000 +0200
@@ -2656,11 +2656,6 @@
with pytest.raises(HfHubHTTPError, match=r".*Repository Not
Found.*"):
_ = api.dataset_info(repo_id=self.repo_id)
- def test_list_private_datasets(self, api: HfApi):
- kwargs = {"sort": "created_at", "limit": 100, "author": USER}
- assert all(dataset.id != self.repo_id for dataset in
api.list_datasets(token=False, **kwargs))
- assert any(dataset.id == self.repo_id for dataset in
api.list_datasets(token=TOKEN, **kwargs))
-
def test_list_private_models(self, api: HfApi):
kwargs = {"sort": "created_at", "limit": 100, "author": USER}
assert all(model.id != self.repo_id for model in
api.list_models(token=False, **kwargs))
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/huggingface_hub-1.24.0/tests/test_hf_file_system.py
new/huggingface_hub-1.25.1/tests/test_hf_file_system.py
--- old/huggingface_hub-1.24.0/tests/test_hf_file_system.py 2026-07-17
11:53:37.000000000 +0200
+++ new/huggingface_hub-1.25.1/tests/test_hf_file_system.py 2026-07-27
11:23:46.000000000 +0200
@@ -817,6 +817,15 @@
fs.resolve_path("username/my_model@dev", revision="main")
+def test_stream_file_open_if_not_found():
+ # Regression test: opening a missing file in streaming mode (block_size=0)
must raise
+ # FileNotFoundError like the buffered HfFileSystemFile, not an
AttributeError.
+ fs = HfFileSystem()
+ with patch.object(fs, "resolve_path",
side_effect=FileNotFoundError("missing/repo")):
+ with pytest.raises(FileNotFoundError):
+ HfFileSystemStreamFile(fs,
"hf://missing/repo/not_existing_file.txt")
+
+
@pytest.mark.parametrize(
("not_supported_path", "expected_error"),
[
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/huggingface_hub-1.24.0/tests/test_jobs_api.py
new/huggingface_hub-1.25.1/tests/test_jobs_api.py
--- old/huggingface_hub-1.24.0/tests/test_jobs_api.py 2026-07-17
11:53:37.000000000 +0200
+++ new/huggingface_hub-1.25.1/tests/test_jobs_api.py 2026-07-27
11:23:46.000000000 +0200
@@ -3,7 +3,11 @@
import pytest
from huggingface_hub import HfApi, JobStage
-from huggingface_hub._jobs_api import JobInfo
+from huggingface_hub._jobs_api import (
+ JobInfo,
+ _default_job_name_from_image,
+ _default_job_name_from_script,
+)
def _job_info(stage: str, job_id: str = "job-id") -> JobInfo:
@@ -74,3 +78,54 @@
):
job = self.api.wait_for_job(job_id="job-id", namespace="user",
stages=[JobStage.RUNNING])
assert job.status.stage == "ERROR"
+
+
[email protected](
+ "image, expected",
+ [
+ # Plain image (no registry, no tag).
+ ("ubuntu", "ubuntu"),
+ # Tag is kept, with disallowed chars replaced by '-'.
+ ("python:3.12", "python-3-12"),
+ # Registry host and namespace are dropped, last component + tag is
kept.
+ ("pytorch/pytorch:2.6.0-cuda12.4-cudnn9-devel",
"pytorch-2-6-0-cuda12-4-cudnn9-devel"),
+ ("ghcr.io/astral-sh/uv:python3.12-bookworm", "uv-python3-12-bookworm"),
+ # Space references keep the `namespace/repo` id (sanitized), for every
supported prefix.
+ ("hf.co/spaces/lhoestq/duckdb", "lhoestq-duckdb"),
+ ("https://huggingface.co/spaces/lhoestq/duckdb", "lhoestq-duckdb"),
+ ],
+)
+def test_default_job_name_from_image(image: str, expected: str) -> None:
+ # The base name is derived from the image; a short hash of the command
line is appended.
+ assert _default_job_name_from_image(image, ["python", "-c",
"print(1)"]).startswith(expected + "-")
+
+
[email protected](
+ "script, expected",
+ [
+ # Local script: keep the stem, drop the '.py' extension.
+ ("my_script.py", "my_script"),
+ ("./train.py", "train"),
+ # URL: keep the last path component, drop query/fragment and extension.
+
("https://raw.githubusercontent.com/huggingface/trl/main/trl/scripts/sft.py",
"sft"),
+ ("https://example.co/a/sft.py?raw=1", "sft"),
+ # Command (no extension): kept as-is.
+ ("lighteval", "lighteval"),
+ # Dots in the stem are disallowed chars and get replaced by '-'.
+ ("my.weird.script.py", "my-weird-script"),
+ ],
+)
+def test_default_job_name_from_script(script: str, expected: str) -> None:
+ # The base name is derived from the script; a short hash of the command
line is appended.
+ assert _default_job_name_from_script(script, []).startswith(expected + "-")
+
+
+def test_default_job_name_hash_groups_and_splits_by_command() -> None:
+ # Same image but different commands must yield different names (splits
distinct runs)...
+ truc = _default_job_name_from_image("python:3.12", ["foo", "--truc"])
+ bar = _default_job_name_from_image("python:3.12", ["foo", "--bar"])
+ assert truc.startswith("python-3-12-")
+ assert bar.startswith("python-3-12-")
+ assert truc != bar
+ # ...while the same command yields the same name (groups identical runs).
+ assert truc == _default_job_name_from_image("python:3.12", ["foo",
"--truc"])
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/huggingface_hub-1.24.0/tests/test_local_folder.py
new/huggingface_hub-1.25.1/tests/test_local_folder.py
--- old/huggingface_hub-1.24.0/tests/test_local_folder.py 2026-07-17
11:53:37.000000000 +0200
+++ new/huggingface_hub-1.25.1/tests/test_local_folder.py 2026-07-27
11:23:46.000000000 +0200
@@ -143,6 +143,33 @@
assert str(paths.metadata_path).startswith("\\\\?\\")
[email protected](os.name != "nt", reason="Windows-specific test.")
[email protected]("cache_dir_len", [252, 300])
+def test_local_download_paths_long_local_dir(tmp_path: Path, cache_dir_len:
int):
+ r"""A deep ``local_dir`` must not crash when its ``.cache/huggingface``
folder exceeds the
+ Windows directory path limit (247 chars, i.e. MAX_PATH minus room for an
8.3 file name).
+
+ Unlike ``test_local_download_paths_long_paths`` (long *filename*, short
dir), here the
+ ``local_dir`` itself is long, so the limit is first hit while
``_huggingface_dir`` creates
+ ``<local_dir>/.cache/huggingface``. Without the extended-length ``\\?\``
prefix that ``mkdir``
+ raised ``FileNotFoundError`` (WinError 206) before the download/upload
paths were built.
+ Both boundary cases are covered: 248-259 (only directory creation exceeds
the limit) and
+ 260+ (file creation would fail too).
+ """
+ # Create a `local_dir` so that `<local_dir>/.cache/huggingface` is
`cache_dir_len` chars long.
+ # Use the extended-length prefix here since a plain mkdir of such a path
would itself fail.
+ padding = "d" * max(1, cache_dir_len - len(str(tmp_path / ".cache" /
"huggingface")) - 1)
+ local_dir = tmp_path / padding
+ os.makedirs("\\\\?\\" + os.path.abspath(local_dir), exist_ok=True)
+ assert len(str(local_dir / ".cache" / "huggingface")) >=
max(cache_dir_len, 248)
+
+ # Must not raise, and the (extended-length) parent directories must have
been created.
+ paths = get_local_download_paths(local_dir, "config.json")
+ assert str(paths.metadata_path).startswith("\\\\?\\")
+ assert paths.metadata_path.parent.is_dir()
+ assert paths.file_path.parent.is_dir()
+
+
def test_write_download_metadata(tmp_path: Path):
"""Test download metadata content is valid."""
# Write metadata
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/huggingface_hub-1.24.0/tests/test_repocard.py
new/huggingface_hub-1.25.1/tests/test_repocard.py
--- old/huggingface_hub-1.24.0/tests/test_repocard.py 2026-07-17
11:53:37.000000000 +0200
+++ new/huggingface_hub-1.25.1/tests/test_repocard.py 2026-07-27
11:23:46.000000000 +0200
@@ -14,6 +14,7 @@
import copy
import os
import re
+import time
from pathlib import Path
import pytest
@@ -730,6 +731,59 @@
def test_does_not_match_with_leading_text(self):
assert REGEX_YAML_BLOCK.search("something\n---\nmetadata: 1\n---") is
None
+ def test_crlf_card_parses_metadata_and_retains_body(self):
+ # A valid CRLF card must parse to the correct metadata and retain the
body: the
+ # opener must consume the full `\r\n` (not just the `\r`), so the YAML
block is
+ # clean and the match ends right after the closing fence line.
+ content = "---\r\nlanguage: en\r\n---\r\nSome body\r\n"
+ match = REGEX_YAML_BLOCK.search(content)
+ assert match is not None
+ assert yaml.safe_load(match.group(2)) == {"language": "en"}
+ assert content[match.end() :] == "Some body\r\n"
+
+ card = RepoCard(content)
+ assert card.data.to_dict() == {"language": "en"}
+ assert "Some body" in card.text
+
+ def test_crlf_empty_frontmatter_retains_trailing_content(self):
+ # An empty-frontmatter card must not silently drop the trailing
content, and the
+ # CRLF form must behave like the LF form: no metadata, text retained.
+ for content in ("---\r\n---\r\ntrailing content\r\n",
"---\n---\ntrailing content\n"):
+ card = RepoCard(content, ignore_metadata_errors=True)
+ assert card.data.to_dict() == {}
+ assert "trailing content" in card.text
+
+ def test_crlf_empty_frontmatter_followed_by_real_frontmatter(self):
+ # Regression test: with a single-char newline class at the delimiters,
the opener of
+ # a CRLF card consumed only the `\r` and the leftover `\n---` faked an
early closer,
+ # demoting the real YAML block between the 2nd and 3rd fences into the
card text.
+ content = "---\r\n---\r\nlanguage: en\r\n---\r\nbody\r\n"
+ match = REGEX_YAML_BLOCK.search(content)
+ assert match is not None
+ assert yaml.safe_load(match.group(2)) == {"language": "en"}
+ assert content[match.end() :] == "body\r\n"
+
+ @pytest.mark.parametrize(
+ "payload",
+ [
+ # Valid `---` opener followed by a long newline run and no closing
fence.
+ pytest.param("---\n" + "\n" * 200_000, id="lf"),
+ # Same, but with a title line and trailing body so it resembles
real card text.
+ pytest.param("---\ntitle: x\n" + "\n" * 200_000 + "not a fence",
id="lf-with-body"),
+ # CRLF variant.
+ pytest.param("---\r\n" + "\r\n" * 200_000, id="crlf"),
+ ],
+ )
+ def test_no_catastrophic_backtracking(self, payload):
+ # A YAML opener followed by a large newline run with no closing fence
must not
+ # trigger catastrophic backtracking. These inputs cannot match, and
the search
+ # must fail quickly instead of backtracking for tens of seconds.
+ start = time.perf_counter()
+ match = REGEX_YAML_BLOCK.search(payload)
+ elapsed = time.perf_counter() - start
+ assert match is None
+ assert elapsed < 1.0, f"REGEX_YAML_BLOCK.search took {elapsed:.2f}s
(catastrophic backtracking)"
+
class TestModelCard:
def test_model_card_with_invalid_model_index(self):
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/huggingface_hub-1.24.0/tests/test_repocard_data.py
new/huggingface_hub-1.25.1/tests/test_repocard_data.py
--- old/huggingface_hub-1.24.0/tests/test_repocard_data.py 2026-07-17
11:53:37.000000000 +0200
+++ new/huggingface_hub-1.25.1/tests/test_repocard_data.py 2026-07-27
11:23:46.000000000 +0200
@@ -73,6 +73,19 @@
# .pop
assert metadata.pop("foo") == "BAR"
+ def test_to_yaml_preserves_order_of_appended_keys(self):
+ # Keys not listed in `original_order` must be appended in their
existing
+ # (insertion) order and deterministically, as documented -- not
shuffled
+ # through set() ordering.
+ metadata = CardData(license="mit", tags=["a"])
+ for key in ["zzz", "aaa", "mmm", "bbb"]:
+ metadata[key] = "x"
+
+ yaml_block = metadata.to_yaml(original_order=["license", "tags"])
+ keys = [line.split(":")[0] for line in yaml_block.splitlines() if line
and not line.startswith((" ", "-"))]
+
+ assert keys == ["license", "tags", "zzz", "aaa", "mmm", "bbb"]
+
class TestModelCardData:
def test_eval_results_to_model_index(self):
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/huggingface_hub-1.24.0/tests/test_utils_tqdm.py
new/huggingface_hub-1.25.1/tests/test_utils_tqdm.py
--- old/huggingface_hub-1.24.0/tests/test_utils_tqdm.py 2026-07-17
11:53:37.000000000 +0200
+++ new/huggingface_hub-1.25.1/tests/test_utils_tqdm.py 2026-07-27
11:23:46.000000000 +0200
@@ -1,6 +1,7 @@
import io
import logging
import sys
+import threading
import time
from pathlib import Path
from unittest.mock import patch
@@ -13,6 +14,7 @@
are_progress_bars_disabled,
disable_progress_bars,
enable_progress_bars,
+ hf_thread_map,
tqdm,
tqdm_stream_file,
)
@@ -257,6 +259,100 @@
assert "10/10" in captured.err
+class TestHfThreadMap:
+ def test_returns_results_in_input_order(self):
+ # Earlier items finish later, so completion order differs from input
order.
+ def fn(x):
+ time.sleep(0.02 * (4 - x))
+ return x * 10
+
+ assert hf_thread_map(fn, range(5), max_workers=5, disable=True) == [0,
10, 20, 30, 40]
+
+ def test_bar_advances_on_completion_not_submission_order(self):
+ # Regression test for
https://github.com/huggingface/huggingface_hub/issues/4518: a slow
+ # first-submitted item must not prevent the bar from advancing when
later items complete.
+ slow_release = threading.Event()
+ fast_done = threading.Event()
+ updates = []
+
+ class RecordingTqdm(vanilla_tqdm):
+ def update(self, n=1):
+ updates.append(n)
+ return super().update(n)
+
+ def fn(x):
+ if x == 0:
+ slow_release.wait(timeout=5) # first-submitted item stays
blocked
+ else:
+ fast_done.set()
+ return x
+
+ results = []
+
+ def run():
+ results.append(hf_thread_map(fn, range(2), max_workers=2,
tqdm_class=RecordingTqdm, disable=True))
+
+ worker = threading.Thread(target=run)
+ worker.start()
+ try:
+ assert fast_done.wait(timeout=5)
+ # The fast item completed; the bar should advance even though the
slow item is still blocked.
+ deadline = time.time() + 5
+ while not updates and time.time() < deadline:
+ time.sleep(0.01)
+ assert updates
+ finally:
+ slow_release.set()
+ worker.join(timeout=5)
+
+ assert not worker.is_alive()
+ assert results == [[0, 1]]
+
+ def test_cancels_pending_tasks_on_error(self):
+ started = []
+ lock = threading.Lock()
+
+ def fn(x):
+ with lock:
+ started.append(x)
+ if x == 0:
+ raise ValueError("boom")
+ time.sleep(0.05)
+ return x
+
+ with pytest.raises(ValueError, match="boom"):
+ hf_thread_map(fn, range(50), max_workers=1, disable=True)
+
+ # The failing first task cancels the queued tasks instead of running
all 50.
+ assert len(started) < 50
+
+ def test_shares_tqdm_lock_with_worker_threads(self):
+ # A custom bar class: hf_thread_map must share a single lock with the
worker threads so bars
+ # created inside `fn` don't race on concurrent updates (mirrors
tqdm.contrib's `ensure_lock`).
+ class CustomTqdm(vanilla_tqdm):
+ pass
+
+ seen_locks = []
+ lock = threading.Lock()
+
+ def fn(x):
+ with lock:
+ seen_locks.append(CustomTqdm.get_lock())
+ return x
+
+ hf_thread_map(fn, range(8), max_workers=4, tqdm_class=CustomTqdm,
disable=True)
+
+ # Every worker thread observed the same, single shared lock.
+ assert seen_locks
+ assert all(observed is seen_locks[0] for observed in seen_locks)
+
+ def test_works_with_class_without_lock_api(self):
+ # Classes that don't implement tqdm's locking API (e.g. `silent_tqdm`)
must not crash.
+ from huggingface_hub.utils import silent_tqdm
+
+ assert hf_thread_map(lambda x: x * 2, range(4), max_workers=2,
tqdm_class=silent_tqdm) == [0, 2, 4, 6]
+
+
class TestCreateProgressBarCustomClass:
def test_custom_class_not_disabled_in_non_tty(self):
fake_stderr = io.StringIO()