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:
parent
bb5b6ffcec
commit
bc10bd9a4d
6 changed files with 155 additions and 567 deletions
117
dosage
117
dosage
|
@ -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()
|
||||
|
|
|
@ -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()
|
|
@ -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
|
|
@ -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())
|
|
@ -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()
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
requests>=2.0
|
||||
pbr
|
||||
colorama
|
||||
lxml
|
||||
pbr
|
||||
requests>=2.0
|
||||
|
|
Loading…
Reference in a new issue