1"""
2Display output in a table format
3=================================
4
5.. versionadded:: 2017.7.0
6
7The ``table`` outputter displays a sequence of rows as table.
8
9Example output:
10
11.. code-block:: text
12
13    edge01.bjm01:
14    ----------
15        comment:
16        ----------
17        out:
18        ----------
19            ______________________________________________________________________________
20            | Active | Interface | Last Move |        Mac        | Moves | Static | Vlan |
21            ______________________________________________________________________________
22            |  True  |  ae1.900  |    0.0    | 40:A6:77:5A:50:01 |   0   | False  | 111  |
23            ______________________________________________________________________________
24            |  True  |  ae1.111  |    0.0    | 64:16:8D:32:26:58 |   0   | False  | 111  |
25            ______________________________________________________________________________
26            |  True  |  ae1.111  |    0.0    | 8C:60:4F:73:2D:57 |   0   | False  | 111  |
27            ______________________________________________________________________________
28            |  True  |  ae1.111  |    0.0    | 8C:60:4F:73:2D:7C |   0   | False  | 111  |
29            ______________________________________________________________________________
30            |  True  |  ae1.222  |    0.0    | 8C:60:4F:73:2D:57 |   0   | False  | 222  |
31            ______________________________________________________________________________
32            |  True  |  ae1.222  |    0.0    | F4:0F:1B:76:9D:97 |   0   | False  | 222  |
33            ______________________________________________________________________________
34        result:
35        ----------
36
37
38CLI Example:
39
40.. code-block:: bash
41
42    salt '*' foo.bar --out=table
43"""
44
45
46import operator
47from functools import reduce  # pylint: disable=redefined-builtin
48
49import salt.output
50import salt.utils.color
51import salt.utils.data
52
53__virtualname__ = "table"
54
55
56def __virtual__():
57    return __virtualname__
58
59
60class TableDisplay:
61    """
62    Manage the table display content.
63    """
64
65    _JUSTIFY_MAP = {
66        "center": str.center,
67        "right": str.rjust,
68        "left": str.ljust,
69    }
70
71    def __init__(
72        self,
73        has_header=True,  # if header will be displayed
74        row_delimiter="-",  # row delimiter char
75        delim=" | ",  # column delimiter
76        justify="center",  # text justify
77        separate_rows=True,  # display the line separating two consecutive rows
78        prefix="| ",  # character to display at the beginning of the row
79        suffix=" |",  # character to display at the end of the row
80        width=50,  # column max width
81        wrapfunc=None,
82    ):  # function wrapper
83        self.__dict__.update(
84            salt.utils.color.get_colors(
85                __opts__.get("color"), __opts__.get("color_theme")
86            )
87        )
88        self.strip_colors = __opts__.get("strip_colors", True)
89
90        self.has_header = has_header
91        self.row_delimiter = row_delimiter
92        self.delim = delim
93        self.justify = justify
94        self.separate_rows = separate_rows
95        self.prefix = prefix
96        self.suffix = suffix
97        self.width = width
98
99        if not (wrapfunc and callable(wrapfunc)):
100            self.wrapfunc = self.wrap_onspace
101        else:
102            self.wrapfunc = wrapfunc
103
104    def ustring(self, indent, color, msg, prefix="", suffix="", endc=None):
105        """Build the unicode string to be displayed."""
106        if endc is None:
107            endc = self.ENDC  # pylint: disable=no-member
108
109        indent *= " "
110        fmt = "{0}{1}{2}{3}{4}{5}"
111
112        try:
113            return fmt.format(indent, color, prefix, msg, endc, suffix)
114        except UnicodeDecodeError:
115            return fmt.format(
116                indent, color, prefix, salt.utils.data.decode(msg), endc, suffix
117            )
118
119    def wrap_onspace(self, text):
120
121        """
122        When the text inside the column is longer then the width, will split by space and continue on the next line."""
123
124        def _truncate(line, word):
125            return "{line}{part}{word}".format(
126                line=line,
127                part=" \n"[
128                    (
129                        len(line[line.rfind("\n") + 1 :]) + len(word.split("\n", 1)[0])
130                        >= self.width
131                    )
132                ],
133                word=word,
134            )
135
136        return reduce(_truncate, text.split(" "))
137
138    def prepare_rows(self, rows, indent, has_header):
139
140        """Prepare rows content to be displayed."""
141
142        out = []
143
144        def row_wrapper(row):
145            new_rows = [self.wrapfunc(item).split("\n") for item in row]
146            rows = []
147            for item in map(lambda *args: args, *new_rows):
148                if isinstance(item, (tuple, list)):
149                    rows.append([substr or "" for substr in item])
150                else:
151                    rows.append([item])
152            return rows
153
154        logical_rows = [row_wrapper(row) for row in rows]
155
156        columns = map(lambda *args: args, *reduce(operator.add, logical_rows))
157
158        max_widths = [max([len(str(item)) for item in column]) for column in columns]
159        row_separator = self.row_delimiter * (
160            len(self.prefix)
161            + len(self.suffix)
162            + sum(max_widths)
163            + len(self.delim) * (len(max_widths) - 1)
164        )
165
166        justify = self._JUSTIFY_MAP[self.justify.lower()]
167
168        if self.separate_rows:
169            out.append(
170                self.ustring(
171                    indent, self.LIGHT_GRAY, row_separator  # pylint: disable=no-member
172                )
173            )
174        for physical_rows in logical_rows:
175            for row in physical_rows:
176                line = (
177                    self.prefix
178                    + self.delim.join(
179                        [
180                            justify(str(item), width)
181                            for (item, width) in zip(row, max_widths)
182                        ]
183                    )
184                    + self.suffix
185                )
186                out.append(
187                    self.ustring(indent, self.WHITE, line)  # pylint: disable=no-member
188                )
189            if self.separate_rows or has_header:
190                out.append(
191                    self.ustring(
192                        indent,
193                        self.LIGHT_GRAY,  # pylint: disable=no-member
194                        row_separator,
195                    )
196                )
197                has_header = False
198        return out
199
200    def display_rows(self, rows, labels, indent):
201
202        """Prepares row content and displays."""
203
204        out = []
205
206        if not rows:
207            return out
208
209        first_row_type = type(rows[0])
210        # all rows must have the same datatype
211        consistent = True
212        for row in rows[1:]:
213            if type(row) != first_row_type:
214                consistent = False
215
216        if not consistent:
217            return out
218
219        if isinstance(labels, dict):
220            labels_temp = []
221            for key in sorted(labels):
222                labels_temp.append(labels[key])
223            labels = labels_temp
224
225        if first_row_type is dict:  # and all the others
226            temp_rows = []
227            if not labels:
228                labels = [
229                    str(label).replace("_", " ").title() for label in sorted(rows[0])
230                ]
231            for row in rows:
232                temp_row = []
233                for key in sorted(row):
234                    temp_row.append(str(row[key]))
235                temp_rows.append(temp_row)
236            rows = temp_rows
237        elif isinstance(rows[0], str):
238            rows = [
239                [row] for row in rows
240            ]  # encapsulate each row in a single-element list
241
242        labels_and_rows = [labels] + rows if labels else rows
243        has_header = self.has_header and labels
244
245        return self.prepare_rows(labels_and_rows, indent + 4, has_header)
246
247    def display(self, ret, indent, out, rows_key=None, labels_key=None):
248
249        """Display table(s)."""
250
251        rows = []
252        labels = None
253
254        if isinstance(ret, dict):
255            if not rows_key or (rows_key and rows_key in list(ret.keys())):
256                # either not looking for a specific key
257                # either looking and found in the current root
258                for key in sorted(ret):
259                    if rows_key and key != rows_key:
260                        continue  # if searching specifics, ignore anything else
261                    val = ret[key]
262                    if not rows_key:
263                        out.append(
264                            self.ustring(
265                                indent,
266                                self.DARK_GRAY,  # pylint: disable=no-member
267                                key,
268                                suffix=":",
269                            )
270                        )
271                        out.append(
272                            self.ustring(
273                                indent,
274                                self.DARK_GRAY,  # pylint: disable=no-member
275                                "----------",
276                            )
277                        )
278                    if isinstance(val, (list, tuple)):
279                        rows = val
280                        if labels_key:
281                            # at the same depth
282                            labels = ret.get(labels_key)  # if any
283                        out.extend(self.display_rows(rows, labels, indent))
284                    else:
285                        self.display(
286                            val,
287                            indent + 4,
288                            out,
289                            rows_key=rows_key,
290                            labels_key=labels_key,
291                        )
292            elif rows_key:
293                # dig deeper
294                for key in sorted(ret):
295                    val = ret[key]
296                    self.display(
297                        val, indent, out, rows_key=rows_key, labels_key=labels_key
298                    )  # same indent
299        elif isinstance(ret, (list, tuple)):
300            if not rows_key:
301                rows = ret
302                out.extend(self.display_rows(rows, labels, indent))
303
304        return out
305
306
307def output(ret, **kwargs):
308    """
309    Display the output as table.
310
311    Args:
312
313        * nested_indent: integer, specify the left alignment.
314        * has_header: boolean specifying if header should be displayed. Default: True.
315        * row_delimiter: character to separate rows. Default: ``_``.
316        * delim: character to separate columns. Default: ``" | "``.
317        * justify: text alignment. Default: ``center``.
318        * separate_rows: boolean specifying if row separator will be displayed between consecutive rows. Default: True.
319        * prefix: character at the beginning of the row. Default: ``"| "``.
320        * suffix: character at the end of the row. Default: ``" |"``.
321        * width: column max width. Default: ``50``.
322        * rows_key: display the rows under a specific key.
323        * labels_key: use the labels under a certain key. Otherwise will try to use the dictionary keys (if any).
324        * title: display title when only one table is selected (using the ``rows_key`` argument).
325    """
326
327    # to facilitate re-use
328    if "opts" in kwargs:
329        global __opts__  # pylint: disable=W0601
330        __opts__ = kwargs.pop("opts")
331
332    # Prefer kwargs before opts
333    base_indent = kwargs.get("nested_indent", 0) or __opts__.get(
334        "out.table.nested_indent", 0
335    )
336    rows_key = kwargs.get("rows_key") or __opts__.get("out.table.rows_key")
337    labels_key = kwargs.get("labels_key") or __opts__.get("out.table.labels_key")
338    title = kwargs.get("title") or __opts__.get("out.table.title")
339
340    class_kvargs = {}
341    argks = (
342        "has_header",
343        "row_delimiter",
344        "delim",
345        "justify",
346        "separate_rows",
347        "prefix",
348        "suffix",
349        "width",
350    )
351
352    for argk in argks:
353        argv = kwargs.get(argk) or __opts__.get("out.table.{key}".format(key=argk))
354        if argv is not None:
355            class_kvargs[argk] = argv
356
357    table = TableDisplay(**class_kvargs)
358
359    out = []
360    if title and rows_key:
361        out.append(
362            table.ustring(
363                base_indent,
364                title,
365                table.WHITE,  # pylint: disable=no-member
366                suffix="\n",
367            )
368        )
369
370    return "\n".join(
371        table.display(
372            salt.utils.data.decode(ret),
373            base_indent,
374            out,
375            rows_key=rows_key,
376            labels_key=labels_key,
377        )
378    )
379