Ejemplo n.º 1
0
 def test_connected_to_terminal(self):
     for stream in [sys.stdin, sys.stdout, sys.stderr]:
         result = connected_to_terminal(stream)
         # We really can't assert a True or False value here because this
         # test suite should be able to run both interactively and
         # non-interactively :-).
         assert isinstance(result, bool)
     # We can at least verify that e.g. /dev/null is never a terminal :-).
     with open(os.devnull) as handle:
         assert not connected_to_terminal(handle)
     # We can also verify that objects without isatty() don't raise an exception.
     assert not connected_to_terminal(object())
Ejemplo n.º 2
0
def report_available_mirrors(updater):
    """Print the available mirrors to the terminal (in a human friendly format)."""
    if connected_to_terminal():
        have_bandwidth = any(c.bandwidth for c in updater.ranked_mirrors)
        have_last_updated = any(c.last_updated is not None
                                for c in updater.ranked_mirrors)
        column_names = ["Rank", "Mirror URL", "Available?", "Updating?"]
        if have_last_updated:
            column_names.append("Last updated")
        if have_bandwidth:
            column_names.append("Bandwidth")
        data = []
        for i, candidate in enumerate(updater.ranked_mirrors, start=1):
            row = [
                i, candidate.mirror_url,
                "Yes" if candidate.is_available else "No",
                "Yes" if candidate.is_updating else "No"
            ]
            if have_last_updated:
                row.append("Up to date" if candidate.last_updated == 0 else (
                    "%s behind" %
                    format_timespan(candidate.last_updated) if candidate.
                    last_updated else "Unknown"))
            if have_bandwidth:
                row.append("%s/s" % format_size(round(candidate.bandwidth, 2))
                           if candidate.bandwidth else "Unknown")
            data.append(row)
        output(format_smart_table(data, column_names=column_names))
    else:
        output(u"\n".join(
            candidate.mirror_url for candidate in updater.ranked_mirrors
            if candidate.is_available and not candidate.is_updating))
def parse_feedback(status, msg, step, **kwargs):
    """
    This is called once per step execution.
    It provides a hook into print messages to the terminal, log files and the JIRA Client.
    """
    # nice_output = '{}\n{}'.format(self.complete_msg, result)
    # pass_back['result'] = result
    # pass_back['exp'] = exp

    the_msg = msg
    task_referal = None
    if status > logging.INFO:
        exp = kwargs['exp']
        if exp.args:
            if isinstance(exp.args[0], TaskReferralBase):
                task_referal = exp.args[0]
                # error_str = task_referal.get_task_unique_summary()
            else:
                error_str = '\n'.join(
                    str(s) for s in exp.args
                    if isinstance(s, six.string_types))

                stack_trace = kwargs['stack_trace']
                # error_str = '\n'.join(str(s) for s in exp.args if isinstance(s, six.string_types))
                the_msg = '{}\nerror message={}\n\n{}'.format(
                    msg, error_str, stack_trace)

    if jira_client:
        jira_client.task_handler(status, msg, task_referal)

    if hft.connected_to_terminal():
        hft.output('{} {} {}'.format(hft.ANSI_ERASE_LINE,
                                     terminal_checkboxs[status], the_msg))
    else:
        logger.log(status, the_msg)
Ejemplo n.º 4
0
    def interactive(self):
        """
        :data:`True` to allow user interaction, :data:`False` otherwise.

        The value of :attr:`interactive` defaults to the return value
        of :func:`~humanfriendly.terminal.connected_to_terminal()`
        when given :data:`sys.stdin`.
        """
        return connected_to_terminal(sys.stdin)
Ejemplo n.º 5
0
def report_available_mirrors(updater):
    """Print the available mirrors to the terminal (in a human friendly format)."""
    if connected_to_terminal() or os.getenv(
            'TRAVIS') == 'true':  # make Travis CI test this code
        # https://docs.travis-ci.com/user/environment-variables/#default-environment-variables
        have_bandwidth = any(c.bandwidth for c in updater.ranked_mirrors)
        have_last_updated = any(c.last_updated is not None
                                for c in updater.ranked_mirrors)
        column_names = ["Rank", "Mirror URL", "Available?", "Updating?"]
        if have_last_updated:
            column_names.append("Last updated")
        if have_bandwidth:
            column_names.append("Bandwidth")
        data = []
        long_mirror_urls = {}
        if os.getenv('TRAVIS') == 'true':
            updater.url_char_len = 50
        for i, candidate in enumerate(updater.ranked_mirrors, start=1):
            if len(candidate.mirror_url) <= updater.url_char_len:
                stripped_mirror_url = candidate.mirror_url
            else:  # the mirror_url is too long, strip it
                stripped_mirror_url = candidate.mirror_url[:updater.
                                                           url_char_len - 3]
                stripped_mirror_url = stripped_mirror_url + "..."
                long_mirror_urls[
                    i] = candidate.mirror_url  # store it, output as full afterwards
            row = [
                i, stripped_mirror_url,
                "Yes" if candidate.is_available else "No",
                "Yes" if candidate.is_updating else "No"
            ]
            if have_last_updated:
                row.append("Up to date" if candidate.last_updated == 0 else (
                    "%s behind" %
                    format_timespan(candidate.last_updated, max_units=1)
                    if candidate.last_updated else "Unknown"))
            if have_bandwidth:
                row.append("%s/s" % format_size(round(candidate.bandwidth, 0))
                           if candidate.bandwidth else "Unknown")
            data.append(row)
        output(format_table(data, column_names=column_names))
        if long_mirror_urls:
            output(u"Full URLs which are too long to be shown in above table:")
            for key in long_mirror_urls:
                output(u"%i: %s", key, long_mirror_urls[key])
    else:
        output(u"\n".join(
            candidate.mirror_url for candidate in updater.ranked_mirrors
            if candidate.is_available and not candidate.is_updating))
Ejemplo n.º 6
0
def prepare_prompt_text(prompt_text, **options):
    """
    Wrap a text to be rendered as an interactive prompt in ANSI escape sequences.

    :param prompt_text: The text to render on the prompt (a string).
    :param options: Any keyword arguments are passed on to :func:`.ansi_wrap()`.
    :returns: The resulting prompt text (a string).

    ANSI escape sequences are only used when the standard output stream is
    connected to a terminal. When the standard input stream is connected to a
    terminal any escape sequences are wrapped in "readline hints".
    """
    return (ansi_wrap(prompt_text, readline_hints=connected_to_terminal(sys.stdin), **options)
            if terminal_supports_colors(sys.stdout)
            else prompt_text)
Ejemplo n.º 7
0
def convert_command_output(*command):
    """
    Command line interface for ``coloredlogs --to-html``.

    Takes a command (and its arguments) and runs the program under ``script``
    (emulating an interactive terminal), intercepts the output of the command
    and converts ANSI escape sequences in the output to HTML.
    """
    html_output = convert(capture(command))
    if connected_to_terminal():
        fd, temporary_file = tempfile.mkstemp(suffix='.html')
        with open(temporary_file, 'w') as handle:
            handle.write(html_output)
        webbrowser.open(temporary_file)
    else:
        print(html_output)
Ejemplo n.º 8
0
def convert_command_output(*command):
    """
    Command line interface for ``coloredlogs --to-html``.

    Takes a command (and its arguments) and runs the program under ``script``
    (emulating an interactive terminal), intercepts the output of the command
    and converts ANSI escape sequences in the output to HTML.
    """
    html_output = convert(capture(command))
    if connected_to_terminal():
        fd, temporary_file = tempfile.mkstemp(suffix='.html')
        with open(temporary_file, 'w') as handle:
            handle.write(html_output)
        webbrowser.open(temporary_file)
    else:
        print(html_output)
def process_stack(step_list, initial_state):
    """
    This is the principal function which executes the stack of `Step` objects. For each step in the stack
    its `func` is called.
    * If `func` returns one or more Step objects (either as a single object or as a list) then these are
      added to the top of the stack. The `state` remains unaltered and is passed to the next Step.
    * If `func` returns any other value (including None and other falsey values) then this is passed as
      the state object to the `func` of the next Step item in the stack.

    :param step_list: A list of initial steps which are used to populate the stack.
    :param initial_state: This value will be passed a the 'state' keyword arg to the first step's `func`.
    :returns: The return value of the final step's `func`.
    """
    hft.enable_ansi_support()
    n_state = initial_state
    step_list.reverse()
    stack = deque(step_list)

    try:
        while stack:
            # Definitions:
            # `n_state` = the state for the current iteration
            # `nplus_state` = the state for the next iteraction (eg N+1)
            step = stack.pop()
            kwargs = {'state': n_state}

            if hft.connected_to_terminal():
                with spinners.AutomaticSpinner(step.running_msg,
                                               show_time=True):
                    nplus_state = step.run(parse_feedback, **kwargs)
            else:
                logger.info('Starting: {}'.format(step.running_msg))
                nplus_state = step.run(parse_feedback, **kwargs)

            # Used to increment the state *only* if no new Steps where returned
            n_state = _add_steps_from_state_to_stack(nplus_state, stack,
                                                     n_state)

        return n_state
    except Exception as exp:
        pass_back = {'exp': exp, 'stack_trace': traceback.format_exc()}
        # print error and then exit with a non-zero exit code
        parse_feedback(logging.ERROR,
                       'Unable to continue following the previous error', None,
                       **pass_back)
        sys.exit(1)
Ejemplo n.º 10
0
 def __init__(self, stream=sys.stderr, level=logging.NOTSET, isatty=None,
              show_name=True, show_severity=True, show_timestamps=True,
              show_hostname=True, use_chroot=True, severity_to_style=None):
     logging.StreamHandler.__init__(self, stream)
     self.level = level
     self.show_timestamps = show_timestamps
     self.show_hostname = show_hostname
     self.show_name = show_name
     self.show_severity = show_severity
     self.severity_to_style = self.default_severity_to_style.copy()
     if severity_to_style:
         self.severity_to_style.update(severity_to_style)
     self.isatty = connected_to_terminal(stream) if isatty is None else isatty
     if show_hostname:
         chroot_file = '/etc/debian_chroot'
         if use_chroot and os.path.isfile(chroot_file):
             with open(chroot_file) as handle:
                 self.hostname = handle.read().strip()
         else:
             self.hostname = re.sub(r'\.local$', '', socket.gethostname())
     if show_name:
         self.pid = os.getpid()
Ejemplo n.º 11
0
def main():
    """Command line interface for the ``apache-manager`` program."""
    # Configure logging output.
    coloredlogs.install(syslog=True)
    # Command line option defaults.
    data_file = '/tmp/apache-manager.txt'
    dry_run = False
    max_memory_active = None
    max_memory_idle = None
    max_ss = None
    watch = False
    zabbix_discovery = False
    verbosity = 0
    # Parse the command line options.
    try:
        options, arguments = getopt.getopt(sys.argv[1:], 'wa:i:t:f:znvqh', [
            'watch', 'max-memory-active=', 'max-memory-idle=', 'max-ss=',
            'max-time=', 'data-file=', 'zabbix-discovery', 'dry-run',
            'simulate', 'verbose', 'quiet', 'help',
        ])
        for option, value in options:
            if option in ('-w', '--watch'):
                watch = True
            elif option in ('-a', '--max-memory-active'):
                max_memory_active = parse_size(value)
            elif option in ('-i', '--max-memory-idle'):
                max_memory_idle = parse_size(value)
            elif option in ('-t', '--max-ss', '--max-time'):
                max_ss = parse_timespan(value)
            elif option in ('-f', '--data-file'):
                data_file = value
            elif option in ('-z', '--zabbix-discovery'):
                zabbix_discovery = True
            elif option in ('-n', '--dry-run', '--simulate'):
                logger.info("Performing a dry run ..")
                dry_run = True
            elif option in ('-v', '--verbose'):
                coloredlogs.increase_verbosity()
                verbosity += 1
            elif option in ('-q', '--quiet'):
                coloredlogs.decrease_verbosity()
                verbosity -= 1
            elif option in ('-h', '--help'):
                usage(__doc__)
                return
    except Exception as e:
        sys.stderr.write("Error: %s!\n" % e)
        sys.exit(1)
    # Execute the requested action(s).
    manager = ApacheManager()
    try:
        if max_memory_active or max_memory_idle or max_ss:
            manager.kill_workers(
                max_memory_active=max_memory_active,
                max_memory_idle=max_memory_idle,
                timeout=max_ss,
                dry_run=dry_run,
            )
        elif watch and connected_to_terminal(sys.stdout):
            watch_metrics(manager)
        elif zabbix_discovery:
            report_zabbix_discovery(manager)
        elif data_file != '-' and verbosity >= 0:
            for line in report_metrics(manager):
                if line_is_heading(line):
                    line = ansi_wrap(line, color=HIGHLIGHT_COLOR)
                print(line)
    finally:
        if (not watch) and (data_file == '-' or not dry_run):
            manager.save_metrics(data_file)
Ejemplo n.º 12
0
 def use_colors(self):
     """Whether to output ANSI escape sequences for text colors and styles (a boolean)."""
     return connected_to_terminal()
Ejemplo n.º 13
0
def main():
    """Command line interface for the ``rsync-system-backup`` program."""
    # Initialize logging to the terminal and system log.
    coloredlogs.install(syslog=True)
    # Parse the command line arguments.
    context_opts = dict()
    program_opts = dict()
    dest_opts = dict()
    try:
        options, arguments = getopt.gnu_getopt(sys.argv[1:], 'bsrm:c:t:i:unx:fvqh', [
            'backup', 'snapshot', 'rotate', 'mount=', 'crypto=', 'tunnel=',
            'ionice=', 'no-sudo', 'dry-run', 'multi-fs', 'exclude=', 'force',
            'disable-notifications', 'verbose', 'quiet', 'help',
        ])
        for option, value in options:
            if option in ('-b', '--backup'):
                enable_explicit_action(program_opts, 'backup_enabled')
            elif option in ('-s', '--snapshot'):
                enable_explicit_action(program_opts, 'snapshot_enabled')
            elif option in ('-r', '--rotate'):
                enable_explicit_action(program_opts, 'rotate_enabled')
            elif option in ('-m', '--mount'):
                program_opts['mount_point'] = value
            elif option in ('-c', '--crypto'):
                program_opts['crypto_device'] = value
            elif option in ('-t', '--tunnel'):
                ssh_user, _, value = value.rpartition('@')
                ssh_alias, _, port_number = value.partition(':')
                tunnel_opts = dict(
                    ssh_alias=ssh_alias,
                    ssh_user=ssh_user,
                    # The port number of the rsync daemon.
                    remote_port=RSYNCD_PORT,
                )
                if port_number:
                    # The port number of the SSH server.
                    tunnel_opts['port'] = int(port_number)
                dest_opts['ssh_tunnel'] = SecureTunnel(**tunnel_opts)
            elif option in ('-i', '--ionice'):
                value = value.lower().strip()
                validate_ionice_class(value)
                program_opts['ionice'] = value
            elif option in ('-u', '--no-sudo'):
                program_opts['sudo_enabled'] = False
            elif option in ('-n', '--dry-run'):
                logger.info("Performing a dry run (because of %s option) ..", option)
                program_opts['dry_run'] = True
            elif option in ('-f', '--force'):
                program_opts['force'] = True
            elif option in ('-x', '--exclude'):
                program_opts.setdefault('exclude_list', [])
                program_opts['exclude_list'].append(value)
            elif option == '--multi-fs':
                program_opts['multi_fs'] = True
            elif option == '--disable-notifications':
                program_opts['notifications_enabled'] = False
            elif option in ('-v', '--verbose'):
                coloredlogs.increase_verbosity()
            elif option in ('-q', '--quiet'):
                coloredlogs.decrease_verbosity()
            elif option in ('-h', '--help'):
                usage(__doc__)
                return
            else:
                raise Exception("Unhandled option! (programming error)")
        if len(arguments) > 2:
            msg = "Expected one or two positional arguments! (got %i)"
            raise Exception(msg % len(arguments))
        if len(arguments) == 2:
            # Get the source from the first of two arguments.
            program_opts['source'] = arguments.pop(0)
        if arguments:
            # Get the destination from the second (or only) argument.
            dest_opts['expression'] = arguments[0]
            program_opts['destination'] = Destination(**dest_opts)
        elif not os.environ.get('RSYNC_MODULE_PATH'):
            # Show a usage message when no destination is given.
            usage(__doc__)
            return
    except Exception as e:
        warning("Error: %s", e)
        sys.exit(1)
    try:
        # Inject the source context into the program options.
        program_opts['source_context'] = create_context(**context_opts)
        # Initialize the program with the command line
        # options and execute the requested action(s).
        RsyncSystemBackup(**program_opts).execute()
    except Exception as e:
        if isinstance(e, RsyncSystemBackupError):
            # Special handling when the backup disk isn't available.
            if isinstance(e, MissingBackupDiskError):
                # Check if we're connected to a terminal to decide whether the
                # error should be propagated or silenced, the idea being that
                # rsync-system-backup should keep quiet when it's being run
                # from cron and the backup disk isn't available.
                if not connected_to_terminal():
                    logger.info("Skipping backup: %s", e)
                    sys.exit(0)
            # Known problems shouldn't produce
            # an intimidating traceback to users.
            logger.error("Aborting due to error: %s", e)
        else:
            # Unhandled exceptions do get a traceback,
            # because it may help fix programming errors.
            logger.exception("Aborting due to unhandled exception!")
        sys.exit(1)
Ejemplo n.º 14
0
def main():
    """Command line interface for the ``apache-manager`` program."""
    # Configure logging output.
    coloredlogs.install()
    # Command line option defaults.
    data_file = '/tmp/apache-manager.txt'
    dry_run = False
    max_memory_active = None
    max_memory_idle = None
    max_ss = None
    watch = False
    zabbix_discovery = False
    verbosity = 0
    # Parse the command line options.
    try:
        options, arguments = getopt.getopt(sys.argv[1:], 'wa:i:t:f:znvqh', [
            'watch', 'max-memory-active=', 'max-memory-idle=', 'max-ss=',
            'max-time=', 'data-file=', 'zabbix-discovery', 'dry-run',
            'simulate', 'verbose', 'quiet', 'help',
        ])
        for option, value in options:
            if option in ('-w', '--watch'):
                watch = True
            elif option in ('-a', '--max-memory-active'):
                max_memory_active = parse_size(value)
            elif option in ('-i', '--max-memory-idle'):
                max_memory_idle = parse_size(value)
            elif option in ('-t', '--max-ss', '--max-time'):
                max_ss = parse_timespan(value)
            elif option in ('-f', '--data-file'):
                data_file = value
            elif option in ('-z', '--zabbix-discovery'):
                zabbix_discovery = True
            elif option in ('-n', '--dry-run', '--simulate'):
                logger.info("Performing a dry run ..")
                dry_run = True
            elif option in ('-v', '--verbose'):
                coloredlogs.increase_verbosity()
                verbosity += 1
            elif option in ('-q', '--quiet'):
                coloredlogs.decrease_verbosity()
                verbosity -= 1
            elif option in ('-h', '--help'):
                usage(__doc__)
                return
    except Exception as e:
        sys.stderr.write("Error: %s!\n" % e)
        sys.exit(1)
    # Execute the requested action(s).
    manager = ApacheManager()
    try:
        if max_memory_active or max_memory_idle or max_ss:
            manager.kill_workers(
                max_memory_active=max_memory_active,
                max_memory_idle=max_memory_idle,
                timeout=max_ss,
                dry_run=dry_run,
            )
        if watch and connected_to_terminal(sys.stdout):
            watch_metrics(manager)
        elif zabbix_discovery:
            report_zabbix_discovery(manager)
        elif data_file != '-' and verbosity >= 0:
            for line in report_metrics(manager):
                if line_is_heading(line):
                    line = ansi_wrap(line, color=HIGHLIGHT_COLOR)
                print(line)
    finally:
        if (not watch) and (data_file == '-' or not dry_run):
            manager.save_metrics(data_file)
Ejemplo n.º 15
0
def format_robust_table(data, column_names):
    """
    Render tabular data with one column per line (allowing columns with line breaks).

    :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).
    :returns: The rendered table (a string).

    Here's an example:

    >>> from humanfriendly.tables import format_robust_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_robust_table(humanfriendly_releases, column_names))
    -----------------------
    Version: 1.23
    Uploaded on: 2015-05-25
    Downloads: 218
    -----------------------
    Version: 1.23.1
    Uploaded on: 2015-05-26
    Downloads: 1354
    -----------------------
    Version: 1.24
    Uploaded on: 2015-05-26
    Downloads: 223
    -----------------------
    Version: 1.25
    Uploaded on: 2015-05-26
    Downloads: 4319
    -----------------------
    Version: 1.25.1
    Uploaded on: 2015-06-02
    Downloads: 197
    -----------------------

    The column names are highlighted in bold font and color so they stand out a
    bit more (see :data:`.HIGHLIGHT_COLOR`).
    """
    blocks = []
    column_names = ["%s:" % n for n in normalize_columns(column_names)]
    if connected_to_terminal():
        column_names = [highlight_column_name(n) for n in column_names]
    # Convert each row into one or more `name: value' lines (one per column)
    # and group each `row of lines' into a block (i.e. rows become blocks).
    for row in data:
        lines = []
        for column_index, column_text in enumerate(normalize_columns(row)):
            stripped_column = column_text.strip()
            if '\n' not in stripped_column:
                # Columns without line breaks are formatted inline.
                lines.append("%s %s" % (column_names[column_index], stripped_column))
            else:
                # Columns with line breaks could very well contain indented
                # lines, so we'll put the column name on a separate line. This
                # way any indentation remains intact, and it's easier to
                # copy/paste the text.
                lines.append(column_names[column_index])
                lines.extend(column_text.rstrip().splitlines())
        blocks.append(lines)
    # Calculate the width of the row delimiter.
    num_rows, num_columns = find_terminal_size()
    longest_line = max(max(map(ansi_width, lines)) for lines in blocks)
    delimiter = u"\n%s\n" % ('-' * min(longest_line, num_columns))
    # Force a delimiter at the start and end of the table.
    blocks.insert(0, "")
    blocks.append("")
    # Embed the row delimiter between every two blocks.
    return delimiter.join(u"\n".join(b) for b in blocks).strip()
Ejemplo n.º 16
0
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 connected_to_terminal():
                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)