Source code for brownie.terminal

# coding: utf-8
"""
    brownie.terminal
    ~~~~~~~~~~~~~~~~

    Utilities for handling simple output on a terminal.


    .. versionadded:: 0.6

    :copyright: 2010-2011 by Daniel Neuhäuser
    :license: BSD, see LICENSE.rst for details
"""
from __future__ import with_statement
import re
import os
import sys
import codecs
import struct
try:
    # all available on unix platforms
    import fcntl
    import termios
except ImportError: # pragma: no cover
    fcntl = None
    termios = None
from itertools import izip, imap
from contextlib import contextmanager

from brownie.datastructures import namedtuple
from brownie.terminal.progress import ProgressBar


_ansi_sequence = '\033[%sm'


ATTRIBUTES = dict((key, _ansi_sequence % value) for key, value in [
    ('reset','00'),
    ('bold',  '01'),
    ('faint', '02'),
    ('standout', '03'),
    ('underline', '04'),
    ('blink', '05')
])
TEXT_COLOURS = {'reset': _ansi_sequence % '39'}
BACKGROUND_COLOURS = {'reset': _ansi_sequence % '49'}
_colour_names = [
    'black',
    'red',
    'green',
    'yellow',
    'blue',
    'purple',
    'teal',
    'white'
]
for i, name in enumerate(_colour_names):
    TEXT_COLOURS[name] = _ansi_sequence % str(i + 30)
    BACKGROUND_COLOURS[name] = _ansi_sequence % (i + 40)


Dimensions = namedtuple('Dimensions', ['height', 'width'], doc="""
    A namedtuple representing the dimensions of a terminal.

    :param height:
        The height of the terminal.

    :param width:
        The width of the terminal.
""")



[docs]class TerminalWriter(object): """ This is a helper for dealing with output to a terminal. :param stream: A stream which takes unicode for writing. :param prefix: A prefix used when an entire line is written. :param indent: String used for indentation. :param autoescape: Defines if everything written is escaped (unless explicitly turned off), see :func:`escape` for more information. :param ignore_options: Defines if options should be ignored or not, per default options are ignored if the `stream` is not a tty. After each call resulting in visible characters to be written to the `stream` the stream is flushed, certain methods allow to override this behaviour. """ #: Specifies the default terminal width. default_width = 80 @classmethod
[docs] def from_bytestream(cls, stream, encoding=None, errors='strict', **kwargs): """ Returns a :class:`TerminalWriter` for the given byte `stream`. If an encoding cannot be determined from the stream it will fallback to the given `encoding`, if it is `None` :meth:`sys.getdefaultencoding` will be used. Should an error occur during encoding you can specify what should happen with the `errors` parameter: ``'strict'`` Raise an exception if an error occurs. ``'ignore'`` Ignore the characters for which the error occurs, these will be removed from the string. ``'replace'`` Replaces the characters for which the error occurs with 'safe' characters, usually '?'. """ encoding = getattr(stream, 'encoding', encoding) if encoding is None: encoding = sys.getdefaultencoding() return cls( codecs.lookup(encoding).streamwriter(stream, errors), **kwargs )
def __init__(self, stream, prefix=u'', indent=' ' * 4, autoescape=True, ignore_options=None): #: The stream to which the output is written. self.stream = stream #: The prefix used by :meth:`writeline`. self.prefix = prefix #: The string used for indentation. self.indent_string = indent #: ``True`` if escaping should be done automatically. self.autoescape = autoescape is_a_tty = getattr(stream, 'isatty', lambda: False)() if ignore_options is None and is_a_tty: self.ignore_options = False elif ignore_options is None: self.ignore_options = True else: self.ignore_options = ignore_options if is_a_tty and termios and hasattr(stream, 'fileno'): self.control_characters = [ c for c in termios.tcgetattr(stream.fileno())[-1] if isinstance(c, basestring) ] else: # just to be on the safe side... self.control_characters = map(chr, range(32) + [127]) self.ansi_re = re.compile('[%s]' % ''.join(self.control_characters)) self.indentation_level = 0
[docs] def escape(self, string): """ Escapes all control characters in the given `string`. This is useful if you are dealing with 'untrusted' strings you want to write to a file, stdout or stderr which may be viewed using tools such as `cat` which execute ANSI escape sequences. .. seealso:: http://www.ush.it/team/ush/hack_httpd_escape/adv.txt """ return self.ansi_re.sub( lambda m: m.group().encode('unicode-escape'), string )
[docs] def get_dimensions(self): """ Returns a :class:`Dimensions` object. May raise :exc:`NotImplementedError` depending on the stream or platform. """ try: fileno = self.stream.fileno() except AttributeError: pass else: return Dimensions(*struct.unpack('hhhh', fcntl.ioctl( fileno, termios.TIOCGWINSZ, '\000' * 8) )[:2]) raise NotImplementedError( 'not implemented for the given stream or platform' )
[docs] def get_width(self, default=None): """ Returns the width of the terminal. This falls back to the `COLUMNS` environment variable and if that fails to :attr:`default_width` unless `default` is not None, in which case `default` would be used. Therefore the returned value might not not be at all correct. """ default = self.default_width if default is None else default try: _, width = self.get_dimensions() except NotImplementedError: width = int(os.environ.get('COLUMNS', default)) return width
[docs] def get_usable_width(self, default_width=None): """ Returns the width of the terminal remaining once the prefix and indentation has been written. :param default_width: The width which is assumed per default for the terminal, see :meth:`get_width` for more information. .. warning:: Tabs are considered to have a length of 1. This problem may be solved in the future until then it is recommended to avoid tabs. """ return self.get_width(default_width) - ( len(self.prefix) + len(self.indent_string * self.indentation_level) )
[docs] def indent(self): """ Indent the following lines with the given :attr:`indent`. """ self.indentation_level += 1
[docs] def dedent(self): """ Dedent the following lines. """ self.indentation_level -= 1
@contextmanager
[docs] def options(self, text_colour=None, background_colour=None, bold=None, faint=None, standout=None, underline=None, blink=None, indentation=False, escape=None): """ A contextmanager which allows you to set certain options for the following writes. :param text_colour: The desired text colour. :param background_colour: The desired background colour. :param bold: If present the text is displayed bold. :param faint: If present the text is displayed faint. :param standout: If present the text stands out. :param underline: If present the text is underlined. :param blink: If present the text blinks. :param indentation: Adds a level of indentation if ``True``. :param escape: Overrides the escaping behaviour for this block. .. note:: The underlying terminal may support only certain options, especially the attributes (`bold`, `faint`, `standout` and `blink`) are not necessarily available. The following colours are available, the exact colour varies between terminals and their configuration. .. ansi-block:: :string_escape: Colors ====== \x1b[30mblack\x1b[0m \x1b[33myellow\x1b[0m \x1b[36mteal\x1b[0m \x1b[31mred\x1b[0m \x1b[34mblue\x1b[0m \x1b[37mwhite\x1b[0m \x1b[32mgreen\x1b[0m \x1b[35mpurple\x1b[0m """ attributes = [ name for name, using in [ ('bold', bold), ('faint', faint), ('standout', standout), ('underline', underline), ('blink', blink) ] if using ] if not self.ignore_options: if text_colour: self.stream.write(TEXT_COLOURS[text_colour]) if background_colour: self.stream.write(BACKGROUND_COLOURS[background_colour]) for attribute in attributes: if attribute: self.stream.write(ATTRIBUTES[attribute]) if indentation: self.indent() if escape is not None: previous_setting = self.autoescape self.autoescape = escape try: yield self finally: if not self.ignore_options: if text_colour: self.stream.write(TEXT_COLOURS['reset']) if background_colour: self.stream.write(BACKGROUND_COLOURS['reset']) if any(attributes): self.stream.write(ATTRIBUTES['reset']) if indentation: self.dedent() if escape is not None: self.autoescape = previous_setting
[docs] def begin_line(self): """ Writes the prefix and indentation to the stream. """ self.write( self.prefix + (self.indent_string * self.indentation_level), escape=False, flush=False )
@contextmanager
[docs] def line(self): """ A contextmanager which can be used instead of :meth:`writeline`. This is useful if you want to write a line with multiple different options. """ self.begin_line() try: yield finally: self.newline()
[docs] def newline(self): """ Writes a newline to the :attr:`stream`. """ self.write('\n', escape=False, flush=False)
[docs] def should_escape(self, escape): """ Returns :attr:`autoescape` if `escape` is None otherwise `escape`. """ return self.autoescape if escape is None else escape
[docs] def write(self, string, escape=None, flush=True, **options): """ Writes the `given` string to the :attr:`stream`. :param escape: Overrides :attr:`autoescape` if given. :param options: Options for this operation, see :meth:`options`. :param flush: If ``True`` flushes the stream. """ with self.options(**options): self.stream.write( self.escape(string) if self.should_escape(escape) else string ) if flush: self.stream.flush()
[docs] def writeline(self, line, escape=None, flush=True, **options): """ Writes the given `line` to the :attr:`stream` respecting indentation. :param escape: Overrides :attr:`autoescape` if given. :param options: Options for this operation, see :meth:`options`. :param flush: If ``True`` flushes the stream. """ with self.options(**options): self.begin_line() self.write(line, escape=self.should_escape(escape), flush=False) self.newline() if flush: self.stream.flush()
[docs] def writelines(self, lines, escape=None, flush=True, **options): """ Writes each line in the given iterable to the :attr:`stream` respecting indentation. :param escape: Overrides :attr:`autoescape` if given. :param options: Options for this operation, see :meth:`options`. :param flush: If ``True`` flushes the stream. """ with self.options(**options): for line in lines: self.writeline(line, escape=escape, flush=False) if flush: self.stream.flush()
[docs] def hr(self, character=u'-'): """ Writes a horizontal ruler with the width of the terminal to the :attr:`stream`. :param character: Specifies the character used for the ruler. """ self.writeline(character * self.get_width())
[docs] def table(self, content, head=None, padding=1): """ Writes a table using a list of rows (`content`) and an optional `head`. :param padding: Specifies the padding used for each cell to the left and right. :: >>> import sys >>> from brownie.terminal import TerminalWriter >>> writer = TerminalWriter.from_bytestream(sys.stdout) >>> writer.table([ ... [u'foo', u'bar'], ... [u'spam', u'eggs'] ... ]) foo | bar spam | eggs <BLANKLINE> >>> writer.table( ... [ ... [u'foo', u'bar'], ... [u'spam', u'eggs'] ... ], ... [u'hello', u'world'] ... ) hello | world ------+------ foo | bar spam | eggs <BLANKLINE> """ if not content: raise ValueError() if head is not None and len(head) != len(content[0]): raise ValueError() if any(len(content[0]) != len(row) for row in content[1:]): raise ValueError() all_rows = [head] if head is not None else [] + content cell_lengths = [ max(map(len, column)) for column in izip(*all_rows) ] def make_line(row): return u'|'.join( u' ' * padding + cell.ljust(cell_lengths[i]) + u' ' * padding for i, cell in enumerate(imap(self.escape, row)) ).strip() result = map(make_line, content) if head: line = make_line(head) self.writeline(line, escape=False) self.writeline( re.sub(r'[^\|]', '-', line) .replace('|', '+') .ljust(max(map(len, result)), '-'), escape=False ) self.writelines(result, flush=False) self.newline() self.stream.flush()
[docs] def progress(self, description, maxsteps=None, widgets=None): """ Returns a :class:`~brownie.terminal.progress.ProgressBar` object which can be used to write the current progress to the stream. The progress bar is created from the `description` which is a string with the following syntax: Widgets -- the parts of the progress bar which are changed with each update -- are represented in the form ``$[a-zA-Z]+``. Some widgets required that you provide an initial value, this can be done by adding ``:string`` where string is either ``[a-zA-Z]+`` or a double-quoted string. If you want to escape a ``$`` you simply precede it with another ``$``, so ``$$foo` will not be treated as a widget and in the progress bar ``$foo`` will be shown. Quotes (``"``) in strings can be escaped with a backslash (``\``). The following widgets are available: `hint` Shows a string of text that can be given using the `hint` argument at any update performed with :meth:`.ProgressBar.init`, :meth:`.ProgressBar.next` or :meth:`.ProgressBar.finish`. If the argument is not given an empty string is used instead. `percentage` Shows the progress in percent; this requires `maxsteps` to be set. `bar` Shows a simple bar which moves which each update not corresponding with the progress being made. This is useful if you just want to show that something is happening. `sizedbar` Shows a simple progress bar which is being filled corresponding to the percentage of progress. This requires `maxsteps` to be set. `step` Shows the current at maximum number of steps as ``step of steps``, this method takes an initial value determining the unit of each step e.g. if each step represents a byte and you choose `bytes` as a unit a reasonable prefix will be chosen. Supported units: - `bytes` - Uses a binary prefix. This requires `maxsteps` to be set. `time` Shows the elapsed time in hours, minutes and seconds. `speed` Shows the speed in bytes (or with a reasonable prefix) per seconds, this assumes that each `step` represents a byte. If you want to implement your own widget(s) take a look at :class:`brownie.terminal.progress.Widget`, you can use them by passing them in a dictionary (mapping the name to the widget class) via the `widgets` argument. You might also want to take a look at the source code of the built-in widgets. There are two things you have to look out for: :class:`~brownie.terminal.progress.ProgressBar` objects are not reusable if you need another object, call this method again. If you attempt to write to the :attr:`stream` while using a progress bar the behaviour is undefined. """ return ProgressBar.from_string( description, self, maxsteps=maxsteps, widgets=None )
def __repr__(self): return '%s(%r, prefix=%r, indent=%r, autoescape=%r)' % ( self.__class__.__name__, self.stream, self.prefix, self.indent_string, self.autoescape )

Navigation

Documentation overview

Fork me on GitHub