From 386d01a3532cf00d169056544f8ec9075c1585cb Mon Sep 17 00:00:00 2001 From: aviat Date: Mon, 10 Nov 2025 12:24:27 +0000 Subject: [PATCH 01/11] support print in pritty key value style for text format --- src/fabric_cli/core/fab_output.py | 7 ++ src/fabric_cli/utils/fab_ui.py | 52 ++++++++- tests/test_core/test_fab_output.py | 15 +++ tests/test_utils/test_fab_ui.py | 170 ++++++++++++++++++++++++++++- 4 files changed, 241 insertions(+), 3 deletions(-) diff --git a/src/fabric_cli/core/fab_output.py b/src/fabric_cli/core/fab_output.py index cd312ae76..838af23f7 100644 --- a/src/fabric_cli/core/fab_output.py +++ b/src/fabric_cli/core/fab_output.py @@ -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. @@ -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. @@ -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, @@ -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 diff --git a/src/fabric_cli/utils/fab_ui.py b/src/fabric_cli/utils/fab_ui.py index ec60d5240..1bcc087cf 100644 --- a/src/fabric_cli/utils/fab_ui.py +++ b/src/fabric_cli/utils/fab_ui.py @@ -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. @@ -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 @@ -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 @@ -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) @@ -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() \ No newline at end of file diff --git a/tests/test_core/test_fab_output.py b/tests/test_core/test_fab_output.py index 0ae7071e4..c0bdf8d02 100644 --- a/tests/test_core/test_fab_output.py +++ b/tests/test_core/test_fab_output.py @@ -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 diff --git a/tests/test_utils/test_fab_ui.py b/tests/test_utils/test_fab_ui.py index 3201023fe..a62ad8f37 100644 --- a/tests/test_utils/test_fab_ui.py +++ b/tests/test_utils/test_fab_ui.py @@ -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 @@ -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") @@ -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) From 2cbe22294b0ea4365bcef92b14d5354204b5edee Mon Sep 17 00:00:00 2001 From: aviat Date: Mon, 10 Nov 2025 13:36:58 +0000 Subject: [PATCH 02/11] resolve comments --- src/fabric_cli/core/fab_output.py | 10 +++---- src/fabric_cli/errors/common.py | 4 +++ src/fabric_cli/utils/fab_ui.py | 24 +++++++-------- tests/test_core/test_fab_output.py | 18 +++++------ tests/test_utils/test_fab_ui.py | 48 +++++++++++++++--------------- 5 files changed, 54 insertions(+), 50 deletions(-) diff --git a/src/fabric_cli/core/fab_output.py b/src/fabric_cli/core/fab_output.py index 838af23f7..a44774b08 100644 --- a/src/fabric_cli/core/fab_output.py +++ b/src/fabric_cli/core/fab_output.py @@ -77,7 +77,7 @@ def __init__( error_code: Optional[str] = None, data: Optional[Any] = None, hidden_data: Optional[Any] = None, - show_key_value_pretty: bool = False, + show_key_value_list: bool = False, ): """Initialize a new FabricCLIOutput instance. @@ -90,7 +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 + 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. @@ -102,7 +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._show_key_value_list = show_key_value_list self._result = OutputResult( data=data, @@ -128,8 +128,8 @@ def show_headers(self) -> bool: return self._show_headers @property - def show_key_value_pretty(self) -> bool: - return self._show_key_value_pretty + def show_key_value_list(self) -> bool: + return self._show_key_value_list def to_json(self, indent: int = 4) -> str: try: diff --git a/src/fabric_cli/errors/common.py b/src/fabric_cli/errors/common.py index 0a9f65f24..e9fa735dc 100644 --- a/src/fabric_cli/errors/common.py +++ b/src/fabric_cli/errors/common.py @@ -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)" diff --git a/src/fabric_cli/utils/fab_ui.py b/src/fabric_cli/utils/fab_ui.py index 1bcc087cf..de7d82e23 100644 --- a/src/fabric_cli/utils/fab_ui.py +++ b/src/fabric_cli/utils/fab_ui.py @@ -95,7 +95,7 @@ def print_output_format( data: Optional[Any] = None, hidden_data: Optional[Any] = None, show_headers: bool = False, - show_key_value_pretty: bool = False, + show_key_value_list: bool = False, ) -> None: """Create a FabricCLIOutput instance and print it depends on the format. @@ -105,7 +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) + show_key_value_list: Whether to show output in key-value list format (default: False) Returns: FabricCLIOutput: Configured output instance ready for printing @@ -122,7 +122,7 @@ def print_output_format( data=data, hidden_data=hidden_data, show_headers=show_headers, - show_key_value_pretty=show_key_value_pretty, + show_key_value_list=show_key_value_list, ) # Get format from output or config @@ -357,8 +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) + elif output.show_key_value_list: + _print_entries_key_value_list_style(output_result.data) else: _print_raw_data(output_result.data) @@ -492,8 +492,8 @@ def _get_visual_length(string: str) -> int: return length -def _print_entries_key_value_pretty_style(entries: Any) -> None: - """Print entries in a key-value list format with pretty-formatted keys. +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 @@ -510,26 +510,26 @@ def _print_entries_key_value_pretty_style(entries: Any) -> None: _entries = entries else: raise FabricCLIError( - ErrorMessages.Labels.invalid_entries_format(), + 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_pretty_name(key) + 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_pretty_name(key: str) -> str: - """Convert a snake_case or camelCase key to a Title Case pretty name. +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. 'logged_in' or 'accountName') Returns: - str: Formatted pretty name (e.g. 'Logged In' or 'Account Name') + str: Formatted to title case name (e.g. 'Logged In' or 'Account Name') """ # Replace underscores and camelCase with spaces pretty = key.replace('_', ' ') diff --git a/tests/test_core/test_fab_output.py b/tests/test_core/test_fab_output.py index c0bdf8d02..f9e8af825 100644 --- a/tests/test_core/test_fab_output.py +++ b/tests/test_core/test_fab_output.py @@ -169,16 +169,16 @@ def test_fabric_cli_output_error_handling_success(): 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 +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_pretty False (default) + # Test with show_key_value_list False (default) output = FabricCLIOutput(data={"test": "data"}) - assert output.show_key_value_pretty is False + assert output.show_key_value_list is False # Test with explicit False - output = FabricCLIOutput(data={"test": "data"}, show_key_value_pretty=False) - assert output.show_key_value_pretty is False + output = FabricCLIOutput(data={"test": "data"}, show_key_value_list=False) + assert output.show_key_value_list is False diff --git a/tests/test_utils/test_fab_ui.py b/tests/test_utils/test_fab_ui.py index a62ad8f37..d0e9a0186 100644 --- a/tests/test_utils/test_fab_ui.py +++ b/tests/test_utils/test_fab_ui.py @@ -560,10 +560,10 @@ def test_print_output_format_with_force_output_success( ) -def test_print_output_format_with_show_key_value_pretty_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_pretty=True calls print_entries_key_value_style.""" + """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") @@ -578,7 +578,7 @@ def test_print_output_format_with_show_key_value_pretty_success( ui.print_output_format( args, data=test_data, - show_key_value_pretty=True + show_key_value_list=True ) assert mock_questionary_print.call_count >= 1 @@ -594,10 +594,10 @@ def test_print_output_format_with_show_key_value_pretty_success( mock_questionary_print.reset_mock() -def test_print_output_format_with_show_key_value_pretty_false_success( +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_pretty=False uses default JSON formatting.""" + """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") @@ -609,7 +609,7 @@ def test_print_output_format_with_show_key_value_pretty_false_success( ui.print_output_format( args, data=test_data, - show_key_value_pretty=False # Explicitly set to False + show_key_value_list=False # Explicitly set to False ) assert mock_questionary_print.call_count == 1 @@ -622,10 +622,10 @@ def test_print_output_format_with_show_key_value_pretty_false_success( mock_questionary_print.reset_mock() -def test_print_output_format_with_show_key_value_pretty_json_format_success( +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_pretty parameter works correctly with JSON output format.""" + """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") @@ -637,10 +637,10 @@ def test_print_output_format_with_show_key_value_pretty_json_format_success( ui.print_output_format( args, data=test_data, - show_key_value_pretty=True # This should be ignored in JSON format + show_key_value_list=True # This should be ignored in JSON format ) - # Verify that JSON output is produced regardless of show_key_value_pretty + # 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]) @@ -687,7 +687,7 @@ def test_print_entries_key_value_style_success(capsys): # Test with single dictionary entry entry = {"logged_in": "true", "account_name": "johndoe@example.com"} - ui.print_entries_key_value_pretty_style(entry) + ui._print_entries_key_value_list_style(entry) captured = capsys.readouterr() # print_grey outputs to stderr with to_stderr=False, so check stdout @@ -700,7 +700,7 @@ def test_print_entries_key_value_style_success(capsys): {"user_name": "john", "status": "active"}, {"user_name": "jane", "status": "inactive"} ] - ui.print_entries_key_value_pretty_style(entries) + ui._print_entries_key_value_list_style(entries) captured = capsys.readouterr() output = captured.out @@ -710,7 +710,7 @@ def test_print_entries_key_value_style_success(capsys): assert "Status: inactive" in output # Test with empty list - ui.print_entries_key_value_pretty_style([]) + ui._print_entries_key_value_list_style([]) captured = capsys.readouterr() # Should not output anything for empty list assert captured.err == "" @@ -722,13 +722,13 @@ def test_print_entries_key_value_style_invalid_input(): # Test with invalid input type (string) with pytest.raises(FabricCLIError) as ex: - ui.print_entries_key_value_pretty_style("invalid_input") + 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_pretty_style(123) + ui._print_entries_key_value_list_style(123) assert ex.value.status_code == fab_constant.ERROR_INVALID_ENTRIES_FORMAT @@ -737,21 +737,21 @@ 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" + assert ui._format_key_to_convert_to_title_case("logged_in") == "Logged In" + assert ui._format_key_to_convert_to_title_case("account_name") == "Account Name" + assert ui._format_key_to_convert_to_title_case("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" + assert ui._format_key_to_convert_to_title_case("accountName") == "Account Name" + assert ui._format_key_to_convert_to_title_case("userName") == "User Name" + assert ui._format_key_to_convert_to_title_case("isActive") == "Is Active" # Test single word - assert ui._format_key_to_pretty_name("status") == "Status" - assert ui._format_key_to_pretty_name("name") == "Name" + assert ui._format_key_to_convert_to_title_case("status") == "Status" + assert ui._format_key_to_convert_to_title_case("name") == "Name" # Test mixed case - assert ui._format_key_to_pretty_name("user_Name") == "User Name" + assert ui._format_key_to_convert_to_title_case("user_Name") == "User Name" def test_print_version_seccess(): From ea2d7b345bc01545ad32aa5fe07747a570636643 Mon Sep 17 00:00:00 2001 From: aviat Date: Tue, 11 Nov 2025 06:16:16 +0000 Subject: [PATCH 03/11] add special char support --- src/fabric_cli/utils/fab_ui.py | 21 +++++++++++++++++---- tests/test_utils/test_fab_ui.py | 21 +++++++-------------- 2 files changed, 24 insertions(+), 18 deletions(-) diff --git a/src/fabric_cli/utils/fab_ui.py b/src/fabric_cli/utils/fab_ui.py index de7d82e23..0116701f9 100644 --- a/src/fabric_cli/utils/fab_ui.py +++ b/src/fabric_cli/utils/fab_ui.py @@ -4,6 +4,7 @@ import builtins import html import json +import re import sys import unicodedata from argparse import Namespace @@ -526,13 +527,25 @@ 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. 'logged_in' or 'accountName') + key: The key to format (e.g. 'user_id' or 'accountName') Returns: - str: Formatted to title case name (e.g. 'Logged In' or 'Account Name') + str: Formatted to title case name (e.g. 'User Id' 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() + # pretty = ''.join(' ' + char if char.isupper() else char for char in pretty).strip() + pretty = re.sub(r'(? Date: Tue, 11 Nov 2025 07:35:47 +0000 Subject: [PATCH 04/11] add comments --- src/fabric_cli/utils/fab_ui.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/fabric_cli/utils/fab_ui.py b/src/fabric_cli/utils/fab_ui.py index 0116701f9..7ca9f828d 100644 --- a/src/fabric_cli/utils/fab_ui.py +++ b/src/fabric_cli/utils/fab_ui.py @@ -534,11 +534,12 @@ def _format_key_to_convert_to_title_case(key: str) -> str: """ # Replace underscores and camelCase with spaces pretty = key.replace('_', ' ') - # pretty = ''.join(' ' + char if char.isupper() else char for char in pretty).strip() + # Replacing the camelCase with spaces only if the previous character is not a space pretty = re.sub(r'(? Date: Tue, 11 Nov 2025 12:34:08 +0000 Subject: [PATCH 05/11] allow convert only snake case to title case --- src/fabric_cli/utils/fab_ui.py | 45 ++++++++++++++++++++++++--------- tests/test_utils/test_fab_ui.py | 36 ++++++++++++++++++++++---- 2 files changed, 64 insertions(+), 17 deletions(-) diff --git a/src/fabric_cli/utils/fab_ui.py b/src/fabric_cli/utils/fab_ui.py index 7ca9f828d..8afdcba14 100644 --- a/src/fabric_cli/utils/fab_ui.py +++ b/src/fabric_cli/utils/fab_ui.py @@ -524,29 +524,50 @@ def _print_entries_key_value_list_style(entries: Any) -> None: def _format_key_to_convert_to_title_case(key: str) -> str: - """Convert a snake_case or camelCase key to a Title Case name. + """Convert a snake_case key to a Title Case name. Args: - key: The key to format (e.g. 'user_id' or 'accountName') + key: The key to format in snake_case format (e.g. 'user_id', 'account_name') Returns: - str: Formatted to title case name (e.g. 'User Id' or 'Account Name') + str: Formatted to title case name (e.g. 'User ID', 'Account Name') + + Raises: + ValueError: If the key is not in the expected underscore-separated format """ - # Replace underscores and camelCase with spaces - pretty = key.replace('_', ' ') - # Replacing the camelCase with spaces only if the previous character is not a space - pretty = re.sub(r'(? str: + """Check for special cases and replace them with the correct value.""" # 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) + for case_key, case_value in special_cases.items(): + pretty = pretty.replace(case_key.title(), case_value) return pretty \ No newline at end of file diff --git a/tests/test_utils/test_fab_ui.py b/tests/test_utils/test_fab_ui.py index 76dabdd6d..1f923b1b6 100644 --- a/tests/test_utils/test_fab_ui.py +++ b/tests/test_utils/test_fab_ui.py @@ -736,15 +736,41 @@ def test_print_entries_key_value_style_invalid_input(): 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" + assert ui._format_key_to_convert_to_title_case("status") == "Status" + # Test snake_case with multiple underscores + assert ui._format_key_to_convert_to_title_case("user_account_name") == "User Account 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" + # Test numbers in keys + assert ui._format_key_to_convert_to_title_case("version_2_settings") == "Version 2 Settings" + # Test mixed case + assert ui._format_key_to_convert_to_title_case("user_Name") == "User Name" + + +def test_format_key_to_title_case_failure(): + """Test that the function throws ValueError for invalid key formats.""" + + # Test camelCase (should fail) + with pytest.raises(ValueError, match="Invalid key format: 'accountName'. Only underscore-separated words are allowed."): + ui._format_key_to_convert_to_title_case("accountName") + + # Test camelCase with ID (should fail) + with pytest.raises(ValueError, match="Invalid key format: 'accountID'. Only underscore-separated words are allowed."): + ui._format_key_to_convert_to_title_case("accountID") + + # Test spaces mixed with underscores (should fail) + with pytest.raises(ValueError, match="Invalid key format: 'user name_test'. Only underscore-separated words are allowed."): + ui._format_key_to_convert_to_title_case("user name_test") + + # Test special characters (should fail) + with pytest.raises(ValueError, match="Invalid key format: 'user@name'. Only underscore-separated words are allowed."): + ui._format_key_to_convert_to_title_case("user@name") + + # Test hyphen separated (should fail) + with pytest.raises(ValueError, match="Invalid key format: 'user-name'. Only underscore-separated words are allowed."): + ui._format_key_to_convert_to_title_case("user-name") def test_print_version_seccess(): From e566ac98aa063ba5e1dd02668b3893e4b11b81b6 Mon Sep 17 00:00:00 2001 From: aviat cohen Date: Tue, 11 Nov 2025 13:40:12 +0000 Subject: [PATCH 06/11] add changie --- .changes/unreleased/added-20251111-133907.yaml | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 .changes/unreleased/added-20251111-133907.yaml diff --git a/.changes/unreleased/added-20251111-133907.yaml b/.changes/unreleased/added-20251111-133907.yaml new file mode 100644 index 000000000..355b50054 --- /dev/null +++ b/.changes/unreleased/added-20251111-133907.yaml @@ -0,0 +1,3 @@ +kind: added +body: output-format:support print in key-value list style +time: 2025-11-11T13:39:07.795283732Z From b01cfeb4f5dddb8e171312e46d5cb513d7641c88 Mon Sep 17 00:00:00 2001 From: aviat cohen Date: Wed, 12 Nov 2025 08:12:10 +0000 Subject: [PATCH 07/11] fix changie row --- .changes/unreleased/added-20251111-133907.yaml | 3 --- .changes/unreleased/fixed-20251112-081104.yaml | 3 +++ 2 files changed, 3 insertions(+), 3 deletions(-) delete mode 100644 .changes/unreleased/added-20251111-133907.yaml create mode 100644 .changes/unreleased/fixed-20251112-081104.yaml diff --git a/.changes/unreleased/added-20251111-133907.yaml b/.changes/unreleased/added-20251111-133907.yaml deleted file mode 100644 index 355b50054..000000000 --- a/.changes/unreleased/added-20251111-133907.yaml +++ /dev/null @@ -1,3 +0,0 @@ -kind: added -body: output-format:support print in key-value list style -time: 2025-11-11T13:39:07.795283732Z diff --git a/.changes/unreleased/fixed-20251112-081104.yaml b/.changes/unreleased/fixed-20251112-081104.yaml new file mode 100644 index 000000000..cdda60916 --- /dev/null +++ b/.changes/unreleased/fixed-20251112-081104.yaml @@ -0,0 +1,3 @@ +kind: fixed +body: refactor output format - adding support for print in key-value list style +time: 2025-11-12T08:11:04.00625845Z From e0e3e24910d0b555a537c3943d853c1aae17b5ac Mon Sep 17 00:00:00 2001 From: aviat cohen Date: Wed, 12 Nov 2025 14:34:27 +0000 Subject: [PATCH 08/11] add more tests case; remove duplicate code --- src/fabric_cli/utils/fab_ui.py | 9 +-------- tests/test_utils/test_fab_ui.py | 4 ++++ 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/src/fabric_cli/utils/fab_ui.py b/src/fabric_cli/utils/fab_ui.py index 8afdcba14..e8b40f752 100644 --- a/src/fabric_cli/utils/fab_ui.py +++ b/src/fabric_cli/utils/fab_ui.py @@ -547,14 +547,7 @@ def _format_key_to_convert_to_title_case(key: str) -> str: if any(char.isupper() for char in key[1:]) and '_' not in key: raise ValueError(f"Invalid key format: '{key}'. Only underscore-separated words are allowed.") - # Single words without underscores are allowed - if '_' not in key and key.islower(): - pretty = key.title() - else: - # Replace underscores with spaces and title case - pretty = key.replace('_', ' ').title() - - pretty = key.replace('_', ' ').title() + pretty = key.replace('_', ' ').title().trim() return _check_special_cases(pretty) diff --git a/tests/test_utils/test_fab_ui.py b/tests/test_utils/test_fab_ui.py index 1f923b1b6..a9ce1085d 100644 --- a/tests/test_utils/test_fab_ui.py +++ b/tests/test_utils/test_fab_ui.py @@ -747,6 +747,10 @@ def test_format_key_to_title_case_success(): assert ui._format_key_to_convert_to_title_case("version_2_settings") == "Version 2 Settings" # Test mixed case assert ui._format_key_to_convert_to_title_case("user_Name") == "User Name" + # Test remove spaces + assert ui._format_key_to_convert_to_title_case(" user") == "User" + # Test already title case + assert ui._format_key_to_convert_to_title_case("User") == "User" def test_format_key_to_title_case_failure(): From c2483162665a88139f7833a4edfdebaa8053d60a Mon Sep 17 00:00:00 2001 From: aviat cohen Date: Wed, 12 Nov 2025 14:50:12 +0000 Subject: [PATCH 09/11] fix type check --- src/fabric_cli/utils/fab_ui.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/fabric_cli/utils/fab_ui.py b/src/fabric_cli/utils/fab_ui.py index e8b40f752..93b9680b2 100644 --- a/src/fabric_cli/utils/fab_ui.py +++ b/src/fabric_cli/utils/fab_ui.py @@ -547,7 +547,7 @@ def _format_key_to_convert_to_title_case(key: str) -> str: if any(char.isupper() for char in key[1:]) and '_' not in key: raise ValueError(f"Invalid key format: '{key}'. Only underscore-separated words are allowed.") - pretty = key.replace('_', ' ').title().trim() + pretty = key.replace('_', ' ').title().strip() return _check_special_cases(pretty) From acd7be2f0b426a66af764754b07336f378b45f23 Mon Sep 17 00:00:00 2001 From: aviat cohen Date: Thu, 13 Nov 2025 09:06:30 +0000 Subject: [PATCH 10/11] remove unused imports --- src/fabric_cli/utils/fab_ui.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/fabric_cli/utils/fab_ui.py b/src/fabric_cli/utils/fab_ui.py index 93b9680b2..cfb1e7f9a 100644 --- a/src/fabric_cli/utils/fab_ui.py +++ b/src/fabric_cli/utils/fab_ui.py @@ -3,8 +3,6 @@ import builtins import html -import json -import re import sys import unicodedata from argparse import Namespace From 36647a123770fe0ed5ad15d7981e28c777c2cea7 Mon Sep 17 00:00:00 2001 From: aviat cohen Date: Thu, 13 Nov 2025 09:48:17 +0000 Subject: [PATCH 11/11] use mock_questionary_print instead of capsys --- src/fabric_cli/utils/fab_ui.py | 4 ++-- tests/test_utils/test_fab_ui.py | 40 ++++++++++++++++----------------- 2 files changed, 22 insertions(+), 22 deletions(-) diff --git a/src/fabric_cli/utils/fab_ui.py b/src/fabric_cli/utils/fab_ui.py index cfb1e7f9a..464dd8a50 100644 --- a/src/fabric_cli/utils/fab_ui.py +++ b/src/fabric_cli/utils/fab_ui.py @@ -513,11 +513,11 @@ def _print_entries_key_value_list_style(entries: Any) -> None: fab_constant.ERROR_INVALID_ENTRIES_FORMAT, ) - for entry in _entries: + for i, entry in enumerate(_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: + if i < len(_entries) - 1: print_grey("", to_stderr=False) # Empty line between entries diff --git a/tests/test_utils/test_fab_ui.py b/tests/test_utils/test_fab_ui.py index a9ce1085d..2c35648e3 100644 --- a/tests/test_utils/test_fab_ui.py +++ b/tests/test_utils/test_fab_ui.py @@ -678,22 +678,20 @@ 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): +def test_print_entries_key_value_style_success(mock_questionary_print): """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 + # Verify the correct formatted output was printed + assert mock_questionary_print.call_count == 2 + printed_calls = [call.args[0] for call in mock_questionary_print.call_args_list] + assert "Logged In: true" in printed_calls + assert "Account Name: johndoe@example.com" in printed_calls + + mock_questionary_print.reset_mock() # Test with list of dictionaries entries = [ @@ -702,19 +700,21 @@ def test_print_entries_key_value_style_success(capsys): ] 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 + # Verify output for list of entries (should include empty line between entries, but not after last) + assert mock_questionary_print.call_count == 5 # 2 for john + 1 empty line + 2 for jane + printed_calls = [call.args[0] for call in mock_questionary_print.call_args_list] + assert "User Name: john" in printed_calls + assert "Status: active" in printed_calls + assert "User Name: jane" in printed_calls + assert "Status: inactive" in printed_calls + assert "" in printed_calls # Empty line between entries (but not after the last entry) + + mock_questionary_print.reset_mock() # 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 == "" + # Should not call print for empty list + mock_questionary_print.assert_not_called() def test_print_entries_key_value_style_invalid_input():