#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
**reporter.py**
**Platform:**
Windows, Linux, Mac Os X.
**Description:**
This module defines the :class:`Reporter` class and various others exceptions handling related objects.
**Others:**
"""
#**********************************************************************************************************************
#*** External imports.
#**********************************************************************************************************************
import functools
import inspect
import os
import re
import sys
if sys.version_info[:2] <= (2, 6):
from ordereddict import OrderedDict
else:
from collections import OrderedDict
import time
import traceback
from PyQt4.QtGui import QApplication
#**********************************************************************************************************************
#*** Internal imports.
#**********************************************************************************************************************
import foundations.core
import foundations.exceptions
import foundations.io
import foundations.verbose
import foundations.strings
import foundations.ui.common
import umbra.ui.common
from umbra.globals.constants import Constants
from umbra.globals.uiConstants import UiConstants
from umbra.globals.runtimeGlobals import RuntimeGlobals
#**********************************************************************************************************************
#*** Module attributes.
#**********************************************************************************************************************
__author__ = "Thomas Mansencal"
__copyright__ = "Copyright (C) 2008 - 2012 - Thomas Mansencal"
__license__ = "GPL V3.0 - http://www.gnu.org/licenses/"
__maintainer__ = "Thomas Mansencal"
__email__ = "thomas.mansencal@gmail.com"
__status__ = "Production"
__all__ = ["LOGGER",
"UI_FILE",
"Reporter",
"baseExceptionHandler",
"systemExitExceptionHandler"
"criticalExceptionHandler",
"installExceptionReporter",
"uninstallExceptionReporter",
"enableExceptionReporter",
"disableExceptionReporter"]
LOGGER = foundations.verbose.installLogger()
UI_FILE = umbra.ui.common.getResourcePath(UiConstants.reporterUiFile)
#**********************************************************************************************************************
#*** Module classes and definitions.
#**********************************************************************************************************************
[docs]class Reporter(foundations.ui.common.QWidgetFactory(uiFile=UI_FILE)):
"""
This class provides an exception reporting Widget.
"""
__instance = None
"""Class instance. ( Reporter )"""
def __new__(cls, *args, **kwargs):
"""
This method is the constructor of the class.
:param \*args: Arguments. ( \* )
:param \*\*kwargs: Keywords arguments. ( \*\* )
:return: Class instance. ( Library )
"""
if not cls._Reporter__instance:
cls._Reporter__instance = super(Reporter, cls).__new__(cls, *args, **kwargs)
return cls._Reporter__instance
def __init__(self, parent=None, report=True, enabled=True, *args, **kwargs):
"""
.. Sphinx: Statements updated for auto-documentation purpose.
:param parent: Object parent. ( QObject )
:param report: Report to Crittercism. ( Boolean )
:param enabled: Is reporter enabled. ( Boolean )
:param \*args: Arguments. ( \* )
:param \*\*kwargs: Keywords arguments. ( \*\* )
"""
if hasattr(self, "_Reporter__initialized"):
return
LOGGER.debug("> Initializing '{0}()' class.".format(self.__class__.__name__))
super(Reporter, self).__init__(parent, *args, **kwargs)
# --- Setting class attributes. ---
self.__initialized = True
self.__report = None
self.report = report
self.__enabled = None
self.enabled = enabled
self.__jqueryJavascriptPath = umbra.ui.common.getResourcePath(os.path.join("javascripts", "jquery.js"))
self.__crittercismJavascriptPath = umbra.ui.common.getResourcePath(os.path.join("javascripts", "crittercism.js"))
self.__reporterJavascriptPath = umbra.ui.common.getResourcePath(os.path.join("javascripts", "reporter.js"))
self.__jqueryJavascript = foundations.io.File(self.__jqueryJavascriptPath).read()
self.__crittercismJavascript = foundations.io.File(self.__crittercismJavascriptPath).read()
self.__reporterJavascript = foundations.io.File(self.__reporterJavascriptPath).read().format(
UiConstants.crittercismId, Constants.releaseVersion)
self.__style = """* {
margin: 0;
padding: 0;
}
::-webkit-scrollbar {
height: 12px;
width: 12px;
}
::-webkit-scrollbar-track-piece {
background-color: rgb(48, 48, 48);
}
::-webkit-scrollbar-thumb:horizontal, ::-webkit-scrollbar-thumb:vertical {
background-color: rgb(96, 96, 96);
}
::-webkit-scrollbar-thumb:horizontal:hover, ::-webkit-scrollbar-thumb:vertical:hover {
background-color: rgb(128, 128, 128);
}
::-webkit-scrollbar-thumb:horizontal {
width: 50px;
}
::-webkit-scrollbar-thumb:vertical {
height: 50px;
}
body {
background-color: rgb(32, 32, 32);
color: rgb(192, 192, 192);
font-size: 12pt;
margin: 16px;
overflow-y: scroll;
}
A:link {
color: rgb(160, 96, 64);
text-decoration: none;
}
A:visited {
text-decoration: none;
color: rgb(160, 96, 64);
}
A:active {
text-decoration: none;
color: rgb(160, 96, 64);
}
A:hover {
text-decoration: underline;
color: rgb(160, 96, 64);
}
.floatRight {
float: right;
}
.textAlignRight {
text-align: right;
}
div {
overflow:hidden;
margin: auto;
text-overflow: ellipsis;
word-wrap: break-word;
}
div.header {
background-color: rgb(210, 64, 32);
color: rgb(32, 32, 32);
padding: 24px;
}
div.traceback {
background-color: rgb(210, 64, 32);
color: rgb(32, 32, 32);
font-size: 16px;
padding: 16px;
}
div.content {
padding: 16px;
}
div.stack {
}
div.location {
background-color: rgb(48, 48, 48);
font-size: 16px;
padding: 8px;
}
div.context {
background-color: rgb(48, 48, 48);
color: rgb(160, 160, 160);
font-family: "Courier New";
font-size: 14px;
padding: 32px;
}
span.highlight {
background-color: rgb(160, 96, 64);
color: rgb(32, 32, 32);
display: block;
font-weight: bold;
}
div.exception {
background-color: rgb(210, 64, 32);
color: rgb(32, 32, 32);
font-size: 16px;
padding: 16px;
}
div.debug {
padding: 16px;
}
div.frame {
background-color: rgb(48, 48, 48);
padding: 8px;
}
div.type {
font-size: 16px;
}
div.locals {
background-color: rgb(48, 48, 48);
color: rgb(160, 160, 160);
line-height: 150%;
padding: 32px;
}"""
self.__html = None
self.__onlineText = "An <b>unhandled</b> exception occured, \
this report has been sent to <b>HDRLabs</b> development team!"
self.__offlineText = "An <b>unhandled</b> exception occured, \
mailing this report to <b>{0}</b> would help improving <b>{1}</b>!".format(__email__, Constants.applicationName)
self.__footerText = \
"The severity of this exception is not critical and <b>{0}</b> will resume!".format(Constants.applicationName)
self.__initializeUi()
#******************************************************************************************************************
#*** Attributes properties.
#******************************************************************************************************************
@property
def report(self):
"""
This method is the property for **self.__report** attribute.
:return: self.__report. ( Boolean )
"""
return self.__report
@report.setter
@foundations.exceptions.handleExceptions(AssertionError)
def report(self, value):
"""
This method is the setter method for **self.__report** attribute.
:param value: Attribute value. ( Boolean )
"""
if value is not None:
assert type(value) is bool, "'{0}' attribute: '{1}' type is not 'bool'!".format("report", value)
self.__report = value
@report.deleter
@foundations.exceptions.handleExceptions(foundations.exceptions.ProgrammingError)
[docs] def report(self):
"""
This method is the deleter method for **self.__report** attribute.
"""
raise foundations.exceptions.ProgrammingError(
"{0} | '{1}' attribute is not deletable!".format(self.__class__.__name__, "report"))
@property
def enabled(self):
"""
This method is the property for **self.__enabled** attribute.
:return: self.__enabled. ( Boolean )
"""
return self.__enabled
@enabled.setter
@foundations.exceptions.handleExceptions(AssertionError)
def enabled(self, value):
"""
This method is the setter method for **self.__enabled** attribute.
:param value: Attribute value. ( Boolean )
"""
if value is not None:
assert type(value) is bool, "'{0}' attribute: '{1}' type is not 'bool'!".format("enabled", value)
self.__enabled = value
@enabled.deleter
@foundations.exceptions.handleExceptions(foundations.exceptions.ProgrammingError)
[docs] def enabled(self):
"""
This method is the deleter method for **self.__enabled** attribute.
"""
raise foundations.exceptions.ProgrammingError(
"{0} | '{1}' attribute is not deletable!".format(self.__class__.__name__, "enabled"))
#******************************************************************************************************************
#*** Class methods.
#******************************************************************************************************************
def __call__(self, *args):
"""
This method is the caller of the class.
:param \*args: Arguments. ( \* )
:param \*\*kwargs: Keywords arguments. ( \*\* )
:return: Class instance. ( Library )
"""
self.handleException(*args)
[docs] def show(self):
"""
This method reimplements the :meth:`QWidget.show` method.
"""
RuntimeGlobals.splashscreen and RuntimeGlobals.splashscreen.hide()
super(Reporter, self).show()
self.raise_()
def __initializeUi(self):
"""
This method initializes the Widget ui.
"""
LOGGER.debug("> Initializing '{0}' Widget ui.".format(self.__class__.__name__))
self.__view = self.Reporter_webView
self.setWindowTitle("{0} - Reporter".format(Constants.applicationName))
self.Footer_label.setText(self.__footerText)
self.__initializeContextUi()
self.__setHtml()
# Signals / Slots.
self.Copy_Report_pushButton.clicked.connect(self.__Copy_Report_pushButton__clicked)
self.Disable_Reporter_pushButton.clicked.connect(self.__Disable_Reporter_pushButton__clicked)
def __initializeContextUi(self):
"""
This method sets the context Widget ui.
"""
if foundations.common.isInternetAvailable():
text = self.__onlineText
else:
text = self.__offlineText
self.Header_label.setText(text)
def __Copy_Report_pushButton__clicked(self, checked):
"""
This method is triggered when **Copy_Report_pushButton** Widget is clicked.
:param checked: Checked state. ( Boolean )
"""
clipboard = QApplication.clipboard()
clipboard.setText(self.__view.page().mainFrame().toPlainText())
def __Disable_Reporter_pushButton__clicked(self, checked):
"""
This method is triggered when **Disable_Reporter_pushButton** Widget is clicked.
:param checked: Checked state. ( Boolean )
"""
uninstallExceptionReporter()
self.__enabled = False
def __getHtml(self, body=None):
"""
This method returns the html content with given body tag content.
:param body: Body tag content. ( String )
:return: Html. ( String )
"""
output = []
output.append("<html>")
output.append("<head>")
for javascript in (self.__jqueryJavascript,
self.__crittercismJavascript,
self.__reporterJavascript):
output.append("<script type=\"text/javascript\">")
output.append(javascript)
output.append("</script>")
output.append("<style type=\"text/css\">")
output.append(self.__style)
output.append("</style>")
output.append("</head>")
if body is not None:
output.append(body)
else:
output.append("<body>")
output.append("</body>")
output.append("</html>")
return "\n".join(output)
def __setHtml(self, body=None):
"""
This method sets the html content in the View using given body.
:param body: Body tag content. ( String )
"""
self.__html = self.__getHtml(body)
self.__view.setHtml(self.__html)
def __evaluateJavascript(self, javascript):
"""
This method evaluates given javascript content in the View.
:param javascript: Javascript. ( String )
"""
self.__view.page().mainFrame().evaluateJavaScript(javascript)
[docs] def handleException(self, *args):
"""
This method handles given exception.
:param \*args: Arguments. ( \* )
"""
if not self.__enabled:
return
cls, instance, trcback = foundations.exceptions.extractException(*args)
LOGGER.info("{0} | Handling '{1}' exception!".format(self.__class__.__name__, str(cls)))
self.__initializeContextUi()
self.__setHtml(self.formatHtmlException(cls, instance, trcback))
self.show()
self.__report and self.reportExceptionToCrittercism(cls, instance, trcback)
foundations.exceptions.baseExceptionHandler(cls, instance, trcback)
self.exec_()
@staticmethod
@staticmethod
[docs] def formatTextException(*args):
"""
This method formats given exception as a text.
:param \*args: Arguments. ( \* )
:return: Exception text. ( String )
"""
format = lambda x: re.sub(r"^(\s+)", lambda y: "{0} ".format("." * len(y.group(0))), x.rstrip().expandtabs(4))
verbose = 10
cls, instance, trcback = args
stack = foundations.exceptions.extractStack(foundations.exceptions.getInnerMostFrame(trcback), verbose)
text = []
text.append(str(cls))
text.append(str())
for line in foundations.exceptions.formatException(cls, instance, trcback):
text.append(format("{0}".format(format(line))))
text.append(str())
text.append("An unhandled exception occured in {0} {1}!".format(Constants.applicationName,
Constants.releaseVersion))
text.append("Sequence of calls leading up to the exception, in their occurring order:")
text.append(str())
for frame, fileName, lineNumber, name, context, index in stack:
location = "{0}{1}".format(name if name != "<module>" else str(),
inspect.formatargvalues(*inspect.getargvalues(frame)))
text.append("File \"{0}\", line {1}, in {2}".format(fileName, lineNumber, location))
for i, line in enumerate(context):
if i == index:
text.append(format("\t{0} {1} <===".format(lineNumber - index + i, format(format(line)))))
else:
text.append(format("\t{0} {1}".format(lineNumber - index + i, format(format(line)))))
text.append(str())
for line in traceback.format_exception_only(cls, instance):
text.append("{0}".format(format(line)))
text.append(str())
text.append("Frames locals by stack ordering, innermost last:")
text.append(str())
for frame, locals in foundations.exceptions.extractLocals(trcback):
name, fileName, lineNumber = frame
text.append("Frame \"{0}\" in \"{1}\" file, line {2}:".format(name, fileName, lineNumber))
arguments, namelessArgs, keywordArgs, locals = locals
hasArguments, hasLocals = any((arguments, namelessArgs, keywordArgs)), any(locals)
hasArguments and text.append(format("\tArguments:"))
for key, value in arguments.iteritems():
text.append(format("\t\t{0} = {1}".format(key, value)))
for value in namelessArgs:
text.append(format("\t\t{0}".format(value)))
for key, value in sorted(keywordArgs.iteritems()):
text.append(format("\\tt{0} = {1}".format(key, value)))
hasLocals and text.append(format("\tLocals:"))
for key, value in sorted(locals.iteritems()):
text.append(format("\t\t{0} = {1}".format(key, value)))
text.append(str())
return text
[docs] def reportExceptionToCrittercism(self, *args):
"""
This method reports given exception to Crittercism.
:param \*args: Arguments. ( \* )
:return: Method success. ( Boolean )
"""
if foundations.common.isInternetAvailable():
cls, instance, trcback = args
title = re.escape(str().join(map(lambda x: x.strip(), traceback.format_exception_only(cls, instance))))
file = trcback.tb_frame.f_code.co_filename
lineNumber = trcback.tb_lineno
stack = repr(self.formatTextException(cls, instance, trcback))
javascript = "Crittercism.logExternalException(\"{0}\", \"{1}\", {2}, {3});".format(
title, file, lineNumber, stack)
self.__evaluateJavascript(javascript)
LOGGER.info("{0} | Exception report sent to Crittercism!".format(self.__class__.__name__))
return True
else:
LOGGER.warning("!> {0} | Failed sending exception report to Crittercism!".format(self.__class__.__name__))
return False
[docs]def baseExceptionHandler(*args):
"""
This definition provides a base exception handler.
:param \*args: Arguments. ( \* )
:return: Definition success. ( Boolean )
"""
Reporter().handleException(*args)
return True
[docs]def systemExitExceptionHandler(*args):
"""
This definition provides a system exit exception handler.
:param \*args: Arguments. ( \* )
:return: Definition success. ( Boolean )
"""
reporter = Reporter()
reporter.Footer_label.setText(
"The severity of this exception is critical, <b>{0}</b> cannot continue and will now close!".format(
Constants.applicationName))
baseExceptionHandler(*args)
foundations.core.exit(1)
return True
[docs]def criticalExceptionHandler(object):
"""
This decorator is used to mark an object that would system exit in case of critical exception.
:param object: Object to decorate. ( Object )
:return: Object. ( Object )
"""
@functools.wraps(object)
def criticalExceptionHandlerWrapper(*args, **kwargs):
"""
This decorator is used to mark an object that would system exit in case of critical exception.
:param \*args: Arguments. ( \* )
:param \*\*kwargs: Keywords arguments. ( \*\* )
"""
_exceptions__frame__ = True
try:
return object(*args, **kwargs)
except Exception as error:
systemExitExceptionHandler(error)
return criticalExceptionHandlerWrapper
[docs]def installExceptionReporter(report=True):
"""
This definition installs the exceptions reporter.
:param report: Report to Crittercism. ( Boolean )
:return: Reporter instance. ( Reporter )
"""
reporter = Reporter(report=report)
sys.excepthook = reporter
return reporter
[docs]def uninstallExceptionReporter():
"""
This definition uninstalls the exceptions reporter.
:return: Definition success. ( Boolean )
"""
return foundations.exceptions.installExceptionHandler()
[docs]def enableExceptionReporter():
"""
This definition enables the exceptions reporter.
:return: Definition success. ( Boolean )
"""
reporter = Reporter().enabled = True
return True
[docs]def disableExceptionReporter():
"""
This definition disables the exceptions reporter.
:return: Definition success. ( Boolean )
"""
reporter = Reporter().enabled = False
return True
#**********************************************************************************************************************
#*** Sphinx: Statements updated for auto-documentation purpose.
#**********************************************************************************************************************