Skip to content

Warn when a restricted Hugging Face download resolves no HUGGING_FACE_TOKEN - #540

Merged
MaxGhenis merged 5 commits into
masterfrom
fix/hf-no-token-warning
Sep 30, 2026
Merged

MaxGhenis merged 5 commits into
masterfrom
fix/hf-no-token-warning

Conversation

@MaxGhenis

@MaxGhenis MaxGhenis commented Sep 3, 2026 •

Copy link
Copy Markdown
Contributor

Follow-up to #529 (fixed by #538). Related: #553.

Summary

download_huggingface_dataset now emits a UserWarning when the repo requires authentication and get_or_prompt_hf_token() returned None. Since #538, a repo requires authentication when model_info reports it private or gated, or raises RepositoryNotFoundError. The warning names the repo, says that no HUGGING_FACE_TOKEN was available, and says that huggingface_hub normally falls back to its own cached token (HF_TOKEN, or the file written by hf auth login, which is huggingface-cli login before huggingface_hub 0.34). It then explains the failure that can follow: RepositoryNotFoundError or GatedRepoError, as a 401 when no token was sent or a 403 when a gated repo has not approved the token.

Behaviour is otherwise unchanged, deliberately (per #422). token=None is still passed to hf_hub_download, there is no raise and no extra prompt, and core does not read HF_TOKEN. The fallback belongs to huggingface_hub: in 1.4.1, utils/_headers.py::get_token_to_send calls get_token() when token is None (unless HF_HUB_DISABLE_IMPLICIT_TOKEN is set), and utils/_http.py::hf_raise_for_status maps X-Error-Code: GatedRepo to GatedRepoError and a 401 on a resolve URL to RepositoryNotFoundError.

The warning uses stacklevel=2, so it points at the caller (for example Dataset.download_from_huggingface). It is not emitted for public, ungated repos, which also get token=None. It still fires for users whose only token is HF_TOKEN or the login file, even though their download may succeed; #553 tracks skipping it (and the prompt) in that case.

This branch is rebased onto current master (4fb64c9, 3.32.10), so it sits on top of #538 (released in 3.32.9) and #539 (3.32.7). The rebase was clean, and git range-diff shows the three original commits unchanged.

Invariants

TestTokenRoutingInvariants runs every combination of 8 repo states × 4 environments × TTY or not × empty or non-empty prompt entry (128 cases) through the real function, with model_info, hf_hub_download and getpass mocked. It compares each result with a spec written out by hand in _expected(), which lists the states that need a token rather than recomputing the predicate:

  • Only gated (auto or manual), private, private-and-gated and not-found repos require authentication.
  • Such a repo gets HUGGING_FACE_TOKEN if it is non-empty; otherwise, on a TTY, whatever is entered at a single prompt; otherwise None.
  • A public, ungated repo always gets None and is never prompted for.
  • The token passed on is None or a non-empty string, never "".
  • Exactly one no-token warning fires when a repo that requires authentication ends up with None, and none fires otherwise.

Tests

  • TestNoTokenWarning:

    • The warning fires for private-flag, gated and not-found lookups when the token is unset, empty, or only HF_TOKEN is set, non-interactively, and when an interactive prompt is left empty.
    • In each of those cases, token=None is passed on and there is no extra prompt.
    • There is no warning for public repos, or when a token came from the environment or a prompt.
    • The message content and stacklevel are asserted. The message must name the repo, both fallbacks (HF_TOKEN, hf auth login and the pre-0.34 huggingface-cli login), RepositoryNotFoundError, GatedRepoError, the 401 and the 403.
  • test_download_private_repo_no_token (from Fix vacuous test for private Hugging Face repo download without a token #539) and test_download_gated_repo_non_interactive_without_token (from Send HUGGING_FACE_TOKEN for public but gated Hugging Face repos #538) now wrap the call in pytest.warns(UserWarning, match="no HUGGING_FACE_TOKEN"). Their existing assertions are unchanged.

  • TestTokenRoutingInvariants, described above.

  • pytest tests/core/tools/test_hugging_face.py on 1de9bc6 gives 167 passed, in the project .venv (CPython 3.14; pytest 9.1.1 and huggingface_hub 1.4.1, the versions in uv.lock): 39 other tests plus the 128-case grid.

  • Mutation checks on 1de9bc6, where each mutant is applied to hugging_face.py alone and the file is then restored:

    Mutant Failures (of 167)
    predicate back to bool(private) alone 39 (both gated states × 16 in the grid, plus 7 other tests)
    warning removed 59 (45 grid + 14 other)
    get_or_prompt_hf_token() or "" 59
    RepositoryNotFoundError treated as public 24 (16 + 8)
    bool(private) != bool(gated) (xor) 16 (the private-gated state; this mutant passed every test before that state was added)
    getpass called twice 31 (30 + 1; 1 before the grid counted calls)
    warning skipped when stdin is a TTY 16
    stacklevel=1 9
    403 clause, GatedRepoError, or the huggingface-cli login caveat deleted from the message (each alone) 9 each (0 before 1de9bc6)
    prompted token not stored in HUGGING_FACE_TOKEN 2
    if not authentication_token: instead of is None 0: equivalent, since get_or_prompt_hf_token() returns None or a non-empty string

    The PR's earlier independent review also checked mutants for "always requires authentication", "warn even with a token", "TTY ignored", "environment token ignored" and several more; each failed in the grid.

  • uvx ruff format --check . and uvx ruff check on the changed files are clean.

  • Not run: make test (the full suite with coverage and reruns) and make documentation. CI covers the tests on Ubuntu and Windows for Python 3.11–3.14, and no docs pages change.

Documentation review

  • Documentation changes: a Warns: section in the download_huggingface_dataset docstring, and the towncrier fragment changelog.d/warn-hf-no-token.changed.md (changed, since this adds a diagnostic and does not change control flow). No docs/ page or README describes this function's token handling, so nothing else needed updating.
  • Impact: low. This adds a warning; control flow is unchanged, which the token=None and getpass assertions and the grid pin down.
  • Confidence: high for the huggingface_hub mechanics. They were read in the installed 1.4.1 source, and this PR's earlier review also checked them against wheels from 0.25.1 to 0.34.0 and saw them in live Hub responses. Medium for the "403 when a gated repo has not approved the token" case, which comes from huggingface_hub's own GatedRepoError docstring and was not reproduced with a real unapproved token.
  • Known gaps: Skip the Hugging Face token prompt and no-token warning when huggingface_hub already has a cached token #553 (the warning fires for users whose HF_TOKEN or login-file token will work).

axiom: n/a: core infrastructure (Hugging Face download diagnostics), no policy change.

🤖 Generated with Claude Code

@MaxGhenis
MaxGhenis force-pushed the fix/hf-no-token-warning branch from 0572d7a to f5992fd Compare September 29, 2026 14:15
MaxGhenis added a commit that referenced this pull request Sep 29, 2026
…vely

Fold in the non-blocking findings from the independent review of #540:

- Say a 401 means no token was sent and a 403 means a gated repo has not
  approved the token; huggingface_hub raises GatedRepoError for both and
  documents the unapproved case as 403.
- Say the fallback to huggingface_hub's cached token is what normally
  happens: HF_HUB_DISABLE_IMPLICIT_TOKEN turns it off.
- Name `huggingface-cli login` as well as `hf auth login`: the `hf`
  command first ships in huggingface_hub 0.34, and pyproject still
  allows 0.25.1.
- Assert the warning in the two older no-token tests that now emit it,
  and give the ungated never-prompts test's getpass mock a string return
  value so a widened predicate fails on the assertion, not on os.environ.
- Reword the changelog fragment: after #538 the repo may be private or
  gated, and a cached token that is not approved gives a 403.

Add TestTokenRoutingInvariants, which runs every combination of repo
state (ungated, gated None, fields missing, gated auto, gated manual,
private, not found), environment (token unset, empty, only HF_TOKEN,
set), TTY and prompt entry (112 cases) through the real function and
checks it against a spec written out in the test: the token passed on,
whether getpass is called, that the token is never "", and that exactly
one warning fires when a repo requiring authentication ends up with no
token.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@MaxGhenis
MaxGhenis force-pushed the fix/hf-no-token-warning branch from f5992fd to ded7983 Compare September 29, 2026 17:35
MaxGhenis and others added 4 commits September 29, 2026 19:18
download_huggingface_dataset keeps passing token=None through to
hf_hub_download when a repo that requires authentication (private, or
gated since #538) yields no HUGGING_FACE_TOKEN (per #422: huggingface_hub
then applies its own cached token, so `hf auth login` users keep
working), but now emits a UserWarning first so that the bare 401
huggingface_hub raises when that fallback is empty too can be traced to
the missing or unapproved token (#529).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
With #538, a public but gated repo (private=False, gated="manual", as
policyengine/policyengine-uk-data-private is) also requires
authentication, so it takes the same path that now warns when no
HUGGING_FACE_TOKEN resolves. Add a "gated" lookup to TestNoTokenWarning:
the three non-interactive no-token environments must warn, and an
environment token must reach hf_hub_download without a warning.

With the private-only predicate restored, the four new gated cases fail;
with the warning removed, all ten warning-asserting cases fail.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…vely

Fold in the non-blocking findings from the independent review of #540:

- Say a 401 means no token was sent and a 403 means a gated repo has not
  approved the token; huggingface_hub raises GatedRepoError for both and
  documents the unapproved case as 403.
- Say the fallback to huggingface_hub's cached token is what normally
  happens: HF_HUB_DISABLE_IMPLICIT_TOKEN turns it off.
- Name `huggingface-cli login` as well as `hf auth login`: the `hf`
  command first ships in huggingface_hub 0.34, and pyproject still
  allows 0.25.1.
- Assert the warning in the two older no-token tests that now emit it,
  and give the ungated never-prompts test's getpass mock a string return
  value so a widened predicate fails on the assertion, not on os.environ.
- Reword the changelog fragment: after #538 the repo may be private or
  gated, and a cached token that is not approved gives a 403.

Add TestTokenRoutingInvariants, which runs every combination of repo
state (ungated, gated None, fields missing, gated auto, gated manual,
private, not found), environment (token unset, empty, only HF_TOKEN,
set), TTY and prompt entry (112 cases) through the real function and
checks it against a spec written out in the test: the token passed on,
whether getpass is called, that the token is never "", and that exactly
one warning fires when a repo requiring authentication ends up with no
token.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Add a "private-gated" repo state (private=True, gated="manual") to
TestTokenRoutingInvariants, so a predicate that is true for private or
gated but false for both (for example an xor) now fails 16 cases instead
of passing all of them. Assert getpass's call_count rather than whether it
was called, so a double prompt fails 30 grid cases, not one side test.
The grid grows from 112 to 128 cases.

Also give the Warns: docstring the same `huggingface-cli login` caveat
the runtime warning already carries.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@MaxGhenis
MaxGhenis force-pushed the fix/hf-no-token-warning branch from ded7983 to 7b8e76c Compare September 30, 2026 00:01
…text

The non-interactive warning test now also asserts that the message names
RepositoryNotFoundError, GatedRepoError, the 403 case and the pre-0.34
`huggingface-cli login` command. Each of those clauses could previously be
deleted without failing a test; each deletion now fails 9 of 167. The
TestNoTokenWarning docstring no longer calls the 401 "missing or
unapproved": with an empty fallback there is no token to be unapproved.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@MaxGhenis

Copy link
Copy Markdown
Contributor Author

Merge audit for head 1de9bc6. This is the round-4 review, after the rebase onto 4fb64c9 (3.32.10).

Independent review. An Opus 5.5 peer (subfleet job 20260929-220116-review-core540, review/standard) returned APPROVE. It had no blocking findings. It worked in a read-only sandbox, so it read the code and could not execute anything. Its non-blocking notes:

Main-session checks on this head:

  • pytest tests/core/tools/test_hugging_face.py: 167 passed (CPython 3.14.7, huggingface_hub 1.4.1 from uv.lock).

  • ruff format --check and ruff check are clean on both changed files.

  • 11 mutants were applied to hugging_face.py one at a time, and every one reproduces the PR body's failure count:

    Mutant Failures
    private-only predicate 39
    xor predicate 16
    warning removed 59
    or "" 59
    not-found treated as public 24
    stacklevel=1 9
    skip warning on a TTY 16
    403 clause removed 9
    GatedRepoError removed from the text 9
    getpass called twice 31
    prompted token not stored in the environment 2
  • The mechanism claims were read in the installed huggingface_hub 1.4.1 source:

    • utils/_headers.py::get_token_to_send returns get_token() for token=None unless HF_HUB_DISABLE_IMPLICIT_TOKEN is set.
    • utils/_http.py maps X-Error-Code: GatedRepo to GatedRepoError, and a 401 on a resolve URL to RepositoryNotFoundError, except for "Invalid credentials".
    • errors.py defines GatedRepoError(RepositoryNotFoundError).
  • Downstream: none of policyengine-uk, policyengine-us or policyengine.py runs pytest with warnings-as-errors. policyengine.py's filterwarnings only ignores PydanticDeprecatedSince20. So this UserWarning can't fail their CI.

Gates:

  • gh pr checks 540 exits 0: all 18 checks pass, including Windows and the country-package jobs.
  • The PR is MERGEABLE and not a draft.
  • It has no CHANGES_REQUESTED review.

Squash-merged with --match-head-commit 1de9bc61e7ff64282670c2984f16d47505c1029a, per d051, which Max ruled on 2026-09-29.

@MaxGhenis
MaxGhenis merged commit 6a05134 into master Sep 30, 2026
18 checks passed
@MaxGhenis
MaxGhenis deleted the fix/hf-no-token-warning branch September 30, 2026 02:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant