import logging
import functools
from collections import OrderedDict
from qtpy.QtCore import QObject, Slot, Signal, Property, Q_ENUMS, QSize
from qtpy.QtWidgets import (
QWidget,
QPlainTextEdit,
QComboBox,
QLabel,
QPushButton,
QHBoxLayout,
QVBoxLayout,
QStyleOption,
QStyle,
)
from qtpy.QtGui import QPainter
logger = logging.getLogger(__name__)
def logger_destroyed(log, obj):
"""
Callback invoked when the Widget is destroyed.
This method is used to ensure that the log handlers are cleared.
Parameters
----------
log : Logger
The logger object being used by the PyDMLogDisplay widget.
obj : QWidget
The widget which is being destroyed. All childs of this widget are
already destroyed and must not be accessed.
"""
if log:
for handler in log.handlers:
log.removeHandler(handler)
class GuiHandler(QObject, logging.Handler):
"""
Handler for PyDM Applications
A composite of a QObject and a logging handler. This can be added to a
``logging.Logger`` object just like any standard ``logging.Handler`` and
will emit logging messages as Signals
.. code:: python
# Create a log and GuiHandler
logger = logging.getLogger()
ui_handler = GuiHandler(level=logging.INFO)
# Attach our handler to the log
logger.addHandler(ui_handler)
# Publish log message via Signal
ui_handler.message.connect(mySlot)
Parameters
----------
level: int
Level of Handler
parent: QObject, optional
"""
message = Signal(str)
def __init__(self, level=logging.NOTSET, parent=None):
logging.Handler.__init__(self, level=level)
QObject.__init__(self, parent=parent)
def emit(self, record):
"""Emit formatted log messages when received but only if level is set."""
# Avoid garbage to be presented when master log is running with DEBUG.
if self.level == logging.NOTSET:
return
try:
self.message.emit(self.format(record))
except RuntimeError:
logger.debug("Handler was destroyed at the C++ level.")
class LogLevels(object):
NOTSET = 0
DEBUG = 10
INFO = 20
WARNING = 30
ERROR = 40
CRITICAL = 50
@staticmethod
def as_dict():
"""
Returns an ordered dict of LogLevels ordered by value.
Returns
-------
OrderedDict
"""
# First let's remove the internals
entries = [
(k, v)
for k, v in LogLevels.__dict__.items()
if not k.startswith("__") and not callable(v) and not isinstance(v, staticmethod)
]
return OrderedDict(sorted(entries, key=lambda x: x[1], reverse=False))
[docs]class PyDMLogDisplay(QWidget, LogLevels):
"""
Standard display for Log Output
This widget handles instantating a ``GuiHandler`` and displaying log
messages to a ``QPlainTextEdit``. The level of the log can be changed from
inside the widget itself, allowing users to select from any of the
``.levels`` specified by the widget.
Parameters
----------
parent : QObject, optional
logname : str
Name of log to display in widget
level : logging.Level
Initial level of log display
"""
Q_ENUMS(LogLevels)
LogLevels = LogLevels
terminator = "\n"
default_format = "%(asctime)s %(message)s"
default_level = logging.INFO
def __init__(self, parent=None, logname=None, level=logging.NOTSET):
QWidget.__init__(self, parent=parent)
# Create Widgets
self.label = QLabel("Minimum displayed log level: ", parent=self)
self.combo = QComboBox(parent=self)
self.text = QPlainTextEdit(parent=self)
self.text.setReadOnly(True)
self.clear_btn = QPushButton("Clear", parent=self)
# Create layout
layout = QVBoxLayout()
level_control = QHBoxLayout()
level_control.addWidget(self.label)
level_control.addWidget(self.combo)
layout.addLayout(level_control)
layout.addWidget(self.text)
layout.addWidget(self.clear_btn)
self.setLayout(layout)
# Allow QCombobox to control log level
for log_level, value in LogLevels.as_dict().items():
self.combo.addItem(log_level, value)
self.combo.currentIndexChanged[str].connect(self.setLevel)
# Allow QPushButton to clear log text
self.clear_btn.clicked.connect(self.clear)
# Create a handler with the default format
self.handler = GuiHandler(level=level, parent=self)
self.logFormat = self.default_format
self.handler.message.connect(self.write)
# Create logger. Either as a root or given logname
self.log = None
self.level = None
self.logName = logname or ""
self.logLevel = level
self.destroyed.connect(functools.partial(logger_destroyed, self.log))
[docs] def sizeHint(self):
return QSize(400, 300)
@Property(LogLevels)
def logLevel(self):
return self.level
@logLevel.setter
def logLevel(self, level):
if level != self.level:
self.level = level
idx = self.combo.findData(level)
self.combo.setCurrentIndex(idx)
@Property(str)
def logName(self):
"""Name of associated log"""
return self.log.name
@logName.setter
def logName(self, name):
# Disconnect prior log from handler
if self.log:
self.log.removeHandler(self.handler)
# Reattach handler to new handler
self.log = logging.getLogger(name)
# Ensure that the log matches level of handler
# only if the handler level is less than the log.
if self.log.level < self.handler.level:
self.log.setLevel(self.handler.level)
# Attach preconfigured handler
self.log.addHandler(self.handler)
@Property(str)
def logFormat(self):
"""Format for log messages"""
return self.handler.formatter._fmt
@logFormat.setter
def logFormat(self, fmt):
self.handler.setFormatter(logging.Formatter(fmt))
[docs] @Slot(str)
def write(self, message):
"""Write a message to the log display"""
# We split the incoming message by new lines. In prior iterations of
# this widget it was discovered that large blocks of text cause issues
# at the Qt level.
for msg in message.split(self.terminator):
self.text.appendPlainText(msg)
[docs] @Slot()
def clear(self):
"""Clear the text area."""
self.text.clear()
[docs] @Slot(str)
def setLevel(self, level):
"""Set the level of the contained logger"""
# Get the level from the incoming string specification
try:
level = getattr(logging, level.upper())
except AttributeError:
logger.exception("Invalid logging level specified %s", level.upper())
else:
# Set the existing handler and logger to this level
self.handler.setLevel(level)
if self.log.level > self.handler.level or self.log.level == logging.NOTSET:
self.log.setLevel(self.handler.level)
[docs] def paintEvent(self, _):
"""
Paint events are sent to widgets that need to update themselves,
for instance when part of a widget is exposed because a covering
widget was moved.
At PyDMDrawing this method handles the alarm painting with parameters
from the stylesheet, configures the brush, pen and calls ```draw_item```
so the specifics can be performed for each of the drawing classes.
Parameters
----------
event : QPaintEvent
"""
painter = QPainter(self)
opt = QStyleOption()
opt.initFrom(self)
self.style().drawPrimitive(QStyle.PE_Widget, opt, painter, self)
painter.setRenderHint(QPainter.Antialiasing)