Skip to content

Commit d46c5fa

Browse files
committed
migrate CLI to typer
1 parent 3b562ca commit d46c5fa

File tree

14 files changed

+1988
-2266
lines changed

14 files changed

+1988
-2266
lines changed

setup.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ def get_version() -> str:
2828

2929
extras["cli"] = [
3030
"InquirerPy==0.3.4", # Note: installs `prompt-toolkit` in the background
31+
"typer",
3132
]
3233

3334
extras["inference"] = [

src/huggingface_hub/cli/__init__.py

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -11,17 +11,3 @@
1111
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
14-
15-
from abc import ABC, abstractmethod
16-
from argparse import _SubParsersAction
17-
18-
19-
class BaseHuggingfaceCLICommand(ABC):
20-
@staticmethod
21-
@abstractmethod
22-
def register_subcommand(parser: _SubParsersAction):
23-
raise NotImplementedError()
24-
25-
@abstractmethod
26-
def run(self):
27-
raise NotImplementedError()

src/huggingface_hub/cli/_cli_utils.py

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,12 @@
1111
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
14-
"""Contains a utility for good-looking prints."""
14+
"""Contains CLI utilities (styling, helpers)."""
1515

1616
import os
17-
from typing import Union
17+
from typing import Optional, Union
18+
19+
import typer
1820

1921

2022
class ANSI:
@@ -67,3 +69,18 @@ def tabulate(rows: list[list[Union[str, int]]], headers: list[str]) -> str:
6769
for row in rows:
6870
lines.append(row_format.format(*row))
6971
return "\n".join(lines)
72+
73+
74+
def validate_repo_type(value: Optional[str], param_name: str = "repo_type") -> Optional[str]:
75+
"""Validate repo type is one of model|dataset|space when provided.
76+
77+
Returns the value if valid or None. Raises a Typer BadParameter otherwise.
78+
"""
79+
if value is None:
80+
return None
81+
if value in ("model", "dataset", "space"):
82+
return value
83+
raise typer.BadParameter(
84+
"Invalid value for '--repo-type': must be one of 'model', 'dataset', 'space'.",
85+
param_name=param_name,
86+
)

src/huggingface_hub/cli/auth.py

Lines changed: 112 additions & 146 deletions
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,10 @@
3030
hf auth whoami
3131
"""
3232

33-
from argparse import _SubParsersAction
3433
from typing import Optional
3534

36-
from huggingface_hub.commands import BaseHuggingfaceCLICommand
35+
import typer
36+
3737
from huggingface_hub.constants import ENDPOINT
3838
from huggingface_hub.errors import HfHubHTTPError
3939
from huggingface_hub.hf_api import HfApi
@@ -54,125 +54,53 @@
5454
_inquirer_py_available = False
5555

5656

57-
class AuthCommands(BaseHuggingfaceCLICommand):
58-
@staticmethod
59-
def register_subcommand(parser: _SubParsersAction):
60-
# Create the main 'auth' command
61-
auth_parser = parser.add_parser("auth", help="Manage authentication (login, logout, etc.).")
62-
auth_subparsers = auth_parser.add_subparsers(help="Authentication subcommands")
63-
64-
# Show help if no subcommand is provided
65-
auth_parser.set_defaults(func=lambda args: auth_parser.print_help())
66-
67-
# Add 'login' as a subcommand of 'auth'
68-
login_parser = auth_subparsers.add_parser(
69-
"login", help="Log in using a token from huggingface.co/settings/tokens"
70-
)
71-
login_parser.add_argument(
72-
"--token",
73-
type=str,
74-
help="Token generated from https://huggingface.co/settings/tokens",
75-
)
76-
login_parser.add_argument(
77-
"--add-to-git-credential",
78-
action="store_true",
79-
help="Optional: Save token to git credential helper.",
80-
)
81-
login_parser.set_defaults(func=lambda args: AuthLogin(args))
82-
83-
# Add 'logout' as a subcommand of 'auth'
84-
logout_parser = auth_subparsers.add_parser("logout", help="Log out")
85-
logout_parser.add_argument(
86-
"--token-name",
87-
type=str,
88-
help="Optional: Name of the access token to log out from.",
89-
)
90-
logout_parser.set_defaults(func=lambda args: AuthLogout(args))
91-
92-
# Add 'whoami' as a subcommand of 'auth'
93-
whoami_parser = auth_subparsers.add_parser(
94-
"whoami", help="Find out which huggingface.co account you are logged in as."
95-
)
96-
whoami_parser.set_defaults(func=lambda args: AuthWhoami(args))
97-
98-
# Existing subcommands
99-
auth_switch_parser = auth_subparsers.add_parser("switch", help="Switch between access tokens")
100-
auth_switch_parser.add_argument(
101-
"--token-name",
102-
type=str,
103-
help="Optional: Name of the access token to switch to.",
104-
)
105-
auth_switch_parser.add_argument(
106-
"--add-to-git-credential",
107-
action="store_true",
108-
help="Optional: Save token to git credential helper.",
109-
)
110-
auth_switch_parser.set_defaults(func=lambda args: AuthSwitch(args))
111-
112-
auth_list_parser = auth_subparsers.add_parser("list", help="List all stored access tokens")
113-
auth_list_parser.set_defaults(func=lambda args: AuthList(args))
114-
115-
116-
class BaseAuthCommand:
117-
def __init__(self, args):
118-
self.args = args
119-
self._api = HfApi()
120-
121-
122-
class AuthLogin(BaseAuthCommand):
123-
def run(self):
124-
logging.set_verbosity_info()
125-
login(
126-
token=self.args.token,
127-
add_to_git_credential=self.args.add_to_git_credential,
128-
)
129-
130-
131-
class AuthLogout(BaseAuthCommand):
132-
def run(self):
133-
logging.set_verbosity_info()
134-
logout(token_name=self.args.token_name)
135-
136-
137-
class AuthSwitch(BaseAuthCommand):
138-
def run(self):
139-
logging.set_verbosity_info()
140-
token_name = self.args.token_name
141-
if token_name is None:
142-
token_name = self._select_token_name()
143-
144-
if token_name is None:
145-
print("No token name provided. Aborting.")
146-
exit()
147-
auth_switch(token_name, add_to_git_credential=self.args.add_to_git_credential)
148-
149-
def _select_token_name(self) -> Optional[str]:
150-
token_names = list(get_stored_tokens().keys())
151-
152-
if not token_names:
153-
logger.error("No stored tokens found. Please login first.")
154-
return None
57+
auth_app = typer.Typer(help="Manage authentication (login, logout, etc.)")
58+
59+
60+
def _api() -> HfApi:
61+
return HfApi()
62+
63+
64+
@auth_app.command("login", help="Login using a token from huggingface.co/settings/tokens")
65+
def auth_login(
66+
token: Optional[str] = typer.Option(
67+
None,
68+
"--token",
69+
help="Token from https://huggingface.co/settings/tokens",
70+
),
71+
add_to_git_credential: bool = typer.Option(
72+
False,
73+
"--add-to-git-credential",
74+
help="Save to git credential helper",
75+
),
76+
) -> None:
77+
logging.set_verbosity_info()
78+
login(token=token, add_to_git_credential=add_to_git_credential)
79+
80+
81+
@auth_app.command(
82+
"logout",
83+
help="Logout from a specific token",
84+
)
85+
def auth_logout(
86+
token_name: Optional[str] = typer.Option(
87+
None,
88+
"--token-name",
89+
help="Name of token to logout",
90+
),
91+
) -> None:
92+
logging.set_verbosity_info()
93+
logout(token_name=token_name)
15594

156-
if _inquirer_py_available:
157-
return self._select_token_name_tui(token_names)
158-
# if inquirer is not available, use a simpler terminal UI
159-
print("Available stored tokens:")
160-
for i, token_name in enumerate(token_names, 1):
161-
print(f"{i}. {token_name}")
162-
while True:
163-
try:
164-
choice = input("Enter the number of the token to switch to (or 'q' to quit): ")
165-
if choice.lower() == "q":
166-
return None
167-
index = int(choice) - 1
168-
if 0 <= index < len(token_names):
169-
return token_names[index]
170-
else:
171-
print("Invalid selection. Please try again.")
172-
except ValueError:
173-
print("Invalid input. Please enter a number or 'q' to quit.")
174-
175-
def _select_token_name_tui(self, token_names: list[str]) -> Optional[str]:
95+
96+
def _select_token_name() -> Optional[str]:
97+
token_names = list(get_stored_tokens().keys())
98+
99+
if not token_names:
100+
logger.error("No stored tokens found. Please login first.")
101+
return None
102+
103+
if _inquirer_py_available:
176104
choices = [Choice(token_name, name=token_name) for token_name in token_names]
177105
try:
178106
return inquirer.select(
@@ -183,30 +111,68 @@ def _select_token_name_tui(self, token_names: list[str]) -> Optional[str]:
183111
except KeyboardInterrupt:
184112
logger.info("Token selection cancelled.")
185113
return None
186-
187-
188-
class AuthList(BaseAuthCommand):
189-
def run(self):
190-
logging.set_verbosity_info()
191-
auth_list()
192-
193-
194-
class AuthWhoami(BaseAuthCommand):
195-
def run(self):
196-
token = get_token()
197-
if token is None:
198-
print("Not logged in")
199-
exit()
114+
# if inquirer is not available, use a simpler terminal UI
115+
print("Available stored tokens:")
116+
for i, token_name in enumerate(token_names, 1):
117+
print(f"{i}. {token_name}")
118+
while True:
200119
try:
201-
info = self._api.whoami(token)
202-
print(ANSI.bold("user: "), info["name"])
203-
orgs = [org["name"] for org in info["orgs"]]
204-
if orgs:
205-
print(ANSI.bold("orgs: "), ",".join(orgs))
206-
207-
if ENDPOINT != "https://huggingface.co":
208-
print(f"Authenticated through private endpoint: {ENDPOINT}")
209-
except HfHubHTTPError as e:
210-
print(e)
211-
print(ANSI.red(e.response.text))
212-
exit(1)
120+
choice = input("Enter the number of the token to switch to (or 'q' to quit): ")
121+
if choice.lower() == "q":
122+
return None
123+
index = int(choice) - 1
124+
if 0 <= index < len(token_names):
125+
return token_names[index]
126+
else:
127+
print("Invalid selection. Please try again.")
128+
except ValueError:
129+
print("Invalid input. Please enter a number or 'q' to quit.")
130+
131+
132+
@auth_app.command("switch", help="Switch between accesstokens")
133+
def auth_switch_cmd(
134+
token_name: Optional[str] = typer.Option(
135+
None,
136+
"--token-name",
137+
help="Name of the token to switch to",
138+
),
139+
add_to_git_credential: bool = typer.Option(
140+
False,
141+
"--add-to-git-credential",
142+
help="Save to git credential",
143+
),
144+
) -> None:
145+
logging.set_verbosity_info()
146+
if token_name is None:
147+
token_name = _select_token_name()
148+
if token_name is None:
149+
print("No token name provided. Aborting.")
150+
raise typer.Exit()
151+
auth_switch(token_name, add_to_git_credential=add_to_git_credential)
152+
153+
154+
@auth_app.command("list", help="List all stored access tokens")
155+
def auth_list_cmd() -> None:
156+
logging.set_verbosity_info()
157+
auth_list()
158+
159+
160+
@auth_app.command("whoami", help="Find out which huggingface.co account you are logged in as.")
161+
def auth_whoami() -> None:
162+
token = get_token()
163+
if token is None:
164+
print("Not logged in")
165+
raise typer.Exit()
166+
try:
167+
info = _api().whoami(token)
168+
print(ANSI.bold("user: "), info["name"])
169+
orgs = [org["name"] for org in info["orgs"]]
170+
if orgs:
171+
print(ANSI.bold("orgs: "), ",".join(orgs))
172+
173+
if ENDPOINT != "https://huggingface.co":
174+
print(f"Authenticated through private endpoint: {ENDPOINT}")
175+
except HfHubHTTPError as e:
176+
print(e)
177+
print(ANSI.red(e.response.text))
178+
raise typer.Exit(code=1)

0 commit comments

Comments
 (0)