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
|
#!/usr/bin/env python
|
||||||
# -*- coding: iso-8859-1 -*-
|
# -*- coding: utf-8 -*-
|
||||||
# Copyright (C) 2004-2005 Tristan Seligmann and Jonathan Jacobs
|
# Copyright (C) 2004-2005 Tristan Seligmann and Jonathan Jacobs
|
||||||
# Copyright (C) 2012-2014 Bastian Kleineidam
|
# 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 sys
|
||||||
import os
|
import os
|
||||||
import argparse
|
import argparse
|
||||||
import pydoc
|
|
||||||
from io import StringIO
|
|
||||||
import dosagelib
|
import dosagelib
|
||||||
from dosagelib import events, configuration, singleton, director
|
from dosagelib import events, configuration, singleton, director
|
||||||
from dosagelib.output import out
|
from dosagelib.output import out
|
||||||
from dosagelib.util import internal_error, strlimit
|
from dosagelib.util import internal_error, strlimit
|
||||||
from dosagelib.ansicolor import get_columns
|
|
||||||
|
|
||||||
|
|
||||||
class ArgumentParser(argparse.ArgumentParser):
|
class ArgumentParser(argparse.ArgumentParser):
|
||||||
|
@ -27,18 +25,12 @@ class ArgumentParser(argparse.ArgumentParser):
|
||||||
|
|
||||||
def print_help(self, file=None):
|
def print_help(self, file=None):
|
||||||
"""Paginate help message on TTYs."""
|
"""Paginate help message on TTYs."""
|
||||||
msg = self.format_help()
|
with out.pager():
|
||||||
if file is None:
|
out.info(self.format_help())
|
||||||
file = sys.stdout
|
|
||||||
if hasattr(file, "isatty") and file.isatty():
|
|
||||||
pydoc.pager(msg)
|
|
||||||
else:
|
|
||||||
print(msg, file=file)
|
|
||||||
|
|
||||||
|
|
||||||
Examples = """\
|
Examples = """\
|
||||||
EXAMPLES
|
EXAMPLES
|
||||||
List available comics (ca. 3000 at the moment):
|
List available comics:
|
||||||
dosage -l
|
dosage -l
|
||||||
|
|
||||||
Get the latest comic of for example CalvinAndHobbes and save it in the "Comics"
|
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.
|
"""Construct option parser.
|
||||||
@return: new option parser
|
@return: new option parser
|
||||||
@rtype argparse.ArgumentParser
|
@rtype argparse.ArgumentParser
|
||||||
"""
|
"""
|
||||||
kwargs = dict(
|
kwargs = dict(
|
||||||
description = "A comic downloader and archiver.",
|
description="A comic downloader and archiver.",
|
||||||
epilog = Examples,
|
epilog=Examples,
|
||||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||||
)
|
)
|
||||||
parser = ArgumentParser(**kwargs)
|
parser = ArgumentParser(**kwargs)
|
||||||
|
@ -92,7 +84,7 @@ def setupOptions():
|
||||||
return parser
|
return parser
|
||||||
|
|
||||||
|
|
||||||
def displayVersion(verbose):
|
def display_version(verbose):
|
||||||
"""Display application name, version, copyright and license."""
|
"""Display application name, version, copyright and license."""
|
||||||
print(configuration.App)
|
print(configuration.App)
|
||||||
print(configuration.Copyright)
|
print(configuration.Copyright)
|
||||||
|
@ -126,29 +118,26 @@ def displayVersion(verbose):
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|
||||||
def setOutputInfo(options):
|
def set_output_info(options):
|
||||||
"""Set global output level and timestamp option."""
|
"""Set global output level and timestamp option."""
|
||||||
out.level = 0
|
out.level = 0
|
||||||
out.level += options.verbose
|
out.level += options.verbose
|
||||||
out.timestamps = options.timestamps
|
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."""
|
"""Print help for comic strips."""
|
||||||
errors = 0
|
errors = 0
|
||||||
try:
|
try:
|
||||||
for scraperobj in director.getScrapers(options.comic, options.basepath, listing=True):
|
for scraperobj in director.getScrapers(options.comic, options.basepath, listing=True):
|
||||||
errors += displayComicHelp(scraperobj)
|
errors += display_comic_help(scraperobj)
|
||||||
except ValueError as msg:
|
except ValueError as msg:
|
||||||
out.exception(msg)
|
out.exception(msg)
|
||||||
return 2
|
return 2
|
||||||
return errors
|
return errors
|
||||||
|
|
||||||
|
|
||||||
def displayComicHelp(scraperobj):
|
def display_comic_help(scraperobj):
|
||||||
"""Print help for a comic."""
|
"""Print help for a comic."""
|
||||||
orig_context = out.context
|
orig_context = out.context
|
||||||
out.context = scraperobj.getName()
|
out.context = scraperobj.getName()
|
||||||
|
@ -171,19 +160,21 @@ def displayComicHelp(scraperobj):
|
||||||
out.context = orig_context
|
out.context = orig_context
|
||||||
|
|
||||||
|
|
||||||
def voteComics(options):
|
def vote_comics(options):
|
||||||
"""Vote for comics."""
|
"""Vote for comics."""
|
||||||
errors = 0
|
errors = 0
|
||||||
try:
|
try:
|
||||||
for scraperobj in director.getScrapers(options.comic, options.basepath, options.adult, options.multimatch):
|
for scraperobj in director.getScrapers(options.comic, options.basepath,
|
||||||
errors += voteComic(scraperobj)
|
options.adult,
|
||||||
|
options.multimatch):
|
||||||
|
errors += vote_comic(scraperobj)
|
||||||
except ValueError as msg:
|
except ValueError as msg:
|
||||||
out.exception(msg)
|
out.exception(msg)
|
||||||
errors += 1
|
errors += 1
|
||||||
return errors
|
return errors
|
||||||
|
|
||||||
|
|
||||||
def voteComic(scraperobj):
|
def vote_comic(scraperobj):
|
||||||
"""Vote for given comic scraper."""
|
"""Vote for given comic scraper."""
|
||||||
errors = 0
|
errors = 0
|
||||||
orig_context = out.context
|
orig_context = out.context
|
||||||
|
@ -211,88 +202,82 @@ def voteComic(scraperobj):
|
||||||
|
|
||||||
def run(options):
|
def run(options):
|
||||||
"""Execute comic commands."""
|
"""Execute comic commands."""
|
||||||
setOutputInfo(options)
|
set_output_info(options)
|
||||||
# ensure only one instance of dosage is running
|
# ensure only one instance of dosage is running
|
||||||
if not options.allow_multiple:
|
if not options.allow_multiple:
|
||||||
me = singleton.SingleInstance()
|
singleton.SingleInstance()
|
||||||
if options.version:
|
if options.version:
|
||||||
return displayVersion(options.verbose)
|
return display_version(options.verbose)
|
||||||
if options.list:
|
if options.list:
|
||||||
return doList()
|
return do_list()
|
||||||
if options.singlelist:
|
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
|
# after this a list of comic strips is needed
|
||||||
if not options.comic:
|
if not options.comic:
|
||||||
out.warn(u'No comics specified, bailing out!')
|
out.warn(u'No comics specified, bailing out!')
|
||||||
return 1
|
return 1
|
||||||
if options.modulehelp:
|
if options.modulehelp:
|
||||||
return displayHelp(options)
|
return display_help(options)
|
||||||
if options.vote:
|
if options.vote:
|
||||||
return voteComics(options)
|
return vote_comics(options)
|
||||||
return director.getComics(options)
|
return director.getComics(options)
|
||||||
|
|
||||||
|
|
||||||
def doList(columnList=True, verbose=False):
|
def do_list(column_list=True, verbose=False):
|
||||||
"""List available comics."""
|
"""List available comics."""
|
||||||
orig_context = out.context
|
with out.pager():
|
||||||
out.context = u''
|
|
||||||
try:
|
|
||||||
page = hasattr(sys.stdout, "isatty") and sys.stdout.isatty()
|
|
||||||
if page:
|
|
||||||
fd = StringIO(u'')
|
|
||||||
out.setStream(fd)
|
|
||||||
out.info(u'Available comic scrapers:')
|
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'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)
|
out.info(u'Non-english comics are tagged with [%s].' % TAG_LANG)
|
||||||
scrapers = sorted(director.getAllScrapers(listing=True), key=lambda s: s.getName())
|
scrapers = sorted(director.getAllScrapers(listing=True),
|
||||||
if columnList:
|
key=lambda s: s.getName())
|
||||||
num, disabled = doColumnList(scrapers)
|
if column_list:
|
||||||
|
num, disabled = do_column_list(scrapers)
|
||||||
else:
|
else:
|
||||||
num, disabled = doSingleList(scrapers, verbose=verbose)
|
num, disabled = do_single_list(scrapers, verbose=verbose)
|
||||||
out.info(u'%d supported comics.' % num)
|
out.info(u'%d supported comics.' % num)
|
||||||
if disabled:
|
if disabled:
|
||||||
out.info('')
|
out.info('')
|
||||||
out.info(u'Some comics are disabled, they are tagged with [%s:REASON], where REASON is one of:' % TAG_DISABLED)
|
out.info(u'Some comics are disabled, they are tagged with [%s:REASON], where REASON is one of:' % TAG_DISABLED)
|
||||||
for k in disabled:
|
for k in disabled:
|
||||||
out.info(u' %-10s %s' % (k, disabled[k]))
|
out.info(u' %-10s %s' % (k, disabled[k]))
|
||||||
if page:
|
return 0
|
||||||
pydoc.pager(fd.getvalue())
|
|
||||||
return 0
|
|
||||||
finally:
|
|
||||||
out.context = orig_context
|
|
||||||
|
|
||||||
|
|
||||||
def doSingleList(scrapers, verbose=False):
|
def do_single_list(scrapers, verbose=False):
|
||||||
"""Get list of scraper names, one per line."""
|
"""Get list of scraper names, one per line."""
|
||||||
disabled = {}
|
disabled = {}
|
||||||
for num, scraperobj in enumerate(scrapers):
|
for num, scraperobj in enumerate(scrapers):
|
||||||
if verbose:
|
if verbose:
|
||||||
displayComicHelp(scraperobj)
|
display_comic_help(scraperobj)
|
||||||
else:
|
else:
|
||||||
out.info(getTaggedScraperName(scraperobj, reasons=disabled))
|
out.info(get_tagged_scraper_name(scraperobj, reasons=disabled))
|
||||||
return num, disabled
|
return num, disabled
|
||||||
|
|
||||||
|
|
||||||
def doColumnList(scrapers):
|
def do_column_list(scrapers):
|
||||||
"""Get list of scraper names with multiple names per line."""
|
"""Get list of scraper names with multiple names per line."""
|
||||||
disabled = {}
|
disabled = {}
|
||||||
screenWidth = get_columns(sys.stdout)
|
width = out.width
|
||||||
# limit name length so at least two columns are there
|
# limit name length so at least two columns are there
|
||||||
limit = (screenWidth // 2) - 8
|
limit = (width // 2) - 8
|
||||||
names = [getTaggedScraperName(scraperobj, limit=limit, reasons=disabled) for scraperobj in scrapers]
|
names = [get_tagged_scraper_name(scraperobj, limit=limit, reasons=disabled)
|
||||||
|
for scraperobj in scrapers]
|
||||||
num = len(names)
|
num = len(names)
|
||||||
maxlen = max(len(name) for name in 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:
|
while names:
|
||||||
out.info(u''.join(name.ljust(maxlen) for name in names[:namesPerLine]))
|
out.info(u''.join(name.ljust(maxlen) for name in
|
||||||
del names[:namesPerLine]
|
names[:names_per_line]))
|
||||||
|
del names[:names_per_line]
|
||||||
return num, disabled
|
return num, disabled
|
||||||
|
|
||||||
TAG_ADULT = "adult"
|
TAG_ADULT = "adult"
|
||||||
TAG_LANG = "lang"
|
TAG_LANG = "lang"
|
||||||
TAG_DISABLED = "dis"
|
TAG_DISABLED = "dis"
|
||||||
|
|
||||||
def getTaggedScraperName(scraperobj, limit=None, reasons=None):
|
|
||||||
|
def get_tagged_scraper_name(scraperobj, limit=None, reasons=None):
|
||||||
"""Get comic scraper name."""
|
"""Get comic scraper name."""
|
||||||
tags = []
|
tags = []
|
||||||
if scraperobj.adult:
|
if scraperobj.adult:
|
||||||
|
@ -317,7 +302,7 @@ def getTaggedScraperName(scraperobj, limit=None, reasons=None):
|
||||||
def main():
|
def main():
|
||||||
"""Parse options and execute commands."""
|
"""Parse options and execute commands."""
|
||||||
try:
|
try:
|
||||||
options = setupOptions().parse_args()
|
options = setup_options().parse_args()
|
||||||
options.basepath = os.path.expanduser(options.basepath)
|
options.basepath = os.path.expanduser(options.basepath)
|
||||||
res = run(options)
|
res = run(options)
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
|
@ -344,5 +329,3 @@ def viewprof():
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
sys.exit(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 -*-
|
# -*- coding: utf-8 -*-
|
||||||
# Copyright (C) 2004-2005 Tristan Seligmann and Jonathan Jacobs
|
# Copyright (C) 2004-2005 Tristan Seligmann and Jonathan Jacobs
|
||||||
# Copyright (C) 2012-2014 Bastian Kleineidam
|
# 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 time
|
||||||
import sys
|
import sys
|
||||||
|
@ -9,7 +11,17 @@ import os
|
||||||
import threading
|
import threading
|
||||||
import traceback
|
import traceback
|
||||||
import codecs
|
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()
|
lock = threading.Lock()
|
||||||
|
@ -23,12 +35,15 @@ def get_threadname():
|
||||||
class Output(object):
|
class Output(object):
|
||||||
"""Print output with context, indentation and optional timestamps."""
|
"""Print output with context, indentation and optional timestamps."""
|
||||||
|
|
||||||
|
DEFAULT_WIDTH = 80
|
||||||
|
|
||||||
def __init__(self, stream=None):
|
def __init__(self, stream=None):
|
||||||
"""Initialize context and indentation."""
|
"""Initialize context and indentation."""
|
||||||
self.context = None
|
self.context = None
|
||||||
self.level = 0
|
self.level = 0
|
||||||
self.timestamps = False
|
self.timestamps = False
|
||||||
if stream is None:
|
if stream is None:
|
||||||
|
colorama.init(wrap=False)
|
||||||
if hasattr(sys.stdout, "encoding") and sys.stdout.encoding:
|
if hasattr(sys.stdout, "encoding") and sys.stdout.encoding:
|
||||||
self.encoding = sys.stdout.encoding
|
self.encoding = sys.stdout.encoding
|
||||||
else:
|
else:
|
||||||
|
@ -38,11 +53,21 @@ class Output(object):
|
||||||
else:
|
else:
|
||||||
stream = sys.stdout
|
stream = sys.stdout
|
||||||
stream = codecs.getwriter(self.encoding)(stream, 'replace')
|
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):
|
@property
|
||||||
"""Initialize context and indentation."""
|
def stream(self):
|
||||||
self.stream = Colorizer(stream)
|
"""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):
|
def info(self, s, level=0):
|
||||||
"""Write an informational message."""
|
"""Write an informational message."""
|
||||||
|
@ -50,15 +75,16 @@ class Output(object):
|
||||||
|
|
||||||
def debug(self, s, level=2):
|
def debug(self, s, level=2):
|
||||||
"""Write a debug message."""
|
"""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):
|
def warn(self, s):
|
||||||
"""Write a warning message."""
|
"""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):
|
def error(self, s, tb=None):
|
||||||
"""Write an error message."""
|
"""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):
|
def exception(self, s):
|
||||||
"""Write error message with traceback info."""
|
"""Write error message with traceback info."""
|
||||||
|
@ -81,11 +107,13 @@ class Output(object):
|
||||||
self.stream.write(u'%s%s> ' % (timestamp, self.context))
|
self.stream.write(u'%s%s> ' % (timestamp, self.context))
|
||||||
elif self.context is None:
|
elif self.context is None:
|
||||||
self.stream.write(u'%s%s> ' % (timestamp, get_threadname()))
|
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:
|
try:
|
||||||
text_type = unicode
|
text_type = unicode
|
||||||
except NameError:
|
except NameError:
|
||||||
text_type = str
|
text_type = str
|
||||||
|
self.stream.write(text_type(s))
|
||||||
self.stream.write(text_type(os.linesep))
|
self.stream.write(text_type(os.linesep))
|
||||||
self.stream.flush()
|
self.stream.flush()
|
||||||
|
|
||||||
|
@ -95,4 +123,68 @@ class Output(object):
|
||||||
for line in line.rstrip(u'\n').split(u'\n'):
|
for line in line.rstrip(u'\n').split(u'\n'):
|
||||||
self.write(line.rstrip(u'\n'), level=level)
|
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()
|
out = Output()
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
requests>=2.0
|
colorama
|
||||||
pbr
|
|
||||||
lxml
|
lxml
|
||||||
|
pbr
|
||||||
|
requests>=2.0
|
||||||
|
|
Loading…
Reference in a new issue