from __future__ import absolute_import
import collections
import logbook
import warnings
from . import hooks
from .utils.warning_capture import warning_callback_context
from .ctx import context
from .exception_handling import handling_exceptions
from contextlib import contextmanager
_native_logger = logbook.Logger('slash.native_warnings')
def capture_all_warnings():
warnings.simplefilter('always')
warnings.filterwarnings('ignore', category=ImportWarning)
class LogbookWarning(UserWarning):
pass
[docs]class SessionWarnings(object):
"""
Holds all warnings emitted during the session
"""
[docs] def __init__(self):
super(SessionWarnings, self).__init__()
self.warnings = []
@contextmanager
def capture_context(self):
capture_all_warnings()
with warning_callback_context(self._capture_native_warning):
yield
def warning_should_be_filtered(self, warning):
for ignored_warning in _ignored_warnings:
if ignored_warning.matches(warning):
return True
return False
def _capture_native_warning(self, message, category, filename, lineno, file=None, line=None): # pylint: disable=unused-argument
warning = RecordedWarning.from_native_warning(message, category, filename, lineno)
if self.warning_should_be_filtered(warning):
return
self.add(warning)
if not issubclass(category, LogbookWarning):
_native_logger.warning('{filename}:{lineno}: {warning!r}', filename=filename, lineno=lineno, warning=warning)
def add(self, warning):
hooks.warning_added(warning=warning) # pylint: disable=no-member
self.warnings.append(warning)
[docs] def __iter__(self):
"Iterates through stored warnings"
return iter(self.warnings)
def __len__(self):
return len(self.warnings)
def __nonzero__(self):
return bool(self.warnings)
__bool__ = __nonzero__
class WarnHandler(logbook.Handler, logbook.StringFormatterHandlerMixin):
"""
Like a stream handler but keeps the values in memory.
This logger provides some ways to store warnings to log again at the end of the session.
"""
default_format_string = (u'[{record.time:%Y-%m-%d %H:%M}] '
'{record.level_name}: {record.extra[source]}: {record.message}')
def __init__(self, session_warnings, format_string=None, filter=None, bubble=True):
logbook.Handler.__init__(self, logbook.WARNING, filter, bubble)
logbook.StringFormatterHandlerMixin.__init__(self, format_string)
self.session_warnings = session_warnings
def should_handle(self, record):
"""Returns `True` if this record is a warning """
if record.channel == _native_logger.name:
return False
return record.level == self.level
def emit(self, record):
warnings.warn_explicit(message=record.message, category=LogbookWarning, filename=record.filename,
lineno=record.lineno, module=record.module)
WarningKey = collections.namedtuple("WarningKey", ("filename", "lineno"))
class RecordedWarning(object):
def __init__(self, details, message, category=None):
super(RecordedWarning, self).__init__()
self.details = details
self.details['session_id'] = context.session_id if context.session else None
self.details['test_id'] = context.test_id if context.test_id else None
self.details.setdefault('func_name', None)
self.key = WarningKey(filename=self.details['filename'], lineno=self.details['lineno'])
self.category = category
self._repr = message
@classmethod
def from_log_record(cls, record, handler):
details = record.to_dict()
return cls(details, handler.format(record))
@classmethod
def from_native_warning(cls, message, category, filename, lineno):
if isinstance(message, Warning):
message = message.args[0]
return cls({
'message': message,
'type': category.__name__,
'filename': filename,
'lineno': lineno,
}, message=message, category=category)
@property
def message(self):
return self.details.get('message')
@property
def lineno(self):
return self.details.get('lineno')
@property
def filename(self):
return self.details.get('filename')
def to_dict(self):
return self.details.copy()
def __repr__(self):
return self._repr
class _IgnoredWarning(object):
def __init__(self, category, filename, lineno, message):
self.category = category
self.filename = filename
self.lineno = lineno
self.message = message
@staticmethod
def _pattern_matches(regex_or_str, warning_str):
if regex_or_str is None:
return False
if regex_or_str == warning_str:
return True
elif hasattr(regex_or_str, 'match'):
if regex_or_str.match(warning_str):
return True
return False
def matches(self, warning):
if self.category and not issubclass(warning.category, self.category):
return False
if self.filename and not self._pattern_matches(self.filename, warning.filename):
return False
if self.lineno and warning.lineno != self.lineno:
return False
if self.message and not self._pattern_matches(self.message, warning.message):
return False
return True
def __repr__(self):
extras = ""
if self.category:
extras += "category: {} ".format(self.category)
if self.message:
extras += "message: {} ".format(self.message)
if self.lineno:
extras += "lineno: {} ".format(self.lineno)
if self.filename:
extras += "filename: {} ".format(self.filename)
if not extras:
extras = "-"
return "<IgnoredWarning: {}>".format(extras.strip())
_ignored_warnings = []
[docs]def ignore_warnings(category=None, message=None, filename=None, lineno=None):
"""
Ignores warnings of specific origin (category/filename/lineno/message) during the session. Unlike
Python's default ``warnings.filterwarnings``, the parameters are matched only if specified (not defaulting to "match all"). Message can also be a
regular expression object compiled with ``re.compile``.
slash.ignore_warnings(category=CustomWarningCategory)
.. note:: Filter arguments are treated as having an ``and`` logical relationship.
.. note:: Calling ignore_warnings() with no arguments will ignore **all** warnings
"""
iw = _IgnoredWarning(category=category, filename=filename, lineno=lineno, message=message)
_ignored_warnings.append(iw)
return iw
@contextmanager
def ignored_warnings(**kwargs):
iw = ignore_warnings(**kwargs)
with handling_exceptions():
yield
try:
_ignored_warnings.remove(iw)
except ValueError:
# In case clear_warnings was called inside this context, ValueError will be raised
pass
def clear_ignored_warnings():
del _ignored_warnings[:]