Source code for cleverly._refusal
"""The reason codes of a refusal, and the one record a refusal is built from.
Every :class:`~cleverly.exceptions.CleverlyError` carries a :class:`Refusal`. The record holds
the reason code, the condition that failed, the alternative a caller can use, and the roadmap
item that tracks a missing capability. A capability row stores the same record before any call,
so the row and the raise state one reason in one text.
A leaf module. It imports the standard library only, as :mod:`cleverly._inference_status` does,
so :mod:`cleverly.exceptions` can import it at module level without an import cycle.
"""
from __future__ import annotations
import re
from dataclasses import dataclass
from enum import StrEnum
from typing import Final
__all__ = [
"NO_ALTERNATIVE",
"Refusal",
"RefusalReason",
]
[docs]
class RefusalReason(StrEnum): # numpydoc ignore=PR01,PR02
"""Why the package refuses a request.
The code says where the problem is, and so what a caller can do about it.
Attributes
----------
INVALID_DECLARATION
The requested analysis cannot run on the declared sample or design. The message states
the failed condition.
DIFFERENT_ESTIMAND
The requested option changes the causal question. The message names both questions.
IMPLEMENTATION_GAP
A valid method has no implementation here yet. The message names the available
alternatives and the roadmap item that tracks the gap.
INFERENCE_GAP
The estimate exists, but the requested uncertainty calculation is incomplete. The
message offers the supported inference.
API_REDIRECTION
The feature exists through another argument or object. The message points to a working
call. This code does not mark a missing capability.
"""
INVALID_DECLARATION = "invalid_declaration"
DIFFERENT_ESTIMAND = "different_estimand"
IMPLEMENTATION_GAP = "implementation_gap"
INFERENCE_GAP = "inference_gap"
API_REDIRECTION = "api_redirection"
#: The alternative of a refusal for which no call in this package answers the request. A
#: refusal with a code other than :attr:`RefusalReason.INVALID_DECLARATION` must name an
#: alternative, and this value states that none exists rather than leaving the field empty.
NO_ALTERNATIVE: Final[str] = "No call in this package answers this request."
[docs]
@dataclass(frozen=True)
class Refusal:
"""One refusal: its reason code, the failed condition, and what to do instead.
Parameters
----------
reason : RefusalReason
Where the problem is.
condition : str
The sentences that state the failed condition. They end with a full stop. A condition
that a raise splices into a longer message can open with a lowercase word.
alternative : str or None
The sentence that names a working call, or :data:`NO_ALTERNATIVE`. Required for every
code except :attr:`RefusalReason.INVALID_DECLARATION`.
roadmap : str or None
The id of the ``docs/roadmap.md`` item that tracks a missing capability, such as
``"F21"``. An :attr:`RefusalReason.IMPLEMENTATION_GAP` refusal always names one.
Raises
------
TypeError
When ``reason`` is not a :class:`RefusalReason`, when ``condition`` is empty, or when a
code that needs an alternative has none.
"""
reason: RefusalReason
condition: str
alternative: str | None = None
roadmap: str | None = None
def __post_init__(self) -> None:
if not isinstance(self.reason, RefusalReason):
raise TypeError(f"a refusal needs a RefusalReason; got {self.reason!r}")
if not self.condition.strip():
raise TypeError("a refusal needs the condition that failed")
if self.reason is not RefusalReason.INVALID_DECLARATION and not self.alternative:
raise TypeError(
f"a {self.reason.value} refusal names an alternative, or NO_ALTERNATIVE"
)
[docs]
def message(self) -> str:
"""The text a raise prints and a capability row quotes.
Returns
-------
str
:attr:`condition`, then a sentence that names :attr:`roadmap` when the condition
does not already name it, then :attr:`alternative`.
"""
parts = [self.condition.strip()]
if self.roadmap is not None and not re.search(
rf"\b{re.escape(self.roadmap)}\b", self.condition
):
parts.append(f"{self.roadmap} in docs/roadmap.md tracks this.")
if self.alternative:
parts.append(self.alternative.strip())
return " ".join(parts)