Import LinuxCNC source tree
This commit is contained in:
213
lib/python/common/colored_formatter.py
Executable file
213
lib/python/common/colored_formatter.py
Executable file
@@ -0,0 +1,213 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
# Copyright (c) 2017 Kurt Jacobson
|
||||
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
|
||||
# The above copyright notice and this permission notice shall be included in all
|
||||
# copies or substantial portions of the Software.
|
||||
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
|
||||
import re
|
||||
from copy import copy
|
||||
from logging import Formatter
|
||||
|
||||
PREFIX = '\033['
|
||||
SUFFIX = '\033[0m'
|
||||
|
||||
COLORS = {
|
||||
'black': 30,
|
||||
'red': 31,
|
||||
'green': 32,
|
||||
'yellow': 33,
|
||||
'blue': 34,
|
||||
'magenta': 35,
|
||||
'cyan': 36,
|
||||
'white': 37,
|
||||
'bgred': 41,
|
||||
'bggreen': 42,
|
||||
'bgyellow': 43,
|
||||
'bggrey': 100
|
||||
}
|
||||
|
||||
MAPPING = {
|
||||
'VERBOSE': 'bggrey',
|
||||
'DEBUG': 'blue',
|
||||
'INFO': 'cyan',
|
||||
'WARNING': 'yellow',
|
||||
'ERROR': 'red',
|
||||
'CRITICAL': 'bgred',
|
||||
}
|
||||
|
||||
KEY_SET = set(list(COLORS))
|
||||
|
||||
# Returns `text` wrapped in ASCII escape codes to produce `color`
|
||||
def COLORIZE(text, color=None):
|
||||
seq = COLORS.get(color, 37) # default to white
|
||||
return '{0}{1}m{2}{3}'.format(PREFIX, seq, text, SUFFIX)
|
||||
|
||||
# Returns `text` wrapped in Qt Richtext Html codes to produce `color`
|
||||
def QTCOLORIZE(text, color=None):
|
||||
if color in('bgred','bggreen','bgyellow','bggrey'):
|
||||
color = color.replace('bg','')
|
||||
return f'<span style="background-color: {color};">{text}</span>'
|
||||
return f'<font color="{color}">{text}</font>'
|
||||
|
||||
# Matches only the first `color<text>` occurrence
|
||||
# ^(.*?)<([^)]+)>
|
||||
|
||||
# Matches all `color<text>` occurrences, both take the same number of steps
|
||||
# ([^<\s]+)<([^>]+)>
|
||||
# (\w+)<([^>]+)>
|
||||
|
||||
RE = re.compile(r'(\w+)<([^>]+)>')
|
||||
|
||||
|
||||
class ColoredFormatter(Formatter):
|
||||
|
||||
def __init__(self, pattern, pass_clean=True):
|
||||
self.pass_clean = pass_clean
|
||||
Formatter.__init__(self, pattern)
|
||||
|
||||
# Override the Formatter's format method to add ASCII colors
|
||||
# to the levelname and any marked words in the log message.
|
||||
def format(self, record):
|
||||
colored_record = copy(record)
|
||||
|
||||
# Add colors to levelname
|
||||
levelname = colored_record.levelname
|
||||
color = MAPPING.get(levelname, 'white')
|
||||
colored_record.levelname = COLORIZE(levelname, color)
|
||||
|
||||
# Add colors to tagged message text
|
||||
msg = colored_record.getMessage()
|
||||
plain_msg, color_msg = self.color_words(msg)
|
||||
# if next logger will colorize too
|
||||
# then don't clean color codes from msg
|
||||
if self.pass_clean:
|
||||
record.msg = plain_msg
|
||||
colored_record.msg = color_msg
|
||||
|
||||
return Formatter.format(self, colored_record)
|
||||
|
||||
# Replace `color<message>` in the log message with ASCII codes to colorize
|
||||
# the word or phrase in the terminal log handler. Also return a cleaned
|
||||
# version of the message with the tags removed for use by the file handler.
|
||||
def color_words(self, raw_msg):
|
||||
# Function to handle color replacement
|
||||
def color_handler(match):
|
||||
group = match.group(0) # The whole thing: <red>hello
|
||||
color = match.group(1) # Group 1: red
|
||||
word = match.group(2) # Group 2: hello
|
||||
|
||||
if color in KEY_SET: # must be one of the color key words
|
||||
return COLORIZE(word, color)
|
||||
return group # If color is invalid, just return the word without tags
|
||||
|
||||
# Function to handle plain text (stripping tags)
|
||||
def plain_handler(match):
|
||||
return match.group(2) # Just return the word part
|
||||
|
||||
# Perform both replacements in one pass each
|
||||
color_msg = RE.sub(color_handler, raw_msg)
|
||||
plain_msg = RE.sub(plain_handler, raw_msg)
|
||||
|
||||
return plain_msg, color_msg
|
||||
|
||||
class QtColoredFormatter(Formatter):
|
||||
|
||||
def __init__(self, pattern, pass_clean=True):
|
||||
self.pass_clean = pass_clean
|
||||
Formatter.__init__(self, pattern)
|
||||
|
||||
# Override the Formatter's format method to add ASCII colors
|
||||
# to the levelname and any marked words in the log message.
|
||||
def format(self, record):
|
||||
colored_record = copy(record)
|
||||
|
||||
# Add colors to levelname
|
||||
levelname = colored_record.levelname
|
||||
color = MAPPING.get(levelname, 'white')
|
||||
colored_record.levelname = QTCOLORIZE(levelname, color)
|
||||
|
||||
# Add colors to tagged message text
|
||||
msg = colored_record.getMessage()
|
||||
plain_msg, color_msg = self.qtcolor_words(msg)
|
||||
# if next logger will colorize too
|
||||
# then don't clean color codes from msg
|
||||
if self.pass_clean:
|
||||
record.msg = plain_msg
|
||||
colored_record.msg = color_msg
|
||||
|
||||
return Formatter.format(self, colored_record)
|
||||
|
||||
# Replace `color<message>` in the log message with Html codes to colorize
|
||||
def qtcolor_words(self, raw_msg):
|
||||
# Function to handle color replacement
|
||||
def color_handler(match):
|
||||
group = match.group(0) # The whole thing: <red>hello
|
||||
color = match.group(1) # Group 1: red
|
||||
word = match.group(2) # Group 2: hello
|
||||
|
||||
if color in KEY_SET: # must be one of the color key words
|
||||
return QTCOLORIZE(word, color)
|
||||
return group # If color is invalid, just return the word without tags
|
||||
|
||||
# Function to handle plain text (stripping tags)
|
||||
def plain_handler(match):
|
||||
return match.group(2) # Just return the word part
|
||||
|
||||
# Perform both replacements in one pass each
|
||||
color_msg = RE.sub(color_handler, raw_msg)
|
||||
plain_msg = RE.sub(plain_handler, raw_msg)
|
||||
|
||||
return plain_msg, color_msg
|
||||
|
||||
# ----------------------- E X A M P L E -----------------------
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
import logging
|
||||
|
||||
# Create logger
|
||||
log = logging.getLogger(__name__)
|
||||
log.setLevel(logging.DEBUG)
|
||||
|
||||
# Add console handler
|
||||
ch = logging.StreamHandler()
|
||||
ch.setLevel(logging.DEBUG)
|
||||
cf = ColoredFormatter("[%(name)s][%(levelname)s] %(message)s (%(filename)s:%(lineno)d)")
|
||||
ch.setFormatter(cf)
|
||||
log.addHandler(ch)
|
||||
|
||||
# Add file handler
|
||||
fh = logging.FileHandler('demo.log')
|
||||
fh.setLevel(logging.DEBUG)
|
||||
ff = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
||||
fh.setFormatter(ff)
|
||||
log.addHandler(fh)
|
||||
|
||||
# Log some stuff
|
||||
log.debug("Logging demo has green<STARTED>")
|
||||
log.info("Logging to yellow<demo.log> in the script dir")
|
||||
log.warning("This is my last warning, red<take heed>")
|
||||
log.error("This is an error")
|
||||
log.critical("He's dead, Jim")
|
||||
|
||||
# Example exception logging
|
||||
try:
|
||||
print(False + "True")
|
||||
except Exception as e:
|
||||
log.debug('That did not work!', exc_info=e)
|
||||
1518
lib/python/common/hal_glib.py
Normal file
1518
lib/python/common/hal_glib.py
Normal file
File diff suppressed because it is too large
Load Diff
1035
lib/python/common/iniinfo.py
Normal file
1035
lib/python/common/iniinfo.py
Normal file
File diff suppressed because it is too large
Load Diff
145
lib/python/common/logger.py
Executable file
145
lib/python/common/logger.py
Executable file
@@ -0,0 +1,145 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
# Common GUI Logging Module
|
||||
# Provides a consistent and easy to use logging facility. Log messages printed
|
||||
# to the terminal will be colorized for easy identification of log level.
|
||||
#
|
||||
# Copyright (c) 2017 Kurt Jacobson
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 2 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
|
||||
|
||||
import os
|
||||
import logging
|
||||
from linuxcnc import ini
|
||||
|
||||
# For convenience import log levels so we don't need to import
|
||||
# logging to set the log level within other modules.
|
||||
from logging import DEBUG, INFO, WARNING, ERROR, CRITICAL
|
||||
|
||||
# add a new verbose level
|
||||
VERBOSE = 5
|
||||
logging.VERBOSE = 5
|
||||
logging.addLevelName(logging.VERBOSE, "VERBOSE")
|
||||
|
||||
|
||||
# add a custom log level function
|
||||
def verbose(self, message, *args, **kws):
|
||||
if self.isEnabledFor(VERBOSE):
|
||||
# Yes, logger takes its '*args' as 'args'.
|
||||
self._log(logging.VERBOSE, message, args, **kws)
|
||||
|
||||
# add the custom log level to the library (class patch)
|
||||
logging.Logger.verbose = verbose
|
||||
logging.Logger.VERBOSE = VERBOSE
|
||||
|
||||
# Our custom colorizing formatters for the terminal/file handler
|
||||
from .colored_formatter import ColoredFormatter, QtColoredFormatter
|
||||
|
||||
# Global name of the base logger
|
||||
BASE_LOGGER_NAME = None
|
||||
BASE_LOGGER_FILE = None
|
||||
|
||||
# Define the log message formats
|
||||
TERM_FORMAT = '[%(name)s][%(levelname)s] %(message)s (%(filename)s:%(lineno)d)'
|
||||
FILE_FORMAT = '%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
||||
QTFILE_FORMAT = '%(levelname)s - %(message)s (%(filename)s:%(lineno)d)<br>'
|
||||
|
||||
# Get logger for module based on module.__name__
|
||||
def getLogger(name):
|
||||
if BASE_LOGGER_NAME is None:
|
||||
initBaseLogger('DEFAULT')
|
||||
name = '{0}.{1}'.format(BASE_LOGGER_NAME, name.upper())
|
||||
return logging.getLogger(name)
|
||||
|
||||
|
||||
# Set global logging level
|
||||
def setGlobalLevel(level):
|
||||
base_log = logging.getLogger(BASE_LOGGER_NAME)
|
||||
base_log.setLevel(level)
|
||||
|
||||
|
||||
# Initialize the base logger
|
||||
def initBaseLogger(name, log_file=None, log_level=DEBUG, logToFile=False):
|
||||
|
||||
global BASE_LOGGER_NAME
|
||||
BASE_LOGGER_NAME = name
|
||||
|
||||
if not log_file:
|
||||
log_file = getLogFile(name)
|
||||
|
||||
global BASE_LOGGER_FILE
|
||||
BASE_LOGGER_FILE = log_file
|
||||
|
||||
# Clear the previous sessions log file
|
||||
with open(log_file, 'w') as fh:
|
||||
pass
|
||||
|
||||
# Create base logger
|
||||
base_log = logging.getLogger(BASE_LOGGER_NAME)
|
||||
base_log.setLevel(log_level)
|
||||
|
||||
# Add console handler
|
||||
ch = logging.StreamHandler()
|
||||
ch.setLevel(logging.VERBOSE)
|
||||
# is this a Qtvcp logger?
|
||||
Qt = bool( 'qt' in name.lower())
|
||||
cf = ColoredFormatter(TERM_FORMAT, pass_clean = not Qt)
|
||||
ch.setFormatter(cf)
|
||||
base_log.addHandler(ch)
|
||||
|
||||
# Add file handler
|
||||
if logToFile:
|
||||
fh = logging.FileHandler(log_file)
|
||||
fh.setLevel(logging.VERBOSE)
|
||||
# if Qtvcp, use Qt Rich text colors
|
||||
# else use plain text
|
||||
if Qt:
|
||||
ff = QtColoredFormatter(QTFILE_FORMAT)
|
||||
else:
|
||||
ff = logging.Formatter(FILE_FORMAT)
|
||||
fh.setFormatter(ff)
|
||||
base_log.addHandler(fh)
|
||||
|
||||
# Get logger for logger
|
||||
log = getLogger(__name__)
|
||||
if logToFile:
|
||||
base_log.info('Logging to: yellow<{}>'.format(log_file))
|
||||
|
||||
return base_log
|
||||
|
||||
|
||||
# Attempt to find the log file specified INI [DISPLAY] LOG_FILE,
|
||||
# failing that log to $HOME/<base_log_name>.log
|
||||
def getLogFile(name):
|
||||
|
||||
# Default log file to use if not specified in INI
|
||||
log_file = os.path.expanduser('~/{}.log').format(name.lower())
|
||||
|
||||
# LinuxCNC may not be running, so use get() to avoid a KeyError
|
||||
ini_file = os.environ.get('INI_FILE_NAME')
|
||||
config_dir = os.environ.get('CONFIG_DIR')
|
||||
|
||||
if ini_file:
|
||||
lcnc_ini = ini(ini_file)
|
||||
path = lcnc_ini.find('DISPLAY', 'LOG_FILE')
|
||||
if path:
|
||||
if path.startswith('~'):
|
||||
# Path is relative to $HOME
|
||||
log_file = os.path.expanduser(path)
|
||||
elif not os.path.isabs(path):
|
||||
# Assume intended path is relative to the INI file
|
||||
log_file = os.path.join(config_dir, path)
|
||||
else:
|
||||
# It must be an absolute path then
|
||||
log_file = os.path.realpath(path)
|
||||
|
||||
return log_file
|
||||
Reference in New Issue
Block a user