Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions src/fabric_cli/core/fab_output.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ def __init__(
error_code: Optional[str] = None,
data: Optional[Any] = None,
hidden_data: Optional[Any] = None,
show_key_value_pretty: bool = False,
):
"""Initialize a new FabricCLIOutput instance.

Expand All @@ -89,6 +90,7 @@ def __init__(
error_code: Optional error code. Only included when status is Failed.
data: The main output data to be displayed
hidden_data: Additional data shown only when --all flag or FAB_SHOW_HIDDEN is true
show_key_value_pretty: Whether to show output in key-value pretty format

Note:
The data parameter is always converted to a list format internally.
Expand All @@ -100,6 +102,7 @@ def __init__(
self._subcommand = subcommand
self._output_format_type = output_format_type
self._show_headers = show_headers
self._show_key_value_pretty = show_key_value_pretty

self._result = OutputResult(
data=data,
Expand All @@ -124,6 +127,10 @@ def result(self) -> OutputResult:
def show_headers(self) -> bool:
return self._show_headers

@property
def show_key_value_pretty(self) -> bool:
return self._show_key_value_pretty

def to_json(self, indent: int = 4) -> str:
try:
from fabric_cli.utils.fab_util import dumps
Expand Down
52 changes: 51 additions & 1 deletion src/fabric_cli/utils/fab_ui.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ def print_output_format(
data: Optional[Any] = None,
hidden_data: Optional[Any] = None,
show_headers: bool = False,
# print_callback: bool = True,
show_key_value_pretty: bool = False,
) -> None:
"""Create a FabricCLIOutput instance and print it depends on the format.

Expand All @@ -105,6 +105,7 @@ def print_output_format(
data: Optional data to include in output
hidden_data: Optional hidden data to include in output
show_headers: Whether to show headers in the output (default: False)
show_key_value_pretty: Whether to show output in key-value pretty format (default: False)

Returns:
FabricCLIOutput: Configured output instance ready for printing
Expand All @@ -121,6 +122,7 @@ def print_output_format(
data=data,
hidden_data=hidden_data,
show_headers=show_headers,
show_key_value_pretty=show_key_value_pretty,
)

# Get format from output or config
Expand Down Expand Up @@ -355,6 +357,8 @@ def _print_output_format_result_text(output: FabricCLIOutput) -> None:
):
data_keys = output.result.get_data_keys() if output_result.data else []
print_entries_unix_style(output_result.data, data_keys, header=show_headers)
elif output.show_key_value_pretty:
_print_entries_key_value_pretty_style(output_result.data)
else:
_print_raw_data(output_result.data)

Expand Down Expand Up @@ -486,3 +490,49 @@ def _get_visual_length(string: str) -> int:
else:
length += 1
return length


def _print_entries_key_value_pretty_style(entries: Any) -> None:
"""Print entries in a key-value list format with pretty-formatted keys.

Args:
entries: Dictionary or list of dictionaries to print

Example output:
Logged In: true
Account: johndoe@example.com
"""
if isinstance(entries, dict):
_entries = [entries]
elif isinstance(entries, list):
if not entries:
return
_entries = entries
else:
raise FabricCLIError(
ErrorMessages.Labels.invalid_entries_format(),
fab_constant.ERROR_INVALID_ENTRIES_FORMAT,
)

for entry in _entries:
for key, value in entry.items():
pretty_key = _format_key_to_pretty_name(key)
print_grey(f"{pretty_key}: {value}", to_stderr=False)
if len(_entries) > 1:
print_grey("", to_stderr=False) # Empty line between entries


def _format_key_to_pretty_name(key: str) -> str:
"""Convert a snake_case or camelCase key to a Title Case pretty name.

Args:
key: The key to format (e.g. 'logged_in' or 'accountName')

Returns:
str: Formatted pretty name (e.g. 'Logged In' or 'Account Name')
"""
# Replace underscores and camelCase with spaces
pretty = key.replace('_', ' ')
pretty = ''.join(' ' + char if char.isupper() else char for char in pretty).strip()
# Title case the result
return pretty.title()
15 changes: 15 additions & 0 deletions tests/test_core/test_fab_output.py
Original file line number Diff line number Diff line change
Expand Up @@ -167,3 +167,18 @@ def test_fabric_cli_output_error_handling_success():

json_output = json.loads(output.to_json())
assert json_output["result"]["error_code"] == "UnexpectedError"


def test_fabric_cli_output_show_key_value_pretty_success():
"""Test show_key_value_pretty property is handled correctly."""
# Test with show_key_value_pretty True
output = FabricCLIOutput(data={"test": "data"}, show_key_value_pretty=True)
assert output.show_key_value_pretty is True

# Test with show_key_value_pretty False (default)
output = FabricCLIOutput(data={"test": "data"})
assert output.show_key_value_pretty is False

# Test with explicit False
output = FabricCLIOutput(data={"test": "data"}, show_key_value_pretty=False)
assert output.show_key_value_pretty is False
170 changes: 168 additions & 2 deletions tests/test_utils/test_fab_ui.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,9 @@
import platform
from argparse import Namespace
from enum import Enum
from typing import Callable, Optional

import pytest

import fabric_cli.core.fab_state_config as state_config
from fabric_cli.core import fab_constant
from fabric_cli.core import fab_constant as constant
from fabric_cli.core.fab_exceptions import FabricCLIError
Expand Down Expand Up @@ -562,6 +560,98 @@ def test_print_output_format_with_force_output_success(
)


def test_print_output_format_with_show_key_value_pretty_success(
mock_questionary_print, mock_fab_set_state_config
):
"""Test print_output_format with show_key_value_pretty=True calls print_entries_key_value_style."""

# Setup text output format
mock_fab_set_state_config(constant.FAB_OUTPUT_FORMAT, "text")

# Test data with multiple entries
test_data = [
{"user_name": "john", "is_active": "true"},
{"user_name": "jane", "is_active": "false"}
]

args = Namespace(command="test")
ui.print_output_format(
args,
data=test_data,
show_key_value_pretty=True
)

assert mock_questionary_print.call_count >= 1

output_calls = [call.args[0] for call in mock_questionary_print.mock_calls]
output_text = " ".join(output_calls)

assert "User Name:" in output_text
assert "Is Active:" in output_text
assert '"user_name"' not in output_text
assert '{\n' not in output_text

mock_questionary_print.reset_mock()


def test_print_output_format_with_show_key_value_pretty_false_success(
mock_questionary_print, mock_fab_set_state_config
):
"""Test print_output_format with show_key_value_pretty=False uses default JSON formatting."""

# Setup text output format
mock_fab_set_state_config(constant.FAB_OUTPUT_FORMAT, "text")

# Test data
test_data = [{"user_name": "john", "is_active": "true"}]

args = Namespace(command="test")
ui.print_output_format(
args,
data=test_data,
show_key_value_pretty=False # Explicitly set to False
)

assert mock_questionary_print.call_count == 1
output = mock_questionary_print.mock_calls[0].args[0]

# Should contain JSON structure, not key-value format
assert '{\n' in output or '[' in output
assert '"user_name": "john"' in output or '"user_name":"john"' in output

mock_questionary_print.reset_mock()


def test_print_output_format_with_show_key_value_pretty_json_format_success(
mock_questionary_print, mock_fab_set_state_config
):
"""Test that show_key_value_pretty parameter works correctly with JSON output format."""

# Setup JSON output format
mock_fab_set_state_config(constant.FAB_OUTPUT_FORMAT, "json")

# Test data
test_data = [{"user_name": "john", "is_active": "true"}]

args = Namespace(command="test", output_format="json")
ui.print_output_format(
args,
data=test_data,
show_key_value_pretty=True # This should be ignored in JSON format
)

# Verify that JSON output is produced regardless of show_key_value_pretty
assert mock_questionary_print.call_count == 1
output = json.loads(mock_questionary_print.mock_calls[0].args[0])

assert isinstance(output, dict)
assert "result" in output
assert "data" in output["result"]
assert output["result"]["data"] == test_data

mock_questionary_print.reset_mock()


def test_print_output_format_failure(mock_fab_set_state_config):
# Mock get_config to return an unsupported format
mock_fab_set_state_config(constant.FAB_OUTPUT_FORMAT, "test")
Expand All @@ -588,6 +678,82 @@ def test_print_output_format_text_no_result_failure():
assert excinfo.value.status_code == constant.ERROR_INVALID_INPUT


@pytest.mark.skipif(
platform.system() == "Windows",
reason="Failed to run on windows with vscode - no real console",
)
def test_print_entries_key_value_style_success(capsys):
"""Test printing entries in key-value format."""

# Test with single dictionary entry
entry = {"logged_in": "true", "account_name": "johndoe@example.com"}
ui.print_entries_key_value_pretty_style(entry)

captured = capsys.readouterr()
# print_grey outputs to stderr with to_stderr=False, so check stdout
output = captured.out
assert "Logged In: true" in output
assert "Account Name: johndoe@example.com" in output

# Test with list of dictionaries
entries = [
{"user_name": "john", "status": "active"},
{"user_name": "jane", "status": "inactive"}
]
ui.print_entries_key_value_pretty_style(entries)

captured = capsys.readouterr()
output = captured.out
assert "User Name: john" in output
assert "Status: active" in output
assert "User Name: jane" in output
assert "Status: inactive" in output

# Test with empty list
ui.print_entries_key_value_pretty_style([])
captured = capsys.readouterr()
# Should not output anything for empty list
assert captured.err == ""
assert captured.out == ""


def test_print_entries_key_value_style_invalid_input():
"""Test error handling for invalid input types."""

# Test with invalid input type (string)
with pytest.raises(FabricCLIError) as ex:
ui.print_entries_key_value_pretty_style("invalid_input")

assert ex.value.status_code == fab_constant.ERROR_INVALID_ENTRIES_FORMAT

# Test with invalid input type (integer)
with pytest.raises(FabricCLIError) as ex:
ui.print_entries_key_value_pretty_style(123)

assert ex.value.status_code == fab_constant.ERROR_INVALID_ENTRIES_FORMAT


def test_format_key_to_pretty_name():
"""Test the key formatting function used in key-value style output."""

# Test snake_case conversion
assert ui._format_key_to_pretty_name("logged_in") == "Logged In"
assert ui._format_key_to_pretty_name("account_name") == "Account Name"
assert ui._format_key_to_pretty_name("user_id") == "User Id"

# Test camelCase conversion
assert ui._format_key_to_pretty_name("accountName") == "Account Name"
assert ui._format_key_to_pretty_name("userName") == "User Name"
assert ui._format_key_to_pretty_name("isActive") == "Is Active"

# Test single word
assert ui._format_key_to_pretty_name("status") == "Status"
assert ui._format_key_to_pretty_name("name") == "Name"

# Test mixed case
assert ui._format_key_to_pretty_name("user_Name") == "User Name"


def test_print_version_seccess():
ui.print_version()
ui.print_version(None)
Expand Down
Loading