From 792b871aadceedc30b6428dbe3ac4c600ef5fd55 Mon Sep 17 00:00:00 2001 From: burtenshaw Date: Thu, 25 Jun 2026 09:03:18 +0200 Subject: [PATCH] refactor CLI Hugging Face username helper --- src/openenv/cli/_cli_utils.py | 16 ++++++++++++++++ src/openenv/cli/commands/fork.py | 30 +++--------------------------- src/openenv/cli/commands/push.py | 18 +----------------- tests/test_cli/test_fork.py | 22 ++++++++++++++++++++++ 4 files changed, 42 insertions(+), 44 deletions(-) diff --git a/src/openenv/cli/_cli_utils.py b/src/openenv/cli/_cli_utils.py index c0f8a3aef..810e63e01 100644 --- a/src/openenv/cli/_cli_utils.py +++ b/src/openenv/cli/_cli_utils.py @@ -15,6 +15,22 @@ console = Console() +def _extract_hf_username(user_info: object) -> str | None: + """Extract a username from the supported Hugging Face whoami shapes.""" + if isinstance(user_info, dict): + return ( + user_info.get("name") + or user_info.get("fullname") + or user_info.get("username") + ) + + return ( + getattr(user_info, "name", None) + or getattr(user_info, "fullname", None) + or getattr(user_info, "username", None) + ) + + def validate_env_structure(env_dir: Path, strict: bool = False) -> List[str]: """ Validate that the directory follows OpenEnv environment structure. diff --git a/src/openenv/cli/commands/fork.py b/src/openenv/cli/commands/fork.py index adcd988ab..4dc558ef7 100644 --- a/src/openenv/cli/commands/fork.py +++ b/src/openenv/cli/commands/fork.py @@ -13,7 +13,7 @@ import typer from huggingface_hub import HfApi, login, whoami -from .._cli_utils import console +from .._cli_utils import _extract_hf_username, console app = typer.Typer( help="Fork (duplicate) an OpenEnv environment on Hugging Face to your account" @@ -37,19 +37,7 @@ def _parse_key_value(s: str) -> tuple[str, str]: def _ensure_hf_authenticated() -> str: """Ensure user is authenticated with Hugging Face. Returns username.""" try: - user_info = whoami() - if isinstance(user_info, dict): - username = ( - user_info.get("name") - or user_info.get("fullname") - or user_info.get("username") - ) - else: - username = ( - getattr(user_info, "name", None) - or getattr(user_info, "fullname", None) - or getattr(user_info, "username", None) - ) + username = _extract_hf_username(whoami()) if not username: raise ValueError("Could not extract username from whoami response") console.print(f"[bold green]✓[/bold green] Authenticated as: {username}") @@ -60,19 +48,7 @@ def _ensure_hf_authenticated() -> str: ) try: login() - user_info = whoami() - if isinstance(user_info, dict): - username = ( - user_info.get("name") - or user_info.get("fullname") - or user_info.get("username") - ) - else: - username = ( - getattr(user_info, "name", None) - or getattr(user_info, "fullname", None) - or getattr(user_info, "username", None) - ) + username = _extract_hf_username(whoami()) if not username: raise ValueError("Could not extract username from whoami response") console.print(f"[bold green]✓[/bold green] Authenticated as: {username}") diff --git a/src/openenv/cli/commands/push.py b/src/openenv/cli/commands/push.py index 2308c2ef8..9eaafbc69 100644 --- a/src/openenv/cli/commands/push.py +++ b/src/openenv/cli/commands/push.py @@ -19,7 +19,7 @@ import yaml from huggingface_hub import HfApi, login, whoami -from .._cli_utils import console, validate_env_structure +from .._cli_utils import _extract_hf_username, console, validate_env_structure app = typer.Typer(help="Push an OpenEnv environment to Hugging Face Spaces") @@ -243,22 +243,6 @@ def _validate_openenv_directory(directory: Path) -> tuple[str, dict]: return env_name, manifest -def _extract_hf_username(user_info: object) -> str | None: - """Extract a username from the supported Hugging Face whoami shapes.""" - if isinstance(user_info, dict): - return ( - user_info.get("name") - or user_info.get("fullname") - or user_info.get("username") - ) - - return ( - getattr(user_info, "name", None) - or getattr(user_info, "fullname", None) - or getattr(user_info, "username", None) - ) - - def _get_hf_username() -> str: """Return the authenticated Hugging Face username from whoami().""" username = _extract_hf_username(whoami()) diff --git a/tests/test_cli/test_fork.py b/tests/test_cli/test_fork.py index f1ab97806..e4255d240 100644 --- a/tests/test_cli/test_fork.py +++ b/tests/test_cli/test_fork.py @@ -6,6 +6,7 @@ """Tests for the openenv fork command.""" +from types import SimpleNamespace from unittest.mock import MagicMock, patch from openenv.cli.__main__ import app @@ -52,6 +53,27 @@ def test_fork_calls_duplicate_space_with_from_id() -> None: assert call_kwargs["hardware"] == "cpu-basic" +def test_fork_authenticates_object_whoami_response() -> None: + """Test that fork accepts object-shaped whoami responses.""" + with ( + patch("openenv.cli.commands.fork.whoami") as mock_whoami, + patch("openenv.cli.commands.fork.login") as mock_login, + patch("openenv.cli.commands.fork.HfApi") as mock_hf_api_class, + ): + mock_whoami.return_value = SimpleNamespace(username="testuser") + mock_api = MagicMock() + mock_api.duplicate_space.return_value = ( + "https://huggingface.co/spaces/testuser/source-space" + ) + mock_hf_api_class.return_value = mock_api + + result = runner.invoke(app, ["fork", "owner/source-space"]) + + assert result.exit_code == 0 + mock_login.assert_not_called() + mock_api.duplicate_space.assert_called_once() + + def test_fork_passes_private_and_to_id() -> None: """Test that fork passes --private and --repo-id to duplicate_space.""" with (