Streamline color output.

- Depend on external colorama instead of embedding an old copy.
- Move most output code into output module.
- Convert pager to context manager.
This commit is contained in:
Tobias Gruetzmacher 2016-04-10 03:45:00 +02:00
parent bb5b6ffcec
commit bc10bd9a4d
6 changed files with 155 additions and 567 deletions

117
dosage
View file

@ -1,7 +1,8 @@
#!/usr/bin/env python
# -*- coding: iso-8859-1 -*-
# -*- coding: utf-8 -*-
# Copyright (C) 2004-2005 Tristan Seligmann and Jonathan Jacobs
# Copyright (C) 2012-2014 Bastian Kleineidam
# Copyright (C) 2015-2016 Tobias Gruetzmacher
# ___
# / \___ ___ __ _ __ _ ___
# / /\ / _ \/ __|/ _` |/ _` |/ _ \
@ -9,17 +10,14 @@
# /___,' \___/|___/\__,_|\__, |\___|
# |___/
from __future__ import division, print_function
from __future__ import absolute_import, division, print_function
import sys
import os
import argparse
import pydoc
from io import StringIO
import dosagelib
from dosagelib import events, configuration, singleton, director
from dosagelib.output import out
from dosagelib.util import internal_error, strlimit
from dosagelib.ansicolor import get_columns
class ArgumentParser(argparse.ArgumentParser):
@ -27,18 +25,12 @@ class ArgumentParser(argparse.ArgumentParser):
def print_help(self, file=None):
"""Paginate help message on TTYs."""
msg = self.format_help()
if file is None:
file = sys.stdout
if hasattr(file, "isatty") and file.isatty():
pydoc.pager(msg)
else:
print(msg, file=file)
with out.pager():
out.info(self.format_help())
Examples = """\
EXAMPLES
List available comics (ca. 3000 at the moment):
List available comics:
dosage -l
Get the latest comic of for example CalvinAndHobbes and save it in the "Comics"
@ -51,14 +43,14 @@ strips of all of them:
"""
def setupOptions():
def setup_options():
"""Construct option parser.
@return: new option parser
@rtype argparse.ArgumentParser
"""
kwargs = dict(
description = "A comic downloader and archiver.",
epilog = Examples,
description="A comic downloader and archiver.",
epilog=Examples,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
parser = ArgumentParser(**kwargs)
@ -92,7 +84,7 @@ def setupOptions():
return parser
def displayVersion(verbose):
def display_version(verbose):
"""Display application name, version, copyright and license."""
print(configuration.App)
print(configuration.Copyright)
@ -126,29 +118,26 @@ def displayVersion(verbose):
return 0
def setOutputInfo(options):
def set_output_info(options):
"""Set global output level and timestamp option."""
out.level = 0
out.level += options.verbose
out.timestamps = options.timestamps
# debug urllib3
#from requests.packages.urllib3 import add_stderr_logger
#add_stderr_logger()
def displayHelp(options):
def display_help(options):
"""Print help for comic strips."""
errors = 0
try:
for scraperobj in director.getScrapers(options.comic, options.basepath, listing=True):
errors += displayComicHelp(scraperobj)
errors += display_comic_help(scraperobj)
except ValueError as msg:
out.exception(msg)
return 2
return errors
def displayComicHelp(scraperobj):
def display_comic_help(scraperobj):
"""Print help for a comic."""
orig_context = out.context
out.context = scraperobj.getName()
@ -171,19 +160,21 @@ def displayComicHelp(scraperobj):
out.context = orig_context
def voteComics(options):
def vote_comics(options):
"""Vote for comics."""
errors = 0
try:
for scraperobj in director.getScrapers(options.comic, options.basepath, options.adult, options.multimatch):
errors += voteComic(scraperobj)
for scraperobj in director.getScrapers(options.comic, options.basepath,
options.adult,
options.multimatch):
errors += vote_comic(scraperobj)
except ValueError as msg:
out.exception(msg)
errors += 1
return errors
def voteComic(scraperobj):
def vote_comic(scraperobj):
"""Vote for given comic scraper."""
errors = 0
orig_context = out.context
@ -211,88 +202,82 @@ def voteComic(scraperobj):
def run(options):
"""Execute comic commands."""
setOutputInfo(options)
set_output_info(options)
# ensure only one instance of dosage is running
if not options.allow_multiple:
me = singleton.SingleInstance()
singleton.SingleInstance()
if options.version:
return displayVersion(options.verbose)
return display_version(options.verbose)
if options.list:
return doList()
return do_list()
if options.singlelist:
return doList(columnList=False, verbose=options.verbose)
return do_list(column_list=False, verbose=options.verbose)
# after this a list of comic strips is needed
if not options.comic:
out.warn(u'No comics specified, bailing out!')
return 1
if options.modulehelp:
return displayHelp(options)
return display_help(options)
if options.vote:
return voteComics(options)
return vote_comics(options)
return director.getComics(options)
def doList(columnList=True, verbose=False):
def do_list(column_list=True, verbose=False):
"""List available comics."""
orig_context = out.context
out.context = u''
try:
page = hasattr(sys.stdout, "isatty") and sys.stdout.isatty()
if page:
fd = StringIO(u'')
out.setStream(fd)
with out.pager():
out.info(u'Available comic scrapers:')
out.info(u'Comics tagged with [%s] require age confirmation with the --adult option.' % TAG_ADULT)
out.info(u'Non-english comics are tagged with [%s].' % TAG_LANG)
scrapers = sorted(director.getAllScrapers(listing=True), key=lambda s: s.getName())
if columnList:
num, disabled = doColumnList(scrapers)
scrapers = sorted(director.getAllScrapers(listing=True),
key=lambda s: s.getName())
if column_list:
num, disabled = do_column_list(scrapers)
else:
num, disabled = doSingleList(scrapers, verbose=verbose)
num, disabled = do_single_list(scrapers, verbose=verbose)
out.info(u'%d supported comics.' % num)
if disabled:
out.info('')
out.info(u'Some comics are disabled, they are tagged with [%s:REASON], where REASON is one of:' % TAG_DISABLED)
for k in disabled:
out.info(u' %-10s %s' % (k, disabled[k]))
if page:
pydoc.pager(fd.getvalue())
return 0
finally:
out.context = orig_context
return 0
def doSingleList(scrapers, verbose=False):
def do_single_list(scrapers, verbose=False):
"""Get list of scraper names, one per line."""
disabled = {}
for num, scraperobj in enumerate(scrapers):
if verbose:
displayComicHelp(scraperobj)
display_comic_help(scraperobj)
else:
out.info(getTaggedScraperName(scraperobj, reasons=disabled))
out.info(get_tagged_scraper_name(scraperobj, reasons=disabled))
return num, disabled
def doColumnList(scrapers):
def do_column_list(scrapers):
"""Get list of scraper names with multiple names per line."""
disabled = {}
screenWidth = get_columns(sys.stdout)
width = out.width
# limit name length so at least two columns are there
limit = (screenWidth // 2) - 8
names = [getTaggedScraperName(scraperobj, limit=limit, reasons=disabled) for scraperobj in scrapers]
limit = (width // 2) - 8
names = [get_tagged_scraper_name(scraperobj, limit=limit, reasons=disabled)
for scraperobj in scrapers]
num = len(names)
maxlen = max(len(name) for name in names)
namesPerLine = max(screenWidth // (maxlen + 1), 1)
names_per_line = max(width // (maxlen + 1), 1)
while names:
out.info(u''.join(name.ljust(maxlen) for name in names[:namesPerLine]))
del names[:namesPerLine]
out.info(u''.join(name.ljust(maxlen) for name in
names[:names_per_line]))
del names[:names_per_line]
return num, disabled
TAG_ADULT = "adult"
TAG_LANG = "lang"
TAG_DISABLED = "dis"
def getTaggedScraperName(scraperobj, limit=None, reasons=None):
def get_tagged_scraper_name(scraperobj, limit=None, reasons=None):
"""Get comic scraper name."""
tags = []
if scraperobj.adult:
@ -317,7 +302,7 @@ def getTaggedScraperName(scraperobj, limit=None, reasons=None):
def main():
"""Parse options and execute commands."""
try:
options = setupOptions().parse_args()
options = setup_options().parse_args()
options.basepath = os.path.expanduser(options.basepath)
res = run(options)
except KeyboardInterrupt:
@ -344,5 +329,3 @@ def viewprof():
if __name__ == '__main__':
sys.exit(main())
#profile()
#viewprof()

View file

@ -1,308 +0,0 @@
# -*- coding: iso-8859-1 -*-
# Copyright (C) 2000-2014 Bastian Kleineidam
"""
ANSI Color definitions and functions. For Windows systems, the colorama module
uses ctypes and Windows DLLs to generate colored output.
From Term::ANSIColor, applies also to this module:
The codes output by this module are standard terminal control codes,
complying with ECMA-48 and ISO 6429 (generally referred to as ``ANSI color''
for the color codes). The non-color control codes (bold, dark, italic,
underline, and reverse) are part of the earlier ANSI X3.64 standard for
control sequences for video terminals and peripherals.
Note that not all displays are ISO 6429-compliant, or even X3.64-compliant
(or are even attempting to be so).
Jean Delvare provided the following table of different common terminal
emulators and their support for the various attributes and others have
helped me flesh it out::
clear bold dark under blink reverse conceal
------------------------------------------------------------------------
xterm yes yes no yes bold yes yes
linux yes yes yes bold yes yes no
rxvt yes yes no yes bold/black yes no
dtterm yes yes yes yes reverse yes yes
teraterm yes reverse no yes rev/red yes no
aixterm kinda normal no yes no yes yes
PuTTY yes color no yes no yes no
Windows yes no no no no yes no
Cygwin SSH yes yes no color color color yes
SEE ALSO
ECMA-048 is available on-line (at least at the time of this writing) at
http://www.ecma-international.org/publications/standards/ECMA-048.HTM.
ISO 6429 is available from ISO for a charge; the author of this module does
not own a copy of it. Since the source material for ISO 6429 was ECMA-048
and the latter is available for free, there seems little reason to obtain
the ISO standard.
"""
import os
import logging
import types
from .fileutil import has_module, is_tty
if os.name == 'nt':
from . import colorama
has_curses = has_module("curses")
# Color constants
# Escape for ANSI colors
AnsiEsc = "\x1b[%sm"
# Control constants
bold = 'bold'
light = 'light'
underline = 'underline'
blink = 'blink'
invert = 'invert'
concealed = 'concealed'
# Control numbers
AnsiControl = {
None: '',
bold: '1',
light: '2',
#italic: '3', # unsupported
underline: '4',
blink: '5',
#rapidblink: '6', # unsupported
invert: '7',
concealed: '8',
#strikethrough: '9', # unsupported
}
# Color constants
default = 'default'
black = 'black'
red = 'red'
green = 'green'
yellow = 'yellow'
blue = 'blue'
purple = 'purple'
cyan = 'cyan'
white = 'white'
# inverse colors
Black = 'Black'
Red = 'Red'
Green = 'Green'
Yellow = 'Yellow'
Blue = 'Blue'
Purple = 'Purple'
Cyan = 'Cyna'
White = 'White'
InverseColors = (Black, Red, Green, Yellow, Blue, Purple, Cyan, White)
# Ansi color numbers; capitalized colors are inverse
AnsiColor = {
None: '0',
default: '0',
black: '30',
red: '31',
green: '32',
yellow: '33',
blue: '34',
purple: '35',
cyan: '36',
white: '37',
Black: '40',
Red: '41',
Green: '42',
Yellow: '43',
Blue: '44',
Purple: '45',
Cyan: '46',
White: '47',
}
if os.name == 'nt':
# Windows color numbers; capitalized colors are used as background
WinColor = {
None: None,
default: colorama.GREY,
black: colorama.BLACK,
red: colorama.RED,
green: colorama.GREEN,
yellow: colorama.YELLOW,
blue: colorama.BLUE,
purple: colorama.MAGENTA,
cyan: colorama.CYAN,
white: colorama.GREY,
Black: colorama.BLACK,
Red: colorama.RED,
Green: colorama.GREEN,
Yellow: colorama.YELLOW,
Blue: colorama.BLUE,
Purple: colorama.MAGENTA,
Cyan: colorama.CYAN,
White: colorama.GREY,
}
# pc speaker beep escape code
Beep = "\007"
def esc_ansicolor (color):
"""convert a named color definition to an escaped ANSI color"""
control = ''
if ";" in color:
control, color = color.split(";", 1)
control = AnsiControl.get(control, '')+";"
cnum = AnsiColor.get(color, '0')
return AnsiEsc % (control+cnum)
AnsiReset = esc_ansicolor(default)
def get_win_color(color):
"""Convert a named color definition to Windows console color foreground,
background and style numbers."""
foreground = background = style = None
control = ''
if ";" in color:
control, color = color.split(";", 1)
if control == bold:
style = colorama.BRIGHT
if color in InverseColors:
background = WinColor[color]
else:
foreground = WinColor.get(color)
return foreground, background, style
def has_colors (fp):
"""Test if given file is an ANSI color enabled tty."""
# The is_tty() function ensures that we do not colorize
# redirected streams, as this is almost never what we want
if not is_tty(fp):
return False
if os.name == 'nt':
return True
elif has_curses:
import curses
try:
curses.setupterm(os.environ.get("TERM"), fp.fileno())
# More than 8 colors are good enough.
return curses.tigetnum("colors") >= 8
except curses.error:
return False
return False
def get_columns (fp):
"""Return number of columns for given file."""
if not is_tty(fp):
return 80
if os.name == 'nt':
return colorama.get_console_size().X
if has_curses:
import curses
try:
curses.setupterm(os.environ.get("TERM"), fp.fileno())
return curses.tigetnum("cols")
except curses.error:
pass
return 80
def _write_color_colorama (fp, text, color):
"""Colorize text with given color."""
foreground, background, style = get_win_color(color)
colorama.set_console(foreground=foreground, background=background,
style=style)
fp.write(text)
colorama.reset_console()
def _write_color_ansi (fp, text, color):
"""Colorize text with given color."""
fp.write(esc_ansicolor(color))
fp.write(text)
fp.write(AnsiReset)
if os.name == 'nt':
write_color = _write_color_colorama
colorama.init()
else:
write_color = _write_color_ansi
class Colorizer (object):
"""Prints colored messages to streams."""
def __init__ (self, fp):
"""Initialize with given stream (file-like object)."""
super(Colorizer, self).__init__()
self.fp = fp
if has_colors(fp):
self.write = self._write_color
else:
self.write = self._write
def _write (self, text, color=None):
"""Print text as-is."""
self.fp.write(text)
def _write_color (self, text, color=None):
"""Print text with given color. If color is None, print text as-is."""
if color is None:
self.fp.write(text)
else:
write_color(self.fp, text, color)
def __getattr__ (self, name):
"""Delegate attribute access to the stored stream object."""
return getattr(self.fp, name)
class ColoredStreamHandler (logging.StreamHandler, object):
"""Send colored log messages to streams (file-like objects)."""
def __init__ (self, strm=None):
"""Log to given stream (a file-like object) or to stderr if
strm is None.
"""
super(ColoredStreamHandler, self).__init__(strm)
self.stream = Colorizer(self.stream)
# standard log level colors (used by get_color)
self.colors = {
logging.WARN: 'bold;yellow',
logging.ERROR: 'light;red',
logging.CRITICAL: 'bold;red',
logging.DEBUG: 'white',
}
def get_color (self, record):
"""Get appropriate color according to log level.
"""
return self.colors.get(record.levelno, 'default')
def emit (self, record):
"""Emit a record.
If a formatter is specified, it is used to format the record.
The record is then written to the stream with a trailing newline
[N.B. this may be removed depending on feedback].
"""
color = self.get_color(record)
msg = self.format(record)
if not hasattr(types, "UnicodeType"):
# no unicode support
self.stream.write("%s" % msg, color=color)
else:
try:
self.stream.write("%s" % msg, color=color)
except UnicodeError:
self.stream.write("%s" % msg.encode("UTF-8"),
color=color)
self.stream.write(os.linesep)
self.flush()

View file

@ -1,157 +0,0 @@
# These functions are part of the python-colorama module
# They have been adjusted slightly for dosage
#
# Copyright: (C) 2010 Jonathan Hartley <tartley@tartley.com>
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
# 1. Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
# 3. Neither the name(s) of the copyright holders nor the
# names of its contributors may be used to endorse or promote products
# derived from this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY
# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
# from winbase.h
STDOUT = -11
STDERR = -12
from ctypes import (windll, byref, Structure, c_char, c_short, c_uint32,
c_ushort, ArgumentError, WinError)
handles = {
STDOUT: windll.kernel32.GetStdHandle(STDOUT),
STDERR: windll.kernel32.GetStdHandle(STDERR),
}
SHORT = c_short
WORD = c_ushort
DWORD = c_uint32
TCHAR = c_char
class COORD(Structure):
"""struct in wincon.h"""
_fields_ = [
('X', SHORT),
('Y', SHORT),
]
class SMALL_RECT(Structure):
"""struct in wincon.h."""
_fields_ = [
("Left", SHORT),
("Top", SHORT),
("Right", SHORT),
("Bottom", SHORT),
]
class CONSOLE_SCREEN_BUFFER_INFO(Structure):
"""struct in wincon.h."""
_fields_ = [
("dwSize", COORD),
("dwCursorPosition", COORD),
("wAttributes", WORD),
("srWindow", SMALL_RECT),
("dwMaximumWindowSize", COORD),
]
def __str__(self):
"""Get string representation of console screen buffer info."""
return '(%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d)' % (
self.dwSize.Y, self.dwSize.X
, self.dwCursorPosition.Y, self.dwCursorPosition.X
, self.wAttributes
, self.srWindow.Top, self.srWindow.Left, self.srWindow.Bottom, self.srWindow.Right
, self.dwMaximumWindowSize.Y, self.dwMaximumWindowSize.X
)
def GetConsoleScreenBufferInfo(stream_id=STDOUT):
"""Get console screen buffer info object."""
handle = handles[stream_id]
csbi = CONSOLE_SCREEN_BUFFER_INFO()
success = windll.kernel32.GetConsoleScreenBufferInfo(
handle, byref(csbi))
if not success:
raise WinError()
return csbi
def SetConsoleTextAttribute(stream_id, attrs):
"""Set a console text attribute."""
handle = handles[stream_id]
return windll.kernel32.SetConsoleTextAttribute(handle, attrs)
# from wincon.h
BLACK = 0
BLUE = 1
GREEN = 2
CYAN = 3
RED = 4
MAGENTA = 5
YELLOW = 6
GREY = 7
# from wincon.h
NORMAL = 0x00 # dim text, dim background
BRIGHT = 0x08 # bright text, dim background
_default_foreground = None
_default_background = None
_default_style = None
def init():
"""Initialize foreground and background attributes."""
global _default_foreground, _default_background, _default_style
try:
attrs = GetConsoleScreenBufferInfo().wAttributes
except (ArgumentError, WindowsError):
_default_foreground = GREY
_default_background = BLACK
_default_style = NORMAL
else:
_default_foreground = attrs & 7
_default_background = (attrs >> 4) & 7
_default_style = attrs & BRIGHT
def get_attrs(foreground, background, style):
"""Get foreground and background attributes."""
return foreground + (background << 4) + style
def set_console(stream=STDOUT, foreground=None, background=None, style=None):
"""Set console foreground and background attributes."""
if foreground is None:
foreground = _default_foreground
if background is None:
background = _default_background
if style is None:
style = _default_style
attrs = get_attrs(foreground, background, style)
SetConsoleTextAttribute(stream, attrs)
def reset_console(stream=STDOUT):
"""Reset the console."""
set_console(stream=stream)
def get_console_size():
"""Get the console size."""
return GetConsoleScreenBufferInfo().dwSize

View file

@ -1,23 +0,0 @@
# -*- coding: iso-8859-1 -*-
# Copyright (C) 2012 Bastian Kleineidam
"""
File and path utilities.
"""
import importlib
def has_module (name):
"""Test if given module can be imported.
@return: flag if import is successful
@rtype: bool
"""
try:
importlib.import_module(name)
return True
except (OSError, ImportError):
# some modules (for example HTMLtidy) raise OSError
return False
def is_tty (fp):
"""Check if a file object is a TTY."""
return (hasattr(fp, "isatty") and fp.isatty())

View file

@ -1,7 +1,9 @@
# -*- coding: utf-8 -*-
# Copyright (C) 2004-2005 Tristan Seligmann and Jonathan Jacobs
# Copyright (C) 2012-2014 Bastian Kleineidam
# Copyright (C) 2005-2016 Tobias Gruetzmacher
# Copyright (C) 2015-2016 Tobias Gruetzmacher
from __future__ import absolute_import, division, print_function
import time
import sys
@ -9,7 +11,17 @@ import os
import threading
import traceback
import codecs
from .ansicolor import Colorizer
import contextlib
import pydoc
import io
try:
import curses
except ImportError:
curses = None
import colorama
from colorama import Fore, Style, win32
lock = threading.Lock()
@ -23,12 +35,15 @@ def get_threadname():
class Output(object):
"""Print output with context, indentation and optional timestamps."""
DEFAULT_WIDTH = 80
def __init__(self, stream=None):
"""Initialize context and indentation."""
self.context = None
self.level = 0
self.timestamps = False
if stream is None:
colorama.init(wrap=False)
if hasattr(sys.stdout, "encoding") and sys.stdout.encoding:
self.encoding = sys.stdout.encoding
else:
@ -38,11 +53,21 @@ class Output(object):
else:
stream = sys.stdout
stream = codecs.getwriter(self.encoding)(stream, 'replace')
self.setStream(stream)
if os.name == 'nt':
stream = colorama.AnsiToWin32(stream).stream
self.stream = stream
def setStream(self, stream):
"""Initialize context and indentation."""
self.stream = Colorizer(stream)
@property
def stream(self):
"""The underlaying stream."""
return self._stream
@stream.setter
def stream(self, attr):
"""Change stream and base stream. base_stream is used for terminal
interaction when _stream is redirected to a pager."""
self._stream = attr
self._base_stream = attr
def info(self, s, level=0):
"""Write an informational message."""
@ -50,15 +75,16 @@ class Output(object):
def debug(self, s, level=2):
"""Write a debug message."""
self.write(s, level=level, color='white')
# "white" is the default color for most terminals...
self.write(s, level=level, color=Fore.WHITE)
def warn(self, s):
"""Write a warning message."""
self.write(u"WARN: %s" % s, color='bold;yellow')
self.write(u"WARN: %s" % s, color=Style.BRIGHT + Fore.YELLOW)
def error(self, s, tb=None):
"""Write an error message."""
self.write(u"ERROR: %s" % s, color='light;red')
self.write(u"ERROR: %s" % s, color=Style.DIM + Fore.RED)
def exception(self, s):
"""Write error message with traceback info."""
@ -81,11 +107,13 @@ class Output(object):
self.stream.write(u'%s%s> ' % (timestamp, self.context))
elif self.context is None:
self.stream.write(u'%s%s> ' % (timestamp, get_threadname()))
self.stream.write(u'%s' % s, color=color)
if color and self.has_color:
s = u'%s%s%s' % (color, s, Style.RESET_ALL)
try:
text_type = unicode
except NameError:
text_type = str
self.stream.write(text_type(s))
self.stream.write(text_type(os.linesep))
self.stream.flush()
@ -95,4 +123,68 @@ class Output(object):
for line in line.rstrip(u'\n').split(u'\n'):
self.write(line.rstrip(u'\n'), level=level)
@property
def has_color(self):
if not self.is_tty:
return False
elif os.name == 'nt':
return True
elif curses:
try:
curses.setupterm(os.environ.get("TERM"),
self._base_stream.fileno())
# More than 8 colors are good enough.
return curses.tigetnum("colors") >= 8
except curses.error:
return False
return False
@property
def width(self):
"""Get width of this output."""
if not self.is_tty:
return self.DEFAULT_WIDTH
elif os.name == 'nt':
csbi = win32.GetConsoleScreenBufferInfo(win32.STDOUT)
return csbi.dwSize.X
elif curses:
try:
curses.setupterm(os.environ.get("TERM"),
self._base_stream.fileno())
return curses.tigetnum("cols")
except curses.error:
pass
return self.DEFAULT_WIDTH
@property
def is_tty(self):
"""Is this output stream a terminal?"""
return (hasattr(self._base_stream, "isatty") and
self._base_stream.isatty())
@contextlib.contextmanager
def temporary_context(self, context):
"""Run a block with a temporary output context"""
orig_context = self.context
self.context = context
try:
yield
finally:
self.context = orig_context
@contextlib.contextmanager
def pager(self):
"""Run the output of a block through a pager."""
try:
if self.is_tty:
fd = io.StringIO()
self._stream = fd
with self.temporary_context(u''):
yield
if self.is_tty:
pydoc.pager(fd.getvalue())
finally:
self._stream = self._base_stream
out = Output()

View file

@ -1,3 +1,4 @@
requests>=2.0
pbr
colorama
lxml
pbr
requests>=2.0