import logging
from enum import Enum
from types import TracebackType
from typing import TYPE_CHECKING, Any, List, Optional, Tuple, Type, Union
from .dist import standard_metadata
if TYPE_CHECKING:
from .manager import PluginManager # noqa: F401
ExcInfoTuple = Tuple[Type[Exception], Exception, Optional[TracebackType]]
# https://www.python.org/dev/peps/pep-0484/#support-for-singleton-types-in-unions
class Empty(Enum):
token = 0
_empty = Empty.token
[docs]class PluginError(Exception):
"""Base class for exceptions relating to plugins.
Parameters
----------
message : str, optional
An optional error message, by default ''
namespace : Optional[Any], optional
The python object that caused the error, by default None
cause : Exception, optional
Exception that caused the error. Same as ``raise * from``.
by default None
"""
_record: List['PluginError'] = []
def __init__(
self,
message: str = '',
*,
plugin: Optional[Any] = None,
plugin_name: Optional[str] = None,
cause: Optional[BaseException] = None,
):
self.plugin = plugin
self.plugin_name = plugin_name
if not message:
name = plugin_name or getattr(plugin, '__name__', str(id(plugin)))
message = f'Error in plugin "{name}"'
if cause:
message += f': {cause}'
super().__init__(message)
self.__cause__ = cause
# store all PluginError instances. can be retrieved with get()
PluginError._record.append(self)
[docs] @classmethod
def get(
cls,
*,
plugin: Union[Any, Empty] = _empty,
plugin_name: Union[str, Empty] = _empty,
error_type: Union[Type['PluginError'], Empty] = _empty,
) -> List['PluginError']:
"""Return errors that have been logged, filtered by parameters.
Parameters
----------
manager : PluginManager, optional
If provided, will restrict errors to those that are owned by
``manager``.
plugin_name : str
If provided, will restrict errors to those that were raised by
``plugin_name``.
error_type : Exception
If provided, will restrict errors to instances of ``error_type``.
Returns
-------
list of PluginError
A list of PluginErrors that have been instantiated during this
session that match the provided parameters.
Raises
------
TypeError
If ``error_type`` is provided and is not an exception class.
"""
errors: List['PluginError'] = []
for error in cls._record:
if plugin is not _empty and error.plugin != plugin:
continue
if plugin_name is not _empty and error.plugin_name != plugin_name:
continue
if error_type is not _empty:
import inspect
if not (
inspect.isclass(error_type)
and issubclass(error_type, BaseException)
):
raise TypeError(
"The `error_type` argument must be an exception class"
)
if not isinstance(error.__cause__, error_type):
continue
errors.append(error)
return errors
def format(self, package_info: bool = True):
msg = f'PluginError: {self}'
if self.__cause__:
msg = msg.replace(str(self.__cause__), '').strip(": ") + "\n"
cause = repr(self.__cause__).replace("\n", "\n" + " " * 13)
msg += f' Cause was: {cause}'
# show the exact file and line where the error occured
cause_tb = self.__cause__.__traceback__
if cause_tb:
while True:
if not cause_tb.tb_next:
break
cause_tb = cause_tb.tb_next
msg += f'\n in file: {cause_tb.tb_frame.f_code.co_filename}'
msg += f'\n at line: {cause_tb.tb_lineno}'
else:
msg += "\n"
if package_info and self.plugin:
try:
meta = standard_metadata(self.plugin)
meta.pop('license', None)
meta.pop('summary', None)
if meta:
msg += "\n" + "\n".join(
[
f'{k: >11}: {v}'
for k, v in sorted(meta.items())
if v
]
)
except ValueError:
pass
msg += '\n'
return msg
[docs] def log(
self,
package_info: bool = True,
logger: Union[logging.Logger, None, str] = None,
level: int = logging.ERROR,
):
"""Log this error with metadata, optionally provide logger and level.
Parameters
----------
package_info : bool, optional
If true, will include package metadata in log, by default True
logger : logging.Logger or str, optional
A Logger instance or name of a logger to use, by default None
level : int, optional
The logging level to use, by default logging.ERROR
"""
if not isinstance(logger, logging.Logger):
logger = logging.getLogger(logger)
logger.log(level, self.format(package_info=package_info))
[docs] def info(self) -> ExcInfoTuple:
"""Return info as would be returned from sys.exc_info()."""
return (self.__class__, self, self.__traceback__)
[docs]class HookCallError(PluginError):
"""If a hook is called incorrectly.
Usually this results when a HookCaller is called without the appropriate
arguments.
"""
[docs]class PluginImportError(PluginError, ImportError):
"""Plugin module is unimportable."""
[docs]class PluginRegistrationError(PluginError):
"""If an unexpected error occurs during registration."""
[docs]class PluginImplementationError(PluginError):
"""Base class for errors pertaining to a specific hook implementation."""
def __init__(self, hook_implementation, msg=None, cause=None):
plugin = hook_implementation.plugin
plugin_name = hook_implementation.plugin_name
specname = hook_implementation.specname
if not msg:
msg = f"Error in plugin '{plugin_name}', hook '{specname}'"
if cause:
msg += f": {str(cause)}"
super().__init__(
msg,
plugin=plugin,
plugin_name=plugin_name,
cause=cause,
)
[docs]class PluginValidationError(PluginImplementationError):
"""When a plugin implementation fails validation."""
[docs]class PluginCallError(PluginImplementationError):
"""Raised when an error is raised when calling a plugin implementation."""