aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorTim Harder <radhermit@gmail.com>2021-03-31 12:17:59 -0600
committerTim Harder <radhermit@gmail.com>2021-03-31 12:39:18 -0600
commit1f7316f6e5b851f8b1f7fed49e444046614b94e4 (patch)
treed487836ee3db2cf881d72a2135e36de723280f87
parenttests: update git repo default branch name (diff)
downloadpkgdev-1f7316f6e5b851f8b1f7fed49e444046614b94e4.tar.gz
pkgdev-1f7316f6e5b851f8b1f7fed49e444046614b94e4.tar.bz2
pkgdev-1f7316f6e5b851f8b1f7fed49e444046614b94e4.zip
pkgdev showkw: initial import, moved from pkgcore's pshowkw
-rw-r--r--.coveragerc2
-rw-r--r--README.rst2
-rw-r--r--src/pkgdev/_vendor/__init__.py1
-rw-r--r--src/pkgdev/_vendor/tabulate.py1503
-rw-r--r--src/pkgdev/scripts/pkgdev_showkw.py255
-rw-r--r--tests/scripts/test_pkgdev_showkw.py19
6 files changed, 1781 insertions, 1 deletions
diff --git a/.coveragerc b/.coveragerc
index c3dcf15..92b97a6 100644
--- a/.coveragerc
+++ b/.coveragerc
@@ -1,7 +1,7 @@
[run]
source = pkgdev
branch = True
-omit = src/*, tests/*
+omit = src/*, tests/*, */_vendor/*
[paths]
source = **/site-packages/pkgdev
diff --git a/README.rst b/README.rst
index d07cf10..838d418 100644
--- a/README.rst
+++ b/README.rst
@@ -14,6 +14,8 @@ pkgdev provides a collection of tools for Gentoo development including:
**pkgdev push**: scan commits for QA issues before pushing upstream
+**pkgdev showkw**: show package keywords
+
Dependencies
============
diff --git a/src/pkgdev/_vendor/__init__.py b/src/pkgdev/_vendor/__init__.py
new file mode 100644
index 0000000..c6f4642
--- /dev/null
+++ b/src/pkgdev/_vendor/__init__.py
@@ -0,0 +1 @@
+"""Vendored external modules with modifications."""
diff --git a/src/pkgdev/_vendor/tabulate.py b/src/pkgdev/_vendor/tabulate.py
new file mode 100644
index 0000000..16d245f
--- /dev/null
+++ b/src/pkgdev/_vendor/tabulate.py
@@ -0,0 +1,1503 @@
+# Copyright (c) 2011-2017 Sergey Astanin
+#
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to
+# the following conditions:
+#
+# The above copyright notice and this permission notice shall be
+# included in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+"""Pretty-print tabular data."""
+
+import math
+import re
+from collections import namedtuple
+from collections.abc import Iterable
+from functools import partial, reduce
+from itertools import zip_longest
+
+_none_type = type(None)
+_bool_type = bool
+_int_type = int
+_long_type = int
+_float_type = float
+_text_type = str
+_binary_type = bytes
+
+try:
+ import wcwidth # optional wide-character (CJK) support
+except ImportError:
+ wcwidth = None
+
+# minimum extra space in headers
+MIN_PADDING = 2
+
+# Whether or not to preserve leading/trailing whitespace in data.
+PRESERVE_WHITESPACE = False
+
+_DEFAULT_FLOATFMT="g"
+_DEFAULT_MISSINGVAL=""
+
+# if True, enable wide-character (CJK) support
+WIDE_CHARS_MODE = wcwidth is not None
+
+Line = namedtuple("Line", ["begin", "hline", "sep", "end"])
+
+DataRow = namedtuple("DataRow", ["begin", "sep", "end"])
+
+
+# A table structure is suppposed to be:
+#
+# --- lineabove ---------
+# headerrow
+# --- linebelowheader ---
+# datarow
+# --- linebewteenrows ---
+# ... (more datarows) ...
+# --- linebewteenrows ---
+# last datarow
+# --- linebelow ---------
+#
+# TableFormat's line* elements can be
+#
+# - either None, if the element is not used,
+# - or a Line tuple,
+# - or a function: [col_widths], [col_alignments] -> string.
+#
+# TableFormat's *row elements can be
+#
+# - either None, if the element is not used,
+# - or a DataRow tuple,
+# - or a function: [cell_values], [col_widths], [col_alignments] -> string.
+#
+# padding (an integer) is the amount of white space around data values.
+#
+# with_header_hide:
+#
+# - either None, to display all table elements unconditionally,
+# - or a list of elements not to be displayed if the table has column headers.
+
+TableFormat = namedtuple("TableFormat", [
+ "lineabove", "linebelowheader", "linebetweenrows", "linebelow",
+ "headerrow", "datarow", "padding", "with_header_hide",
+ "vertical_headers",
+])
+# TODO: use defaults param when >= python3.7 only
+# set default for vertical_headers params
+TableFormat.__new__.__defaults__ = (False,)
+
+
+def _pipe_segment_with_colons(align, colwidth):
+ """Return a segment of a horizontal line with optional colons which
+ indicate column's alignment (as in `pipe` output format)."""
+ w = colwidth
+ if align in ["right", "decimal"]:
+ return ('-' * (w - 1)) + ":"
+ elif align == "center":
+ return ":" + ('-' * (w - 2)) + ":"
+ elif align == "left":
+ return ":" + ('-' * (w - 1))
+ else:
+ return '-' * w
+
+
+def _pipe_line_with_colons(colwidths, colaligns):
+ """Return a horizontal line with optional colons to indicate column's
+ alignment (as in `pipe` output format)."""
+ segments = [_pipe_segment_with_colons(a, w) for a, w in zip(colaligns, colwidths)]
+ return "|" + "|".join(segments) + "|"
+
+
+def _mediawiki_row_with_attrs(separator, cell_values, colwidths, colaligns):
+ alignment = { "left": '',
+ "right": 'align="right"| ',
+ "center": 'align="center"| ',
+ "decimal": 'align="right"| ' }
+ # hard-coded padding _around_ align attribute and value together
+ # rather than padding parameter which affects only the value
+ values_with_attrs = [' ' + alignment.get(a, '') + c + ' '
+ for c, a in zip(cell_values, colaligns)]
+ colsep = separator*2
+ return (separator + colsep.join(values_with_attrs)).rstrip()
+
+
+def _textile_row_with_attrs(cell_values, colwidths, colaligns):
+ cell_values[0] += ' '
+ alignment = { "left": "<.", "right": ">.", "center": "=.", "decimal": ">." }
+ values = (alignment.get(a, '') + v for a, v in zip(colaligns, cell_values))
+ return '|' + '|'.join(values) + '|'
+
+
+def _html_begin_table_without_header(colwidths_ignore, colaligns_ignore):
+ # this table header will be suppressed if there is a header row
+ return "\n".join(["<table>", "<tbody>"])
+
+
+def _html_row_with_attrs(celltag, cell_values, colwidths, colaligns):
+ alignment = { "left": '',
+ "right": ' style="text-align: right;"',
+ "center": ' style="text-align: center;"',
+ "decimal": ' style="text-align: right;"' }
+ values_with_attrs = ["<{0}{1}>{2}</{0}>".format(celltag, alignment.get(a, ''), c)
+ for c, a in zip(cell_values, colaligns)]
+ rowhtml = "<tr>" + "".join(values_with_attrs).rstrip() + "</tr>"
+ if celltag == "th": # it's a header row, create a new table header
+ rowhtml = "\n".join(["<table>",
+ "<thead>",
+ rowhtml,
+ "</thead>",
+ "<tbody>"])
+ return rowhtml
+
+def _moin_row_with_attrs(celltag, cell_values, colwidths, colaligns, header=''):
+ alignment = { "left": '',
+ "right": '<style="text-align: right;">',
+ "center": '<style="text-align: center;">',
+ "decimal": '<style="text-align: right;">' }
+ values_with_attrs = ["{0}{1} {2} ".format(celltag,
+ alignment.get(a, ''),
+ header+c+header)
+ for c, a in zip(cell_values, colaligns)]
+ return "".join(values_with_attrs)+"||"
+
+def _latex_line_begin_tabular(colwidths, colaligns, booktabs=False):
+ alignment = { "left": "l", "right": "r", "center": "c", "decimal": "r" }
+ tabular_columns_fmt = "".join([alignment.get(a, "l") for a in colaligns])
+ return "\n".join(["\\begin{tabular}{" + tabular_columns_fmt + "}",
+ "\\toprule" if booktabs else "\\hline"])
+
+LATEX_ESCAPE_RULES = {r"&": r"\&", r"%": r"\%", r"$": r"\$", r"#": r"\#",
+ r"_": r"\_", r"^": r"\^{}", r"{": r"\{", r"}": r"\}",
+ r"~": r"\textasciitilde{}", "\\": r"\textbackslash{}",
+ r"<": r"\ensuremath{<}", r">": r"\ensuremath{>}"}
+
+def _latex_row(cell_values, colwidths, colaligns, escrules=LATEX_ESCAPE_RULES):
+ def escape_char(c):
+ return escrules.get(c, c)
+ escaped_values = ["".join(map(escape_char, cell)) for cell in cell_values]
+ rowfmt = DataRow("", "&", "\\\\")
+ return _build_simple_row(escaped_values, rowfmt)
+
+def _rst_escape_first_column(rows, headers):
+ def escape_empty(val):
+ if isinstance(val, (_text_type, _binary_type)) and not val.strip():
+ return ".."
+ else:
+ return val
+ new_headers = list(headers)
+ new_rows = []
+ if headers:
+ new_headers[0] = escape_empty(headers[0])
+ for row in rows:
+ new_row = list(row)
+ if new_row:
+ new_row[0] = escape_empty(row[0])
+ new_rows.append(new_row)
+ return new_rows, new_headers
+
+
+_table_formats = {
+ "simple":
+ TableFormat(lineabove=Line("", "-", " ", ""),
+ linebelowheader=Line("", "-", " ", ""),
+ linebetweenrows=None,
+ linebelow=Line("", "-", " ", ""),
+ headerrow=DataRow("", " ", ""),
+ datarow=DataRow("", " ", ""),
+ padding=0,
+ with_header_hide=["lineabove", "linebelow"],
+ ),
+ "plain":
+ TableFormat(lineabove=None, linebelowheader=None,
+ linebetweenrows=None, linebelow=None,
+ headerrow=DataRow("", " ", ""),
+ datarow=DataRow("", " ", ""),
+ padding=0, with_header_hide=None,
+ ),
+ "grid":
+ TableFormat(lineabove=Line("+", "-", "+", "+"),
+ linebelowheader=Line("+", "=", "+", "+"),
+ linebetweenrows=Line("+", "-", "+", "+"),
+ linebelow=Line("+", "-", "+", "+"),
+ headerrow=DataRow("|", "|", "|"),
+ datarow=DataRow("|", "|", "|"),
+ padding=1, with_header_hide=None,
+ ),
+ "fancy_grid":
+ TableFormat(lineabove=Line("╒", "═", "╤", "╕"),
+ linebelowheader=Line("╞", "═", "╪", "╡"),
+ linebetweenrows=Line("├", "─", "┼", "┤"),
+ linebelow=Line("╘", "═", "╧", "╛"),
+ headerrow=DataRow("│", "│", "│"),
+ datarow=DataRow("│", "│", "│"),
+ padding=1, with_header_hide=None,
+ ),
+ "github":
+ TableFormat(lineabove=Line("|", "-", "|", "|"),
+ linebelowheader=Line("|", "-", "|", "|"),
+ linebetweenrows=None,
+ linebelow=None,
+ headerrow=DataRow("|", "|", "|"),
+ datarow=DataRow("|", "|", "|"),
+ padding=1,
+ with_header_hide=["lineabove"],
+ ),
+ "pipe":
+ TableFormat(lineabove=_pipe_line_with_colons,
+ linebelowheader=_pipe_line_with_colons,
+ linebetweenrows=None,
+ linebelow=None,
+ headerrow=DataRow("|", "|", "|"),
+ datarow=DataRow("|", "|", "|"),
+ padding=1,
+ with_header_hide=["lineabove"],
+ ),
+ "orgtbl":
+ TableFormat(lineabove=None,
+ linebelowheader=Line("|", "-", "+", "|"),
+ linebetweenrows=None,
+ linebelow=None,
+ headerrow=DataRow("|", "|", "|"),
+ datarow=DataRow("|", "|", "|"),
+ padding=1, with_header_hide=None,
+ ),
+ "jira":
+ TableFormat(lineabove=None,
+ linebelowheader=None,
+ linebetweenrows=None,
+ linebelow=None,
+ headerrow=DataRow("||", "||", "||"),
+ datarow=DataRow("|", "|", "|"),
+ padding=1, with_header_hide=None,
+ ),
+ "presto":
+ TableFormat(lineabove=None,
+ linebelowheader=Line("", "-", "+", ""),
+ linebetweenrows=None,
+ linebelow=None,
+ headerrow=DataRow("", "|", ""),
+ datarow=DataRow("", "|", ""),
+ padding=1, with_header_hide=None,
+ ),
+ "showkw":
+ TableFormat(lineabove=None,
+ linebelowheader=Line("", "-", "-", ""),
+ linebetweenrows=None,
+ linebelow=None,
+ headerrow=DataRow("", "", ""),
+ datarow=DataRow("", "", ""),
+ padding=1, with_header_hide=None,
+ vertical_headers=True,
+ ),
+ "psql":
+ TableFormat(lineabove=Line("+", "-", "+", "+"),
+ linebelowheader=Line("|", "-", "+", "|"),
+ linebetweenrows=None,
+ linebelow=Line("+", "-", "+", "+"),
+ headerrow=DataRow("|", "|", "|"),
+ datarow=DataRow("|", "|", "|"),
+ padding=1, with_header_hide=None,
+ ),
+ "rst":
+ TableFormat(lineabove=Line("", "=", " ", ""),
+ linebelowheader=Line("", "=", " ", ""),
+ linebetweenrows=None,
+ linebelow=Line("", "=", " ", ""),
+ headerrow=DataRow("", " ", ""),
+ datarow=DataRow("", " ", ""),
+ padding=0, with_header_hide=None,
+ ),
+ "mediawiki":
+ TableFormat(lineabove=Line("{| class=\"wikitable\" style=\"text-align: left;\"",
+ "", "", "\n|+ <!-- caption -->\n|-"),
+ linebelowheader=Line("|-", "", "", ""),
+ linebetweenrows=Line("|-", "", "", ""),
+ linebelow=Line("|}", "", "", ""),
+ headerrow=partial(_mediawiki_row_with_attrs, "!"),
+ datarow=partial(_mediawiki_row_with_attrs, "|"),
+ padding=0, with_header_hide=None,
+ ),
+ "moinmoin":
+ TableFormat(lineabove=None,
+ linebelowheader=None,
+ linebetweenrows=None,
+ linebelow=None,
+ headerrow=partial(_moin_row_with_attrs,"||",header="'''"),
+ datarow=partial(_moin_row_with_attrs,"||"),
+ padding=1, with_header_hide=None,
+ ),
+ "youtrack":
+ TableFormat(lineabove=None,
+ linebelowheader=None,
+ linebetweenrows=None,
+ linebelow=None,
+ headerrow=DataRow("|| ", " || ", " || "),
+ datarow=DataRow("| ", " | ", " |"),
+ padding=1, with_header_hide=None,
+ ),
+ "html":
+ TableFormat(lineabove=_html_begin_table_without_header,
+ linebelowheader="",
+ linebetweenrows=None,
+ linebelow=Line("</tbody>\n</table>", "", "", ""),
+ headerrow=partial(_html_row_with_attrs, "th"),
+ datarow=partial(_html_row_with_attrs, "td"),
+ padding=0, with_header_hide=["lineabove"],
+ ),
+ "latex":
+ TableFormat(lineabove=_latex_line_begin_tabular,
+ linebelowheader=Line("\\hline", "", "", ""),
+ linebetweenrows=None,
+ linebelow=Line("\\hline\n\\end{tabular}", "", "", ""),
+ headerrow=_latex_row,
+ datarow=_latex_row,
+ padding=1, with_header_hide=None,
+ ),
+ "latex_raw":
+ TableFormat(lineabove=_latex_line_begin_tabular,
+ linebelowheader=Line("\\hline", "", "", ""),
+ linebetweenrows=None,
+ linebelow=Line("\\hline\n\\end{tabular}", "", "", ""),
+ headerrow=partial(_latex_row, escrules={}),
+ datarow=partial(_latex_row, escrules={}),
+ padding=1, with_header_hide=None,
+ ),
+ "latex_booktabs":
+ TableFormat(lineabove=partial(_latex_line_begin_tabular, booktabs=True),
+ linebelowheader=Line("\\midrule", "", "", ""),
+ linebetweenrows=None,
+ linebelow=Line("\\bottomrule\n\\end{tabular}", "", "", ""),
+ headerrow=_latex_row,
+ datarow=_latex_row,
+ padding=1, with_header_hide=None,
+ ),
+ "tsv":
+ TableFormat(lineabove=None, linebelowheader=None,
+ linebetweenrows=None, linebelow=None,
+ headerrow=DataRow("", "\t", ""),
+ datarow=DataRow("", "\t", ""),
+ padding=0, with_header_hide=None,
+ ),
+ "textile":
+ TableFormat(lineabove=None, linebelowheader=None,
+ linebetweenrows=None, linebelow=None,
+ headerrow=DataRow("|_. ", "|_.", "|"),
+ datarow=_textile_row_with_attrs,
+ padding=1, with_header_hide=None,
+ ),
+}
+
+
+tabulate_formats = sorted(_table_formats.keys())
+
+# The table formats for which multiline cells will be folded into subsequent
+# table rows. The key is the original format specified at the API. The value is
+# the format that will be used to represent the original format.
+multiline_formats = {
+ "plain": "plain",
+ "simple": "simple",
+ "grid": "grid",
+ "fancy_grid": "fancy_grid",
+ "pipe": "pipe",
+ "orgtbl": "orgtbl",
+ "jira": "jira",
+ "presto": "presto",
+ "psql": "psql",
+ "rst": "rst",
+}
+
+# TODO: Add multiline support for the remaining table formats:
+# - mediawiki: Replace \n with <br>
+# - moinmoin: TBD
+# - youtrack: TBD
+# - html: Replace \n with <br>
+# - latex*: Use "makecell" package: In header, replace X\nY with
+# \thead{X\\Y} and in data row, replace X\nY with \makecell{X\\Y}
+# - tsv: TBD
+# - textile: Replace \n with <br/> (must be well-formed XML)
+
+_multiline_codes = re.compile(r"\r|\n|\r\n")
+_multiline_codes_bytes = re.compile(rb"\r|\n|\r\n")
+_invisible_codes = re.compile(r"\x1b\[\d+[;\d]*m|\x1b\[\d*\;\d*\;\d*m") # ANSI color codes
+_invisible_codes_bytes = re.compile(rb"\x1b\[\d+[;\d]*m|\x1b\[\d*\;\d*\;\d*m") # ANSI color codes
+
+
+def simple_separated_format(separator):
+ """Construct a simple TableFormat with columns separated by a separator.
+
+ >>> tsv = simple_separated_format("\\t") ; \
+ tabulate([["foo", 1], ["spam", 23]], tablefmt=tsv) == 'foo \\t 1\\nspam\\t23'
+ True
+
+ """
+ return TableFormat(None, None, None, None,
+ headerrow=DataRow('', separator, ''),
+ datarow=DataRow('', separator, ''),
+ padding=0, with_header_hide=None)
+
+
+def _isconvertible(conv, string):
+ try:
+ n = conv(string)
+ return True
+ except (ValueError, TypeError):
+ return False
+
+
+def _isnumber(string):
+ """
+ >>> _isnumber("123.45")
+ True
+ >>> _isnumber("123")
+ True
+ >>> _isnumber("spam")
+ False
+ >>> _isnumber("123e45678")
+ False
+ >>> _isnumber("inf")
+ True
+ """
+ if not _isconvertible(float, string):
+ return False
+ elif isinstance(string, (_text_type, _binary_type)) and (
+ math.isinf(float(string)) or math.isnan(float(string))):
+ return string.lower() in ['inf', '-inf', 'nan']
+ return True
+
+
+def _isint(string, inttype=int):
+ """
+ >>> _isint("123")
+ True
+ >>> _isint("123.45")
+ False
+ """
+ return type(string) is inttype or\
+ (isinstance(string, _binary_type) or isinstance(string, _text_type))\
+ and\
+ _isconvertible(inttype, string)
+
+
+def _isbool(string):
+ """
+ >>> _isbool(True)
+ True
+ >>> _isbool("False")
+ True
+ >>> _isbool(1)
+ False
+ """
+ return type(string) is _bool_type or\
+ (isinstance(string, (_binary_type, _text_type))\
+ and\
+ string in ("True", "False"))
+
+
+def _type(string, has_invisible=True, numparse=True):
+ """The least generic type (type(None), int, float, str, unicode).
+
+ >>> _type(None) is type(None)
+ True
+ >>> _type("foo") is type("")
+ True
+ >>> _type("1") is type(1)
+ True
+ >>> _type('\x1b[31m42\x1b[0m') is type(42)
+ True
+ >>> _type('\x1b[31m42\x1b[0m') is type(42)
+ True
+
+ """
+
+ if has_invisible and \
+ (isinstance(string, _text_type) or isinstance(string, _binary_type)):
+ string = _strip_invisible(string)
+
+ if string is None:
+ return _none_type
+ elif hasattr(string, "isoformat"): # datetime.datetime, date, and time
+ return _text_type
+ elif _isbool(string):
+ return _bool_type
+ elif _isint(string) and numparse:
+ return int
+ elif _isint(string, _long_type) and numparse:
+ return int
+ elif _isnumber(string) and numparse:
+ return float
+ elif isinstance(string, _binary_type):
+ return _binary_type
+ else:
+ return _text_type
+
+
+def _afterpoint(string):
+ """Symbols after a decimal point, -1 if the string lacks the decimal point.
+
+ >>> _afterpoint("123.45")
+ 2
+ >>> _afterpoint("1001")
+ -1
+ >>> _afterpoint("eggs")
+ -1
+ >>> _afterpoint("123e45")
+ 2
+
+ """
+ if _isnumber(string):
+ if _isint(string):
+ return -1
+ else:
+ pos = string.rfind(".")
+ pos = string.lower().rfind("e") if pos < 0 else pos
+ if pos >= 0:
+ return len(string) - pos - 1
+ else:
+ return -1 # no point
+ else:
+ return -1 # not a number
+
+
+def _padleft(width, s):
+ """Flush right.
+
+ >>> _padleft(6, '\u044f\u0439\u0446\u0430') == ' \u044f\u0439\u0446\u0430'
+ True
+
+ """
+ fmt = "{0:>%ds}" % width
+ return fmt.format(s)
+
+
+def _padright(width, s):
+ """Flush left.
+
+ >>> _padright(6, '\u044f\u0439\u0446\u0430') == '\u044f\u0439\u0446\u0430 '
+ True
+
+ """
+ fmt = "{0:<%ds}" % width
+ return fmt.format(s)
+
+
+def _padboth(width, s):
+ """Center string.
+
+ >>> _padboth(6, '\u044f\u0439\u0446\u0430') == ' \u044f\u0439\u0446\u0430 '
+ True
+
+ """
+ fmt = "{0:^%ds}" % width
+ return fmt.format(s)
+
+
+def _padnone(ignore_width, s):
+ return s
+
+
+def _strip_invisible(s):
+ "Remove invisible ANSI color codes."
+ if isinstance(s, _text_type):
+ return re.sub(_invisible_codes, "", s)
+ else: # a bytestring
+ return re.sub(_invisible_codes_bytes, "", s)
+
+
+def _visible_width(s):
+ """Visible width of a printed string. ANSI color codes are removed.
+
+ >>> _visible_width('\x1b[31mhello\x1b[0m'), _visible_width("world")
+ (5, 5)
+
+ """
+ # optional wide-character support
+ if wcwidth is not None and WIDE_CHARS_MODE:
+ len_fn = wcwidth.wcswidth
+ else:
+ len_fn = len
+ if isinstance(s, _text_type) or isinstance(s, _binary_type):
+ return len_fn(_strip_invisible(s))
+ else:
+ return len_fn(_text_type(s))
+
+
+def _is_multiline(s):
+ if isinstance(s, _text_type):
+ return bool(re.search(_multiline_codes, s))
+ else: # a bytestring
+ return bool(re.search(_multiline_codes_bytes, s))
+
+
+def _multiline_width(multiline_s, line_width_fn=len):
+ """Visible width of a potentially multiline content."""
+ return max(map(line_width_fn, re.split("[\r\n]", multiline_s)))
+
+
+def _choose_width_fn(has_invisible, enable_widechars, is_multiline):
+ """Return a function to calculate visible cell width."""
+ if has_invisible:
+ line_width_fn = _visible_width
+ elif enable_widechars: # optional wide-character support if available
+ line_width_fn = wcwidth.wcswidth
+ else:
+ line_width_fn = len
+ if is_multiline:
+ width_fn = lambda s: _multiline_width(s, line_width_fn)
+ else:
+ width_fn = line_width_fn
+ return width_fn
+
+
+def _align_column_choose_padfn(strings, alignment, has_invisible):
+ if alignment == "right":
+ if not PRESERVE_WHITESPACE:
+ strings = [s.strip() for s in strings]
+ padfn = _padleft
+ elif alignment == "center":
+ if not PRESERVE_WHITESPACE:
+ strings = [s.strip() for s in strings]
+ padfn = _padboth
+ elif alignment == "decimal":
+ if has_invisible:
+ decimals = [_afterpoint(_strip_invisible(s)) for s in strings]
+ else:
+ decimals = [_afterpoint(s) for s in strings]
+ maxdecimals = max(decimals)
+ strings = [s + (maxdecimals - decs) * " "
+ for s, decs in zip(strings, decimals)]
+ padfn = _padleft
+ elif not alignment:
+ padfn = _padnone
+ else:
+ if not PRESERVE_WHITESPACE:
+ strings = [s.strip() for s in strings]
+ padfn = _padright
+ return strings, padfn
+
+
+def _align_column(strings, alignment, minwidth=0,
+ has_invisible=True, enable_widechars=False, is_multiline=False):
+ """[string] -> [padded_string]"""
+ strings, padfn = _align_column_choose_padfn(strings, alignment, has_invisible)
+ width_fn = _choose_width_fn(has_invisible, enable_widechars, is_multiline)
+
+ s_widths = list(map(width_fn, strings))
+ maxwidth = max(max(s_widths), minwidth)
+ # TODO: refactor column alignment in single-line and multiline modes
+ if is_multiline:
+ if not enable_widechars and not has_invisible:
+ padded_strings = [
+ "\n".join([padfn(maxwidth, s) for s in ms.splitlines()])
+ for ms in strings]
+ else:
+ # enable wide-character width corrections
+ s_lens = [max((len(s) for s in re.split("[\r\n]", ms))) for ms in strings]
+ visible_widths = [maxwidth - (w - l) for w, l in zip(s_widths, s_lens)]
+ # wcswidth and _visible_width don't count invisible characters;
+ # padfn doesn't need to apply another correction
+ padded_strings = ["\n".join([padfn(w, s) for s in (ms.splitlines() or ms)])
+ for ms, w in zip(strings, visible_widths)]
+ else: # single-line cell values
+ if not enable_widechars and not has_invisible:
+ padded_strings = [padfn(maxwidth, s) for s in strings]
+ else:
+ # enable wide-character width corrections
+ s_lens = list(map(len, strings))
+ visible_widths = [maxwidth - (w - l) for w, l in zip(s_widths, s_lens)]
+ # wcswidth and _visible_width don't count invisible characters;
+ # padfn doesn't need to apply another correction
+ padded_strings = [padfn(w, s) for s, w in zip(strings, visible_widths)]
+ return padded_strings
+
+
+def _more_generic(type1, type2):
+ types = { _none_type: 0, _bool_type: 1, int: 2, float: 3, _binary_type: 4, _text_type: 5 }
+ invtypes = { 5: _text_type, 4: _binary_type, 3: float, 2: int, 1: _bool_type, 0: _none_type }
+ moregeneric = max(types.get(type1, 5), types.get(type2, 5))
+ return invtypes[moregeneric]
+
+
+def _column_type(strings, has_invisible=True, numparse=True):
+ """The least generic type all column values are convertible to.
+
+ >>> _column_type([True, False]) is _bool_type
+ True
+ >>> _column_type(["1", "2"]) is _int_type
+ True
+ >>> _column_type(["1", "2.3"]) is _float_type
+ True
+ >>> _column_type(["1", "2.3", "four"]) is _text_type
+ True
+ >>> _column_type(["four", '\u043f\u044f\u0442\u044c']) is _text_type
+ True
+ >>> _column_type([None, "brux"]) is _text_type
+ True
+ >>> _column_type([1, 2, None]) is _int_type
+ True
+ >>> import datetime as dt
+ >>> _column_type([dt.datetime(1991,2,19), dt.time(17,35)]) is _text_type
+ True
+
+ """
+ types = [_type(s, has_invisible, numparse) for s in strings ]
+ return reduce(_more_generic, types, _bool_type)
+
+
+def _format(val, valtype, floatfmt, missingval="", has_invisible=True):
+ """Format a value accoding to its type.
+
+ Unicode is supported:
+
+ >>> hrow = ['\u0431\u0443\u043a\u0432\u0430', '\u0446\u0438\u0444\u0440\u0430'] ; \
+ tbl = [['\u0430\u0437', 2], ['\u0431\u0443\u043a\u0438', 4]] ; \
+ good_result = '\\u0431\\u0443\\u043a\\u0432\\u0430 \\u0446\\u0438\\u0444\\u0440\\u0430\\n------- -------\\n\\u0430\\u0437 2\\n\\u0431\\u0443\\u043a\\u0438 4' ; \
+ tabulate(tbl, headers=hrow) == good_result
+ True
+
+ """
+ if val is None:
+ return missingval
+
+ if valtype in [int, _text_type]:
+ return "{0}".format(val)
+ elif valtype is _binary_type:
+ try:
+ return _text_type(val, "ascii")
+ except TypeError:
+ return _text_type(val)
+ elif valtype is float:
+ is_a_colored_number = has_invisible and isinstance(val, (_text_type, _binary_type))
+ if is_a_colored_number:
+ raw_val = _strip_invisible(val)
+ formatted_val = format(float(raw_val), floatfmt)
+ return val.replace(raw_val, formatted_val)
+ else:
+ return format(float(val), floatfmt)
+ else:
+ return "{0}".format(val)
+
+
+def _align_header(header, alignment, width, visible_width, is_multiline=False, width_fn=None):
+ "Pad string header to width chars given known visible_width of the header."
+ if is_multiline:
+ header_lines = re.split(_multiline_codes, header)
+ padded_lines = [_align_header(h, alignment, width, width_fn(h)) for h in header_lines]
+ return "\n".join(padded_lines)
+ # else: not multiline
+ ninvisible = len(header) - visible_width
+ width += ninvisible
+ if alignment == "left":
+ return _padright(width, header)
+ elif alignment == "center":
+ return _padboth(width, header)
+ elif not alignment:
+ return "{0}".format(header)
+ else:
+ return _padleft(width, header)
+
+
+def _prepend_row_index(rows, index):
+ """Add a left-most index column."""
+ if index is None or index is False:
+ return rows
+ if len(index) != len(rows):
+ print('index=', index)
+ print('rows=', rows)
+ raise ValueError('index must be as long as the number of data rows')
+ rows = [[v]+list(row) for v,row in zip(index, rows)]
+ return rows
+
+
+def _bool(val):
+ "A wrapper around standard bool() which doesn't throw on NumPy arrays"
+ try:
+ return bool(val)
+ except ValueError: # val is likely to be a numpy array with many elements
+ return False
+
+
+def _normalize_tabular_data(tabular_data, headers, showindex="default"):
+ """Transform a supported data type to a list of lists, and a list of headers.
+
+ Supported tabular data types:
+
+ * list-of-lists or another iterable of iterables
+
+ * list of named tuples (usually used with headers="keys")
+
+ * list of dicts (usually used with headers="keys")
+
+ * list of OrderedDicts (usually used with headers="keys")
+
+ * 2D NumPy arrays
+
+ * NumPy record arrays (usually used with headers="keys")
+
+ * dict of iterables (usually used with headers="keys")
+
+ * pandas.DataFrame (usually used with headers="keys")
+
+ The first row can be used as headers if headers="firstrow",
+ column indices can be used as headers if headers="keys".
+
+ If showindex="default", show row indices of the pandas.DataFrame.
+ If showindex="always", show row indices for all types of data.
+ If showindex="never", don't show row indices for all types of data.
+ If showindex is an iterable, show its values as row indices.
+
+ """
+
+ try:
+ bool(headers)
+ is_headers2bool_broken = False
+ except ValueError: # numpy.ndarray, pandas.core.index.Index, ...
+ is_headers2bool_broken = True
+ headers = list(headers)
+
+ index = None
+ if hasattr(tabular_data, "keys") and hasattr(tabular_data, "values"):
+ # dict-like and pandas.DataFrame?
+ if hasattr(tabular_data.values, "__call__"):
+ # likely a conventional dict
+ keys = tabular_data.keys()
+ rows = list(zip_longest(*tabular_data.values())) # columns have to be transposed
+ elif hasattr(tabular_data, "index"):
+ # values is a property, has .index => it's likely a pandas.DataFrame (pandas 0.11.0)
+ keys = list(tabular_data)
+ if tabular_data.index.name is not None:
+ if isinstance(tabular_data.index.name, list):
+ keys[:0] = tabular_data.index.name
+ else:
+ keys[:0] = [tabular_data.index.name]
+ vals = tabular_data.values # values matrix doesn't need to be transposed
+ # for DataFrames add an index per default
+ index = list(tabular_data.index)
+ rows = [list(row) for row in vals]
+ else:
+ raise ValueError("tabular data doesn't appear to be a dict or a DataFrame")
+
+ if headers == "keys":
+ headers = list(map(_text_type,keys)) # headers should be strings
+
+ else: # it's a usual an iterable of iterables, or a NumPy array
+ rows = list(tabular_data)
+
+ if (headers == "keys" and not rows):
+ # an empty table (issue #81)
+ headers = []
+ elif (headers == "keys" and
+ hasattr(tabular_data, "dtype") and
+ getattr(tabular_data.dtype, "names")):
+ # numpy record array
+ headers = tabular_data.dtype.names
+ elif (headers == "keys"
+ and len(rows) > 0
+ and isinstance(rows[0], tuple)
+ and hasattr(rows[0], "_fields")):
+ # namedtuple
+ headers = list(map(_text_type, rows[0]._fields))
+ elif (len(rows) > 0
+ and isinstance(rows[0], dict)):
+ # dict or OrderedDict
+ uniq_keys = set() # implements hashed lookup
+ keys = [] # storage for set
+ if headers == "firstrow":
+ firstdict = rows[0] if len(rows) > 0 else {}
+ keys.extend(firstdict.keys())
+ uniq_keys.update(keys)
+ rows = rows[1:]
+ for row in rows:
+ for k in row.keys():
+ #Save unique items in input order
+ if k not in uniq_keys:
+ keys.append(k)
+ uniq_keys.add(k)
+ if headers == 'keys':
+ headers = keys
+ elif isinstance(headers, dict):
+ # a dict of headers for a list of dicts
+ headers = [headers.get(k, k) for k in keys]
+ headers = list(map(_text_type, headers))
+ elif headers == "firstrow":
+ if len(rows) > 0:
+ headers = [firstdict.get(k, k) for k in keys]
+ headers = list(map(_text_type, headers))
+ else:
+ headers = []
+ elif headers:
+ raise ValueError('headers for a list of dicts is not a dict or a keyword')
+ rows = [[row.get(k) for k in keys] for row in rows]
+
+ elif (headers == "keys"
+ and hasattr(tabular_data, "description")
+ and hasattr(tabular_data, "fetchone")
+ and hasattr(tabular_data, "rowcount")):
+ # Python Database API cursor object (PEP 0249)
+ # print tabulate(cursor, headers='keys')
+ headers = [column[0] for column in tabular_data.description]
+
+ elif headers == "keys" and len(rows) > 0:
+ # keys are column indices
+ headers = list(map(_text_type, range(len(rows[0]))))
+
+ # take headers from the first row if necessary
+ if headers == "firstrow" and len(rows) > 0:
+ if index is not None:
+ headers = [index[0]] + list(rows[0])
+ index = index[1:]
+ else:
+ headers = rows[0]
+ headers = list(map(_text_type, headers)) # headers should be strings
+ rows = rows[1:]
+
+ headers = list(map(_text_type,headers))
+ rows = list(map(list,rows))
+
+ # add or remove an index column
+ showindex_is_a_str = type(showindex) in [_text_type, _binary_type]
+ if showindex == "default" and index is not None:
+ rows = _prepend_row_index(rows, index)
+ elif isinstance(showindex, Iterable) and not showindex_is_a_str:
+ rows = _prepend_row_index(rows, list(showindex))
+ elif showindex == "always" or (_bool(showindex) and not showindex_is_a_str):
+ if index is None:
+ index = list(range(len(rows)))
+ rows = _prepend_row_index(rows, index)
+ elif showindex == "never" or (not _bool(showindex) and not showindex_is_a_str):
+ pass
+
+ # pad with empty headers for initial columns if necessary
+ if headers and len(rows) > 0:
+ nhs = len(headers)
+ ncols = len(rows[0])
+ if nhs < ncols:
+ headers = [""]*(ncols - nhs) + headers
+
+ return rows, headers
+
+
+
+def tabulate(tabular_data, headers=(), tablefmt="simple",
+ floatfmt=_DEFAULT_FLOATFMT, numalign="decimal", stralign="left",
+ missingval=_DEFAULT_MISSINGVAL, showindex="default", disable_numparse=False,
+ colalign=None):
+ """Format a fixed width table for pretty printing.
+
+ >>> print(tabulate([[1, 2.34], [-56, "8.999"], ["2", "10001"]]))
+ --- ---------
+ 1 2.34
+ -56 8.999
+ 2 10001
+ --- ---------
+
+ The first required argument (`tabular_data`) can be a
+ list-of-lists (or another iterable of iterables), a list of named
+ tuples, a dictionary of iterables, an iterable of dictionaries,
+ a two-dimensional NumPy array, NumPy record array, or a Pandas'
+ dataframe.
+
+
+ Table headers
+ -------------
+
+ To print nice column headers, supply the second argument (`headers`):
+
+ - `headers` can be an explicit list of column headers
+ - if `headers="firstrow"`, then the first row of data is used
+ - if `headers="keys"`, then dictionary keys or column indices are used
+
+ Otherwise a headerless table is produced.
+
+ If the number of headers is less than the number of columns, they
+ are supposed to be names of the last columns. This is consistent
+ with the plain-text format of R and Pandas' dataframes.
+
+ >>> print(tabulate([["sex","age"],["Alice","F",24],["Bob","M",19]],
+ ... headers="firstrow"))
+ sex age
+ ----- ----- -----
+ Alice F 24
+ Bob M 19
+
+ By default, pandas.DataFrame data have an additional column called
+ row index. To add a similar column to all other types of data,
+ use `showindex="always"` or `showindex=True`. To suppress row indices
+ for all types of data, pass `showindex="never" or `showindex=False`.
+ To add a custom row index column, pass `showindex=some_iterable`.
+
+ >>> print(tabulate([["F",24],["M",19]], showindex="always"))
+ - - --
+ 0 F 24
+ 1 M 19
+ - - --
+
+
+ Column alignment
+ ----------------
+
+ `tabulate` tries to detect column types automatically, and aligns
+ the values properly. By default it aligns decimal points of the
+ numbers (or flushes integer numbers to the right), and flushes
+ everything else to the left. Possible column alignments
+ (`numalign`, `stralign`) are: "right", "center", "left", "decimal"
+ (only for `numalign`), and None (to disable alignment).
+
+
+ Table formats
+ -------------
+
+ `floatfmt` is a format specification used for columns which
+ contain numeric data with a decimal point. This can also be
+ a list or tuple of format strings, one per column.
+
+ `None` values are replaced with a `missingval` string (like
+ `floatfmt`, this can also be a list of values for different
+ columns):
+
+ >>> print(tabulate([["spam", 1, None],
+ ... ["eggs", 42, 3.14],
+ ... ["other", None, 2.7]], missingval="?"))
+ ----- -- ----
+ spam 1 ?
+ eggs 42 3.14
+ other ? 2.7
+ ----- -- ----
+
+ Various plain-text table formats (`tablefmt`) are supported:
+ 'plain', 'simple', 'grid', 'pipe', 'orgtbl', 'rst', 'mediawiki',
+ 'latex', 'latex_raw' and 'latex_booktabs'. Variable `tabulate_formats`
+ contains the list of currently supported formats.
+
+ "plain" format doesn't use any pseudographics to draw tables,
+ it separates columns with a double space:
+
+ >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]],
+ ... ["strings", "numbers"], "plain"))
+ strings numbers
+ spam 41.9999
+ eggs 451
+
+ >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]], tablefmt="plain"))
+ spam 41.9999
+ eggs 451
+
+ "simple" format is like Pandoc simple_tables:
+
+ >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]],
+ ... ["strings", "numbers"], "simple"))
+ strings numbers
+ --------- ---------
+ spam 41.9999
+ eggs 451
+
+ >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]], tablefmt="simple"))
+ ---- --------
+ spam 41.9999
+ eggs 451
+ ---- --------
+
+ "grid" is similar to tables produced by Emacs table.el package or
+ Pandoc grid_tables:
+
+ >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]],
+ ... ["strings", "numbers"], "grid"))
+ +-----------+-----------+
+ | strings | numbers |
+ +===========+===========+
+ | spam | 41.9999 |
+ +-----------+-----------+
+ | eggs | 451 |
+ +-----------+-----------+
+
+ >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]], tablefmt="grid"))
+ +------+----------+
+ | spam | 41.9999 |
+ +------+----------+
+ | eggs | 451 |
+ +------+----------+
+
+ "fancy_grid" draws a grid using box-drawing characters:
+
+ >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]],
+ ... ["strings", "numbers"], "fancy_grid"))
+ ╒═══════════╤═══════════╕
+ │ strings │ numbers │
+ ╞═══════════╪═══════════╡
+ │ spam │ 41.9999 │
+ ├───────────┼───────────┤
+ │ eggs │ 451 │
+ ╘═══════════╧═══════════╛
+
+ "pipe" is like tables in PHP Markdown Extra extension or Pandoc
+ pipe_tables:
+
+ >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]],
+ ... ["strings", "numbers"], "pipe"))
+ | strings | numbers |
+ |:----------|----------:|
+ | spam | 41.9999 |
+ | eggs | 451 |
+
+ "presto" is like tables produce by the Presto CLI:
+
+ >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]],
+ ... ["strings", "numbers"], "presto"))
+ strings | numbers
+ -----------+-----------
+ spam | 41.9999
+ eggs | 451
+
+ >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]], tablefmt="pipe"))
+ |:-----|---------:|
+ | spam | 41.9999 |
+ | eggs | 451 |
+
+ "orgtbl" is like tables in Emacs org-mode and orgtbl-mode. They
+ are slightly different from "pipe" format by not using colons to
+ define column alignment, and using a "+" sign to indicate line
+ intersections:
+
+ >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]],
+ ... ["strings", "numbers"], "orgtbl"))
+ | strings | numbers |
+ |-----------+-----------|
+ | spam | 41.9999 |
+ | eggs | 451 |
+
+
+ >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]], tablefmt="orgtbl"))
+ | spam | 41.9999 |
+ | eggs | 451 |
+
+ "rst" is like a simple table format from reStructuredText; please
+ note that reStructuredText accepts also "grid" tables:
+
+ >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]],
+ ... ["strings", "numbers"], "rst"))
+ ========= =========
+ strings numbers
+ ========= =========
+ spam 41.9999
+ eggs 451
+ ========= =========
+
+ >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]], tablefmt="rst"))
+ ==== ========
+ spam 41.9999
+ eggs 451
+ ==== ========
+
+ "mediawiki" produces a table markup used in Wikipedia and on other
+ MediaWiki-based sites:
+
+ >>> print(tabulate([["strings", "numbers"], ["spam", 41.9999], ["eggs", "451.0"]],
+ ... headers="firstrow", tablefmt="mediawiki"))
+ {| class="wikitable" style="text-align: left;"
+ |+ <!-- caption -->
+ |-
+ ! strings !! align="right"| numbers
+ |-
+ | spam || align="right"| 41.9999
+ |-
+ | eggs || align="right"| 451
+ |}
+
+ "html" produces HTML markup:
+
+ >>> print(tabulate([["strings", "numbers"], ["spam", 41.9999], ["eggs", "451.0"]],
+ ... headers="firstrow", tablefmt="html"))
+ <table>
+ <thead>
+ <tr><th>strings </th><th style="text-align: right;"> numbers</th></tr>
+ </thead>
+ <tbody>
+ <tr><td>spam </td><td style="text-align: right;"> 41.9999</td></tr>
+ <tr><td>eggs </td><td style="text-align: right;"> 451 </td></tr>
+ </tbody>
+ </table>
+
+ "latex" produces a tabular environment of LaTeX document markup:
+
+ >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]], tablefmt="latex"))
+ \\begin{tabular}{lr}
+ \\hline
+ spam & 41.9999 \\\\
+ eggs & 451 \\\\
+ \\hline
+ \\end{tabular}
+
+ "latex_raw" is similar to "latex", but doesn't escape special characters,
+ such as backslash and underscore, so LaTeX commands may embedded into
+ cells' values:
+
+ >>> print(tabulate([["spam$_9$", 41.9999], ["\\\\emph{eggs}", "451.0"]], tablefmt="latex_raw"))
+ \\begin{tabular}{lr}
+ \\hline
+ spam$_9$ & 41.9999 \\\\
+ \\emph{eggs} & 451 \\\\
+ \\hline
+ \\end{tabular}
+
+ "latex_booktabs" produces a tabular environment of LaTeX document markup
+ using the booktabs.sty package:
+
+ >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]], tablefmt="latex_booktabs"))
+ \\begin{tabular}{lr}
+ \\toprule
+ spam & 41.9999 \\\\
+ eggs & 451 \\\\
+ \\bottomrule
+ \\end{tabular}
+
+ Number parsing
+ --------------
+ By default, anything which can be parsed as a number is a number.
+ This ensures numbers represented as strings are aligned properly.
+ This can lead to weird results for particular strings such as
+ specific git SHAs e.g. "42992e1" will be parsed into the number
+ 429920 and aligned as such.
+
+ To completely disable number parsing (and alignment), use
+ `disable_numparse=True`. For more fine grained control, a list column
+ indices is used to disable number parsing only on those columns
+ e.g. `disable_numparse=[0, 2]` would disable number parsing only on the
+ first and third columns.
+ """
+ if tabular_data is None:
+ tabular_data = []
+ list_of_lists, headers = _normalize_tabular_data(
+ tabular_data, headers, showindex=showindex)
+
+ fmt = tablefmt
+ if not isinstance(fmt, TableFormat):
+ fmt = _table_formats.get(fmt, _table_formats["simple"])
+
+ # empty values in the first column of RST tables should be escaped (issue #82)
+ # "" should be escaped as "\\ " or ".."
+ if tablefmt == 'rst':
+ list_of_lists, headers = _rst_escape_first_column(list_of_lists, headers)
+
+ # optimization: look for ANSI control codes once,
+ # enable smart width functions only if a control code is found
+ plain_text = '\t'.join(['\t'.join(map(_text_type, headers))] + \
+ ['\t'.join(map(_text_type, row)) for row in list_of_lists])
+
+ has_invisible = re.search(_invisible_codes, plain_text)
+ enable_widechars = wcwidth is not None and WIDE_CHARS_MODE
+ if tablefmt in multiline_formats and _is_multiline(plain_text):
+ tablefmt = multiline_formats.get(tablefmt, tablefmt)
+ is_multiline = True
+ else:
+ is_multiline = False
+ width_fn = _choose_width_fn(has_invisible, enable_widechars, is_multiline)
+
+ # format rows and columns, convert numeric values to strings
+ cols = list(zip_longest(*list_of_lists))
+ numparses = _expand_numparse(disable_numparse, len(cols))
+ coltypes = [_column_type(col, numparse=np) for col, np in
+ zip(cols, numparses)]
+ if isinstance(floatfmt, str): #old version
+ float_formats = len(cols) * [floatfmt] # just duplicate the string to use in each column
+ else: # if floatfmt is list, tuple etc we have one per column
+ float_formats = list(floatfmt)
+ if len(float_formats) < len(cols):
+ float_formats.extend( (len(cols)-len(float_formats)) * [_DEFAULT_FLOATFMT] )
+ if isinstance(missingval, str):
+ missing_vals = len(cols) * [missingval]
+ else:
+ missing_vals = list(missingval)
+ if len(missing_vals) < len(cols):
+ missing_vals.extend( (len(cols)-len(missing_vals)) * [_DEFAULT_MISSINGVAL] )
+ cols = [[_format(v, ct, fl_fmt, miss_v, has_invisible) for v in c]
+ for c, ct, fl_fmt, miss_v in zip(cols, coltypes, float_formats, missing_vals)]
+
+ # align columns
+ aligns = [numalign if ct in {int, float} else stralign for ct in coltypes]
+ if colalign is not None:
+ assert isinstance(colalign, Iterable)
+ for idx, align in enumerate(colalign):
+ aligns[idx] = align
+ if not headers:
+ minwidths = [0] * len(cols)
+ elif fmt.vertical_headers:
+ minwidths = [1] * len(cols)
+ else:
+ minwidths = [width_fn(h) + MIN_PADDING for h in headers]
+ cols = [_align_column(c, a, minw, has_invisible, enable_widechars, is_multiline)
+ for c, a, minw in zip(cols, aligns, minwidths)]
+
+ if headers:
+ # align headers and add headers
+ t_cols = cols or [['']] * len(headers)
+ t_aligns = aligns or [stralign] * len(headers)
+ minwidths = [
+ max(minw, max(width_fn(cl) for cl in c))
+ for minw, c in zip(minwidths, t_cols)]
+ if fmt.vertical_headers:
+ max_len = max(len(x) for x in headers)
+ headers = [x.rjust(max_len) for x in headers]
+ headers = [
+ [_align_header(h[i], a, minw, width_fn(h[i])) for h, a, minw
+ in zip(headers, t_aligns, minwidths)]
+ for i in range(max_len)
+ ]
+ else:
+ headers = [
+ _align_header(h, a, minw, width_fn(h), is_multiline, width_fn)
+ for h, a, minw in zip(headers, t_aligns, minwidths)]
+ rows = list(zip(*cols))
+ else:
+ minwidths = [max(width_fn(cl) for cl in c) for c in cols]
+ rows = list(zip(*cols))
+
+ return _format_table(fmt, headers, rows, minwidths, aligns, is_multiline)
+
+
+def _expand_numparse(disable_numparse, column_count):
+ """
+ Return a list of bools of length `column_count` which indicates whether
+ number parsing should be used on each column.
+ If `disable_numparse` is a list of indices, each of those indices are False,
+ and everything else is True.
+ If `disable_numparse` is a bool, then the returned list is all the same.
+ """
+ if isinstance(disable_numparse, Iterable):
+ numparses = [True] * column_count
+ for index in disable_numparse:
+ numparses[index] = False
+ return numparses
+ else:
+ return [not disable_numparse] * column_count
+
+
+def _pad_row(cells, padding, squash=False):
+ if cells:
+ pad = " " * padding
+ if squash:
+ padded_cells = [pad + cell for cell in cells]
+ else:
+ padded_cells = [pad + cell + pad for cell in cells]
+ return padded_cells
+ else:
+ return cells
+
+
+def _build_simple_row(padded_cells, rowfmt):
+ "Format row according to DataRow format without padding."
+ begin, sep, end = rowfmt
+ return (begin + sep.join(padded_cells) + end).rstrip()
+
+
+def _build_row(padded_cells, colwidths, colaligns, rowfmt):
+ "Return a string which represents a row of data cells."
+ if not rowfmt:
+ return None
+ if hasattr(rowfmt, "__call__"):
+ return rowfmt(padded_cells, colwidths, colaligns)
+ else:
+ return _build_simple_row(padded_cells, rowfmt)
+
+
+def _append_basic_row(lines, padded_cells, colwidths, colaligns, rowfmt):
+ lines.append(_build_row(padded_cells, colwidths, colaligns, rowfmt))
+ return lines
+
+
+def _append_multiline_row(lines, padded_multiline_cells, padded_widths, colaligns, rowfmt, pad):
+ colwidths = [w - 2*pad for w in padded_widths]
+ cells_lines = [c.splitlines() for c in padded_multiline_cells]
+ nlines = max(map(len, cells_lines)) # number of lines in the row
+ # vertically pad cells where some lines are missing
+ cells_lines = [(cl + [' '*w]*(nlines - len(cl))) for cl, w in zip(cells_lines, colwidths)]
+ lines_cells = [[cl[i] for cl in cells_lines] for i in range(nlines)]
+ for ln in lines_cells:
+ padded_ln = _pad_row(ln, pad)
+ _append_basic_row(lines, padded_ln, colwidths, colaligns, rowfmt)
+ return lines
+
+
+def _build_line(colwidths, colaligns, linefmt):
+ "Return a string which represents a horizontal line."
+ if not linefmt:
+ return None
+ if hasattr(linefmt, "__call__"):
+ return linefmt(colwidths, colaligns)
+ else:
+ begin, fill, sep, end = linefmt
+ cells = [fill*w for w in colwidths]
+ return _build_simple_row(cells, (begin, sep, end))
+
+
+def _append_line(lines, colwidths, colaligns, linefmt):
+ lines.append(_build_line(colwidths, colaligns, linefmt))
+ return lines
+
+
+def _format_table(fmt, headers, rows, colwidths, colaligns, is_multiline):
+ """Produce a plain-text representation of the table."""
+ lines = []
+ hidden = fmt.with_header_hide if (headers and fmt.with_header_hide) else []
+ pad = fmt.padding
+ headerrow = fmt.headerrow
+
+ if fmt.vertical_headers:
+ colwidths[0] += 1
+ padded_widths = colwidths
+ else:
+ padded_widths = [(w + 2*pad) for w in colwidths]
+
+ if is_multiline:
+ pad_row = lambda row, _: row # do it later, in _append_multiline_row
+ append_row = partial(_append_multiline_row, pad=pad)
+ else:
+ pad_row = _pad_row
+ append_row = _append_basic_row
+
+ if fmt.vertical_headers:
+ padded_headers = [pad_row(line, pad, squash=True) for line in headers]
+ else:
+ padded_headers = pad_row(headers, pad)
+ padded_rows = [pad_row(row, pad, squash=fmt.vertical_headers) for row in rows]
+
+ if fmt.lineabove and "lineabove" not in hidden:
+ _append_line(lines, padded_widths, colaligns, fmt.lineabove)
+
+ if padded_headers:
+ if fmt.vertical_headers:
+ for line in padded_headers:
+ append_row(lines, line, padded_widths, colaligns, headerrow)
+ else:
+ append_row(lines, padded_headers, padded_widths, colaligns, headerrow)
+ if fmt.linebelowheader and "linebelowheader" not in hidden:
+ _append_line(lines, padded_widths, colaligns, fmt.linebelowheader)
+
+ if padded_rows and fmt.linebetweenrows and "linebetweenrows" not in hidden:
+ # initial rows with a line below
+ for row in padded_rows[:-1]:
+ append_row(lines, row, padded_widths, colaligns, fmt.datarow)
+ _append_line(lines, padded_widths, colaligns, fmt.linebetweenrows)
+ # the last row without a line below
+ append_row(lines, padded_rows[-1], padded_widths, colaligns, fmt.datarow)
+ else:
+ for row in padded_rows:
+ append_row(lines, row, padded_widths, colaligns, fmt.datarow)
+
+ if fmt.linebelow and "linebelow" not in hidden:
+ _append_line(lines, padded_widths, colaligns, fmt.linebelow)
+
+ if headers or rows:
+ return "\n".join(lines)
+ else: # a completely empty table
+ return ""
diff --git a/src/pkgdev/scripts/pkgdev_showkw.py b/src/pkgdev/scripts/pkgdev_showkw.py
new file mode 100644
index 0000000..1b5ab8e
--- /dev/null
+++ b/src/pkgdev/scripts/pkgdev_showkw.py
@@ -0,0 +1,255 @@
+"""display package keywords"""
+
+import os
+from functools import partial
+
+from pkgcore.ebuild import restricts
+from pkgcore.util import commandline
+from pkgcore.util import packages as pkgutils
+from snakeoil.cli import arghparse
+from snakeoil.strings import pluralism
+
+from .._vendor.tabulate import tabulate, tabulate_formats
+
+
+showkw = arghparse.ArgumentParser(
+ prog='pkgdev showkw', description='show package keywords')
+showkw.add_argument(
+ 'targets', metavar='target', nargs='*',
+ action=commandline.StoreTarget,
+ help='extended atom matching of packages')
+
+output_opts = showkw.add_argument_group('output options')
+output_opts.add_argument(
+ '-f', '--format', default='showkw', metavar='FORMAT',
+ choices=tabulate_formats,
+ help='keywords table format',
+ docs=f"""
+ Output table using specified tabular format (defaults to compressed,
+ custom format).
+
+ Available formats: {', '.join(tabulate_formats)}
+ """)
+output_opts.add_argument(
+ '-c', '--collapse', action='store_true',
+ help='show collapsed list of arches')
+
+arch_options = showkw.add_argument_group('arch options')
+arch_options.add_argument(
+ '-s', '--stable', action='store_true',
+ help='show stable arches')
+arch_options.add_argument(
+ '-u', '--unstable', action='store_true',
+ help='show unstable arches')
+arch_options.add_argument(
+ '-o', '--only-unstable', action='store_true',
+ help='show arches that only have unstable keywords')
+arch_options.add_argument(
+ '-p', '--prefix', action='store_true',
+ help='show prefix and non-native arches')
+arch_options.add_argument(
+ '-a', '--arch', action='csv_negations',
+ help='select arches to display')
+
+# TODO: allow multi-repo comma-separated input
+target_opts = showkw.add_argument_group('target options')
+target_opts.add_argument(
+ '-r', '--repo', dest='selected_repo', metavar='REPO', priority=29,
+ action=commandline.StoreRepoObject,
+ repo_type='all-raw', allow_external_repos=True,
+ help='repo to query (defaults to all ebuild repos)')
+@showkw.bind_delayed_default(30, 'repos')
+def _setup_repos(namespace, attr):
+ target_repo = namespace.selected_repo
+ all_ebuild_repos = namespace.domain.all_ebuild_repos_raw
+ namespace.cwd = os.getcwd()
+
+ # TODO: move this to StoreRepoObject
+ if target_repo is None:
+ # determine target repo from the target directory
+ for repo in all_ebuild_repos.trees:
+ if namespace.cwd in repo:
+ target_repo = repo
+ break
+ else:
+ # determine if CWD is inside an unconfigured repo
+ target_repo = namespace.domain.find_repo(
+ namespace.cwd, config=namespace.config)
+
+ # fallback to using all, unfiltered ebuild repos if no target repo can be found
+ namespace.repo = target_repo if target_repo is not None else all_ebuild_repos
+
+
+@showkw.bind_delayed_default(40, 'arches')
+def _setup_arches(namespace, attr):
+ default_repo = namespace.config.get_default('repo')
+
+ try:
+ known_arches = {arch for r in namespace.repo.trees
+ for arch in r.config.known_arches}
+ except AttributeError:
+ try:
+ # binary/vdb repos use known arches from the default repo
+ known_arches = default_repo.config.known_arches
+ except AttributeError:
+ # TODO: remove fallback for tests after fixing default repo pull
+ # from faked config
+ known_arches = set()
+
+ arches = known_arches
+ if namespace.arch is not None:
+ disabled_arches, enabled_arches = namespace.arch
+ disabled_arches = set(disabled_arches)
+ enabled_arches = set(enabled_arches)
+ unknown_arches = disabled_arches.difference(known_arches) | enabled_arches.difference(known_arches)
+ if unknown_arches:
+ unknown = ', '.join(map(repr, sorted(unknown_arches)))
+ known = ', '.join(sorted(known_arches))
+ es = pluralism(unknown_arches, plural='es')
+ showkw.error(f'unknown arch{es}: {unknown} (choices: {known})')
+ if enabled_arches:
+ arches = arches.intersection(enabled_arches)
+ if disabled_arches:
+ arches = arches - disabled_arches
+
+ prefix_arches = set(x for x in arches if '-' in x)
+ native_arches = arches.difference(prefix_arches)
+ arches = native_arches
+ if namespace.prefix:
+ arches = arches.union(prefix_arches)
+ if namespace.stable:
+ try:
+ stable_arches = {arch for r in namespace.repo.trees
+ for arch in r.config.profiles.arches('stable')}
+ except AttributeError:
+ # binary/vdb repos use stable arches from the default repo
+ stable_arches = default_repo.config.profiles.arches('stable')
+ arches = arches.intersection(stable_arches)
+
+ namespace.known_arches = known_arches
+ namespace.prefix_arches = prefix_arches
+ namespace.native_arches = native_arches
+ namespace.arches = arches
+
+
+def _colormap(colors, line):
+ if colors is None:
+ return line
+ return colors[line] + line + colors['reset']
+
+
+@showkw.bind_final_check
+def _validate_args(parser, namespace):
+ namespace.pkg_dir = False
+
+ # disable colors when not using the native output format
+ if namespace.format != 'showkw':
+ namespace.color = False
+
+ if namespace.color:
+ # default colors to use for keyword types
+ _COLORS = {
+ '+': '\u001b[32m',
+ '~': '\u001b[33m',
+ '-': '\u001b[31m',
+ '*': '\u001b[31m',
+ 'o': '\u001b[30;1m',
+ 'reset': '\u001b[0m',
+ }
+ else:
+ _COLORS = None
+ namespace.colormap = partial(_colormap, _COLORS)
+
+ if not namespace.targets:
+ if namespace.selected_repo:
+ # use repo restriction since no targets specified
+ restriction = restricts.RepositoryDep(namespace.selected_repo.repo_id)
+ token = namespace.selected_repo.repo_id
+ else:
+ # Use a path restriction if we're in a repo, obviously it'll work
+ # faster if we're in an invididual ebuild dir but we're not that
+ # restrictive.
+ try:
+ restriction = namespace.repo.path_restrict(namespace.cwd)
+ token = namespace.cwd
+ except (AttributeError, ValueError):
+ parser.error('missing target argument and not in a supported repo')
+
+ # determine if we're grabbing the keywords for a single pkg in cwd
+ namespace.pkg_dir = any(
+ isinstance(x, restricts.PackageDep)
+ for x in reversed(restriction.restrictions))
+
+ namespace.targets = [(token, restriction)]
+
+
+def _collapse_arches(options, pkgs):
+ """Collapse arches into a single set."""
+ keywords = set()
+ stable_keywords = set()
+ unstable_keywords = set()
+ for pkg in pkgs:
+ for x in pkg.keywords:
+ if x[0] == '~':
+ unstable_keywords.add(x[1:])
+ elif x in options.arches:
+ stable_keywords.add(x)
+ if options.unstable:
+ keywords.update(unstable_keywords)
+ if options.only_unstable:
+ keywords.update(unstable_keywords.difference(stable_keywords))
+ if not keywords or options.stable:
+ keywords.update(stable_keywords)
+ return (
+ sorted(keywords.intersection(options.native_arches)) +
+ sorted(keywords.intersection(options.prefix_arches)))
+
+
+def _render_rows(options, pkgs, arches):
+ """Build rows for tabular data output."""
+ for pkg in sorted(pkgs):
+ keywords = set(pkg.keywords)
+ row = [pkg.fullver]
+ for arch in arches:
+ if arch in keywords:
+ line = '+'
+ elif f'~{arch}' in keywords:
+ line = '~'
+ elif f'-{arch}' in keywords:
+ line = '-'
+ elif '-*' in keywords:
+ line = '*'
+ else:
+ line = 'o'
+ row.append(options.colormap(line))
+ row.extend([pkg.eapi, pkg.fullslot, pkg.repo.repo_id])
+ yield row
+
+
+@showkw.bind_main_func
+def main(options, out, err):
+ continued = False
+ for token, restriction in options.targets:
+ for pkgs in pkgutils.groupby_pkg(options.repo.itermatch(restriction, sorter=sorted)):
+ if options.collapse:
+ out.write(' '.join(_collapse_arches(options, pkgs)))
+ else:
+ arches = sorted(options.arches.intersection(options.native_arches))
+ if options.prefix:
+ arches += sorted(options.arches.intersection(options.prefix_arches))
+ headers = [''] + arches + ['eapi', 'slot', 'repo']
+ if continued:
+ out.write()
+ if not options.pkg_dir:
+ pkgs = list(pkgs)
+ out.write(f'keywords for {pkgs[0].unversioned_atom}:')
+ data = _render_rows(options, pkgs, arches)
+ table = tabulate(
+ data, headers=headers, tablefmt=options.format,
+ disable_numparse=True)
+ out.write(table)
+ continued = True
+
+ if not continued:
+ err.write(f"{options.prog}: no matches for {token!r}")
+ return 1
diff --git a/tests/scripts/test_pkgdev_showkw.py b/tests/scripts/test_pkgdev_showkw.py
new file mode 100644
index 0000000..635cb69
--- /dev/null
+++ b/tests/scripts/test_pkgdev_showkw.py
@@ -0,0 +1,19 @@
+import pytest
+
+
+class TestPkgdevShowkwParseArgs:
+
+ def test_missing_target(self, capsys, tool):
+ with pytest.raises(SystemExit):
+ tool.parse_args(['showkw'])
+ captured = capsys.readouterr()
+ assert captured.err.strip() == (
+ 'pkgdev showkw: error: missing target argument and not in a supported repo')
+
+ def test_unknown_arches(self, capsys, tool, make_repo):
+ repo = make_repo(arches=['amd64'])
+ with pytest.raises(SystemExit):
+ tool.parse_args(['showkw', '-a', 'unknown', '-r', repo.location])
+ captured = capsys.readouterr()
+ assert captured.err.strip() == (
+ "pkgdev showkw: error: unknown arch: 'unknown' (choices: amd64)")