2626import enum
2727import functools
2828import getpass
29+ import json
2930import logging
3031import os
3132import pathlib
3233import select
3334import sys
3435import threading
3536import traceback
37+ from abc import ABC , abstractmethod
3638from collections .abc import Callable , Generator
3739from contextlib import contextmanager
3840from datetime import datetime
39- from typing import TYPE_CHECKING , Any , Literal , TextIO , TypeVar , cast , Union , List , Dict
40- import json
41- import platformdirs
41+ from typing import TYPE_CHECKING , Any , Literal , TextIO , TypeVar , cast
42+
43+ import platformdirs # type: ignore [import-not-found]
4244
4345from craft_cli import errors
4446from craft_cli .printer import Printer
45- from abc import ABC , abstractmethod
46- #from .formatters import BaseFormatter
4747
48- TabularData = Union [List [Dict [str ,Any ]],Dict [str ,Any ]]
48+ TabularData = list [dict [str , Any ]] | dict [str , Any ]
49+
4950
5051class BaseFormatter (ABC ):
5152 @abstractmethod
52- def format (self ,data :TabularData ,headers :dict [str ,str ] | None = None )-> str :
53+ def format (self , data : TabularData , headers : dict [str , str ] | None = None ) -> str :
5354 pass
55+
56+
5457class JSONFormatter (BaseFormatter ):
55- """
56- Format data into JSON.
58+ """Format data into JSON.
5759
58- Example
60+ Example:
5961 -------
60- >>> formatter= JsonFormatter()
62+ >>> formatter = JsonFormatter()
6163 >>> formatter.format([{"name":"Alice","age":30})
6264 '{"name":"Alice","age":30}'
65+
6366 """
64- def format (self ,data : TabularData ,headers : dict [str ,str ] | None = None )-> str :
65- return json .dumps (data ,indent = 2 ,default = str )
67+
68+ def format (self , data : TabularData , headers : dict [str , str ] | None = None ) -> str :
69+ _ = headers
70+ return json .dumps (data , indent = 2 , default = str )
71+
72+
6673class TableFormatter (BaseFormatter ):
67- """
68- Format data into a pretty table.
74+ r"""Format data into a pretty table.
6975
70- Example
76+ Example:
7177 -------
72- >>> formatter= TableFormatter()
73- >>> formatter.format([["Name","Age"],["Alice",30]])
78+ >>> formatter = TableFormatter()
79+ >>> formatter.format([["Name", "Age"], ["Alice", 30]])
7480 'Name Age\nAlice 30'
81+
7582 """
76- def format (self ,data :TabularData ,headers :dict [str ,str ] | None = None )-> str :
83+
84+ def format (self , data : TabularData , headers : dict [str , str ] | None = None ) -> str :
85+ _ = headers
7786 """Format a list of rows into a table string."""
7887 if not data :
7988 return "[no data]"
80- table_headers : list [ str ]
81- if isinstance ( data , list ):
82- #list_data=cast(List[Dict[str,Any]], data)
83- all_keys : set [ str ] = set (). union ( * ( row . keys ( ) for row in data ) )
84- table_headers : list [ str ] = sorted (list ( all_keys ) )
85- rows = [[str (row .get (h ,"" )) for h in table_headers ] for row in data ]
89+ if isinstance ( data , list ):
90+ all_keys : set [ str ] = set ()
91+ for row in data :
92+ all_keys . update ( str ( k ) for k in row )
93+ table_headers = sorted (all_keys )
94+ rows = [[str (row .get (h , "" )) for h in table_headers ] for row in data ]
8695 else :
87- table_headers = ["Key" , "Value" ]
88- #data_dict=cast(Dict[str,Any],data)
89- rows = [[str (k ),str (v )] for k ,v in data .items ()]
90- cols_widths = [max (len (str (item )) for item in col ) for col in zip (* ([table_headers ]+ rows ))]
91-
92- def format_row (row : list [str ])-> str :
93- return " | " .join (str (cell ).ljust (width ) for cell , width in zip (row ,cols_widths ))
94- table = [format_row (table_headers )]
95- table .append ("-" * sum (cols_widths )+ "---" * (len (table_headers )- 1 ))
96- table .extend (format_row (row ) for row in rows )
96+ table_headers = ["Key" , "Value" ]
97+ rows = [[str (k ), str (v )] for k , v in data .items ()]
98+ cols_widths = [
99+ max (len (str (item )) for item in col ) for col in zip (table_headers , * rows )
100+ ]
101+
102+ def format_row (row : list [str ]) -> str :
103+ return " | " .join (
104+ cell .ljust (width ) for cell , width in zip (row , cols_widths )
105+ )
106+
107+ separator = "-+-" .join ("-" * width for width in cols_widths )
108+ table = [
109+ format_row (table_headers ),
110+ separator ,
111+ * (format_row (row ) for row in rows ),
112+ ]
97113 return "\n " .join (table )
98-
99-
114+
115+
100116if TYPE_CHECKING :
101117 from types import TracebackType
102118
@@ -516,10 +532,10 @@ def __init__(self) -> None:
516532 self ._log_handler : _Handler = None # type: ignore[assignment]
517533 self ._streaming_brief = False
518534 self ._docs_base_url : str | None = None
519- self ._formatters = {
520- "json" :JSONFormatter (),
521- "table" :TableFormatter (),
522- }
535+ self ._formatters = {
536+ "json" : JSONFormatter (),
537+ "table" : TableFormatter (),
538+ }
523539
524540 def init (
525541 self ,
@@ -785,6 +801,45 @@ def open_stream(self, text: str | None = None) -> _StreamContextManager:
785801 ephemeral_mode = ephemeral ,
786802 )
787803
804+ @_active_guard ()
805+ def data (
806+ self ,
807+ data : TabularData ,
808+ output_format : Literal ["json" , "table" ] = "table" ,
809+ headers : dict [str , str ] | None = None ,
810+ ) -> None :
811+ """Output structured data to the terminal in a specific format.
812+
813+ :param data: The structured data to output( list of dicts or a single dict).
814+ :param format: The format( defaults to 'table')
815+ :param headers: Optional dictionary to map internal data keys to displayed header.
816+ """
817+ formatter = self ._formatters .get (output_format )
818+ if not formatter :
819+ raise ValueError (f"Unsupported format: { format } " )
820+
821+ formatted_data = formatter .format (data , headers )
822+ stream = None if self ._mode == EmitterMode .QUIET else sys .stdout
823+ self ._printer .show (stream , formatted_data , raw_output = True )
824+
825+ @_active_guard ()
826+ def table (
827+ self ,
828+ data : TabularData ,
829+ headers : dict [str , str ] | None = None ,
830+ ) -> None :
831+ """Output data as table."""
832+ self .data (data , output_format = "table" , headers = headers )
833+
834+ @_active_guard ()
835+ def json (
836+ self ,
837+ data : TabularData ,
838+ headers : dict [str , str ] | None = None ,
839+ ) -> None :
840+ """Output data as JSON."""
841+ self .data (data , output_format = "json" , headers = headers )
842+
788843 @_active_guard ()
789844 @contextmanager
790845 def pause (self ) -> Generator [None , None , None ]:
@@ -946,14 +1001,6 @@ def prompt(self, prompt_text: str, *, hide: bool = False) -> str:
9461001 if not val :
9471002 raise errors .CraftError ("input cannot be empty" )
9481003 return val
949-
950- @_active_guard ()
951- def data (self ,records : list [dict ], format :str = "table" )-> None :
952- if format not in self ._formatters :
953- raise ValueError (f"Unsupported format: { format } " )
954- formatter = self ._formatters [format ]
955- formatted_output = formatter .format (records )
956- self .message (formatted_output )
9571004
9581005 @property
9591006 def log_filepath (self ) -> pathlib .Path :
@@ -971,4 +1018,3 @@ def _format_details(details: str) -> str:
9711018 if "\n " in details :
9721019 return details if details .startswith ("\n " ) else f"\n { details } "
9731020 return details
974-
0 commit comments