Source code for pyhs3.exceptions
"""
Exception classes for pyhs3.
Custom exception hierarchy for better error handling and debugging.
"""
from __future__ import annotations
from typing import Any
from pydantic import (
ValidationError,
ValidationInfo,
WrapValidator,
)
from pydantic_core import ErrorDetails, InitErrorDetails, PydanticCustomError
[docs]
class HS3Exception(Exception):
"""
Base exception class for all pyhs3-related errors.
This serves as the root exception that all other pyhs3 exceptions inherit from,
allowing users to catch all pyhs3-specific errors with a single except clause.
"""
[docs]
class ExpressionParseError(HS3Exception):
"""
Exception raised when a mathematical expression cannot be parsed.
This typically occurs when:
- The expression contains invalid syntax
- Unsupported mathematical operations are used
- Variable names are malformed
"""
[docs]
class ExpressionEvaluationError(HS3Exception):
"""
Exception raised when a parsed expression cannot be evaluated.
This typically occurs when:
- Required variables are missing from the evaluation context
- The expression results in mathematical errors (division by zero, etc.)
- PyTensor conversion fails
"""
[docs]
class WorkspaceValidationError(HS3Exception):
"""
Raised when a workspace fails to validate.
This typically occurs when:
- Foreign key references cannot be resolved (e.g. a likelihood references
a distribution that doesn't exist in the workspace)
- A JSON file cannot be parsed into a valid workspace
"""
class DuplicateEntityError(HS3Exception):
"""
Raised when two distinct entities share a single name in the dependency graph.
Distributions, functions, parameters, constants, and HistFactory modifiers
all live in one shared name namespace when the computation graph is built.
Two distinct entities with the same name (for example a function and a
distribution both named ``shared``, or two distributions named ``model``)
would silently shadow one another — only the last one built would survive,
and references to that name would be wired to the wrong node. This error is
raised instead so the collision surfaces loudly at model-construction time.
A parameter declared in ``parameter_points`` that also appears as a graph
parameter is the *same* logical entity and is not a collision.
"""
def custom_error_msg(custom_messages: dict[str, str]) -> Any:
r"""
Customize an error message for pydantic validation errors.
See https://github.com/pydantic/pydantic/discussions/8468.
Example:
>>> from typing import Annotated
>>> from pydantic import BaseModel
>>> from pydantic.types import StringConstraints
>>> NameString = Annotated[
... str,
... StringConstraints(pattern=r"^[a-zA-Z0-9]*$"),
... custom_error_msg({"string_pattern_mismatch": "The field {field_name} can only contain letters and numbers."}),
... ]
>>> class Model(BaseModel):
... name: NameString
>>> Model(name="dog@123") # doctest: +IGNORE_EXCEPTION_DETAIL
Traceback (most recent call last):
...
pydantic_core._pydantic_core.ValidationError: 1 validation error for Model
name
The field name can only contain letters and numbers. ...
"""
def _validator(v: Any, next_: Any, ctx: ValidationInfo) -> Any:
try:
return next_(v, ctx)
except ValidationError as exc:
new_errors: list[InitErrorDetails | ErrorDetails] = []
for error in exc.errors():
error["loc"] = error["loc"][1:] # to skip current location
custom_message = custom_messages.get(error["type"])
if custom_message:
err_ctx = error.get("ctx", {}).copy()
# Add input and ValidationInfo data to context
err_ctx["input"] = error["input"]
if ctx.data:
err_ctx.update(ctx.data)
new_error = InitErrorDetails(
type=PydanticCustomError(
error["type"], custom_message, err_ctx
),
loc=error["loc"],
input=error["input"],
)
new_errors.append(new_error)
else:
new_errors.append(error)
raise ValidationError.from_exception_data(
title=exc.title,
line_errors=new_errors, # type: ignore[arg-type]
) from None
return WrapValidator(_validator)