diff --git a/quartodoc/autosummary.py b/quartodoc/autosummary.py index 47ef7636..5c59cff8 100644 --- a/quartodoc/autosummary.py +++ b/quartodoc/autosummary.py @@ -15,13 +15,14 @@ from plum import dispatch # noqa from pathlib import Path from types import ModuleType -from pydantic import ValidationError from .inventory import create_inventory, convert_inventory from . import layout from .parsers import get_parser_defaults from .renderers import Renderer -from .validation import fmt +from .validation import fmt_all +from ._pydantic_compat import ValidationError + from typing import Any @@ -485,12 +486,7 @@ def load_layout(self, sections: dict, package: str, options=None): try: return layout.Layout(sections=sections, package=package, options=options) except ValidationError as e: - msg = "Configuration error for YAML:\n - " - errors = [fmt(err) for err in e.errors() if fmt(err)] - first_error = errors[ - 0 - ] # we only want to show one error at a time b/c it is confusing otherwise - msg += first_error + msg = fmt_all(e) raise ValueError(msg) from None # building ---------------------------------------------------------------- diff --git a/quartodoc/tests/__snapshots__/test_validation.ambr b/quartodoc/tests/__snapshots__/test_validation.ambr index 7503cebc..0f99be86 100644 --- a/quartodoc/tests/__snapshots__/test_validation.ambr +++ b/quartodoc/tests/__snapshots__/test_validation.ambr @@ -15,7 +15,7 @@ ''' # --- -# name: test_missing_name_contents_2 +# name: test_missing_name_contents ''' Code: @@ -25,7 +25,10 @@ - title: Section 1 - title: Section 2 contents: + + # name missing here ---- - children: linked + - name: MdRenderer Error: diff --git a/quartodoc/validation.py b/quartodoc/validation.py index 9fa76222..420e9913 100644 --- a/quartodoc/validation.py +++ b/quartodoc/validation.py @@ -1,5 +1,67 @@ +"""User-friendly messages for configuration validation errors. + +This module has largely two goals: + +* Show the most useful error (pydantic starts with the highest, broadest one). +* Make the error message very user-friendly. + +The key dynamic for understanding formatting is that pydantic is very forgiving. +It will coerce values to the target type, and by default allows extra fields. +Critically, if you have a union of types, it will try each type in order until it +finds a match. In this case, it reports errors for everything it tried. + +For example, consider this config: + +quartodoc: + package: zzz + sections: + - title: Section 1 + contents: + # name missing here ---- + - children: linked + +In this case, the first element of contents is missing a name field. Since the +first type in the union for content elements is _AutoDefault, that is what it +tries (and logs an error about name). However, it then goes down the list of other +types in the union and logs errors for those (e.g. Doc). This produce a lot of +confusing messages, because nowhere does it make clear what type its trying to create. + +We don't want error messages for everything it tried, just the first type in the union. +(For a discriminated union, it uses the `kind:` field to know what the first type to try is). +""" + + +def fmt_all(e): + # if not pydantic.__version__.startswith("1."): + # # error reports are much better in pydantic v2 + # # so we just use those. + # return str(e) + + errors = [fmt(err) for err in e.errors() if fmt(err)] + + # the last error is the most specific, while earlier ones can be + # for alternative union types that didn't work out. + main_error = errors[0] + + msg = f"Configuration error for YAML:\n - {main_error}" + return msg + + def fmt(err: dict): "format error messages from pydantic." + + # each entry of loc is a new level in the config tree + # 0 is root + # 1 is sections + # 2 is a section entry + # 3 is contents + # 4 is a content item + # 5 might be Auto.members, etc.. + # 6 might be an Auto.members item + + # type: value_error.discriminated_union.missing_discriminator + # type: value_error.missing + # type: value_error.extra msg = "" if err["msg"].startswith("Discriminator"): return msg