diff --git a/.gitignore b/.gitignore index 3c0bc1ff8ce0c409325c62442e015405066f5071..962a904dca6d63b531ee49d1cfdb4bb0bdfc110f 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,6 @@ */raw/ .idea/ + +*.cfg +*.log \ No newline at end of file diff --git a/color.py b/color.py index 5d1f58c007fbc1c4dc74223cf47f1e2efb7bfa69..59fc7d6bc356b77085ad6e097407195d394791a0 100644 --- a/color.py +++ b/color.py @@ -21,26 +21,26 @@ import re from ammsml.config import constants as C -IS_COLOR: bool = True - -if not hasattr(sys.stdout, 'isatty') or not sys.stdout.isatty(): - IS_COLOR = False +AMMSML_COLOR: bool = True +if C.AMMSML_NOCOLOR: + AMMSML_COLOR = False +elif not hasattr(sys.stdout, 'isatty') or not sys.stdout.isatty(): + AMMSML_COLOR = False else: try: import curses curses.setupterm() if curses.tigetnum('colors') < 0: - IS_COLOR = False + AMMSML_COLOR = False except ImportError: # curses library was not found pass except curses.error: # curses returns an error (e.g. could not find terminal) - IS_COLOR = False + AMMSML_COLOR = False -# TODO implement some Config entry for Color -# if C.IS_FORCE_COLOR: -# IS_COLOR = True +if C.AMMSML_FORCE_COLOR: + AMMSML_COLOR = True # --- begin "pretty" # @@ -88,7 +88,7 @@ def parsecolor(color): def stringc(text: str, color) -> str: """String in color.""" - if IS_COLOR: + if AMMSML_COLOR: color_code = parsecolor(color) return "\n".join(["\033[%sm%s\033[0m" % (color_code, t) for t in text.split('\n')]) else: @@ -98,13 +98,13 @@ def stringc(text: str, color) -> str: def colorize(lead, num, color): """ Print 'lead' = 'num' in 'color' """ s = "%s=%-4s" % (lead, str(num)) - if num != 0 and IS_COLOR and color is not None: + if num != 0 and AMMSML_COLOR and color is not None: s = stringc(s, color) return s def hostcolor(host: str, stats: list, color: bool = True) -> str: - if IS_COLOR and color: + if AMMSML_COLOR and color: if stats['failures'] != 0 or stats['unreachable'] != 0: return "%-37s" % stringc(host, C.COLOR_ERROR) elif stats['changed'] != 0: diff --git a/display.py b/display.py index 14820797de908ddfbdf541a30f942123bfbe8fcc..978d6efdd326002cc9ccca1ba8911e10504ed5fc 100644 --- a/display.py +++ b/display.py @@ -25,13 +25,14 @@ import time import textwrap import subprocess import logging +import getpass from struct import unpack, pack from termios import TIOCGWINSZ from ammsml.utils.color import stringc from ammsml.utils.parsing import to_text, boolean -from ammsml.utils.errors import AMMSMLError +from ammsml.utils.errors import AMMSMLError, AMMSMLAssertionError from ammsml.utils.singleton import Singleton, with_metaclass from ammsml.config import constants as C from ammsml.utils.wrap import wrap_var @@ -47,15 +48,16 @@ class FilterBlackList(logging.Filter): logger = None # TODO: make this a callback event instead -# if getattr(C, 'DEFAULT_LOG_PATH'): -# path = C.DEFAULT_LOG_PATH -# if path and (os.path.exists(path) and os.access(path, os.W_OK)) or os.access(os.path.dirname(path), os.W_OK): -# logging.basicConfig(filename=path, level=logging.INFO, format='%(asctime)s p=%(user)s u=%(process)d | %(message)s') -# logger = logging.LoggerAdapter(logging.getLogger('ammsml'), {'user': getpass.getuser()}) -# for handler in logging.root.handlers: -# handler.addFilter(FilterBlackList(getattr(C, 'DEFAULT_LOG_FILTER', []))) -# else: -# print("[WARNING]: log file at %s is not writeable and we cannot create it, aborting\n" % path, file=sys.stderr) +if getattr(C, 'DEFAULT_LOG_PATH'): + path = C.DEFAULT_LOG_PATH + if path and (os.path.exists(path) and os.access(path, os.W_OK)) or os.access(os.path.dirname(path), os.W_OK): + logging.basicConfig(filename=path, level=logging.INFO, + format='%(asctime)s p=%(user)s u=%(process)d | %(message)s') + logger = logging.LoggerAdapter(logging.getLogger('ammsml'), {'user': getpass.getuser()}) + for handler in logging.root.handlers: + handler.addFilter(FilterBlackList(getattr(C, 'DEFAULT_LOG_FILTER', []))) + else: + print("[WARNING]: log file at %s is not writeable and we cannot create it, aborting\n" % path, file=sys.stderr) # map color to log levels @@ -150,6 +152,26 @@ class Display(with_metaclass(Singleton, object)): if e.errno != errno.EPIPE: raise + if logger and not screen_only: + # We first convert to a byte string so that we get rid of + # color and characters that are invalid in the user's locale + msg2 = to_text(nocolor.lstrip(u'\n')) + + if sys.version_info >= (3,): + # Convert back to text string on python3 + msg2 = to_text(msg2, self._output_encoding(stderr=stderr)) + + lvl = logging.INFO + if color: + # set logger level based on color (not great) + try: + lvl = color_to_log_level[color] + except KeyError: + # this should not happen, but JIC + raise AMMSMLAssertionError('Invalid color supplied to display: %s' % color) + # actually log + logger.log(lvl, msg2) + def v(self, msg, host=None): return self.verbose(msg, host=host, caplevel=0) @@ -169,7 +191,6 @@ class Display(with_metaclass(Singleton, object)): return self.verbose(msg, host=host, caplevel=5) def debug(self, msg, host=None): - # TODO boolean() neccesary? if boolean(C.DEFAULT_DEBUG): if host is None: self.display("%6d %0.5f: %s" % (os.getpid(), time.time(), msg), color=C.COLOR_DEBUG) diff --git a/errors/__init__.py b/errors/__init__.py index 7596195c08db81fbc1f06854df37d7a54e542a5a..dec48f73ba46f3d8c5a7ace3b11160c199716c7f 100644 --- a/errors/__init__.py +++ b/errors/__init__.py @@ -195,3 +195,8 @@ class TemplateParsingError(AMMSMLError): class AMMSMLOptionsError(AMMSMLError): pass + + +class AMMSMLAssertionError(AMMSMLError, AssertionError): + """Invalid assertion""" + pass diff --git a/path.py b/path.py index 8c7e365ba6693ece17d10ba5de8d025478dc1242..fec51a44fe51d53442312696c5d42209889bbc21 100644 --- a/path.py +++ b/path.py @@ -38,9 +38,9 @@ def unfrackpath(path, follow=True, basedir=None): """ Returns a path that is free of symlinks (if follow=True), environment variables, relative path traversals and symbols (~) - :param path: A byte or text string representing a path to be canonicalized + :param path: A byte or text string representing a path to be canonised :param follow: A boolean to indicate of symlinks should be resolved or not - :raises UnicodeDecodeError: If the canonicalized version of the path + :raises UnicodeDecodeError: If the canonised version of the path contains non-utf8 byte sequences. :rtype: A text string :returns: An absolute path with symlinks, environment variables, and tilde