#
# Copyright (c) 2015, Adam Meily <meily.adam@gmail.com>
# Pypsi - https://github.com/ameily/pypsi
#
# Permission to use, copy, modify, and/or distribute this software for any
# purpose with or without fee is hereby granted, provided that the above
# copyright notice and this permission notice appear in all copies.
#
# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
#
'''
Provides functions and classes for dealing with command line input and output.
'''
from pypsi.ansi import ansi_ljust, ansi_rjust, ansi_center, ansi_len
[docs]def get_lines(txt):
'''
Break text to individual lines.
:returns tuple: a tuple containing the next line and whether the line
contained a newline charater.
'''
if not txt:
return iter([])
start = 0
try:
while True:
i = txt.index('\n', start)
yield (txt[start:i], True)
start = i + 1
if start >= len(txt):
break
except:
yield (txt[start:], False)
[docs]def wrap_line(txt, width, wrap_prefix=None):
'''
Word wrap a single line.
:param str txt: the line to wrap
:param int width: the maximum width of a wrapped line
:returns str: the next wrapped line
'''
if width is None or width <= 0:
yield txt
else:
wrap_prefix = wrap_prefix or ''
start = 0
count = 0
i = 0
total = len(txt)
first_line = True
while i < total:
esc_code = False
prev = None
while count <= width and i < total:
c = txt[i]
if c == '\x1b':
esc_code = True
elif esc_code:
if c in 'ABCDEFGHJKSTfmnsulh':
esc_code = False
else:
count += 1
if c in ' \t':
prev = i
i += 1
if i >= total:
prev = i
else:
prev = prev or i
if wrap_prefix and first_line:
first_line = False
width -= len(wrap_prefix)
yield txt[start:prev]
else:
yield wrap_prefix + txt[start:prev]
start = prev
while start < total and txt[start] in '\t ':
start += 1
i = start
count = 0
if count:
if not first_line:
yield wrap_prefix + txt[start:]
else:
yield txt[start:]
[docs]def highlight(target, term, color='1;32'):
'''
Find and highlight a term inside of a block of text.
:param str target: the text to search in
:param str term: the term to search for
:param str color: the color to output when the term is found
:returns str: the input string with all occurrences of the term highlighted
'''
if not color:
return target
s = target.lower()
t = term.lower()
start = 0
end = s.find(t)
ret = ''
while end >= 0:
ret += target[start:end]
ret += '\x1b[{color}m{term}\x1b[0m'.format(
color=color, term=target[end:end+len(term)]
)
start = end + len(term)
end = s.find(t, start)
ret += target[start:]
return ret
[docs]def file_size_str(value):
'''
Get the human readable file size string from a number. This will convert a
value of 1024 to 1Kb and 1048576 to 1Mb.
:param int value: the value to convert
:returns str: the human readable file size string
'''
value = float(value)
units = ['Kb', 'Mb', 'Gb', 'Tb']
unit = 'B'
for u in units:
v = value / 1024.0
if v < 1.0:
break
value = v
unit = u
if unit == 'B':
return "{} B".format(int(value))
return "{:.2f} {}".format(value, unit)
[docs]def obj_str(obj, max_children=3, stream=None):
'''
Pretty format an object with colored type information. Examples:
- `list`: ``list( item1, item2, item3, ...)``
- `bool`: ``bool( True )``
- `None`: ``<null>``
:param object obj: object to format
:param int max_children: maximum number of children to print for lists
:param file stream: target stream, used to determine if color will be used.
:returns str: the formatted object
'''
tmpl = "{blue}{type}({reset} {value} {blue}){reset}"
if stream:
def format_value(type, value):
return stream.ansi_format(tmpl, type=type, value=value)
else:
def format_value(type, value):
return "{type}( {value} )".format(type=type, value=value)
if isinstance(obj, bool):
return format_value("bool", obj)
elif isinstance(obj, int):
return format_value("int", "{:d}".format(obj))
elif isinstance(obj, float):
return format_value("float", "{:g}".format(obj))
elif isinstance(obj, (list, tuple)):
if max_children > 0 and len(obj) > max_children:
obj = [o for o in obj[:max_children]]
obj.append('...')
return format_value(
"list",
', '.join([
obj_str(child, max_children=max_children, stream=stream)
for child in obj
])
)
elif obj is None:
if stream:
return stream.ansi_format("{blue}<null>{reset}")
else:
return "<null>"
elif isinstance(obj, str):
return obj
return str(obj)
def title_str(title, width=80, align='left', hr='=', box=False):
lines = []
if box:
border = '+' + ('-'*(width-2)) + '+'
t = None
if align == 'left':
t = ansi_ljust(title, width-4)
elif align == 'center':
t = ansi_center(title, width-4)
else:
t = ansi_rjust(title, width-4)
lines.append(border)
lines.append('| ' + t + ' |')
lines.append(border)
else:
if align == 'left':
lines.append(title)
elif align == 'center':
lines.append(ansi_center(title, width))
elif align == 'right':
lines.append(ansi_rjust(title, width))
lines.append(hr * width)
return '\n'.join(lines)
[docs]class Table(object):
'''
Variable width table.
'''
def __init__(self, columns, width=80, spacing=1, header=True):
'''
:param multiple columns: a list of either class:`Column` instances or a
single `int` defining how many columns to create
:param int width: the maximum width of the entire table
:param int spacing: the amount of space characters to display between
columns
:param bool header: whether to display header row with the column names
'''
if isinstance(columns, int):
self.columns = [Column()] * columns
header = False
else:
self.columns = columns
self.width = width
self.spacing = spacing
self.header = header
self.rows = []
[docs] def append(self, *args):
'''
Add a row to the table.
:param list args: the column values
'''
self.rows.append(args)
for (col, value) in zip(self.columns, args):
col.width = max(col.width, ansi_len(str(value)))
return self
[docs] def extend(self, *args):
'''
Add multiple rows to the table, each argument should be a list of
column values.
'''
for row in args:
self.append(*row)
return self
[docs] def write(self, fp):
'''
Print the table to a specified file stream.
:param file fp: output stream
'''
def write_overflow(row):
overflow = [''] * len(self.columns)
column_idx = 0
for (col, value) in zip(self.columns, row):
if column_idx > 0:
fp.write(' ' * self.spacing)
if isinstance(value, str):
pass
else:
value = str(value)
if(ansi_len(value) <= col.width):
fp.write(ansi_ljust(value, col.width))
else:
wrapped_line = [
line for line in wrap_line(value, col.width)
]
if len(wrapped_line) > 1:
overflow[column_idx] = ' '.join(wrapped_line[1:])
fp.write(wrapped_line[0])
# Move to next column
column_idx += 1
fp.write('\n')
# deal with overflowed data
if ''.join(overflow):
write_overflow(overflow)
total = sum([col.width for col in self.columns])
# Resize columns if last too wide
# TODO: Smarter column resizing, maybe pick widest column
if (total + self.spacing * (len(self.columns)-1)) > self.width:
self.columns[-1].mode = Column.Grow
for col in self.columns:
if col.mode == Column.Grow:
remaining = (
self.width - ((len(self.columns) - 1) * self.spacing) -
total
)
col.width += remaining
if self.header:
i = 0
for col in self.columns:
if i > 0:
fp.write(' ' * self.spacing)
fp.write(ansi_ljust(col.text, col.width))
i += 1
fp.write('\n')
fp.write('='*self.width)
fp.write('\n')
for row in self.rows:
write_overflow(row)
return 0
[docs]class Column(object):
'''
A table column.
'''
#: Size mode to have the column shrink to its contents
Shrink = 0
#: Size mode to have the column grow to the maximum width it can have
Grow = 1
def __init__(self, text='', mode=0):
'''
:param str text: the column name
:param int mode: the column size mode
'''
self.text = text
self.mode = mode
self.width = ansi_len(text)
[docs]class FixedColumnTable(object):
'''
A table that has preset column widths.
'''
def __init__(self, widths):
'''
:param list widths: the list of column widths (`int`)
'''
self.widths = [int(width) for width in widths]
self.buffer = []
[docs] def write_row(self, fp, *args):
'''
Print a single row.
:param file fp: the output file stream (usually sys.stdout or
sys.stderr)
:param list args: the column values for the row
'''
for (width, value) in zip(self.widths, args):
fp.write(ansi_ljust(value, width))
fp.write('\n')
[docs] def add_cell(self, fp, col):
'''
Add a single cell to the table. The current row is printed if the
column completes the row.
'''
self.buffer.append(col)
if len(self.buffer) == len(self.widths):
self.write_row(fp, *self.buffer)
self.buffer = []
[docs] def flush(self, fp):
'''
Force a write of the table.
:param file fp: the output file stream
'''
if self.buffer:
self.write_row(fp, *self.buffer)
self.buffer = []