Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
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_list: 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_list: Whether to show output in key-value list 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_list = show_key_value_list

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_list(self) -> bool:
return self._show_key_value_list

def to_json(self, indent: int = 4) -> str:
try:
from fabric_cli.utils.fab_util import dumps
Expand Down
4 changes: 4 additions & 0 deletions src/fabric_cli/errors/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@

class CommonErrors:

@staticmethod
def invalid_entries_format() -> str:
return "Invalid entries format"

@staticmethod
def invalid_jmespath_query() -> str:
return f"Invalid jmespath query (https://jmespath.org)"
Expand Down
66 changes: 65 additions & 1 deletion src/fabric_cli/utils/fab_ui.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import builtins
import html
import json
import re
Comment thread
aviatco marked this conversation as resolved.
Outdated
import sys
import unicodedata
from argparse import Namespace
Expand Down Expand Up @@ -95,7 +96,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_list: bool = False,
) -> None:
"""Create a FabricCLIOutput instance and print it depends on the format.

Expand All @@ -105,6 +106,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_list: Whether to show output in key-value list format (default: False)

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

# Get format from output or config
Expand Down Expand Up @@ -355,6 +358,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_list:
_print_entries_key_value_list_style(output_result.data)
else:
_print_raw_data(output_result.data)

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


def _print_entries_key_value_list_style(entries: Any) -> None:
"""Print entries in a key-value list format with 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.Common.invalid_entries_format(),
fab_constant.ERROR_INVALID_ENTRIES_FORMAT,
)

for entry in _entries:
for key, value in entry.items():
pretty_key = _format_key_to_convert_to_title_case(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_convert_to_title_case(key: str) -> str:
"""Convert a snake_case or camelCase key to a Title Case name.

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

Returns:
str: Formatted to title case name (e.g. 'User Id' or 'Account Name')
"""
# Replace underscores and camelCase with spaces
pretty = key.replace('_', ' ')
Comment thread
aviatco marked this conversation as resolved.
Outdated
# Replacing the camelCase with spaces only if the previous character is not a space
pretty = re.sub(r'(?<!^)(?<! )(?=[A-Z])', ' ', pretty)
Comment thread
aviatco marked this conversation as resolved.
Outdated
# Title case the result
pretty = pretty.title()

# Here add special cases for specific keys that need to be formatted differently
special_cases = {
"Id": "ID",
"Powerbi": "PowerBI",
}

# Replace special cases
for key, value in special_cases.items():
pretty = pretty.replace(key.title(), value)

return pretty
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_list_success():
"""Test show_key_value_list property is handled correctly."""
# Test with show_key_value_list True
output = FabricCLIOutput(data={"test": "data"}, show_key_value_list=True)
assert output.show_key_value_list is True

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

# Test with explicit False
output = FabricCLIOutput(data={"test": "data"}, show_key_value_list=False)
assert output.show_key_value_list is False
163 changes: 161 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_list_success(
mock_questionary_print, mock_fab_set_state_config
):
"""Test print_output_format with show_key_value_list=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_list=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_list_false_success(
mock_questionary_print, mock_fab_set_state_config
):
"""Test print_output_format with show_key_value_list=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_list=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_list_json_format_success(
mock_questionary_print, mock_fab_set_state_config
):
"""Test that show_key_value_list 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_list=True # This should be ignored in JSON format
)

# Verify that JSON output is produced regardless of show_key_value_list
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,75 @@ 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_list_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_list_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_list_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_list_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_list_style(123)

assert ex.value.status_code == fab_constant.ERROR_INVALID_ENTRIES_FORMAT


def test_format_key_to_title_case_success():
# Test snake_case conversion
assert ui._format_key_to_convert_to_title_case("account_name") == "Account Name"
# Test camelCase conversion
assert ui._format_key_to_convert_to_title_case("accountName") == "Account Name"
# Test single word
assert ui._format_key_to_convert_to_title_case("status") == "Status"
# Test mixed case
assert ui._format_key_to_convert_to_title_case("user_Name") == "User Name"
# Test special cases from the function
assert ui._format_key_to_convert_to_title_case("user_id") == "User ID"
assert ui._format_key_to_convert_to_title_case("powerbi_settings") == "PowerBI Settings"


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