1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135
|
# SPDX-License-Identifier: MIT
"""
This module defines exceptions and error handling utilities. It is the
recommend path to access ``ConfiguratonError``, ``ConfigurationWarning``, and
``ExceptionGroup``. For backward compatibility, ``ConfigurationError`` is
re-exported in the top-level package.
"""
from __future__ import annotations
import builtins
import contextlib
import dataclasses
import sys
import typing
import warnings
__all__ = [
"ConfigurationError",
"ConfigurationWarning",
"ExceptionGroup",
]
def __dir__() -> list[str]:
return __all__
class ConfigurationError(Exception):
"""
Error in the backend metadata.
Has an optional key attribute, which will be non-None if the error is
related to a single key in the pyproject.toml file.
"""
def __init__(self, msg: str, *, key: str | None = None) -> None:
"""
Create a new error with a key (can be None).
"""
super().__init__(msg)
self._key = key
@property
def key(self) -> str | None: # pragma: no cover
"""
Return the stored key.
"""
return self._key
class ConfigurationWarning(UserWarning):
"""Warnings about backend metadata."""
if sys.version_info >= (3, 11):
ExceptionGroup = builtins.ExceptionGroup
else:
class ExceptionGroup(Exception): # noqa: N818
"""A minimal implementation of `ExceptionGroup` from Python 3.11.
Users can replace this with a more complete implementation, such as from
the exceptiongroup backport package, if better error messages and
integration with tooling is desired and the addition of a dependency is
acceptable.
"""
message: str
exceptions: list[Exception]
def __init__(self, message: str, exceptions: list[Exception]) -> None:
"""
Create a new group with a message and a list of exceptions.
"""
self.message = message
self.exceptions = exceptions
def __repr__(self) -> str:
"""
Return a repr similar to the stdlib ExceptionGroup.
"""
return f"{self.__class__.__name__}({self.message!r}, {self.exceptions!r})"
@dataclasses.dataclass
class ErrorCollector:
"""
Collect errors and raise them as a group at the end (if collect_errors is True),
otherwise raise them immediately.
"""
collect_errors: bool
errors: list[Exception] = dataclasses.field(default_factory=list)
def config_error(
self,
msg: str,
*,
key: str | None = None,
got: object = None,
got_type: type[typing.Any] | None = None,
warn: bool = False,
**kwargs: object,
) -> None:
"""Raise a configuration error, or add it to the error list."""
msg = msg.format(key=f'"{key}"', **kwargs)
if got is not None:
msg = f"{msg} (got {got!r})"
if got_type is not None:
msg = f"{msg} (got {got_type.__name__})"
if warn:
warnings.warn(msg, ConfigurationWarning, stacklevel=3)
elif self.collect_errors:
self.errors.append(ConfigurationError(msg, key=key))
else:
raise ConfigurationError(msg, key=key)
def finalize(self, msg: str) -> None:
"""Raise a group exception if there are any errors."""
if self.errors:
raise ExceptionGroup(msg, self.errors)
@contextlib.contextmanager
def collect(self) -> typing.Generator[None, None, None]:
"""Support nesting; add any grouped errors to the error list."""
if self.collect_errors:
try:
yield
except ExceptionGroup as error:
self.errors.extend(error.exceptions)
else:
yield
|