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-10-30 18:17:29
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-huggingface-hub (Old)
 and      /work/SRC/openSUSE:Factory/.python-huggingface-hub.new.1255 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "python-huggingface-hub"

Fri Oct 30 18:17:29 2026 rev:21 rq:1393851 version:2.4.0

Changes:
--------
--- 
/work/SRC/openSUSE:Factory/python-huggingface-hub/python-huggingface-hub.changes
    2026-10-26 10:04:41.402508818 +0100
+++ 
/work/SRC/openSUSE:Factory/.python-huggingface-hub.new.1255/python-huggingface-hub.changes
  2026-10-30 18:23:31.837474733 +0100
@@ -1,0 +2,14 @@
+Fri Oct 30 07:59:09 UTC 2026 - Martin Pluskal <[email protected]>
+
+- Update to 2.4.0:
+  * HfFileSystem can copy files into Buckets (same or
+    another Bucket, or from model/dataset/Space repos);
+    mv and rename inside Buckets now work too
+  * hf extensions list gains VERSION and LATEST columns
+    with outdated hints; update repairs extensions whose
+    Python no longer starts
+  * Webhooks watch every domain by default and use the
+    discussion domain; cache loader rejects path-traversal
+    filenames; cache verify fixes
+
+-------------------------------------------------------------------

Old:
----
  huggingface_hub-2.3.0.tar.gz

New:
----
  huggingface_hub-2.4.0.tar.gz

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ python-huggingface-hub.spec ++++++
--- /var/tmp/diff_new_pack.e7ra9n/_old  2026-10-30 18:23:33.152529784 +0100
+++ /var/tmp/diff_new_pack.e7ra9n/_new  2026-10-30 18:23:33.168530453 +0100
@@ -23,7 +23,7 @@
 %endif
 %{?sle15_python_module_pythons}
 Name:           python-huggingface-hub
-Version:        2.3.0
+Version:        2.4.0
 Release:        0
 Summary:        Client library for interaction with the huggingface hub
 License:        Apache-2.0
@@ -101,7 +101,7 @@
 %python_uninstall_alternative hf
 %python_uninstall_alternative tiny-agents
 
-%files %{python_files}
+%files %python_files
 %python_alternative %{_bindir}/hf
 %python_alternative %{_bindir}/tiny-agents
 %{python_sitelib}/huggingface_hub

++++++ huggingface_hub-2.3.0.tar.gz -> huggingface_hub-2.4.0.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/huggingface_hub-2.3.0/PKG-INFO 
new/huggingface_hub-2.4.0/PKG-INFO
--- old/huggingface_hub-2.3.0/PKG-INFO  2026-10-23 10:51:50.269081600 +0200
+++ new/huggingface_hub-2.4.0/PKG-INFO  2026-10-29 16:29:50.476030000 +0100
@@ -1,6 +1,6 @@
 Metadata-Version: 2.4
 Name: huggingface_hub
-Version: 2.3.0
+Version: 2.4.0
 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.
@@ -63,7 +63,6 @@
 Requires-Dist: pytest-asyncio; extra == "testing"
 Requires-Dist: pytest-rerunfailures>=16.2; extra == "testing"
 Requires-Dist: pytest-mock; extra == "testing"
-Requires-Dist: urllib3<2.0; extra == "testing"
 Requires-Dist: soundfile; extra == "testing"
 Requires-Dist: Pillow; extra == "testing"
 Requires-Dist: numpy; extra == "testing"
@@ -98,7 +97,6 @@
 Requires-Dist: pytest-asyncio; extra == "all"
 Requires-Dist: pytest-rerunfailures>=16.2; extra == "all"
 Requires-Dist: pytest-mock; extra == "all"
-Requires-Dist: urllib3<2.0; extra == "all"
 Requires-Dist: soundfile; extra == "all"
 Requires-Dist: Pillow; extra == "all"
 Requires-Dist: numpy; extra == "all"
@@ -128,7 +126,6 @@
 Requires-Dist: pytest-asyncio; extra == "dev"
 Requires-Dist: pytest-rerunfailures>=16.2; extra == "dev"
 Requires-Dist: pytest-mock; extra == "dev"
-Requires-Dist: urllib3<2.0; extra == "dev"
 Requires-Dist: soundfile; extra == "dev"
 Requires-Dist: Pillow; extra == "dev"
 Requires-Dist: numpy; extra == "dev"
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/huggingface_hub-2.3.0/setup.py 
new/huggingface_hub-2.4.0/setup.py
--- old/huggingface_hub-2.3.0/setup.py  2026-10-23 10:51:44.476740100 +0200
+++ new/huggingface_hub-2.4.0/setup.py  2026-10-29 16:29:41.028048800 +0100
@@ -49,29 +49,25 @@
 
 extras["mcp"] = ["mcp>=1.9.4, <2.0.0"]  # list_tools(cursor=...) only 
paginates correctly from 1.9.4
 
-extras["testing"] = (
-    extras["oauth"]
-    + [
-        "jedi",
-        "Jinja2",
-        "pytest>=8.4.2",  # we need 
https://github.com/pytest-dev/pytest/pull/12436
-        "pytest-cov",
-        "pytest-env",
-        "pytest-xdist",
-        "pytest-vcr",  # to mock Inference
-        "pytest-asyncio",  # for AsyncInferenceClient
-        # 16.2 clears fixture finalizers when rerunning a test whose fixture 
failed at setup.
-        # Without it, any flaky fixture (e.g. a 502 from hub-ci) makes the 
rerun itself crash on pytest>=9.
-        "pytest-rerunfailures>=16.2",  # to rerun flaky tests in CI
-        "pytest-mock",
-        "urllib3<2.0",  # VCR.py broken with urllib3 2.0 (see 
https://urllib3.readthedocs.io/en/stable/v2-migration-guide.html)
-        "soundfile",
-        "Pillow",
-        "numpy",  # for embeddings
-        "duckdb; python_version < '3.15'",  # for datasets SQL end-to-end 
tests (no 3.15 wheels yet)
-        "fastapi",  # To build the documentation
-    ]
-)
+extras["testing"] = extras["oauth"] + [
+    "jedi",
+    "Jinja2",
+    "pytest>=8.4.2",  # we need https://github.com/pytest-dev/pytest/pull/12436
+    "pytest-cov",
+    "pytest-env",
+    "pytest-xdist",
+    "pytest-vcr",  # to mock Inference
+    "pytest-asyncio",  # for AsyncInferenceClient
+    # 16.2 clears fixture finalizers when rerunning a test whose fixture 
failed at setup.
+    # Without it, any flaky fixture (e.g. a 502 from hub-ci) makes the rerun 
itself crash on pytest>=9.
+    "pytest-rerunfailures>=16.2",  # to rerun flaky tests in CI
+    "pytest-mock",
+    "soundfile",
+    "Pillow",
+    "numpy",  # for embeddings
+    "duckdb; python_version < '3.15'",  # for datasets SQL end-to-end tests 
(no 3.15 wheels yet)
+    "fastapi",  # To build the documentation
+]
 
 extras["gradio"] = [
     "gradio>=5.0.0",
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/huggingface_hub-2.3.0/src/huggingface_hub/__init__.py 
new/huggingface_hub-2.4.0/src/huggingface_hub/__init__.py
--- old/huggingface_hub-2.3.0/src/huggingface_hub/__init__.py   2026-10-23 
10:51:44.477263700 +0200
+++ new/huggingface_hub-2.4.0/src/huggingface_hub/__init__.py   2026-10-29 
16:29:41.028558700 +0100
@@ -46,7 +46,7 @@
 from typing import TYPE_CHECKING
 
 
-__version__ = "2.3.0"
+__version__ = "2.4.0"
 
 # 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-2.3.0/src/huggingface_hub/cli/cache.py 
new/huggingface_hub-2.4.0/src/huggingface_hub/cli/cache.py
--- old/huggingface_hub-2.3.0/src/huggingface_hub/cli/cache.py  2026-10-23 
10:51:44.480729000 +0200
+++ new/huggingface_hub-2.4.0/src/huggingface_hub/cli/cache.py  2026-10-29 
16:29:41.031862700 +0100
@@ -41,7 +41,7 @@
 from ..utils._parsing import parse_duration, parse_size
 from ._cli_utils import RepoIdArg, RepoTypeOpt, RevisionOpt, TokenOpt, 
get_hf_api, typer_factory
 from ._framework import Argument, Option
-from ._output import out
+from ._output import _ascii_safe, out
 
 
 cache_cli = typer_factory(help="Manage local cache directory.")
@@ -856,19 +856,20 @@
     )
 
     exit_code = 0
+    failed = _ascii_safe("❌", "[FAIL]")  # stdout may not be able to encode 
"❌" (e.g. redirected on Windows)
 
     if result.mismatches:
         details = "\n".join(
             f"  - {m['path']}: expected {m['expected']} ({m['algorithm']}), 
got {m['actual']}"
             for m in result.mismatches
         )
-        out.text(f"❌ Checksum verification failed for the following 
file(s):\n{details}")
+        out.text(f"{failed} Checksum verification failed for the following 
file(s):\n{details}")
         exit_code = 1
 
     if result.missing_paths:
         if fail_on_missing_files:
             details = "\n".join(f"  - {p}" for p in result.missing_paths)
-            out.text(f"❌ Missing files (present remotely, absent 
locally):\n{details}")
+            out.text(f"{failed} Missing files (present remotely, absent 
locally):\n{details}")
             exit_code = 1
         else:
             out.warning(
@@ -879,7 +880,7 @@
     if result.extra_paths:
         if fail_on_extra_files:
             details = "\n".join(f"  - {p}" for p in result.extra_paths)
-            out.text(f"❌ Extra files (present locally, absent 
remotely):\n{details}")
+            out.text(f"{failed} Extra files (present locally, absent 
remotely):\n{details}")
             exit_code = 1
         else:
             out.warning(
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/huggingface_hub-2.3.0/src/huggingface_hub/cli/extensions.py 
new/huggingface_hub-2.4.0/src/huggingface_hub/cli/extensions.py
--- old/huggingface_hub-2.3.0/src/huggingface_hub/cli/extensions.py     
2026-10-23 10:51:44.480729000 +0200
+++ new/huggingface_hub-2.4.0/src/huggingface_hub/cli/extensions.py     
2026-10-29 16:29:41.031862700 +0100
@@ -19,7 +19,9 @@
 import re
 import shutil
 import subprocess
+import time
 import venv
+from concurrent.futures import ThreadPoolExecutor
 from dataclasses import asdict, dataclass
 from datetime import datetime, timezone
 from enum import Enum
@@ -30,6 +32,7 @@
 import click
 import httpx2
 
+from huggingface_hub import constants
 from huggingface_hub.errors import CLIError, CLIExtensionInstallError, 
ConfirmationError
 from huggingface_hub.utils import get_session, logging
 
@@ -41,6 +44,7 @@
 DEFAULT_EXTENSION_OWNER = "huggingface"
 EXTENSIONS_ROOT = Path("~/.local/share/hf/extensions")
 MANIFEST_FILENAME = "manifest.json"
+_LATEST_COMMIT_FILENAME = ".latest_commit"
 EXTENSIONS_HELP = (
     "Manage hf CLI extensions.\n\n"
     "Security Warning: extensions are third-party executables or Python 
packages. "
@@ -234,6 +238,7 @@
     if not executable_path.is_file():
         raise CLIError(f"Extension '{short_name}' is not installed.")
 
+    _check_extension_update(short_name)
     exit_code = _execute_extension_binary(executable_path=executable_path, 
args=list(ctx.args))
     raise click.exceptions.Exit(code=exit_code)
 
@@ -241,17 +246,24 @@
 @extensions_cli.command("list | ls", examples=["hf extensions list"])
 def extension_list() -> None:
     """List installed extension commands."""
+    manifests = _list_installed_extensions()
+    with ThreadPoolExecutor() as executor:
+        latest_shas = list(executor.map(_get_latest_commit_sha, manifests))
     rows = [
         {
             "command": f"hf {manifest.short_name}",
             "source": str(manifest.repo_id),
             "type": str(manifest.type),
+            "version": manifest.commit_sha[:7] if manifest.commit_sha else 
None,
+            "latest": manifest.commit_sha == latest_sha if latest_sha is not 
None else None,
             "installed": manifest.installed_at.strftime("%Y-%m-%d"),
             "description": manifest.description,
         }
-        for manifest in _list_installed_extensions()
+        for manifest, latest_sha in zip(manifests, latest_shas)
     ]
     out.table(rows, id_key="command")
+    if outdated := [manifest.short_name for manifest, row in zip(manifests, 
rows) if row["latest"] is False]:
+        out.hint(f"Run `hf extensions update` to update {', 
'.join(outdated)}.")
 
 
 @extensions_cli.command("search", examples=["hf extensions search"])
@@ -354,9 +366,33 @@
     if executable_path is None or not executable_path.is_file():
         return None
 
+    _check_extension_update(short_name)
     return _execute_extension_binary(executable_path=executable_path, 
args=list(args[1:]))
 
 
+def _check_extension_update(short_name: str) -> None:
+    """Hint at `hf extensions update` when a newer commit is available, at 
most once every 24 hours per extension.
+
+    Only prints a hint: updating stays an explicit user action. Skipped 
entirely if `HF_HUB_DISABLE_UPDATE_CHECK`
+    is set. Called right before running the extension, so any error is caught 
and logged.
+    """
+    if constants.HF_HUB_DISABLE_UPDATE_CHECK:
+        return
+    try:
+        if _is_latest_commit_cached(short_name):
+            return
+        manifest = ExtensionManifest.load(_get_extension_dir(short_name))
+        latest_sha = _get_latest_commit_sha(manifest)
+    except Exception:
+        logger.debug(f"Error while checking for an update of extension 
'{short_name}'.", exc_info=True)
+        return
+    if latest_sha is not None and latest_sha != manifest.commit_sha:
+        out.hint(
+            f"A new version of the '{short_name}' extension is available 
({manifest.repo_id})."
+            f" Run `hf extensions update {short_name}` to update it."
+        )
+
+
 def _auto_install_official_extension(short_name: str) -> Path | None:
     """Try to auto-install huggingface/hf-<name>. Returns executable path or 
None."""
     owner, repo_name = DEFAULT_EXTENSION_OWNER, f"hf-{short_name}"
@@ -403,7 +439,11 @@
         out.warning(f"Could not check updates for '{short_name}' 
({owner}/{repo_name}): {error}. Skipping.")
         return _ExtensionUpdateStatus.SKIPPED
 
-    if latest_sha == manifest.commit_sha:
+    # A Python extension whose venv no longer starts is reinstalled even when 
up to date.
+    if latest_sha == manifest.commit_sha and (
+        manifest.type == "binary" or 
_can_run(_get_venv_python(_get_extension_dir(short_name)))
+    ):
+        _save_latest_commit_sha(short_name, latest_sha)
         return _ExtensionUpdateStatus.UP_TO_DATE
 
     _install_extension(owner=owner, repo_name=repo_name, 
short_name=short_name, commit_sha=latest_sha)
@@ -466,6 +506,7 @@
             commit_sha=commit_sha,
         )
         manifest.save(extension_dir)
+        _save_latest_commit_sha(short_name, commit_sha)
         installed = True
         return manifest
     except CLIError:
@@ -487,18 +528,56 @@
             shutil.rmtree(extension_dir, ignore_errors=True)
 
 
-def _fetch_latest_commit_sha(*, owner: str, repo_name: str) -> str:
+_GIT_HEAD_REF_RE = re.compile(r"([0-9a-f]{40}) HEAD\x00")
+
+
+def _fetch_latest_commit_sha(*, owner: str, repo_name: str, timeout: float = 
_EXTENSIONS_DOWNLOAD_TIMEOUT) -> str:
     """Fetch the latest commit SHA on the default branch, used to detect 
available updates.
 
-    The only REST API call left in the install and update paths. Raises on 
failure: callers decide
-    whether a missing SHA is fatal, and report the reason themselves.
+    Read from the git smart HTTP endpoint (the one `git ls-remote` uses), 
which is not metered like the
+    REST API. Raises on failure: callers decide whether a missing SHA is 
fatal, and report the reason themselves.
     """
     response = _github_request(
         "GET",
-        f"https://api.github.com/repos/{owner}/{repo_name}/commits/HEAD";,
-        headers={"Accept": "application/vnd.github.sha"},
+        f"https://github.com/{owner}/{repo_name}.git/info/refs";,
+        params={"service": "git-upload-pack"},
+        timeout=timeout,
     )
-    return response.text.strip()
+    if (match := _GIT_HEAD_REF_RE.search(response.text)) is None:
+        raise CLIError(f"Could not find the latest commit of 
'{owner}/{repo_name}'.")
+    return match.group(1)
+
+
+def _get_latest_commit_sha(manifest: ExtensionManifest) -> str | None:
+    """Latest commit SHA of an installed extension, requested from GitHub at 
most once every 24 hours.
+
+    Shared by `hf extensions list` and the update hint so that running them 
repeatedly never hits GitHub's rate
+    limits for unauthenticated requests. Failures are cached too (as `None`): 
an offline or rate-limited machine
+    backs off instead of retrying on every call. Skipped if 
`HF_HUB_DISABLE_UPDATE_CHECK` is set.
+    """
+    if constants.HF_HUB_DISABLE_UPDATE_CHECK:
+        return None
+    if _is_latest_commit_cached(manifest.short_name):
+        return (_get_extension_dir(manifest.short_name) / 
_LATEST_COMMIT_FILENAME).read_text() or None
+    try:
+        latest_sha = _fetch_latest_commit_sha(owner=manifest.owner, 
repo_name=manifest.repo, timeout=2)
+    except Exception as error:
+        logger.debug(f"Could not fetch the latest commit of 
'{manifest.repo_id}': {error}")
+        latest_sha = None
+    _save_latest_commit_sha(manifest.short_name, latest_sha)
+    return latest_sha
+
+
+def _is_latest_commit_cached(short_name: str) -> bool:
+    cache_path = _get_extension_dir(short_name) / _LATEST_COMMIT_FILENAME
+    return cache_path.is_file() and (time.time() - cache_path.stat().st_mtime) 
< 24 * 3600
+
+
+def _save_latest_commit_sha(short_name: str, sha: str | None) -> None:
+    try:
+        (_get_extension_dir(short_name) / 
_LATEST_COMMIT_FILENAME).write_text(sha or "")
+    except OSError as error:
+        logger.debug(f"Could not cache the latest commit of extension 
'{short_name}': {error}")
 
 
 def _fetch_remote_binary(*, owner: str, repo_name: str, short_name: str) -> 
bytes:
@@ -521,17 +600,19 @@
 def _install_python_extension(*, extension_dir: Path, owner: str, repo_name: 
str, short_name: str) -> Path:
     source_url = f"https://github.com/{owner}/{repo_name}/archive/HEAD.zip";
     venv_dir = extension_dir / "venv"
-    venv_python = _get_venv_bin_path(venv_dir, "python.exe" if os.name == "nt" 
else "python")
+    venv_python = _get_venv_python(extension_dir)
     uv_path = shutil.which("uv")
 
     status = out.status()
-    if not venv_python.is_file():
+    if not _can_run(venv_python):
         status.update(f"Creating virtual environment in {venv_dir}")
+        shutil.rmtree(venv_dir, ignore_errors=True)
         extension_dir.mkdir(parents=True, exist_ok=True)
         if uv_path:
             subprocess.run([uv_path, "venv", str(venv_dir)], check=True)
         else:
-            venv.EnvBuilder(with_pip=True).create(str(venv_dir))
+            # Symlinks like `python -m venv`: a copied Homebrew python breaks 
on its next patch upgrade.
+            venv.EnvBuilder(with_pip=True, symlinks=os.name != 
"nt").create(str(venv_dir))
         status.done(f"Virtual environment created in {venv_dir}")
 
     status.update(f"Installing package from {source_url}")
@@ -619,7 +700,7 @@
 
 
 def _github_request(
-    method: str, url: str, *, params: dict | None = None, headers: dict | None 
= None
+    method: str, url: str, *, params: dict | None = None, timeout: float = 
_EXTENSIONS_DOWNLOAD_TIMEOUT
 ) -> httpx2.Response:
     """Perform a GitHub request.
 
@@ -629,16 +710,15 @@
     Unauthenticated `api.github.com` calls are capped per IP address, and that 
quota is shared with
     everything else calling GitHub from the same network (NAT, VPN, CI 
egress). Remote refs are therefore
     always addressed as `HEAD`, which GitHub resolves to the repo's default 
branch on
-    `raw.githubusercontent.com`, on the archive endpoint and on the REST API 
alike: no
+    `raw.githubusercontent.com`, on the archive endpoint and in the git refs 
advertisement alike: no
     `GET /repos/{owner}/{repo}` lookup is needed to discover the branch name.
     """
     response = get_session().request(
         method,
         url,
         params=params,
-        headers=headers,
         follow_redirects=True,
-        timeout=_EXTENSIONS_DOWNLOAD_TIMEOUT,
+        timeout=timeout,
     )
     # Primary limits report an exhausted quota, secondary ones only ask to 
back off.
     if response.status_code in (403, 429) and (
@@ -710,6 +790,18 @@
     return venv_dir / "bin" / executable_name
 
 
+def _get_venv_python(extension_dir: Path) -> Path:
+    return _get_venv_bin_path(extension_dir / "venv", "python.exe" if os.name 
== "nt" else "python")
+
+
+def _can_run(python: Path) -> bool:
+    """Whether `python` starts: a venv's python doesn't once its base 
interpreter is gone (e.g. Homebrew upgrade)."""
+    try:
+        return subprocess.run([str(python), "-c", ""], 
capture_output=True).returncode == 0
+    except OSError:
+        return False
+
+
 _ALLOWED_EXTENSION_NAME = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]*$")
 
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/huggingface_hub-2.3.0/src/huggingface_hub/cli/webhooks.py 
new/huggingface_hub-2.4.0/src/huggingface_hub/cli/webhooks.py
--- old/huggingface_hub-2.3.0/src/huggingface_hub/cli/webhooks.py       
2026-10-23 10:51:44.482729200 +0200
+++ new/huggingface_hub-2.4.0/src/huggingface_hub/cli/webhooks.py       
2026-10-29 16:29:41.033862800 +0100
@@ -158,7 +158,7 @@
         list[WebhookDomain] | None,
         Option(
             "--domain",
-            help="Domain to watch: 'repo' or 'discussions'. Repeatable. 
Defaults to all domains.",
+            help="Domain to watch: 'repo' or 'discussion'. Repeatable. 
Defaults to all domains.",
         ),
     ] = None,
     secret: Annotated[
@@ -229,7 +229,7 @@
         list[WebhookDomain] | None,
         Option(
             "--domain",
-            help="New list of domains to watch: 'repo' or 'discussions'. 
Repeatable.",
+            help="New list of domains to watch: 'repo' or 'discussion'. 
Repeatable.",
         ),
     ] = None,
     secret: Annotated[
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/huggingface_hub-2.3.0/src/huggingface_hub/constants.py 
new/huggingface_hub-2.4.0/src/huggingface_hub/constants.py
--- old/huggingface_hub-2.3.0/src/huggingface_hub/constants.py  2026-10-23 
10:51:44.482729200 +0200
+++ new/huggingface_hub-2.4.0/src/huggingface_hub/constants.py  2026-10-29 
16:29:41.033862800 +0100
@@ -153,7 +153,7 @@
 DISCUSSION_STATUS: tuple[DiscussionTypeFilter, ...] = 
typing.get_args(DiscussionStatusFilter)
 
 # Webhook subscription types
-WEBHOOK_DOMAIN_T = Literal["repo", "discussions"]
+WEBHOOK_DOMAIN_T = Literal["repo", "discussion"]
 
 # default cache
 default_home = os.path.join(os.path.expanduser("~"), ".cache")
@@ -233,7 +233,7 @@
 CLAUDE_SKILLS_LOCAL_PATH = Path(".claude/skills")
 CLAUDE_SKILLS_GLOBAL_PATH = Path(os.environ.get("CLAUDE_CONFIG_DIR") or 
"~/.claude") / "skills"
 
-# Set to skip the CLI update check (PyPI query + "new version available" 
warning at startup).
+# Set to skip the CLI update checks (PyPI, `hf-cli` skill and installed 
extensions) and their hints.
 HF_HUB_DISABLE_UPDATE_CHECK = 
_is_true(os.environ.get("HF_HUB_DISABLE_UPDATE_CHECK"))
 
 # If set, log level will be set to DEBUG and all requests made to the Hub will 
be logged
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/huggingface_hub-2.3.0/src/huggingface_hub/file_download.py 
new/huggingface_hub-2.4.0/src/huggingface_hub/file_download.py
--- old/huggingface_hub-2.3.0/src/huggingface_hub/file_download.py      
2026-10-23 10:51:44.483729100 +0200
+++ new/huggingface_hub-2.4.0/src/huggingface_hub/file_download.py      
2026-10-29 16:29:41.033862800 +0100
@@ -1577,6 +1577,7 @@
         raise ValueError(
             f"Invalid repo type: {repo_type}. Accepted repo types are: 
{str(constants.REPO_TYPES_WITH_KERNEL)}"
         )
+    _validate_relative_filename(filename)
     if cache_dir is None:
         cache_dir = constants.HF_HUB_CACHE
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/huggingface_hub-2.3.0/src/huggingface_hub/hf_api.py 
new/huggingface_hub-2.4.0/src/huggingface_hub/hf_api.py
--- old/huggingface_hub-2.3.0/src/huggingface_hub/hf_api.py     2026-10-23 
10:51:44.485729200 +0200
+++ new/huggingface_hub-2.4.0/src/huggingface_hub/hf_api.py     2026-10-29 
16:29:41.035863000 +0100
@@ -30,7 +30,7 @@
 from itertools import islice
 from pathlib import Path, PurePosixPath
 from secrets import token_hex
-from typing import TYPE_CHECKING, Any, BinaryIO, Literal, TypeVar, cast, 
overload
+from typing import TYPE_CHECKING, Any, BinaryIO, Literal, TypeVar, cast, 
get_args, overload
 from urllib.parse import quote
 
 import httpcore2
@@ -508,7 +508,7 @@
         watched (`list[WebhookWatchedItem]`):
             List of items watched by the webhook, see [`WebhookWatchedItem`].
         domains (`list[WEBHOOK_DOMAIN_T]`):
-            List of domains the webhook is watching. Can be one of `["repo", 
"discussions"]`.
+            List of domains the webhook is watching. Can be one of `["repo", 
"discussion"]`.
         secret (`str`, *optional*):
             Secret of the webhook.
         disabled (`bool`):
@@ -7658,7 +7658,7 @@
 
             >>> HfApi().comment_discussion(
             ...     repo_id="username/repo_name",
-            ...     discussion_num=34
+            ...     discussion_num=34,
             ...     comment=comment
             ... )
             # DiscussionComment(id='deadbeef0000000', type='comment', ...)
@@ -7725,7 +7725,7 @@
             >>> new_title = "New title, fixing a typo"
             >>> HfApi().rename_discussion(
             ...     repo_id="username/repo_name",
-            ...     discussion_num=34
+            ...     discussion_num=34,
             ...     new_title=new_title
             ... )
             # DiscussionTitleChange(id='deadbeef0000000', type='title-change', 
...)
@@ -7792,11 +7792,10 @@
 
         Examples:
             ```python
-            >>> new_title = "New title, fixing a typo"
-            >>> HfApi().rename_discussion(
+            >>> HfApi().change_discussion_status(
             ...     repo_id="username/repo_name",
-            ...     discussion_num=34
-            ...     new_title=new_title
+            ...     discussion_num=34,
+            ...     new_status="closed"
             ... )
             # DiscussionStatusChange(id='deadbeef0000000', 
type='status-change', ...)
 
@@ -10305,7 +10304,7 @@
         >>> from huggingface_hub import update_collection_metadata
         >>> collection = update_collection_metadata(
         ...     collection_slug="username/iccv-2023-64f9a55bb3115b4f513ec026",
-        ...     title="ICCV Oct. 2023"
+        ...     title="ICCV Oct. 2023",
         ...     description="Portfolio of models, datasets, papers and demos I 
presented at ICCV Oct. 2023",
         ...     private=False,
         ...     theme="pink",
@@ -10469,7 +10468,7 @@
         >>> add_collection_item(
         ...     collection_slug="davanstrien/climate-64f99dc2a5067f6b65531bab",
         ...     item_id="datasets/climate_fever",
-        ...     item_type="dataset"
+        ...     item_type="dataset",
         ...     note="This dataset adopts the FEVER methodology that consists 
of 1,535 real-world claims regarding climate-change collected on the internet."
         ... )
         (...)
@@ -10532,7 +10531,7 @@
         >>> update_collection_item(
         ...     
collection_slug="TheBloke/recent-models-64f9a55bb3115b4f513ec026",
         ...     item_object_id=collection.items[-1].item_object_id,
-        ...     note="Newly updated model!"
+        ...     note="Newly updated model!",
         ...     position=0,
         ... )
         ```
@@ -11201,7 +11200,7 @@
                 List of [`WebhookWatchedItem`] to be watched by the webhook. 
It can be users, orgs, models, datasets, spaces or buckets.
                 Watched items can also be provided as plain dictionaries.
             domains (`list[Literal["repo", "discussion"]]`, optional):
-                List of domains to watch. It can be "repo", "discussion" or 
both.
+                List of domains to watch. It can be "repo", "discussion" or 
both. Defaults to both.
             secret (`str`, optional):
                 A secret to sign the payload with.
             secrets (`dict[str, str]`, optional):
@@ -11277,9 +11276,10 @@
         """
         watched_dicts = [asdict(item) if isinstance(item, WebhookWatchedItem) 
else item for item in watched]
 
-        post_webhooks_json: dict = {"watched": watched_dicts}
-        if domains is not None:
-            post_webhooks_json["domains"] = domains
+        post_webhooks_json: dict = {
+            "watched": watched_dicts,
+            "domains": domains if domains is not None else 
list(get_args(constants.WEBHOOK_DOMAIN_T)),
+        }
         if secret is not None:
             post_webhooks_json["secret"] = secret
         if url is not None and job_id is not None:
@@ -12236,7 +12236,7 @@
             ```python
             >>> from huggingface_hub import run_job
             >>> image = "pytorch/pytorch:2.6.0-cuda12.4-cudnn9-devel"
-            >>> command = ["python", "-c", "import torch; print(f"This code 
ran with the following GPU: {torch.cuda.get_device_name()}")"]
+            >>> command = ["python", "-c", "import torch; print(f'This code 
ran with the following GPU: {torch.cuda.get_device_name()}')"]
             >>> run_job(image=image, command=command, flavor="a10g-small")
             ```
 
@@ -13185,8 +13185,8 @@
             ```python
             >>> from huggingface_hub import create_scheduled_job
             >>> image = "pytorch/pytorch:2.6.0-cuda12.4-cudnn9-devel"
-            >>> command = ["python", "-c", "import torch; print(f"This code 
ran with the following GPU: {torch.cuda.get_device_name()}")"]
-            >>> create_scheduled_job(image, command, flavor="a10g-small", 
schedule="@hourly")
+            >>> command = ["python", "-c", "import torch; print(f'This code 
ran with the following GPU: {torch.cuda.get_device_name()}')"]
+            >>> create_scheduled_job(image=image, command=command, 
flavor="a10g-small", schedule="@hourly")
             ```
 
         """
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/huggingface_hub-2.3.0/src/huggingface_hub/hf_file_system.py 
new/huggingface_hub-2.4.0/src/huggingface_hub/hf_file_system.py
--- old/huggingface_hub-2.3.0/src/huggingface_hub/hf_file_system.py     
2026-10-23 10:51:44.485729200 +0200
+++ new/huggingface_hub-2.4.0/src/huggingface_hub/hf_file_system.py     
2026-10-29 16:29:41.036862900 +0100
@@ -1094,10 +1094,8 @@
         except EntryNotFoundError:
             # Path could be a file
             try:
-                if self.info(path, revision=revision, **kwargs)["type"] == 
"file":
-                    out = {path: {}}
-                else:
-                    out = {}
+                path_info = self.info(path, revision=revision, **kwargs)
+                out = {path: path_info} if path_info["type"] == "file" else {}
             except FileNotFoundError:
                 out = {}
         else:
@@ -1116,7 +1114,7 @@
 
     def cp_file(self, path1: str, path2: str, revision: str | None = None, 
**kwargs) -> None:
         """
-        Copy a file within or between repositories.
+        Copy a file within or between repositories, or into a bucket.
 
         > [!WARNING]
         > Note: When possible, use `HfApi.upload_file()` for better 
performance.
@@ -1132,10 +1130,36 @@
         """
         resolved_path1 = self.resolve_path(path1, revision=revision)
         resolved_path2 = self.resolve_path(path2, revision=revision)
-        if isinstance(resolved_path1, HfFileSystemResolvedBucketPath) or 
isinstance(
-            resolved_path2, HfFileSystemResolvedBucketPath
-        ):
-            raise NotImplementedError("Copy from/to buckets is not available 
yet")
+
+        # --- Destination is a bucket ---
+        if isinstance(resolved_path2, HfFileSystemResolvedBucketPath):
+            source_info = self.info(path1, revision=revision)
+
+            if source_info["type"] == "directory":
+                return  # Folders are implicit in buckets (fsspec's recursive 
copy calls cp_file on them too)
+
+            if isinstance(resolved_path1, HfFileSystemResolvedBucketPath):
+                source_type, source_id = "bucket", resolved_path1.bucket_id
+            else:
+                source_type, source_id = resolved_path1.repo_type, 
resolved_path1.repo_id
+            if (xet_hash := source_info.get("xet_hash")) is not None:
+                # Server-side copy by xet hash (no data transfer)
+                self._api.batch_bucket_files(
+                    resolved_path2.bucket_id, copy=[(source_type, source_id, 
xet_hash, resolved_path2.path)]
+                )
+            else:
+                # Non-Xet repo file: download and upload via 
batch_bucket_files add
+                with self.open(path1, "rb", revision=revision) as f:
+                    content = f.read()
+                self._api.batch_bucket_files(resolved_path2.bucket_id, 
add=[(content, resolved_path2.path)])
+
+            self.invalidate_cache(path=resolved_path1.unresolve())
+            self.invalidate_cache(path=resolved_path2.unresolve())
+            return
+
+        # --- Source is a bucket, destination is a repository (unsupported) ---
+        if isinstance(resolved_path1, HfFileSystemResolvedBucketPath):
+            raise NotImplementedError("Copy from buckets to repositories is 
not available yet")
 
         same_repo = (
             resolved_path1.repo_type == resolved_path2.repo_type and 
resolved_path1.repo_id == resolved_path2.repo_id
@@ -1760,7 +1784,11 @@
 def _raise_file_not_found(path: str, err: Exception | None) -> NoReturn:
     msg = path
     if isinstance(err, RepositoryNotFoundError):
-        msg = f"{path} (repository not found)"
+        msg = (
+            f"{path} (repository not found). "
+            "If the repo is private, make sure you are authenticated "
+            "and your token has the required permissions."
+        )
     elif isinstance(err, RevisionNotFoundError):
         msg = f"{path} (revision not found)"
     elif isinstance(err, HFValidationError):
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/huggingface_hub-2.3.0/src/huggingface_hub/utils/_paths.py 
new/huggingface_hub-2.4.0/src/huggingface_hub/utils/_paths.py
--- old/huggingface_hub-2.3.0/src/huggingface_hub/utils/_paths.py       
2026-10-23 10:51:44.491879500 +0200
+++ new/huggingface_hub-2.4.0/src/huggingface_hub/utils/_paths.py       
2026-10-29 16:29:41.043375300 +0100
@@ -122,10 +122,10 @@
     ```python
     >>> list(filter_repo_objects(
     ... [
-    ...     CommitOperationAdd(path_or_fileobj="/tmp/aaa.pdf", 
path_in_repo="aaa.pdf")
-    ...     CommitOperationAdd(path_or_fileobj="/tmp/bbb.jpg", 
path_in_repo="bbb.jpg")
-    ...     CommitOperationAdd(path_or_fileobj="/tmp/.ccc.pdf", 
path_in_repo=".ccc.pdf")
-    ...     CommitOperationAdd(path_or_fileobj="/tmp/.ddd.png", 
path_in_repo=".ddd.png")
+    ...     CommitOperationAdd(path_or_fileobj="/tmp/aaa.pdf", 
path_in_repo="aaa.pdf"),
+    ...     CommitOperationAdd(path_or_fileobj="/tmp/bbb.jpg", 
path_in_repo="bbb.jpg"),
+    ...     CommitOperationAdd(path_or_fileobj="/tmp/.ccc.pdf", 
path_in_repo=".ccc.pdf"),
+    ...     CommitOperationAdd(path_or_fileobj="/tmp/.ddd.png", 
path_in_repo=".ddd.png"),
     ... ],
     ... allow_patterns=["*.pdf"],
     ... ignore_patterns=[".*"],
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/huggingface_hub-2.3.0/src/huggingface_hub/utils/_verification.py 
new/huggingface_hub-2.4.0/src/huggingface_hub/utils/_verification.py
--- old/huggingface_hub-2.3.0/src/huggingface_hub/utils/_verification.py        
2026-10-23 10:51:44.492729200 +0200
+++ new/huggingface_hub-2.4.0/src/huggingface_hub/utils/_verification.py        
2026-10-29 16:29:41.043863000 +0100
@@ -5,6 +5,7 @@
 
 from .. import constants
 from ..file_download import repo_folder_name
+from ._paths import DEFAULT_IGNORE_PATTERNS, filter_repo_objects
 from .sha import git_hash, sha_fileobj
 
 
@@ -39,8 +40,12 @@
 def collect_local_files(root: Path) -> dict[str, Path]:
     """
     Return a mapping of repo-relative path -> absolute path for all files 
under `root`.
+
+    Files matching `DEFAULT_IGNORE_PATTERNS` (`.git/` and the 
`.cache/huggingface/` metadata that downloads to a
+    `local_dir` write) are skipped, since they can never be part of a repo.
     """
-    return {p.relative_to(root).as_posix(): p for p in root.rglob("*") if 
p.is_file()}
+    files = {p.relative_to(root).as_posix(): p for p in root.rglob("*") if 
p.is_file()}
+    return {path: files[path] for path in filter_repo_objects(files, 
ignore_patterns=DEFAULT_IGNORE_PATTERNS)}
 
 
 def _resolve_commit_hash_from_cache(storage_folder: Path, revision: str | 
None) -> str:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/huggingface_hub-2.3.0/src/huggingface_hub/utils/logging.py 
new/huggingface_hub-2.4.0/src/huggingface_hub/utils/logging.py
--- old/huggingface_hub-2.3.0/src/huggingface_hub/utils/logging.py      
2026-10-23 10:51:44.492729200 +0200
+++ new/huggingface_hub-2.4.0/src/huggingface_hub/utils/logging.py      
2026-10-29 16:29:41.043863000 +0100
@@ -87,10 +87,10 @@
         Example:
 
     ```python
-    >>> from huggingface_hub import get_logger
+    >>> from huggingface_hub import logging
 
-    >>> logger = get_logger(__file__)
-    >>> logger.set_verbosity_info()
+    >>> logger = logging.get_logger(__name__)
+    >>> logging.set_verbosity_info()
     ```
     """
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/huggingface_hub-2.3.0/src/huggingface_hub/utils/tqdm.py 
new/huggingface_hub-2.4.0/src/huggingface_hub/utils/tqdm.py
--- old/huggingface_hub-2.3.0/src/huggingface_hub/utils/tqdm.py 2026-10-23 
10:51:44.493729400 +0200
+++ new/huggingface_hub-2.4.0/src/huggingface_hub/utils/tqdm.py 2026-10-29 
16:29:41.044863000 +0100
@@ -55,17 +55,17 @@
     >>> disable_progress_bars("peft.foo")
 
     # Check state of different groups
-    >>> assert not are_progress_bars_disabled("peft"))
+    >>> assert not are_progress_bars_disabled("peft")
     >>> assert not are_progress_bars_disabled("peft.something")
-    >>> assert are_progress_bars_disabled("peft.foo"))
-    >>> assert are_progress_bars_disabled("peft.foo.bar"))
+    >>> assert are_progress_bars_disabled("peft.foo")
+    >>> assert are_progress_bars_disabled("peft.foo.bar")
 
     # Enable progress bars for a subgroup
     >>> enable_progress_bars("peft.foo.bar")
 
     # Check if enabling a subgroup affects the parent group
-    >>> assert are_progress_bars_disabled("peft.foo"))
-    >>> assert not are_progress_bars_disabled("peft.foo.bar"))
+    >>> assert are_progress_bars_disabled("peft.foo")
+    >>> assert not are_progress_bars_disabled("peft.foo.bar")
 
     # No progress bar for `name="peft.foo"`
     >>> for _ in tqdm(range(5), name="peft.foo"):
@@ -290,7 +290,7 @@
     Example:
     ```py
     >>> with tqdm_stream_file("config.json") as f:
-    >>>     httpx2.put(url, data=f)
+    ...     httpx2.put(url, data=f)
     config.json: 100%|█████████████████████████| 8.19k/8.19k [00:02<00:00, 
3.72kB/s]
     ```
     """
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/huggingface_hub-2.3.0/src/huggingface_hub.egg-info/PKG-INFO 
new/huggingface_hub-2.4.0/src/huggingface_hub.egg-info/PKG-INFO
--- old/huggingface_hub-2.3.0/src/huggingface_hub.egg-info/PKG-INFO     
2026-10-23 10:51:50.185049300 +0200
+++ new/huggingface_hub-2.4.0/src/huggingface_hub.egg-info/PKG-INFO     
2026-10-29 16:29:50.385028600 +0100
@@ -1,6 +1,6 @@
 Metadata-Version: 2.4
 Name: huggingface_hub
-Version: 2.3.0
+Version: 2.4.0
 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.
@@ -63,7 +63,6 @@
 Requires-Dist: pytest-asyncio; extra == "testing"
 Requires-Dist: pytest-rerunfailures>=16.2; extra == "testing"
 Requires-Dist: pytest-mock; extra == "testing"
-Requires-Dist: urllib3<2.0; extra == "testing"
 Requires-Dist: soundfile; extra == "testing"
 Requires-Dist: Pillow; extra == "testing"
 Requires-Dist: numpy; extra == "testing"
@@ -98,7 +97,6 @@
 Requires-Dist: pytest-asyncio; extra == "all"
 Requires-Dist: pytest-rerunfailures>=16.2; extra == "all"
 Requires-Dist: pytest-mock; extra == "all"
-Requires-Dist: urllib3<2.0; extra == "all"
 Requires-Dist: soundfile; extra == "all"
 Requires-Dist: Pillow; extra == "all"
 Requires-Dist: numpy; extra == "all"
@@ -128,7 +126,6 @@
 Requires-Dist: pytest-asyncio; extra == "dev"
 Requires-Dist: pytest-rerunfailures>=16.2; extra == "dev"
 Requires-Dist: pytest-mock; extra == "dev"
-Requires-Dist: urllib3<2.0; extra == "dev"
 Requires-Dist: soundfile; extra == "dev"
 Requires-Dist: Pillow; extra == "dev"
 Requires-Dist: numpy; extra == "dev"
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/huggingface_hub-2.3.0/src/huggingface_hub.egg-info/requires.txt 
new/huggingface_hub-2.4.0/src/huggingface_hub.egg-info/requires.txt
--- old/huggingface_hub-2.3.0/src/huggingface_hub.egg-info/requires.txt 
2026-10-23 10:51:50.189984800 +0200
+++ new/huggingface_hub-2.4.0/src/huggingface_hub.egg-info/requires.txt 
2026-10-29 16:29:50.390696800 +0100
@@ -27,7 +27,6 @@
 pytest-asyncio
 pytest-rerunfailures>=16.2
 pytest-mock
-urllib3<2.0
 soundfile
 Pillow
 numpy
@@ -59,7 +58,6 @@
 pytest-asyncio
 pytest-rerunfailures>=16.2
 pytest-mock
-urllib3<2.0
 soundfile
 Pillow
 numpy
@@ -117,7 +115,6 @@
 pytest-asyncio
 pytest-rerunfailures>=16.2
 pytest-mock
-urllib3<2.0
 soundfile
 Pillow
 numpy
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/huggingface_hub-2.3.0/tests/test_buckets_hf_file_system.py 
new/huggingface_hub-2.4.0/tests/test_buckets_hf_file_system.py
--- old/huggingface_hub-2.3.0/tests/test_buckets_hf_file_system.py      
2026-10-23 10:51:44.495489000 +0200
+++ new/huggingface_hub-2.4.0/tests/test_buckets_hf_file_system.py      
2026-10-29 16:29:41.047031000 +0100
@@ -104,9 +104,60 @@
         yield
         self.api.delete_bucket(self.bucket_id)
 
-    @pytest.mark.skip("Not implemented yet")
     def test_copy_file(self):
-        pass
+        self.hffs.cp_file(self.text_file, self.hf_path + 
"/data/text_data_copy.txt")
+        with self.hffs.open(self.hf_path + "/data/text_data_copy.txt", "r") as 
f:
+            assert f.read() == "dummy text data"
+
+    def test_move_and_rename_file(self):
+        copied_file = self.hf_path + "/data/text_data_copy.txt"
+        moved_file = self.hf_path + "/data/text_data_moved.txt"
+        renamed_file = self.hf_path + "/data/text_data_renamed.txt"
+
+        self.hffs.cp_file(self.text_file, copied_file)
+        self.hffs.mv(copied_file, moved_file)
+        assert not self.hffs.exists(copied_file)
+        assert self.hffs.exists(moved_file)
+
+        self.hffs.rename(moved_file, renamed_file)
+        assert not self.hffs.exists(moved_file)
+        with self.hffs.open(renamed_file, "r") as f:
+            assert f.read() == "dummy text data"
+
+    def test_copy_between_buckets(self):
+        other_bucket_id = self.api.create_bucket(repo_name()).bucket_id
+        try:
+            self.hffs.copy(self.hf_path + "/data", 
f"buckets/{other_bucket_id}/data", recursive=True)
+            assert 
self.hffs.cat(f"buckets/{other_bucket_id}/data/text_data.txt") == b"dummy text 
data"
+            assert 
self.hffs.cat(f"buckets/{other_bucket_id}/data/binary_data.bin") == b"dummy 
binary data"
+        finally:
+            self.api.delete_bucket(other_bucket_id)
+
+    def test_copy_repo_to_bucket(self):
+        repo_id = self.api.create_repo(repo_name(), repo_type="model").repo_id
+        try:
+            self.api.upload_file(
+                path_or_fileobj=b"dummy text data",
+                path_in_repo="data/text_data.txt",
+                repo_id=repo_id,
+                repo_type="model",
+            )
+            self.hffs.cp_file(
+                f"models/{repo_id}/data/text_data.txt",
+                self.hf_path + "/data/text_data_from_repo.txt",
+            )
+            with self.hffs.open(self.hf_path + 
"/data/text_data_from_repo.txt", "r") as f:
+                assert f.read() == "dummy text data"
+        finally:
+            self.api.delete_repo(repo_id, repo_type="model")
+
+    def test_copy_bucket_to_repo_is_not_supported(self):
+        repo_id = self.api.create_repo(repo_name(), repo_type="model").repo_id
+        try:
+            with pytest.raises(NotImplementedError, match="buckets to 
repositories"):
+                self.hffs.cp_file(self.text_file, 
f"models/{repo_id}/data/text_data_copy.txt")
+        finally:
+            self.api.delete_repo(repo_id, repo_type="model")
 
 
 class TestHfFileSystemBucketLiveFollow:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/huggingface_hub-2.3.0/tests/test_cli.py 
new/huggingface_hub-2.4.0/tests/test_cli.py
--- old/huggingface_hub-2.3.0/tests/test_cli.py 2026-10-23 10:51:44.495729200 
+0200
+++ new/huggingface_hub-2.4.0/tests/test_cli.py 2026-10-29 16:29:41.047031000 
+0100
@@ -5683,7 +5683,7 @@
 
 BINARY_URL = "raw.githubusercontent.com/huggingface/hf-demo/HEAD/hf-demo"
 REPO_PAGE_URL = "https://github.com/huggingface/hf-demo";
-COMMITS_URL = "https://api.github.com/repos/huggingface/hf-demo/commits/HEAD";
+REFS_URL = "https://github.com/huggingface/hf-demo.git/info/refs";
 
 
 class TestExtensionsGitHubAccess:
@@ -5712,15 +5712,15 @@
         ],
     )
     @pytest.mark.skipif(os.name == "nt", reason="Shell-script extensions are 
not supported on Windows.")
-    def test_install_uses_head_refs_and_a_single_api_call(
+    def test_install_uses_head_refs_and_no_api_call(
         self, github: _FakeGitHubSession, about: str, expected_description: 
str | None
     ) -> None:
         # Shell-script extensions ship neither manifest.json nor 
pyproject.toml, so the repo's "About"
         # field is their only description. It is served by github.com, off the 
REST API quota.
         github.responses = {
             BINARY_URL: httpx2.Response(200, content=b"#!/bin/sh"),
+            REFS_URL: httpx2.Response(200, text=f"{'a' * 40} 
HEAD\x00symref=HEAD:refs/heads/main\n"),
             REPO_PAGE_URL: httpx2.Response(200, text=f'<meta 
name="description" content="{about}">'),
-            COMMITS_URL: httpx2.Response(200, text="a" * 40),
         }
         manifest = extensions._install_extension(owner="huggingface", 
repo_name="hf-demo", short_name="demo")
 
@@ -5728,18 +5728,18 @@
         assert manifest.commit_sha == "a" * 40
         assert manifest.description == expected_description
         # The default branch is addressed as `HEAD`, so no `GET 
/repos/{owner}/{repo}` lookup is needed.
-        assert github.api_urls == [COMMITS_URL]
+        assert github.api_urls == []
         raw_prefix = "https://raw.githubusercontent.com/huggingface/hf-demo/";
         raw_urls = [url for url in github.urls if url.startswith(raw_prefix)]
         assert raw_urls and 
all(url.removeprefix(raw_prefix).startswith("HEAD/") for url in raw_urls)
 
     @pytest.mark.skipif(os.name == "nt", reason="Shell-script extensions are 
not supported on Windows.")
-    def test_install_completes_when_the_api_quota_is_exhausted(self, github: 
_FakeGitHubSession) -> None:
+    def test_install_completes_when_the_commit_lookup_is_rate_limited(self, 
github: _FakeGitHubSession) -> None:
         # The extension itself comes from the CDN, so only the optional 
version marker is lost.
         github.responses = {
             BINARY_URL: httpx2.Response(200, content=b"#!/bin/sh"),
             "HEAD/manifest.json": httpx2.Response(200, json={"description": 
"Demo extension"}),
-            "api.github.com": httpx2.Response(403, 
headers={"x-ratelimit-remaining": "0"}),
+            REFS_URL: httpx2.Response(429, headers={"retry-after": "60"}),
         }
         manifest = extensions._install_extension(owner="huggingface", 
repo_name="hf-demo", short_name="demo")
 
@@ -5765,7 +5765,7 @@
         # A secondary limit: GitHub asks for a back-off and rides the 
*primary* window's reset
         # timestamp alongside it, on a quota that is not exhausted. The 
back-off is what applies.
         github.responses = {
-            "api.github.com": httpx2.Response(
+            ".git/info/refs": httpx2.Response(
                 429,
                 headers={"x-ratelimit-remaining": "53", "x-ratelimit-reset": 
"1786500000", "retry-after": "60"},
             )
@@ -5777,4 +5777,4 @@
         assert "Retry in 60s." in str(result.exception)
         assert "It resets at" not in str(result.exception)
         # Bailed out on the first extension rather than repeating the failure 
for every one of them.
-        assert len(github.api_urls) == 1
+        assert len(github.urls) == 1
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/huggingface_hub-2.3.0/tests/test_hf_file_system.py 
new/huggingface_hub-2.4.0/tests/test_hf_file_system.py
--- old/huggingface_hub-2.3.0/tests/test_hf_file_system.py      2026-10-23 
10:51:44.497729300 +0200
+++ new/huggingface_hub-2.4.0/tests/test_hf_file_system.py      2026-10-29 
16:29:41.048863200 +0100
@@ -311,6 +311,9 @@
         files = self.hffs.find(self.text_file, detail=False)
         assert files == [self.text_file]
 
+        files = self.hffs.find(self.text_file, detail=True)
+        assert files == {self.text_file: self.hffs.info(self.text_file)}
+
     def test_find_maxdepth(self):
         text_file_depth = self.text_file_path.count("/") + 1
         files = self.hffs.find(self.hf_path, detail=False, 
maxdepth=text_file_depth - 1)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/huggingface_hub-2.3.0/tests/test_oauth.py 
new/huggingface_hub-2.4.0/tests/test_oauth.py
--- old/huggingface_hub-2.3.0/tests/test_oauth.py       2026-10-23 
10:51:44.498729200 +0200
+++ new/huggingface_hub-2.4.0/tests/test_oauth.py       2026-10-29 
16:29:41.049863000 +0100
@@ -122,13 +122,14 @@
     assert data["msg"] == "Hello, hub.js!"
     assert data["oauth_info"]["access_token"] is not None
     assert data["oauth_info"]["scope"] == "openid profile email"
-    assert data["oauth_info"]["user_info"] == {
+    user_info = data["oauth_info"]["user_info"]
+    assert 
user_info.pop("picture").startswith("https://hub-ci.huggingface.co/avatars/";)
+    assert user_info == {
         "sub": "62f264b9f3c90f4b6514a269",
         "name": "@huggingface/hub CI bot",
         "preferred_username": "hub.js",
         "email_verified": True,
         "email": "[email protected]",
-        "picture": 
"https://hub-ci.huggingface.co/avatars/934b830e9fdaa879487852f79eef7165.svg";,
         "profile": "https://hub-ci.huggingface.co/hub.js";,
         "website": "https://github.com/huggingface/hub.js";,
         "is_pro": None,
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/huggingface_hub-2.3.0/tests/test_verification.py 
new/huggingface_hub-2.4.0/tests/test_verification.py
--- old/huggingface_hub-2.3.0/tests/test_verification.py        2026-10-23 
10:51:44.501729200 +0200
+++ new/huggingface_hub-2.4.0/tests/test_verification.py        2026-10-29 
16:29:41.052863100 +0100
@@ -33,6 +33,18 @@
     assert mapping["c.bin"].read_bytes() == b"y"
 
 
+def test_collect_local_files_skips_local_dir_metadata(tmp_path: Path) -> None:
+    # `hf download --local-dir` writes its metadata under 
`.cache/huggingface/`; it is never part of the repo
+    (tmp_path / "config.json").write_text("{}")
+    (tmp_path / ".cache" / "huggingface" / "download").mkdir(parents=True)
+    (tmp_path / ".cache" / "huggingface" / ".gitignore").write_text("*")
+    (tmp_path / ".cache" / "huggingface" / "CACHEDIR.TAG").write_text("tag")
+    (tmp_path / ".cache" / "huggingface" / "download" / 
"config.json.metadata").write_text("meta")
+    (tmp_path / ".cache" / "other.txt").write_text("kept")
+
+    assert sorted(collect_local_files(tmp_path)) == [".cache/other.txt", 
"config.json"]
+
+
 @pytest.mark.parametrize(
     "algorithm,data,expected_fn",
     [

Reply via email to