Source code for pcapkit.utilities.exceptions

# -*- coding: utf-8 -*-
"""User Defined Exceptions
=============================

.. module:: pcapkit.utilities.exceptions

:mod:`pcapkit.utilities.exceptions` refined built-in exceptions.
Make it possible to show only user error stack infomation [*]_,
when exception raised on user's operation.

.. [*] See |tbtrim|_ project for Pythonic implementation.

.. |tbtrim| replace:: ``tbtrim``
.. _tbtrim: https://github.com/gousaiyang/tbtrim

"""
import inspect
import io
import os
import struct
import sys
from typing import TYPE_CHECKING

from pcapkit.utilities.compat import ModuleNotFoundError  # pylint: disable=redefined-builtin
from pcapkit.utilities.logging import DEVMODE, VERBOSE, get_logger

if TYPE_CHECKING:
    from typing import Any

__all__ = [
    'stacklevel',

    'BaseError',                                                    # Exception
    'DigitError', 'IntError', 'RealError', 'ComplexError',          # TypeError
    'BoolError', 'BytesError', 'StringError', 'BytearrayError',     # TypeError
    'DictError', 'ListError', 'TupleError', 'IterableError',        # TypeError
    'IOObjError', 'ProtocolUnbound', 'CallableError',               # TypeError
    'InfoError', 'IPError', 'EnumError', 'ComparisonError',         # TypeError
    'RegistryError', 'FieldError',                                  # TypeError
    'FormatError', 'UnsupportedCall',                               # AttributeError
    'FileError', 'UnsupportedOperation',                            # IOError
    'FileExists',                                                   # FileExistsError
    'FileNotFound',                                                 # FileNotFoundError
    'ProtocolNotFound',                                             # IndexError
    'VersionError', 'IndexNotFound', 'ProtocolError',               # ValueError
    'EndianError', 'KeyExists', 'NoDefaultValue',                   # ValueError
    'FieldValueError', 'SchemaError', 'SeekError', 'TruncateError', # ValueError
    'ProtocolNotImplemented', 'VendorNotImplemented',               # NotImplementedError
    'StructError',                                                  # struct.error
    'StreamEOFError',                                               # EOFError
    'MissingKeyError', 'FragmentError', 'PacketError',              # KeyError
    'ModuleNotFound',                                               # ModuleNotFoundError
]


#: logging.Logger: Module-level logger, a child of the package-wide
#: :data:`pcapkit.utilities.logging.logger`.
logger = get_logger(__name__)


[docs] def stacklevel() -> 'int': """Stack level of the innermost frame outside :mod:`pcapkit`. The value is a *relative* level, in the sense both :func:`warnings.warn` and the :mod:`logging` module use: level ``1`` is the frame that called :func:`stacklevel`, level ``2`` its caller, and so on outwards. Handing it to either of them attributes the complaint to the caller who reached into :mod:`pcapkit`, rather than to whichever :mod:`pcapkit` internal happened to notice the problem -- which is the whole point of the function, since the internal frames are noise to the user reading the report. The arithmetic, since it is easy to get backwards. Number the frames outwards from this one, so that a number *is* the relative level a consumer wants:: level 0 stacklevel() itself level 1 whoever called stacklevel() ... level ``boundary`` the outermost frame inside pcapkit ... level ``outermost`` the interpreter entry point The frame to name is the first one *past* the boundary, hence ``boundary + 1``. What makes this a fix rather than a rewrite is that ``boundary`` is measured from the *inside* out: it depends only on how deep the :mod:`pcapkit` frames run, never on how deep the caller's own stack is. Numbering from the outside in, as this function once did, grew with the outer stack, so the frame it named drifted one further out for every extra frame above the boundary -- under :program:`pytest`, dozens of them. Both bounds are enforced, as neither consumer copes with a level outside them: * Never below ``1``. ``0`` and negative values mean "do not walk out at all" to :meth:`logging.Logger.findCaller`, which then attributes the record to :mod:`logging` itself; :func:`warnings.warn` treats them as ``1``. * Never above ``outermost``. That bound is reached when the whole stack is inside :mod:`pcapkit`, e.g. running the package as a script, and walking past the outermost frame makes :func:`warnings.warn` fall back to blaming the :mod:`sys` module. The walk goes through :func:`inspect.currentframe` and ``f_back`` rather than :func:`traceback.extract_stack`, which cannot be used here: :meth:`traceback.StackSummary.extract` honours :data:`sys.tracebacklimit`, and :class:`BaseError` sets that to ``0`` for every loud error outside development mode. One such error therefore made ``extract_stack()`` return an *empty* list for the rest of the process, which is where the old ``-1`` came from -- so in ordinary use the first error silently broke the attribution of every warning after it. Walking frames also skips building the :class:`~traceback.FrameSummary` objects and the :mod:`linecache` lookups behind them, which is worth having on a function called once per warning. Important: The level is relative to the *caller* of :func:`stacklevel`. A function that forwards it to :func:`warnings.warn` on its caller's behalf has to add one for its own frame -- see :func:`pcapkit.utilities.warnings.warn`, which does exactly that. Returns: Number of frames from the caller of :func:`stacklevel` outwards to the innermost frame whose path does not contain ``/pcapkit/``. """ pcapkit = f'{os.path.sep}pcapkit{os.path.sep}' frame = inspect.currentframe() if frame is None: # pragma: no cover # No Python stack frame support, so there is no boundary to find. Blaming # the immediate caller is the least wrong answer available. return 1 try: boundary = 0 # level of the outermost pcapkit frame seen so far outermost = 0 # level of the outermost frame there is level = 0 # level 0 is this function's own frame, which is in pcapkit while frame is not None: if pcapkit in frame.f_code.co_filename: boundary = level outermost = level frame = frame.f_back level += 1 finally: # A frame reachable from a local keeps its whole chain alive as soon as a # traceback references this one, so the name is dropped rather than left # bound. The loop exits with `frame` at :data:`None` anyway; this covers # leaving it early. del frame return max(1, min(boundary + 1, outermost))
############################################################################## # BaseError (abc of exceptions) session. ##############################################################################
[docs] class BaseError(Exception): """Base error class of all kinds. A loud error -- the default -- is reported once, at :data:`logging.CRITICAL` level, on the :data:`~pcapkit.utilities.logging.logger` logger. Outside development mode it also sets :data:`sys.tracebacklimit` to ``0``, which suppresses the traceback frames entirely, so a user sees the exception line rather than a walk through :mod:`pcapkit`'s internals. A **quiet** error (``quiet=True``) is one :mod:`pcapkit` raises as internal control flow and expects to catch itself, such as the :exc:`~pcapkit.utilities.exceptions.MissingKeyError` behind :meth:`MultiDict.get <pcapkit.corekit.multidict.MultiDict.get>`. It is therefore silent and free of side effects: nothing is logged, and :data:`sys.tracebacklimit` is left alone. It is still a perfectly ordinary exception, carrying its message for whoever catches it. Important: * :data:`sys.tracebacklimit` is process-global, so it is only set for a loud error -- a quiet one used as control flow must not truncate the tracebacks of unrelated exceptions for the rest of the process. * The ``stacklevel`` of the log record is the relative level :func:`stacklevel` computes, so the record is attributed to the caller whose operation failed rather than to this module. It used to be *negated*, which :meth:`logging.Logger.findCaller` reads as "do not walk out at all" and which therefore blamed :mod:`logging` itself for every error pcapkit raised. See Also: :func:`pcapkit.utilities.exceptions.stacklevel` """ def __init__(self, *args: 'Any', quiet: 'bool' = False, **kwargs: 'Any') -> 'None': # log error -- a quiet error emits nothing and mutates nothing if not quiet: if DEVMODE: logger.critical('%s: %s', type(self).__name__, str(self), exc_info=self if VERBOSE else False, stack_info=VERBOSE, stacklevel=stacklevel()) else: logger.critical('%s: %s', type(self).__name__, str(self)) sys.tracebacklimit = 0 super().__init__(*args, **kwargs)
############################################################################## # TypeError session. ##############################################################################
[docs] class DigitError(BaseError, TypeError): """The argument(s) must be (a) number(s)."""
[docs] class IntError(BaseError, TypeError): """The argument(s) must be integral."""
[docs] class RealError(BaseError, TypeError): """The function is not defined for real number."""
[docs] class ComplexError(BaseError, TypeError): """The function is not defined for complex instance."""
[docs] class BytesError(BaseError, TypeError): """The argument(s) must be :obj:`bytes` type."""
[docs] class BytearrayError(BaseError, TypeError): """The argument(s) must be :obj:`bytearray` type."""
[docs] class BoolError(BaseError, TypeError): """The argument(s) must be :obj:`bool` type."""
[docs] class StringError(BaseError, TypeError): """The argument(s) must be :obj:`str` type."""
[docs] class DictError(BaseError, TypeError): """The argument(s) must be :obj:`dict` type."""
[docs] class ListError(BaseError, TypeError): """The argument(s) must be :obj:`list` type."""
[docs] class TupleError(BaseError, TypeError): """The argument(s) must be :obj:`tuple` type."""
[docs] class IterableError(BaseError, TypeError): """The argument(s) must be *iterable*."""
[docs] class CallableError(BaseError, TypeError): """The argument(s) must be *callable*."""
[docs] class ProtocolUnbound(BaseError, TypeError): """Protocol slice unbound."""
[docs] class IOObjError(BaseError, TypeError): """The argument(s) must be *file-like object*."""
[docs] class InfoError(BaseError, TypeError): """The argument(s) must be :class:`~pcapkit.corekit.infoclass.Info` instance."""
[docs] class IPError(BaseError, TypeError): """The argument(s) must be *IP address*."""
[docs] class EnumError(BaseError, TypeError): """The argument(s) must be *enumeration protocol* type."""
[docs] class ComparisonError(BaseError, TypeError): """Rich comparison not supported between instances."""
[docs] class RegistryError(BaseError, TypeError): """The argument(s) must be *registry* type."""
[docs] class FieldError(BaseError, TypeError): """The argument(s) must be *field* type."""
############################################################################## # AttributeError session. ##############################################################################
[docs] class FormatError(BaseError, AttributeError): """Unknown format(s)."""
[docs] class UnsupportedCall(BaseError, AttributeError): """Unsupported function or property call."""
############################################################################## # IOError session. ##############################################################################
[docs] class FileError(BaseError, IOError): """[Errno 5] Wrong file format."""
# args: errno, strerror, filename, winerror, filename2 ############################################################################## # FileExistsError session. ##############################################################################
[docs] class FileExists(BaseError, FileExistsError): """[Errno 17] File already exists."""
# args: errno, strerror, filename, winerror, filename2 ############################################################################## # FileNotFoundError session. ##############################################################################
[docs] class FileNotFound(BaseError, FileNotFoundError): """[Errno 2] File not found."""
# args: errno, strerror, filename, winerror, filename2 ############################################################################## # IndexError session. ##############################################################################
[docs] class ProtocolNotFound(BaseError, IndexError): """Protocol not found in ProtoChain."""
############################################################################## # ValueError session. ##############################################################################
[docs] class VersionError(BaseError, ValueError): """Unknown IP version."""
[docs] class IndexNotFound(BaseError, ValueError): """Protocol not in ProtoChain."""
[docs] class ProtocolError(BaseError, ValueError): """Invalid protocol format."""
[docs] class EndianError(BaseError, ValueError): """Invalid endian (byte order)."""
[docs] class KeyExists(BaseError, ValueError): """Key already exists."""
[docs] class NoDefaultValue(BaseError, ValueError): """No default value."""
[docs] class FieldValueError(BaseError, ValueError): """Invalid field value."""
[docs] class SchemaError(BaseError, ValueError): """Invalid schema."""
[docs] class SeekError(BaseError, ValueError): """Invalid seek position."""
[docs] class TruncateError(BaseError, ValueError): """Invalid truncate size."""
############################################################################## # NotImplementedError session. ##############################################################################
[docs] class ProtocolNotImplemented(BaseError, NotImplementedError): """Protocol not implemented."""
[docs] class VendorNotImplemented(BaseError, NotImplementedError): """Vendor not implemented."""
############################################################################## # struct.error session. ##############################################################################
[docs] class StructError(BaseError, struct.error): """Unpack failed.""" def __init__(self, *args: 'Any', eof: 'bool' = False, **kwargs: 'Any') -> 'None': self.eof = eof super().__init__(*args, **kwargs)
############################################################################## # EOFError session. ##############################################################################
[docs] class StreamEOFError(BaseError, EOFError): """Underlying stream exhausted; no data left to read. Raised by :func:`~pcapkit.utilities.decorators.prepare` when the *length* of a schema's read was derived by measuring what is actually left in the stream -- rather than declared by the caller -- and that measurement came back zero. This is the frame reader's "no more packets" signal, so it subclasses :exc:`EOFError` rather than replacing it: existing ``except (EOFError, StopIteration)`` handlers keep working unchanged, and a caller that wants to be more specific can catch this instead. A *declared* zero length -- a nested schema legitimately sized to have nothing to read -- is a different situation and does not raise this. Note: :func:`~pcapkit.utilities.decorators.prepare` always raises this with ``quiet=True``: reaching end of stream is the frame reader's ordinary way of finding out there is nothing left to parse, not a fault to log -- the same convention :exc:`~pcapkit.utilities.exceptions.StructError` follows for the same situation via its own ``eof=True``. """
############################################################################## # KeyError session. ##############################################################################
[docs] class MissingKeyError(BaseError, KeyError): """Key not found."""
[docs] class FragmentError(BaseError, KeyError): """Invalid fragment dict."""
[docs] class PacketError(BaseError, KeyError): """Invalid packet dict."""
############################################################################## # ModuleNotFoundError session. ##############################################################################
[docs] class ModuleNotFound(BaseError, ModuleNotFoundError): """Module not found."""
# kwargs: name, path ############################################################################## # io.UnsupportedOperation session. ##############################################################################
[docs] class UnsupportedOperation(BaseError, io.UnsupportedOperation): """Unsupported operation."""