Skip to content

Commit 6d25ecf

Browse files
aviatcoaviat
andauthored
refactor(output_format): support print key value list style for text format (#63)
Co-authored-by: aviat <aviatcohen@microsoft.com>
1 parent 74c48f4 commit 6d25ecf

File tree

6 files changed

+298
-4
lines changed

6 files changed

+298
-4
lines changed
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
kind: fixed
2+
body: refactor output format - adding support for print in key-value list style
3+
time: 2025-11-12T08:11:04.00625845Z

src/fabric_cli/core/fab_output.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ def __init__(
7777
error_code: Optional[str] = None,
7878
data: Optional[Any] = None,
7979
hidden_data: Optional[Any] = None,
80+
show_key_value_list: bool = False,
8081
):
8182
"""Initialize a new FabricCLIOutput instance.
8283
@@ -89,6 +90,7 @@ def __init__(
8990
error_code: Optional error code. Only included when status is Failed.
9091
data: The main output data to be displayed
9192
hidden_data: Additional data shown only when --all flag or FAB_SHOW_HIDDEN is true
93+
show_key_value_list: Whether to show output in key-value list format
9294
9395
Note:
9496
The data parameter is always converted to a list format internally.
@@ -100,6 +102,7 @@ def __init__(
100102
self._subcommand = subcommand
101103
self._output_format_type = output_format_type
102104
self._show_headers = show_headers
105+
self._show_key_value_list = show_key_value_list
103106

104107
self._result = OutputResult(
105108
data=data,
@@ -124,6 +127,10 @@ def result(self) -> OutputResult:
124127
def show_headers(self) -> bool:
125128
return self._show_headers
126129

130+
@property
131+
def show_key_value_list(self) -> bool:
132+
return self._show_key_value_list
133+
127134
def to_json(self, indent: int = 4) -> str:
128135
try:
129136
from fabric_cli.utils.fab_util import dumps

src/fabric_cli/errors/common.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@
77

88
class CommonErrors:
99

10+
@staticmethod
11+
def invalid_entries_format() -> str:
12+
return "Invalid entries format"
13+
1014
@staticmethod
1115
def invalid_jmespath_query() -> str:
1216
return f"Invalid jmespath query (https://jmespath.org)"

src/fabric_cli/utils/fab_ui.py

Lines changed: 78 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33

44
import builtins
55
import html
6-
import json
76
import sys
87
import unicodedata
98
from argparse import Namespace
@@ -95,7 +94,7 @@ def print_output_format(
9594
data: Optional[Any] = None,
9695
hidden_data: Optional[Any] = None,
9796
show_headers: bool = False,
98-
# print_callback: bool = True,
97+
show_key_value_list: bool = False,
9998
) -> None:
10099
"""Create a FabricCLIOutput instance and print it depends on the format.
101100
@@ -105,6 +104,7 @@ def print_output_format(
105104
data: Optional data to include in output
106105
hidden_data: Optional hidden data to include in output
107106
show_headers: Whether to show headers in the output (default: False)
107+
show_key_value_list: Whether to show output in key-value list format (default: False)
108108
109109
Returns:
110110
FabricCLIOutput: Configured output instance ready for printing
@@ -121,6 +121,7 @@ def print_output_format(
121121
data=data,
122122
hidden_data=hidden_data,
123123
show_headers=show_headers,
124+
show_key_value_list=show_key_value_list,
124125
)
125126

126127
# Get format from output or config
@@ -355,6 +356,8 @@ def _print_output_format_result_text(output: FabricCLIOutput) -> None:
355356
):
356357
data_keys = output.result.get_data_keys() if output_result.data else []
357358
print_entries_unix_style(output_result.data, data_keys, header=show_headers)
359+
elif output.show_key_value_list:
360+
_print_entries_key_value_list_style(output_result.data)
358361
else:
359362
_print_raw_data(output_result.data)
360363

@@ -486,3 +489,76 @@ def _get_visual_length(string: str) -> int:
486489
else:
487490
length += 1
488491
return length
492+
493+
494+
def _print_entries_key_value_list_style(entries: Any) -> None:
495+
"""Print entries in a key-value list format with formatted keys.
496+
497+
Args:
498+
entries: Dictionary or list of dictionaries to print
499+
500+
Example output:
501+
Logged In: true
502+
Account: johndoe@example.com
503+
"""
504+
if isinstance(entries, dict):
505+
_entries = [entries]
506+
elif isinstance(entries, list):
507+
if not entries:
508+
return
509+
_entries = entries
510+
else:
511+
raise FabricCLIError(
512+
ErrorMessages.Common.invalid_entries_format(),
513+
fab_constant.ERROR_INVALID_ENTRIES_FORMAT,
514+
)
515+
516+
for i, entry in enumerate(_entries):
517+
for key, value in entry.items():
518+
pretty_key = _format_key_to_convert_to_title_case(key)
519+
print_grey(f"{pretty_key}: {value}", to_stderr=False)
520+
if i < len(_entries) - 1:
521+
print_grey("", to_stderr=False) # Empty line between entries
522+
523+
524+
def _format_key_to_convert_to_title_case(key: str) -> str:
525+
"""Convert a snake_case key to a Title Case name.
526+
527+
Args:
528+
key: The key to format in snake_case format (e.g. 'user_id', 'account_name')
529+
530+
Returns:
531+
str: Formatted to title case name (e.g. 'User ID', 'Account Name')
532+
533+
Raises:
534+
ValueError: If the key is not in the expected underscore-separated format
535+
"""
536+
# Allow letters, numbers, and underscores only
537+
if not key.replace('_', '').replace(' ', '').isalnum():
538+
raise ValueError(f"Invalid key format: '{key}'. Only underscore-separated words are allowed.")
539+
540+
# Check for invalid patterns (camelCase, spaces mixed with underscores, etc.)
541+
if ' ' in key and '_' in key:
542+
raise ValueError(f"Invalid key format: '{key}'. Only underscore-separated words are allowed.")
543+
544+
# Check for camelCase pattern (uppercase letters not at the start)
545+
if any(char.isupper() for char in key[1:]) and '_' not in key:
546+
raise ValueError(f"Invalid key format: '{key}'. Only underscore-separated words are allowed.")
547+
548+
pretty = key.replace('_', ' ').title().strip()
549+
550+
return _check_special_cases(pretty)
551+
552+
553+
def _check_special_cases(pretty: str) -> str:
554+
"""Check for special cases and replace them with the correct value."""
555+
# Here add special cases for specific keys that need to be formatted differently
556+
special_cases = {
557+
"Id": "ID",
558+
"Powerbi": "PowerBI",
559+
}
560+
561+
for case_key, case_value in special_cases.items():
562+
pretty = pretty.replace(case_key.title(), case_value)
563+
564+
return pretty

tests/test_core/test_fab_output.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,3 +167,18 @@ def test_fabric_cli_output_error_handling_success():
167167

168168
json_output = json.loads(output.to_json())
169169
assert json_output["result"]["error_code"] == "UnexpectedError"
170+
171+
172+
def test_fabric_cli_output_show_key_value_list_success():
173+
"""Test show_key_value_list property is handled correctly."""
174+
# Test with show_key_value_list True
175+
output = FabricCLIOutput(data={"test": "data"}, show_key_value_list=True)
176+
assert output.show_key_value_list is True
177+
178+
# Test with show_key_value_list False (default)
179+
output = FabricCLIOutput(data={"test": "data"})
180+
assert output.show_key_value_list is False
181+
182+
# Test with explicit False
183+
output = FabricCLIOutput(data={"test": "data"}, show_key_value_list=False)
184+
assert output.show_key_value_list is False

0 commit comments

Comments
 (0)