def test_smart_tables(self): """Test :func:`humanfriendly.tables.format_smart_table()`.""" column_names = ['One', 'Two', 'Three'] data = [['1', '2', '3'], ['a', 'b', 'c']] assert ansi_strip(format_smart_table(data, column_names)) == dedent(""" --------------------- | One | Two | Three | --------------------- | 1 | 2 | 3 | | a | b | c | --------------------- """).strip() column_names = ['One', 'Two', 'Three'] data = [['1', '2', '3'], ['a', 'b', 'Here comes a\nmulti line column!']] assert ansi_strip(format_smart_table(data, column_names)) == dedent(""" ------------------ One: 1 Two: 2 Three: 3 ------------------ One: a Two: b Three: Here comes a multi line column! ------------------ """).strip()
def test_robust_tables(self): column_names = ['One', 'Two', 'Three'] data = [['1', '2', '3'], ['a', 'b', 'c']] assert ansi_strip(format_robust_table(data, column_names)) == dedent(""" -------- One: 1 Two: 2 Three: 3 -------- One: a Two: b Three: c -------- """).strip() column_names = ['One', 'Two', 'Three'] data = [['1', '2', '3'], ['a', 'b', 'Here comes a\nmulti line column!']] assert ansi_strip(format_robust_table(data, column_names)) == dedent(""" ------------------ One: 1 Two: 2 Three: 3 ------------------ One: a Two: b Three: Here comes a multi line column! ------------------ """).strip()
def demonstrate_256_colors(i, j, group=None): """Demonstrate 256 color mode support.""" # Generate the label. label = '256 color mode' if group: label += ' (%s)' % group output('\n' + ansi_wrap('%s:' % label, bold=True)) # Generate a simple rendering of the colors in the requested range and # check if it will fit on a single line (given the terminal's width). single_line = ''.join(' ' + ansi_wrap(str(n), color=n) for n in range(i, j + 1)) lines, columns = find_terminal_size() if columns >= len(ansi_strip(single_line)): output(single_line) else: # Generate a more complex rendering of the colors that will nicely wrap # over multiple lines without using too many lines. width = len(str(j)) + 1 colors_per_line = int(columns / width) colors = [ ansi_wrap(str(n).rjust(width), color=n) for n in range(i, j + 1) ] blocks = [ colors[n:n + colors_per_line] for n in range(0, len(colors), colors_per_line) ] output('\n'.join(''.join(b) for b in blocks))
def test_pretty_tables(self): """Test :func:`humanfriendly.tables.format_pretty_table()`.""" # The simplest case possible :-). data = [['Just one column']] assert format_pretty_table(data) == dedent(""" ------------------- | Just one column | ------------------- """).strip() # A bit more complex: two rows, three columns, varying widths. data = [['One', 'Two', 'Three'], ['1', '2', '3']] assert format_pretty_table(data) == dedent(""" --------------------- | One | Two | Three | | 1 | 2 | 3 | --------------------- """).strip() # A table including column names. column_names = ['One', 'Two', 'Three'] data = [['1', '2', '3'], ['a', 'b', 'c']] assert ansi_strip(format_pretty_table(data, column_names)) == dedent(""" --------------------- | One | Two | Three | --------------------- | 1 | 2 | 3 | | a | b | c | --------------------- """).strip() # A table that contains a column with only numeric data (will be right aligned). column_names = ['Just a label', 'Important numbers'] data = [['Row one', '15'], ['Row two', '300']] assert ansi_strip(format_pretty_table(data, column_names)) == dedent(""" ------------------------------------ | Just a label | Important numbers | ------------------------------------ | Row one | 15 | | Row two | 300 | ------------------------------------ """).strip()
def test_format_text(self): """Test human friendly formatting of password store entries.""" entry = PasswordEntry(name='some/random/password', store=object()) set_property(entry, 'text', random_string()) self.assertEquals( # We enable ANSI escape sequences but strip them before we # compare the generated string. This may seem rather pointless # but it ensures that the relevant code paths are covered :-). dedent( ansi_strip( entry.format_text(include_password=True, use_colors=True))), dedent(''' some / random / password Password: {value} ''', value=entry.text))
def interpret_script(shell_script): """Make it appear as if commands are typed into the terminal.""" with CaptureOutput() as capturer: shell = subprocess.Popen(['bash', '-'], stdin=subprocess.PIPE) with open(shell_script) as handle: for line in handle: sys.stdout.write(ansi_wrap('$', color='green') + ' ' + line) sys.stdout.flush() shell.stdin.write(line) shell.stdin.flush() shell.stdin.close() time.sleep(12) # Get the text that was shown in the terminal. captured_output = capturer.get_text() # Store the text that was shown in the terminal. filename, extension = os.path.splitext(shell_script) transcript_file = '%s.txt' % filename logger.info("Updating %s ..", format_path(transcript_file)) with open(transcript_file, 'w') as handle: handle.write(ansi_strip(captured_output))
def demonstrate_256_colors(i, j, group=None): """Demonstrate 256 color mode support.""" # Generate the label. label = '256 color mode' if group: label += ' (%s)' % group output('\n' + ansi_wrap('%s:' % label, bold=True)) # Generate a simple rendering of the colors in the requested range and # check if it will fit on a single line (given the terminal's width). single_line = ''.join(' ' + ansi_wrap(str(n), color=n) for n in range(i, j + 1)) lines, columns = find_terminal_size() if columns >= len(ansi_strip(single_line)): output(single_line) else: # Generate a more complex rendering of the colors that will nicely wrap # over multiple lines without using too many lines. width = len(str(j)) + 1 colors_per_line = int(columns / width) colors = [ansi_wrap(str(n).rjust(width), color=n) for n in range(i, j + 1)] blocks = [colors[n:n + colors_per_line] for n in range(0, len(colors), colors_per_line)] output('\n'.join(''.join(b) for b in blocks))
def format_pretty_table(data, column_names=None, horizontal_bar='-', vertical_bar='|'): """ Render a table using characters like dashes and vertical bars to emulate borders. :param data: An iterable (e.g. a :func:`tuple` or :class:`list`) containing the rows of the table, where each row is an iterable containing the columns of the table (strings). :param column_names: An iterable of column names (strings). :param horizontal_bar: The character used to represent a horizontal bar (a string). :param vertical_bar: The character used to represent a vertical bar (a string). :returns: The rendered table (a string). Here's an example: >>> from humanfriendly.tables import format_pretty_table >>> column_names = ['Version', 'Uploaded on', 'Downloads'] >>> humanfriendly_releases = [ ... ['1.23', '2015-05-25', '218'], ... ['1.23.1', '2015-05-26', '1354'], ... ['1.24', '2015-05-26', '223'], ... ['1.25', '2015-05-26', '4319'], ... ['1.25.1', '2015-06-02', '197'], ... ] >>> print(format_pretty_table(humanfriendly_releases, column_names)) ------------------------------------- | Version | Uploaded on | Downloads | ------------------------------------- | 1.23 | 2015-05-25 | 218 | | 1.23.1 | 2015-05-26 | 1354 | | 1.24 | 2015-05-26 | 223 | | 1.25 | 2015-05-26 | 4319 | | 1.25.1 | 2015-06-02 | 197 | ------------------------------------- Notes about the resulting table: - If a column contains numeric data (integer and/or floating point numbers) in all rows (ignoring column names of course) then the content of that column is right-aligned, as can be seen in the example above. The idea here is to make it easier to compare the numbers in different columns to each other. - The column names are highlighted in color so they stand out a bit more (see also :data:`.HIGHLIGHT_COLOR`). The following screen shot shows what that looks like (my terminals are always set to white text on a black background): .. image:: images/pretty-table.png """ # Normalize the input because we'll have to iterate it more than once. data = [normalize_columns(r) for r in data] if column_names is not None: column_names = normalize_columns(column_names) if column_names: if terminal_supports_colors(): column_names = [highlight_column_name(n) for n in column_names] data.insert(0, column_names) # Calculate the maximum width of each column. widths = collections.defaultdict(int) numeric_data = collections.defaultdict(list) for row_index, row in enumerate(data): for column_index, column in enumerate(row): widths[column_index] = max(widths[column_index], ansi_width(column)) if not (column_names and row_index == 0): numeric_data[column_index].append(bool(NUMERIC_DATA_PATTERN.match(ansi_strip(column)))) # Create a horizontal bar of dashes as a delimiter. line_delimiter = horizontal_bar * (sum(widths.values()) + len(widths) * 3 + 1) # Start the table with a vertical bar. lines = [line_delimiter] # Format the rows and columns. for row_index, row in enumerate(data): line = [vertical_bar] for column_index, column in enumerate(row): padding = ' ' * (widths[column_index] - ansi_width(column)) if all(numeric_data[column_index]): line.append(' ' + padding + column + ' ') else: line.append(' ' + column + padding + ' ') line.append(vertical_bar) lines.append(u''.join(line)) if column_names and row_index == 0: lines.append(line_delimiter) # End the table with a vertical bar. lines.append(line_delimiter) # Join the lines, returning a single string. return u'\n'.join(lines)
def prompt_for_confirmation(question, default=None, padding=True): """ Prompt the user for confirmation. :param question: The text that explains what the user is confirming (a string). :param default: The default value (a boolean) or :data:`None`. :param padding: Refer to the documentation of :func:`prompt_for_input()`. :returns: - If the user enters 'yes' or 'y' then :data:`True` is returned. - If the user enters 'no' or 'n' then :data:`False` is returned. - If the user doesn't enter any text or standard input is not connected to a terminal (which makes it impossible to prompt the user) the value of the keyword argument ``default`` is returned (if that value is not :data:`None`). :raises: - Any exceptions raised by :func:`retry_limit()`. - Any exceptions raised by :func:`prompt_for_input()`. When `default` is :data:`False` and the user doesn't enter any text an error message is printed and the prompt is repeated: >>> prompt_for_confirmation("Are you sure?") <BLANKLINE> Are you sure? [y/n] <BLANKLINE> Error: Please enter 'yes' or 'no' (there's no default choice). <BLANKLINE> Are you sure? [y/n] The same thing happens when the user enters text that isn't recognized: >>> prompt_for_confirmation("Are you sure?") <BLANKLINE> Are you sure? [y/n] about what? <BLANKLINE> Error: Please enter 'yes' or 'no' (the text 'about what?' is not recognized). <BLANKLINE> Are you sure? [y/n] """ # Generate the text for the prompt. prompt_text = question if terminal_supports_colors(): prompt_text = ansi_wrap(prompt_text, bold=True, readline_hints=True) # Append the valid replies (and default reply) to the prompt text. hint = "[Y/n]" if default else "[y/N]" if default is not None else "[y/n]" if terminal_supports_colors(): hint = ansi_wrap(hint, color=HIGHLIGHT_COLOR, readline_hints=True) prompt_text += " %s " % hint # Loop until a valid response is given. logger.debug("Requesting interactive confirmation from terminal: %r", ansi_strip(prompt_text).rstrip()) for attempt in retry_limit(): reply = prompt_for_input(prompt_text, '', padding=padding, strip=True) if reply.lower() in ('y', 'yes'): logger.debug("Confirmation granted by reply (%r).", reply) return True elif reply.lower() in ('n', 'no'): logger.debug("Confirmation denied by reply (%r).", reply) return False elif (not reply) and default is not None: logger.debug("Default choice selected by empty reply (%r).", "granted" if default else "denied") return default else: details = ("the text '%s' is not recognized" % reply if reply else "there's no default choice") logger.debug("Got %s reply (%s), retrying (%i/%i) ..", "invalid" if reply else "empty", details, attempt, MAX_ATTEMPTS) warning("{indent}Error: Please enter 'yes' or 'no' ({details}).", indent=' ' if padding else '', details=details)
def prompt_for_confirmation(question, default=None, padding=True): """ Prompt the user for confirmation. :param question: The text that explains what the user is confirming (a string). :param default: The default value (a boolean) or :data:`None`. :param padding: Refer to the documentation of :func:`prompt_for_input()`. :returns: - If the user enters 'yes' or 'y' then :data:`True` is returned. - If the user enters 'no' or 'n' then :data:`False` is returned. - If the user doesn't enter any text or standard input is not connected to a terminal (which makes it impossible to prompt the user) the value of the keyword argument ``default`` is returned (if that value is not :data:`None`). :raises: - Any exceptions raised by :func:`retry_limit()`. - Any exceptions raised by :func:`prompt_for_input()`. When `default` is :data:`False` and the user doesn't enter any text an error message is printed and the prompt is repeated: >>> prompt_for_confirmation("Are you sure?") <BLANKLINE> Are you sure? [y/n] <BLANKLINE> Error: Please enter 'yes' or 'no' (there's no default choice). <BLANKLINE> Are you sure? [y/n] The same thing happens when the user enters text that isn't recognized: >>> prompt_for_confirmation("Are you sure?") <BLANKLINE> Are you sure? [y/n] about what? <BLANKLINE> Error: Please enter 'yes' or 'no' (the text 'about what?' is not recognized). <BLANKLINE> Are you sure? [y/n] """ # Generate the text for the prompt. prompt_text = prepare_prompt_text(question, bold=True) # Append the valid replies (and default reply) to the prompt text. hint = "[Y/n]" if default else "[y/N]" if default is not None else "[y/n]" prompt_text += " %s " % prepare_prompt_text(hint, color=HIGHLIGHT_COLOR) # Loop until a valid response is given. logger.debug("Requesting interactive confirmation from terminal: %r", ansi_strip(prompt_text).rstrip()) for attempt in retry_limit(): reply = prompt_for_input(prompt_text, '', padding=padding, strip=True) if reply.lower() in ('y', 'yes'): logger.debug("Confirmation granted by reply (%r).", reply) return True elif reply.lower() in ('n', 'no'): logger.debug("Confirmation denied by reply (%r).", reply) return False elif (not reply) and default is not None: logger.debug("Default choice selected by empty reply (%r).", "granted" if default else "denied") return default else: details = ("the text '%s' is not recognized" % reply if reply else "there's no default choice") logger.debug("Got %s reply (%s), retrying (%i/%i) ..", "invalid" if reply else "empty", details, attempt, MAX_ATTEMPTS) warning("{indent}Error: Please enter 'yes' or 'no' ({details}).", indent=' ' if padding else '', details=details)
def format_pretty_table(data, column_names=None, horizontal_bar='-', vertical_bar='|'): """ Render a table using characters like dashes and vertical bars to emulate borders. :param data: An iterable (e.g. a :func:`tuple` or :class:`list`) containing the rows of the table, where each row is an iterable containing the columns of the table (strings). :param column_names: An iterable of column names (strings). :param horizontal_bar: The character used to represent a horizontal bar (a string). :param vertical_bar: The character used to represent a vertical bar (a string). :returns: The rendered table (a string). Here's an example: >>> from humanfriendly.tables import format_pretty_table >>> column_names = ['Version', 'Uploaded on', 'Downloads'] >>> humanfriendly_releases = [ ... ['1.23', '2015-05-25', '218'], ... ['1.23.1', '2015-05-26', '1354'], ... ['1.24', '2015-05-26', '223'], ... ['1.25', '2015-05-26', '4319'], ... ['1.25.1', '2015-06-02', '197'], ... ] >>> print(format_pretty_table(humanfriendly_releases, column_names)) ------------------------------------- | Version | Uploaded on | Downloads | ------------------------------------- | 1.23 | 2015-05-25 | 218 | | 1.23.1 | 2015-05-26 | 1354 | | 1.24 | 2015-05-26 | 223 | | 1.25 | 2015-05-26 | 4319 | | 1.25.1 | 2015-06-02 | 197 | ------------------------------------- Notes about the resulting table: - If a column contains numeric data (integer and/or floating point numbers) in all rows (ignoring column names of course) then the content of that column is right-aligned, as can be seen in the example above. The idea here is to make it easier to compare the numbers in different columns to each other. - The column names are highlighted in color so they stand out a bit more (see also :data:`.HIGHLIGHT_COLOR`). The following screen shot shows what that looks like (my terminals are always set to white text on a black background): .. image:: images/pretty-table.png """ # Normalize the input because we'll have to iterate it more than once. data = [normalize_columns(r) for r in data] if column_names is not None: column_names = normalize_columns(column_names) if column_names: if terminal_supports_colors(): column_names = [highlight_column_name(n) for n in column_names] data.insert(0, column_names) # Calculate the maximum width of each column. widths = collections.defaultdict(int) numeric_data = collections.defaultdict(list) for row_index, row in enumerate(data): for column_index, column in enumerate(row): widths[column_index] = max(widths[column_index], ansi_width(column)) if not (column_names and row_index == 0): numeric_data[column_index].append( bool(NUMERIC_DATA_PATTERN.match(ansi_strip(column)))) # Create a horizontal bar of dashes as a delimiter. line_delimiter = horizontal_bar * (sum(widths.values()) + len(widths) * 3 + 1) # Start the table with a vertical bar. lines = [line_delimiter] # Format the rows and columns. for row_index, row in enumerate(data): line = [vertical_bar] for column_index, column in enumerate(row): padding = ' ' * (widths[column_index] - ansi_width(column)) if all(numeric_data[column_index]): line.append(' ' + padding + column + ' ') else: line.append(' ' + column + padding + ' ') line.append(vertical_bar) lines.append(u''.join(line)) if column_names and row_index == 0: lines.append(line_delimiter) # End the table with a vertical bar. lines.append(line_delimiter) # Join the lines, returning a single string. return u'\n'.join(lines)