Skip to content

Make ValidationError behave more like an ExceptionGroup #593

Open
@adriangb

Description

@adriangb

The plan is to gradually migrate towards an API that is at least compatible in theory and usage (if not physically as in class hierarchy) with PEP 654 ExceptionGroups. This has a lot of benefits:

  • Already established API. Less design work for us, less learning for users. We can even point them at the Python docs for how the thing works in general.
  • Established(ing) tooling. I expect that Rich and many other things will support rich rending or tracebacks from ExceptionGroups by default.
  • More powerful API. If we move locs to the ValidationError itself and make LineError a public ExceptionGroup in its own right we'll have a really powerful API (and even syntax, thanks to PEP 654!) to traverse and filter Pydantic's errors. I know @hoodmane had been asking for this. We can easily (and relatively performantly I expect) provide methods for iterating through exceptions as a flat list, as a tree and with original or resolved loc's.

Here's my draft for the target API:

from __future__ import annotations

import json
from typing import (
    Any,
    Callable,
    Literal,
    Mapping,
    Sequence,
    TypeAlias,
    TypedDict,
)

from typing_extensions import final

KnownError = Literal["string_type"]


@final
class LineError(Exception):
    def __init__(
        self,
        input: Any,
        type: str,
        message_template: str,
        ctx: Mapping[str, Any],
    ) -> None:
        self.input = input
        self.type = type
        self.message = message_template.format(**ctx)

    @staticmethod
    def known(type: KnownError, input: Any, ctx: Mapping[str, Any]) -> LineError:
        ...

    @staticmethod
    def custom(
        type: str, input: Any, message_template: str, ctx: Mapping[str, Any]
    ) -> LineError:
        ...


class ErrorDetail(TypedDict):
    pass


ValidationErrorExc = type[LineError]
ExcT: TypeAlias = ValueError | AssertionError | LineError | "ValidationError"

TypeCondition = ExcT | tuple[ExcT, ...]
CallableCondition = Callable[[ExcT], bool]


class ValidationError(ValueError):
    def __init__(
        self,
        message: str,
        errors: Sequence[ExcT],
        loc_prefix: tuple[int | str, ...] = (),
    ) -> None:
        self._message = message
        self._errors = errors
        self._loc_prefix = loc_prefix

    def errors(self) -> list[ErrorDetail]:
        ...

    def json(self) -> bytes:
        return json.dumps(self.errors()).encode()

    @property
    def loc_prefix(self) -> tuple[int | str, ...]:
        return self._loc_prefix

    @property
    def exceptions(
        self,
    ) -> list[ExcT]:
        ...

    def subgroup(self, __condition: TypeCondition | CallableCondition) -> ValidationError | None:
        ...

    def split(
        self, __condition: TypeCondition | CallableCondition
    ) -> tuple[ValidationError, ValidationError]:
        ...

    @property
    def message(self) -> str:
        return "Validation error"

    def derive(self, __excs: Sequence[ExcT]) -> ValidationError:
        return ValidationError(
            self.message,
            __excs,
            loc_prefix=self.loc_prefix,
        )


v1 = ValidationError(
    "error",
    [
        LineError.known(
            type="string_type",
            input="foo",
            ctx={"min_length": 10},
        ),
        LineError.custom(
            type="form_parse",
            input="a=1&b=2&c=3,4",
            message_template="Could not parse form w/ style={style}",
            ctx={"style": "form"},
        ),
        ValueError("Bad bad int!"),
    ]
)

v2 = ValidationError(
    "error2",
    v1.exceptions,
)

One thing omitted here is the InitError types we currently have. I think we can do without those but we should keep them around for now to minimize changes, and maybe forever.

Metadata

Metadata

Assignees

Labels

enhancementNew feature or request

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions