diff --git a/src/openenv/cli/_cli_utils.py b/src/openenv/cli/_cli_utils.py index 404b57f55..105c34c3a 100644 --- a/src/openenv/cli/_cli_utils.py +++ b/src/openenv/cli/_cli_utils.py @@ -11,6 +11,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 c9e0bb3cd..44e27e383 100644 --- a/src/openenv/cli/commands/fork.py +++ b/src/openenv/cli/commands/fork.py @@ -9,7 +9,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" @@ -33,19 +33,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}") @@ -56,19 +44,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 b32de7e08..e40aee24c 100644 --- a/src/openenv/cli/commands/push.py +++ b/src/openenv/cli/commands/push.py @@ -15,7 +15,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") @@ -239,22 +239,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 cca65cd31..a9c8c6fb8 100644 --- a/tests/test_cli/test_fork.py +++ b/tests/test_cli/test_fork.py @@ -2,6 +2,7 @@ """Tests for the openenv fork command.""" +from types import SimpleNamespace from unittest.mock import MagicMock, patch from openenv.cli.__main__ import app @@ -48,6 +49,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 (