-
Notifications
You must be signed in to change notification settings - Fork 59
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
vdk-control-cli: refactor output printing with printer class
Currently if a command needs to print similar types of data in multiple places or formats, the developer would have to duplicate the printing code for each location or format. This would lead to a lot of redundant code, which is difficult to maintain and prone to errors. This is making the code more "DRY". Also, users and devs are limited to a fixed set of output formats provided by the application. This could be restricting if the devs needs to print data in a format that is not supported by the application. E.g I wanted to add `rich` or `streamlit` type potentially. By introducing support for customizable output formats with the Printer class and related functions, users can define their own output formats and register them with the application using the printer decorator. This allows users to print data in any format they desire, providing greater flexibility and customization options. Testing Done: unit tests (incl new ones) Signed-off-by: Antoni Ivanov <[email protected]>
- Loading branch information
1 parent
5b116dc
commit ea36e8c
Showing
9 changed files
with
243 additions
and
35 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -12,6 +12,7 @@ __pycache__/* | |
.cache/* | ||
.*.swp | ||
*/.ipynb_checkpoints/* | ||
out | ||
.DS_Store | ||
.eggs | ||
build | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
132 changes: 132 additions & 0 deletions
132
projects/vdk-control-cli/src/vdk/internal/control/utils/output_printer.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,132 @@ | ||
# Copyright 2023-2023 VMware, Inc. | ||
# SPDX-License-Identifier: Apache-2.0 | ||
import abc | ||
import json | ||
from enum import Enum | ||
from enum import unique | ||
from typing import Any | ||
from typing import Dict | ||
from typing import List | ||
from typing import Optional | ||
|
||
import click | ||
from tabulate import tabulate | ||
|
||
|
||
class Printer(abc.ABC): | ||
""" | ||
The abstract base class for all printer classes. | ||
A printer is responsible for printing data in a specific format, such as text or JSON. | ||
Subclasses must implement the abstract methods below | ||
""" | ||
|
||
@abc.abstractmethod | ||
def print_table(self, data: Optional[List[Dict[str, Any]]]) -> None: | ||
""" | ||
Prints the table in the desired format (text, json, etc) | ||
:param data: the table to print | ||
""" | ||
|
||
@abc.abstractmethod | ||
def print_dict(self, data: Optional[Dict[str, Any]]) -> None: | ||
""" | ||
Prints dictionary in the desired format (text, json, etc) | ||
:param data: the dict to print | ||
""" | ||
|
||
|
||
""" | ||
This dictionary contains all the registered printers. | ||
The key is the output format and the value is the printer class. | ||
Do not access this dictionary directly, use the printer decorator instead. | ||
""" | ||
_registered_printers = {} | ||
|
||
|
||
def printer(output_format: str) -> callable: | ||
""" | ||
A decorator that registers a printer class for the given output format. | ||
The class must implement the Printer interface and have a constructor with no parameters. | ||
:param output_format: The output format to register the printer for. | ||
""" | ||
|
||
def decorator(cls): | ||
_registered_printers[output_format.lower()] = cls | ||
return cls | ||
|
||
return decorator | ||
|
||
|
||
@printer("text") | ||
class _PrinterText(Printer): | ||
def print_table(self, table: Optional[List[Dict[str, Any]]]) -> None: | ||
if table and len(table) > 0: | ||
click.echo(tabulate(table, headers="keys", tablefmt="fancy_grid")) | ||
else: | ||
click.echo("No Data.") | ||
|
||
def print_dict(self, data: Optional[Dict[str, Any]]) -> None: | ||
if data: | ||
click.echo( | ||
tabulate( | ||
[[k, v] for k, v in data.items()], | ||
headers=("key", "value"), | ||
) | ||
) | ||
else: | ||
click.echo("No Data.") | ||
|
||
|
||
def json_format(data, indent=None): | ||
from datetime import date, datetime | ||
|
||
def json_serial(obj): | ||
"""JSON serializer for objects not serializable by default json code""" | ||
|
||
if isinstance(obj, (datetime, date)): | ||
return obj.isoformat() | ||
raise TypeError("Type %s not serializable" % type(obj)) | ||
|
||
return json.dumps(data, default=json_serial, indent=indent) | ||
|
||
|
||
@printer("json") | ||
class _PrinterJson(Printer): | ||
def print_table(self, data: List[Dict[str, Any]]) -> None: | ||
if data: | ||
click.echo(json_format(data)) | ||
else: | ||
click.echo("[]") | ||
|
||
def print_dict(self, data: Dict[str, Any]) -> None: | ||
if data: | ||
click.echo(json_format(data)) | ||
else: | ||
click.echo("{}") | ||
|
||
|
||
def create_printer(output_format: str) -> Printer: | ||
""" | ||
Creates a printer instance for the given output format. | ||
:param output_format: the desired output format | ||
:return: An instance of a Printer subclass that can print data in the desired format. | ||
""" | ||
if output_format.lower() in _registered_printers: | ||
printer_class = _registered_printers[output_format.lower()] | ||
return printer_class() | ||
else: | ||
raise ValueError(f"Printer for output format {output_format} not registered") | ||
|
||
|
||
@unique | ||
class OutputFormat(str, Enum): | ||
""" | ||
An enum used to specify the output formatting of a command. | ||
""" | ||
|
||
TEXT = "TEXT" | ||
JSON = "JSON" |
98 changes: 98 additions & 0 deletions
98
projects/vdk-control-cli/tests/vdk/internal/control/utils/test_output_printer.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,98 @@ | ||
# Copyright 2023-2023 VMware, Inc. | ||
# SPDX-License-Identifier: Apache-2.0 | ||
from typing import Any | ||
from typing import Dict | ||
from typing import List | ||
from unittest.mock import patch | ||
|
||
import pytest | ||
from vdk.internal.control.utils import output_printer | ||
from vdk.internal.control.utils.output_printer import _PrinterJson | ||
from vdk.internal.control.utils.output_printer import _PrinterText | ||
from vdk.internal.control.utils.output_printer import create_printer | ||
from vdk.internal.control.utils.output_printer import Printer | ||
|
||
|
||
class TestPrinterText: | ||
def test_print_dict(self): | ||
with patch("click.echo") as mock_echo: | ||
printer = _PrinterText() | ||
data = {"key": "value"} | ||
|
||
printer.print_dict(data) | ||
|
||
expected_output = "key value\n" "----- -------\n" "key value" | ||
mock_echo.assert_called_once_with(expected_output) | ||
|
||
def test_print_table_with_data(self): | ||
with patch("click.echo") as mock_echo: | ||
printer = _PrinterText() | ||
|
||
data = [{"key1": "value1", "key2": 2}, {"key1": "value3", "key2": 4}] | ||
|
||
printer.print_table(data) | ||
|
||
expected_output = ( | ||
"╒════════╤════════╕\n" | ||
"│ key1 │ key2 │\n" | ||
"╞════════╪════════╡\n" | ||
"│ value1 │ 2 │\n" | ||
"├────────┼────────┤\n" | ||
"│ value3 │ 4 │\n" | ||
"╘════════╧════════╛" | ||
) | ||
mock_echo.assert_called_once_with(expected_output) | ||
|
||
def test_print_table_with_no_data(self): | ||
with patch("click.echo") as mock_echo: | ||
printer = _PrinterText() | ||
data = [] | ||
|
||
printer.print_table(data) | ||
|
||
expected_output = "No Data." | ||
mock_echo.assert_called_once_with(expected_output) | ||
|
||
|
||
class TestPrinterJson: | ||
def test_print_dict(self): | ||
with patch("click.echo") as mock_echo: | ||
printer = _PrinterJson() | ||
|
||
data = {"key": "value"} | ||
|
||
printer.print_dict(data) | ||
|
||
expected_output = '{"key": "value"}' | ||
mock_echo.assert_called_once_with(expected_output) | ||
|
||
def test_print_table(self): | ||
with patch("click.echo") as mock_echo: | ||
printer = _PrinterJson() | ||
data = [ | ||
{"key1": "value1", "key2": "value2"}, | ||
{"key1": "value3", "key2": "value4"}, | ||
] | ||
printer.print_table(data) | ||
|
||
expected_output = '[{"key1": "value1", "key2": "value2"}, {"key1": "value3", "key2": "value4"}]' | ||
mock_echo.assert_called_once_with(expected_output) | ||
|
||
|
||
class TestCreatePrinter: | ||
def test_create_printer_with_registered_format(self): | ||
class MockPrinter(Printer): | ||
def print_dict(self, data: Dict[str, Any]) -> None: | ||
pass | ||
|
||
def print_table(self, data: List[Dict[str, Any]]) -> None: | ||
pass | ||
|
||
output_printer._registered_printers["mock"] = MockPrinter | ||
|
||
printer = create_printer("mock") | ||
assert isinstance(printer, MockPrinter) | ||
|
||
def test_create_printer_with_unregistered_format(self): | ||
with pytest.raises(ValueError): | ||
create_printer("invalid_format") |