#!/usr/bin/env python
"""This module contains custom exception and warning classes, implements
a custom warning filter action, called `"onceperfamily"`, and monkey-patches
warning output to improve legibility.
Contents:
.. contents::
:local:
The `onceperfamily` action
--------------------------
`onceperfamily` groups warning messages by families regular expressions,
and only prints the first warning instance that matches a given family's
regular expression. In contrast, Python's native `once` action prints any string
literal once, even if it matches the same regex as another warning already given.
To use this action, use the following two functions:
- :func:`filterwarnings` to create the warnings filter. Because :func:`filterwarnings`
wraps Python's :func:`warnings.filterwarnings`, it may be used as a drop-in
replacement for creation of any warnings filter.
- :func:`warn` or :func:`warn_explicit`. Again, these are drop-in replacements
for Python's :func:`warnings.warn` and :func:`warnings.warn_explicit` that
additionally check the `onceperfamily` filters.
For convenience, there are also functions that both issue a warning, and create
a `onceperfamily` filter for it if one doesn't already exist:
- :func:`warn_onceperfamily`
- :func:`warn_explicit_onceperfamily`
Exception types
---------------
|MalformedFileError|
Raised when a file cannot be parsed as expected, and
execution must halt
Warning types
-------------
|ArgumentWarning|
Warning for command-line arguments that:
- are nonsenical, but recoverable
- together might cause very slow execution
(e.g. run would be optimized by other combinations)
|FileFormatWarning|
Warning for slightly malformed but usable files
|DataWarning|
Warning raised when:
- data has unexpected attributes
- data has nonsensical, but recoverable values for attributes
- when values are out of the domain of a given operation,
but skipping the operation or estimating the value is permissible
See also
--------
:mod:`warnings`
Warnings module
"""
import re
import warnings
import inspect
import linecache
import textwrap
from plastid.util.io.filters import colored
_wrapper = textwrap.TextWrapper(break_long_words=False, width=77)
#===============================================================================
# INDEX: Warning and exception classes
#===============================================================================
[docs]class ArgumentWarning(Warning):
"""Warning for nonsensical but recoverable combinations of command-line arguments,
or arguments that risk slow program execution"""
pass
[docs]class DataWarning(Warning):
"""Warning for unexpected attributes of data.
Raised when:
- data has unexpected attributes
- data has nonsensical, but recoverable values
- values are out of the domain of a given operation, but execution
can continue if the value is estimated or the operation skipped
"""
#===============================================================================
# INDEX: Plastid's extensions to Python warnings
#===============================================================================
pl_once_registry = {}
"""Registry of `onceperfamily` warnings that have been seen in the current execution context"""
pl_filters = []
"""Plastid's own warnings filters, which allow additional actions compared to Python's"""
[docs]def filterwarnings(action, message="", category=Warning, module="", lineno=0, append=0):
"""Insert an entry into the warnings filter. Behaviors are as in :func:`warnings.filterwarnings`,
except the additional action `'onceperfamily'` can be used to allow one warning per `family`
of messages, specified by a regex.
This allows individual warnings to give more detailed information, without each being
regarded as its own warning by Python's warning system (the defualt behavior of `'once'`).
Parameters
----------
action : str
How the warning should be filtered. Accceptable values are "error",
"ignore", "always", "default", 'module", "once", and "onceperfamily"
message : str, optional
str that can be compiled to a regex, used to detect warnings. If "onceperfamily"
is chosen, only the first warning to give a string that matches the regex
will be shown. For other actions, behaviors are as described in :mod:`warnings`.
(Default: `""`, match any message)
category : Warning or subclass, optional
Type of warning. (Default: :class:`Warning`)
module : str, optional
str that can be compiled to a regex, limiting the warning behavior to modules
that match that regex. (Default: `""`, match all modules)
lineno : int, optional
integer line used to specify warning in source code. If 0 (default), match
all warnings regardless of line number.
append : int, optional
If 1, add warning to end of filter list. If 0 (default), insert warning at
beginning of filters list.
See also
--------
warnings.filterwarnings
Python's warnings filter
"""
tup = (action, re.compile(message, re.I), category, re.compile(module), lineno)
if action == "onceperfamily":
if tup in pl_filters:
return
else:
if append == 1:
pl_filters.append(tup)
else:
pl_filters.insert(0, tup)
else:
warnings.filterwarnings(
action, message=message, category=category, module=module, lineno=lineno, append=append
)
[docs]def warn_onceperfamily(message, pattern=None, category=None, stacklevel=1):
"""Issue a warning and create a warning filter for that warning if it does not already exist
Parameters
----------
message : str
Message of warning. Can be a string that compiles to a regex.
Printed as warning text and used to create warning filter if `pattern`
is `None`.
pattern : str or None, optional
If not `None`, override message when generating warnings filter
category: :class:`Warning`, or subclass, optional
Type of warning
stacklevel : int
Ignored
See also
--------
plastid.util.services.exceptions.filterwarnings
plastid-specific warnings filters
warnings.warn
Python's warning system, which this wraps
"""
if pattern is None:
pattern = message
filterwarnings("onceperfamily", message=pattern, category=category)
warn(message, category=category, stacklevel=stacklevel)
[docs]def warn_explicit_onceperfamily(
message,
category,
filename,
lineno,
pattern=None,
module=None,
registry=None,
module_globals=None
):
"""Low-level interface to issue warnings, allowing `plastid`-specific warnings filters
Parameters
----------
message : str
Message
pattern : str or None, optional
If not `None`, override message when generating warnings filter
category: :class:`Warning`, or subclass, optional
Type of warning
filename : str
Name of module from which warning is issued
lineno : int, optional
Line in module at which warning is called
module : str, optional
Module name
registry : dict, optional
Registry of ignore filters (see source code for :func:`warnings.warn_explicit`
module_globals : dict, optional
Dictionary of module-level variables
See also
--------
plastid.util.services.exceptions.filterwarnings
plastid-specific warnings filters
warnings.warn_explicit
Python's warning system, which this wraps
"""
if pattern is None:
pattern = message
filterwarnings("onceperfamily", message=pattern, category=category)
if module is None: # or module_globals is None:
frame = inspect.currentframe()
if frame is None:
module = __name__
else:
try:
module = inspect.getmodule(frame.f_back.f_code).__name__
finally:
del frame
warn_explicit(
pattern,
category,
filename,
lineno,
module=module,
registry=registry,
module_globals=module_globals
)
[docs]def warn(message, category=None, stacklevel=1):
"""Issue a non-essential warning to users, allowing `plastid`-specific warnings filters
Parameters
----------
message : str
Message
category: :class:`Warning`, or subclass, optional
Type of warning
stacklevel : int
Ignored
See also
--------
plastid.util.services.exceptions.filterwarnings
plastid-specific warnings filters
warnings.warn
Python's warning system, which this wraps
"""
if category is None:
category = UserWarning
_, filename, lineno, _, _, _ = inspect.stack()[stacklevel]
warn_explicit(message, category, filename, lineno, module=filename)
[docs]def warn_explicit(
message, category, filename, lineno, module=None, registry=None, module_globals=None
):
"""Low-level interface to issue warnings, allowing `plastid`-specific warnings filters
Parameters
----------
message : str
Message
category: :class:`Warning`, or subclass, optional
Type of warning
filename : str
Name of module from which warning is issued
lineno : int, optional
Line in module at which warning is called
module : str, optional
Module name
registry : dict, optional
Registry of ignore filters (see source code for :func:`warnings.warn_explicit`
module_globals : dict, optional
Dictionary of module-level variables
See also
--------
plastid.util.services.exceptions.filterwarnings
plastid-specific warnings filters
warnings.warn_explicit
Python's warning system, which this wraps
"""
global pl_once_registry
if module is None:
frame = inspect.currentframe()
if frame is None:
module = __name__
else:
try:
module = inspect.getmodule(frame.f_back.f_code).__name__
finally:
del frame
for _, pat, filter_category, mod, filter_line in pl_filters:
if pat.match(message) and issubclass(category,filter_category) and\
(module is None or mod.match(module)) and\
(filter_line == 0 or filter_line == lineno):
tup = (pat.pattern, filter_category, mod, filter_line)
if tup in pl_once_registry:
return
else:
pl_once_registry[tup] = 1
break
warnings.warn_explicit(
message,
category,
filename,
lineno,
module=module,
registry=registry,
module_globals=module_globals
)
warnings.formatwarning = formatwarning