Skip to content

Commit 9e7fbad

Browse files
Wauplinclaudehanouticelina
authored
[CLI] Accept spaces/user/repo as repo ID prefix shorthand (#3929)
* [CLI] Accept `spaces/user/repo` as repo ID prefix shorthand (global) Rewrite prefixed repo IDs (e.g. `spaces/user/repo` → `user/repo --type space`) at the HFCliTyperGroup level so it works automatically for all commands that accept `--type`/`--repo-type`, without per-command changes. Follows the same pattern as the `--json` → `--format json` rewrite from #3919. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * style * reuse constants.REPO_TYPES_MAPPING * only repo_id is checked * handle from_id and to_id as well * make style --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: célina <hanouticelina@gmail.com>
1 parent da46c21 commit 9e7fbad

3 files changed

Lines changed: 269 additions & 40 deletions

File tree

docs/source/en/package_reference/cli.md

Lines changed: 25 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -640,7 +640,7 @@ $ hf cache verify [OPTIONS] REPO_ID
640640

641641
**Arguments**:
642642

643-
* `REPO_ID`: The ID of the repo (e.g. `username/repo-name`). [required]
643+
* `REPO_ID`: The ID of the repo (e.g. `username/repo-name` or `spaces/username/repo-name`). [required]
644644

645645
**Options**:
646646

@@ -1112,7 +1112,7 @@ $ hf discussions close [OPTIONS] REPO_ID NUM
11121112

11131113
**Arguments**:
11141114

1115-
* `REPO_ID`: The ID of the repo (e.g. `username/repo-name`). [required]
1115+
* `REPO_ID`: The ID of the repo (e.g. `username/repo-name` or `spaces/username/repo-name`). [required]
11161116
* `NUM`: The discussion or pull request number. [required]
11171117

11181118
**Options**:
@@ -1144,7 +1144,7 @@ $ hf discussions comment [OPTIONS] REPO_ID NUM
11441144

11451145
**Arguments**:
11461146

1147-
* `REPO_ID`: The ID of the repo (e.g. `username/repo-name`). [required]
1147+
* `REPO_ID`: The ID of the repo (e.g. `username/repo-name` or `spaces/username/repo-name`). [required]
11481148
* `NUM`: The discussion or pull request number. [required]
11491149

11501150
**Options**:
@@ -1176,7 +1176,7 @@ $ hf discussions create [OPTIONS] REPO_ID
11761176

11771177
**Arguments**:
11781178

1179-
* `REPO_ID`: The ID of the repo (e.g. `username/repo-name`). [required]
1179+
* `REPO_ID`: The ID of the repo (e.g. `username/repo-name` or `spaces/username/repo-name`). [required]
11801180

11811181
**Options**:
11821182

@@ -1211,7 +1211,7 @@ $ hf discussions diff [OPTIONS] REPO_ID NUM
12111211

12121212
**Arguments**:
12131213

1214-
* `REPO_ID`: The ID of the repo (e.g. `username/repo-name`). [required]
1214+
* `REPO_ID`: The ID of the repo (e.g. `username/repo-name` or `spaces/username/repo-name`). [required]
12151215
* `NUM`: The discussion or pull request number. [required]
12161216

12171217
**Options**:
@@ -1240,7 +1240,7 @@ $ hf discussions info [OPTIONS] REPO_ID NUM
12401240

12411241
**Arguments**:
12421242

1243-
* `REPO_ID`: The ID of the repo (e.g. `username/repo-name`). [required]
1243+
* `REPO_ID`: The ID of the repo (e.g. `username/repo-name` or `spaces/username/repo-name`). [required]
12441244
* `NUM`: The discussion or pull request number. [required]
12451245

12461246
**Options**:
@@ -1276,7 +1276,7 @@ $ hf discussions list [OPTIONS] REPO_ID
12761276

12771277
**Arguments**:
12781278

1279-
* `REPO_ID`: The ID of the repo (e.g. `username/repo-name`). [required]
1279+
* `REPO_ID`: The ID of the repo (e.g. `username/repo-name` or `spaces/username/repo-name`). [required]
12801280

12811281
**Options**:
12821282

@@ -1313,7 +1313,7 @@ $ hf discussions merge [OPTIONS] REPO_ID NUM
13131313

13141314
**Arguments**:
13151315

1316-
* `REPO_ID`: The ID of the repo (e.g. `username/repo-name`). [required]
1316+
* `REPO_ID`: The ID of the repo (e.g. `username/repo-name` or `spaces/username/repo-name`). [required]
13171317
* `NUM`: The discussion or pull request number. [required]
13181318

13191319
**Options**:
@@ -1345,7 +1345,7 @@ $ hf discussions rename [OPTIONS] REPO_ID NUM NEW_TITLE
13451345

13461346
**Arguments**:
13471347

1348-
* `REPO_ID`: The ID of the repo (e.g. `username/repo-name`). [required]
1348+
* `REPO_ID`: The ID of the repo (e.g. `username/repo-name` or `spaces/username/repo-name`). [required]
13491349
* `NUM`: The discussion or pull request number. [required]
13501350
* `NEW_TITLE`: The new title. [required]
13511351

@@ -1375,7 +1375,7 @@ $ hf discussions reopen [OPTIONS] REPO_ID NUM
13751375

13761376
**Arguments**:
13771377

1378-
* `REPO_ID`: The ID of the repo (e.g. `username/repo-name`). [required]
1378+
* `REPO_ID`: The ID of the repo (e.g. `username/repo-name` or `spaces/username/repo-name`). [required]
13791379
* `NUM`: The discussion or pull request number. [required]
13801380

13811381
**Options**:
@@ -1407,7 +1407,7 @@ $ hf download [OPTIONS] REPO_ID [FILENAMES]...
14071407

14081408
**Arguments**:
14091409

1410-
* `REPO_ID`: The ID of the repo (e.g. `username/repo-name`). [required]
1410+
* `REPO_ID`: The ID of the repo (e.g. `username/repo-name` or `spaces/username/repo-name`). [required]
14111411
* `[FILENAMES]...`: Files to download (e.g. `config.json`, `data/metadata.jsonl`).
14121412

14131413
**Options**:
@@ -2848,7 +2848,7 @@ $ hf repos branch create [OPTIONS] REPO_ID BRANCH
28482848

28492849
**Arguments**:
28502850

2851-
* `REPO_ID`: The ID of the repo (e.g. `username/repo-name`). [required]
2851+
* `REPO_ID`: The ID of the repo (e.g. `username/repo-name` or `spaces/username/repo-name`). [required]
28522852
* `BRANCH`: The name of the branch to create. [required]
28532853

28542854
**Options**:
@@ -2880,7 +2880,7 @@ $ hf repos branch delete [OPTIONS] REPO_ID BRANCH
28802880

28812881
**Arguments**:
28822882

2883-
* `REPO_ID`: The ID of the repo (e.g. `username/repo-name`). [required]
2883+
* `REPO_ID`: The ID of the repo (e.g. `username/repo-name` or `spaces/username/repo-name`). [required]
28842884
* `BRANCH`: The name of the branch to delete. [required]
28852885

28862886
**Options**:
@@ -2909,7 +2909,7 @@ $ hf repos create [OPTIONS] REPO_ID
29092909

29102910
**Arguments**:
29112911

2912-
* `REPO_ID`: The ID of the repo (e.g. `username/repo-name`). [required]
2912+
* `REPO_ID`: The ID of the repo (e.g. `username/repo-name` or `spaces/username/repo-name`). [required]
29132913

29142914
**Options**:
29152915

@@ -2952,7 +2952,7 @@ $ hf repos delete [OPTIONS] REPO_ID
29522952

29532953
**Arguments**:
29542954

2955-
* `REPO_ID`: The ID of the repo (e.g. `username/repo-name`). [required]
2955+
* `REPO_ID`: The ID of the repo (e.g. `username/repo-name` or `spaces/username/repo-name`). [required]
29562956

29572957
**Options**:
29582958

@@ -2981,7 +2981,7 @@ $ hf repos delete-files [OPTIONS] REPO_ID PATTERNS...
29812981

29822982
**Arguments**:
29832983

2984-
* `REPO_ID`: The ID of the repo (e.g. `username/repo-name`). [required]
2984+
* `REPO_ID`: The ID of the repo (e.g. `username/repo-name` or `spaces/username/repo-name`). [required]
29852985
* `PATTERNS...`: Glob patterns to match files to delete. Based on fnmatch, '*' matches files recursively. [required]
29862986

29872987
**Options**:
@@ -3016,7 +3016,7 @@ $ hf repos duplicate [OPTIONS] FROM_ID [TO_ID]
30163016

30173017
**Arguments**:
30183018

3019-
* `FROM_ID`: The ID of the repo (e.g. `username/repo-name`). [required]
3019+
* `FROM_ID`: The ID of the repo (e.g. `username/repo-name` or `spaces/username/repo-name`). [required]
30203020
* `[TO_ID]`: Destination repo ID (e.g. `myorg/my-copy`). Defaults to your namespace with the same repo name.
30213021

30223022
**Options**:
@@ -3057,8 +3057,8 @@ $ hf repos move [OPTIONS] FROM_ID TO_ID
30573057

30583058
**Arguments**:
30593059

3060-
* `FROM_ID`: The ID of the repo (e.g. `username/repo-name`). [required]
3061-
* `TO_ID`: The ID of the repo (e.g. `username/repo-name`). [required]
3060+
* `FROM_ID`: The ID of the repo (e.g. `username/repo-name` or `spaces/username/repo-name`). [required]
3061+
* `TO_ID`: The ID of the repo (e.g. `username/repo-name` or `spaces/username/repo-name`). [required]
30623062

30633063
**Options**:
30643064

@@ -3086,7 +3086,7 @@ $ hf repos settings [OPTIONS] REPO_ID
30863086

30873087
**Arguments**:
30883088

3089-
* `REPO_ID`: The ID of the repo (e.g. `username/repo-name`). [required]
3089+
* `REPO_ID`: The ID of the repo (e.g. `username/repo-name` or `spaces/username/repo-name`). [required]
30903090

30913091
**Options**:
30923092

@@ -3140,7 +3140,7 @@ $ hf repos tag create [OPTIONS] REPO_ID TAG
31403140

31413141
**Arguments**:
31423142

3143-
* `REPO_ID`: The ID of the repo (e.g. `username/repo-name`). [required]
3143+
* `REPO_ID`: The ID of the repo (e.g. `username/repo-name` or `spaces/username/repo-name`). [required]
31443144
* `TAG`: The name of the tag to create. [required]
31453145

31463146
**Options**:
@@ -3172,7 +3172,7 @@ $ hf repos tag delete [OPTIONS] REPO_ID TAG
31723172

31733173
**Arguments**:
31743174

3175-
* `REPO_ID`: The ID of the repo (e.g. `username/repo-name`). [required]
3175+
* `REPO_ID`: The ID of the repo (e.g. `username/repo-name` or `spaces/username/repo-name`). [required]
31763176
* `TAG`: The name of the tag to delete. [required]
31773177

31783178
**Options**:
@@ -3202,7 +3202,7 @@ $ hf repos tag list [OPTIONS] REPO_ID
32023202

32033203
**Arguments**:
32043204

3205-
* `REPO_ID`: The ID of the repo (e.g. `username/repo-name`). [required]
3205+
* `REPO_ID`: The ID of the repo (e.g. `username/repo-name` or `spaces/username/repo-name`). [required]
32063206

32073207
**Options**:
32083208

@@ -3494,7 +3494,7 @@ $ hf upload [OPTIONS] REPO_ID [LOCAL_PATH] [PATH_IN_REPO]
34943494

34953495
**Arguments**:
34963496

3497-
* `REPO_ID`: The ID of the repo (e.g. `username/repo-name`). [required]
3497+
* `REPO_ID`: The ID of the repo (e.g. `username/repo-name` or `spaces/username/repo-name`). [required]
34983498
* `[LOCAL_PATH]`: Local path to the file or folder to upload. Wildcard patterns are supported. Defaults to current directory.
34993499
* `[PATH_IN_REPO]`: Path of the file or folder in the repo. Defaults to the relative path of the file or folder.
35003500

@@ -3538,7 +3538,7 @@ $ hf upload-large-folder [OPTIONS] REPO_ID LOCAL_PATH
35383538

35393539
**Arguments**:
35403540

3541-
* `REPO_ID`: The ID of the repo (e.g. `username/repo-name`). [required]
3541+
* `REPO_ID`: The ID of the repo (e.g. `username/repo-name` or `spaces/username/repo-name`). [required]
35423542
* `LOCAL_PATH`: Local path to the folder to upload. [required]
35433543

35443544
**Options**:

src/huggingface_hub/cli/_cli_utils.py

Lines changed: 123 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -98,25 +98,133 @@ class HFCliTyperGroup(typer.core.TyperGroup):
9898
- formats epilog without extra indentation.
9999
- supports aliases via pipe-separated names (e.g. ``name="list | ls"``).
100100
- rewrites ``--json`` to ``--format json`` for commands that accept ``--format``.
101+
- rewrites ``spaces/user/repo`` to ``user/repo --type space`` for commands that accept ``--type``.
101102
"""
102103

103104
def resolve_command(self, ctx: click.Context, args: list[str]) -> tuple:
104-
# Rewrite hidden --json shorthand to --format json, but only for commands that accept --format.
105-
# This avoids rewriting `--json` for commands that pass args through to external binaries
106-
# (e.g. `hf extensions exec`) or that simply don't support `--format`.
107-
if "--json" in args:
108-
cmd_name = args[0] if args and not args[0].startswith("-") else None
109-
cmd = self.get_command(ctx, cmd_name) if cmd_name else None
110-
has_format_option = cmd is not None and any(
111-
isinstance(param, click.Option) and "--format" in param.opts for param in cmd.params
112-
)
113-
if has_format_option:
114-
if any(arg == "--format" or arg.startswith("--format=") for arg in args):
115-
raise click.UsageError("'--json' and '--format' are mutually exclusive.")
116-
idx = args.index("--json")
117-
args[idx : idx + 1] = ["--format", "json"]
105+
cmd_name = args[0] if args and not args[0].startswith("-") else None
106+
cmd = self.get_command(ctx, cmd_name) if cmd_name else None
107+
108+
if cmd is not None:
109+
self._rewrite_json_shorthand(cmd, args)
110+
self._rewrite_repo_type_prefix(cmd, args)
111+
118112
return super().resolve_command(ctx, args)
119113

114+
@staticmethod
115+
def _rewrite_json_shorthand(cmd: click.Command, args: list[str]) -> None:
116+
"""Rewrite hidden ``--json`` shorthand to ``--format json``.
117+
118+
Only applies to commands that accept ``--format``. This avoids rewriting
119+
``--json`` for commands that pass args through to external binaries
120+
(e.g. ``hf extensions exec``) or that simply don't support ``--format``.
121+
"""
122+
if "--json" not in args:
123+
return
124+
has_format_option = any(isinstance(param, click.Option) and "--format" in param.opts for param in cmd.params)
125+
if has_format_option:
126+
if any(arg == "--format" or arg.startswith("--format=") for arg in args):
127+
raise click.UsageError("'--json' and '--format' are mutually exclusive.")
128+
idx = args.index("--json")
129+
args[idx : idx + 1] = ["--format", "json"]
130+
131+
@staticmethod
132+
def _rewrite_repo_type_prefix(cmd: click.Command, args: list[str]) -> None:
133+
"""Rewrite prefixed repo IDs (e.g. ``spaces/user/repo``) to ``user/repo --type space``.
134+
135+
Only applies to commands that have a ``--type`` / ``--repo-type`` option and
136+
at least one repo-ID positional argument (any ``click.Argument`` whose name
137+
ends with ``_id``, e.g. ``repo_id``, ``from_id``, ``to_id``). When the
138+
token that maps to such an argument matches ``{prefix}/org/repo`` (where
139+
*prefix* is one of ``spaces``, ``datasets``, or ``models``), the prefix is
140+
stripped and an implicit ``--type {type}`` is appended. An error is raised
141+
if ``--type`` is also provided explicitly or if multiple prefixed arguments
142+
disagree on the repo type.
143+
144+
Only repo-ID positional slots are inspected so that other positional
145+
arguments (filenames, local paths, patterns …) are never misinterpreted as
146+
prefixed repo IDs.
147+
"""
148+
has_type_option = any(isinstance(param, click.Option) and "--type" in param.opts for param in cmd.params)
149+
if not has_type_option:
150+
return
151+
152+
# Locate all repo-ID positional arguments and their indices among Arguments.
153+
repo_id_positions: set[int] = set()
154+
arg_idx = 0
155+
for param in cmd.params:
156+
if isinstance(param, click.Argument):
157+
if param.name in ("repo_id", "from_id", "to_id"):
158+
repo_id_positions.add(arg_idx)
159+
arg_idx += 1
160+
161+
if not repo_id_positions:
162+
return
163+
164+
# Build a set of option names that consume a following value token.
165+
value_options: set[str] = set()
166+
for param in cmd.params:
167+
if isinstance(param, click.Option) and not param.is_flag:
168+
for opt in (*param.opts, *param.secondary_opts):
169+
value_options.add(opt)
170+
171+
# Walk through args (skipping args[0] = command name) to map positional
172+
# slots to their indices in `args`.
173+
positional_count = 0
174+
repo_id_arg_indices: list[int] = []
175+
i = 1
176+
while i < len(args):
177+
arg = args[i]
178+
if arg == "--":
179+
break # everything after -- is positional literal; stop rewriting
180+
if arg.startswith("-"):
181+
if "=" in arg or arg not in value_options:
182+
i += 1 # flag or --opt=val — single token
183+
else:
184+
i += 2 # value-taking option — skip the value too
185+
else:
186+
if positional_count in repo_id_positions:
187+
repo_id_arg_indices.append(i)
188+
positional_count += 1
189+
i += 1
190+
191+
if not repo_id_arg_indices:
192+
return
193+
194+
# Check each repo-ID arg for a type prefix and collect rewrites.
195+
inferred_type: Optional[str] = None
196+
first_prefix: Optional[str] = None
197+
rewrites: list[tuple[int, str]] = [] # (args index, new value without prefix)
198+
199+
for arg_index in repo_id_arg_indices:
200+
parts = args[arg_index].split("/", 2)
201+
if len(parts) != 3 or parts[0] not in constants.REPO_TYPES_MAPPING:
202+
continue
203+
prefix = parts[0]
204+
mapped_type = constants.REPO_TYPES_MAPPING[prefix]
205+
if inferred_type is not None and mapped_type != inferred_type:
206+
raise click.UsageError(f"Conflicting repo type prefixes: '{first_prefix}/' and '{prefix}/'.")
207+
inferred_type = mapped_type
208+
first_prefix = prefix
209+
rewrites.append((arg_index, f"{parts[1]}/{parts[2]}"))
210+
211+
if not rewrites:
212+
return
213+
214+
# Error if --type / --repo-type was also provided explicitly.
215+
if any(
216+
arg == "--type" or arg.startswith("--type=") or arg == "--repo-type" or arg.startswith("--repo-type=")
217+
for arg in args
218+
):
219+
raise click.UsageError(
220+
f"Ambiguous repo type: got prefix '{first_prefix}/' in repo ID and explicit --type. Use one or the other."
221+
)
222+
223+
# Apply all rewrites and append --type once.
224+
for arg_index, new_value in rewrites:
225+
args[arg_index] = new_value
226+
args.extend(["--type", inferred_type]) # type: ignore[list-item]
227+
120228
def get_command(self, ctx: click.Context, cmd_name: str) -> Optional[click.Command]:
121229
# Try exact match first
122230
cmd = super().get_command(ctx, cmd_name)
@@ -309,7 +417,7 @@ class RepoType(str, Enum):
309417
RepoIdArg = Annotated[
310418
str,
311419
typer.Argument(
312-
help="The ID of the repo (e.g. `username/repo-name`).",
420+
help="The ID of the repo (e.g. `username/repo-name` or `spaces/username/repo-name`).",
313421
),
314422
]
315423

0 commit comments

Comments
 (0)