Coverage for src / rendercv / schema / rendercv_model_builder.py: 100%
73 statements
« prev ^ index » next coverage.py v7.13.4, created at 2026-03-25 13:49 +0000
« prev ^ index » next coverage.py v7.13.4, created at 2026-03-25 13:49 +0000
1import pathlib
2from typing import Any, TypedDict, Unpack
4import pydantic
5import ruamel.yaml
6from ruamel.yaml.comments import CommentedMap
8from rendercv.exception import (
9 OVERLAY_SOURCE_TO_YAML_SOURCE,
10 OverlaySourceKey,
11 RenderCVUserValidationError,
12 RenderCVValidationError,
13 YamlLocation,
14 YamlSource,
15)
17from .models.rendercv_model import RenderCVModel
18from .models.validation_context import ValidationContext
19from .override_dictionary import apply_overrides_to_dictionary
20from .pydantic_error_handling import parse_validation_errors
21from .yaml_reader import read_yaml
24class BuildRendercvModelArguments(TypedDict, total=False):
25 design_yaml_file: str | None
26 locale_yaml_file: str | None
27 settings_yaml_file: str | None
28 output_folder: pathlib.Path | str | None
29 typst_path: pathlib.Path | str | None
30 pdf_path: pathlib.Path | str | None
31 markdown_path: pathlib.Path | str | None
32 html_path: pathlib.Path | str | None
33 png_path: pathlib.Path | str | None
34 dont_generate_typst: bool | None
35 dont_generate_html: bool | None
36 dont_generate_markdown: bool | None
37 dont_generate_pdf: bool | None
38 dont_generate_png: bool | None
39 overrides: dict[str, str] | None
42def get_yaml_error_location(error: ruamel.yaml.YAMLError) -> YamlLocation | None:
43 """Extract 1-indexed line/column coordinates from ruamel parser errors.
45 Args:
46 error: YAML parsing exception raised by ruamel.
48 Returns:
49 Start/end coordinates when available, otherwise None.
50 """
51 context_mark = getattr(error, "context_mark", None)
52 problem_mark = getattr(error, "problem_mark", None)
53 start_mark = context_mark or problem_mark
54 end_mark = problem_mark or context_mark
56 if start_mark is None or end_mark is None:
57 return None
59 return (
60 (start_mark.line + 1, start_mark.column + 1),
61 (end_mark.line + 1, end_mark.column + 1),
62 )
65def read_yaml_with_validation_errors(
66 yaml_content: str, yaml_source: YamlSource
67) -> CommentedMap:
68 """Parse YAML content and convert parser failures into validation errors.
70 Why:
71 YAML syntax errors should use the same error pipeline as schema validation,
72 so the CLI can display all input issues through one consistent path.
74 Args:
75 yaml_content: YAML string content.
76 yaml_source: Which input file this YAML content came from.
78 Returns:
79 Parsed YAML map preserving source coordinates.
81 Raises:
82 RenderCVUserValidationError: If YAML cannot be parsed.
83 """
84 try:
85 return read_yaml(yaml_content)
86 except ruamel.yaml.YAMLError as e:
87 parser_message = str(e).splitlines()[0].strip()
88 if not parser_message.endswith("."):
89 parser_message += "."
91 raise RenderCVUserValidationError(
92 validation_errors=[
93 RenderCVValidationError(
94 schema_location=None,
95 yaml_location=get_yaml_error_location(e),
96 yaml_source=yaml_source,
97 message=f"This is not a valid YAML file. {parser_message}",
98 input="...",
99 )
100 ]
101 ) from e
104def build_rendercv_dictionary(
105 main_yaml_file: str,
106 **kwargs: Unpack[BuildRendercvModelArguments],
107) -> tuple[CommentedMap, dict[str, CommentedMap]]:
108 """Merge main YAML with overlays and CLI overrides into final dictionary.
110 Args:
111 main_yaml_file: Primary CV YAML content string.
112 kwargs: Optional YAML overlay strings, output paths, generation flags, and CLI overrides.
114 Returns:
115 Tuple of merged dictionary and overlay source CommentedMaps (for error reporting).
116 """
117 input_dict = read_yaml_with_validation_errors(main_yaml_file, "main_yaml_file")
118 input_dict.setdefault("settings", {}).setdefault("render_command", {})
120 yaml_overlays: dict[OverlaySourceKey, str | None] = {
121 "settings": kwargs.get("settings_yaml_file"),
122 "design": kwargs.get("design_yaml_file"),
123 "locale": kwargs.get("locale_yaml_file"),
124 }
126 overlay_sources: dict[str, CommentedMap] = {}
127 for key, yaml_content in yaml_overlays.items():
128 if yaml_content:
129 overlay_cm = read_yaml_with_validation_errors(
130 yaml_content, OVERLAY_SOURCE_TO_YAML_SOURCE[key]
131 )
132 input_dict[key] = overlay_cm[key]
133 overlay_sources[key] = overlay_cm
135 render_overrides: dict[str, pathlib.Path | str | bool | None] = {
136 "output_folder": kwargs.get("output_folder"),
137 "typst_path": kwargs.get("typst_path"),
138 "pdf_path": kwargs.get("pdf_path"),
139 "markdown_path": kwargs.get("markdown_path"),
140 "html_path": kwargs.get("html_path"),
141 "png_path": kwargs.get("png_path"),
142 "dont_generate_typst": kwargs.get("dont_generate_typst"),
143 "dont_generate_html": kwargs.get("dont_generate_html"),
144 "dont_generate_markdown": kwargs.get("dont_generate_markdown"),
145 "dont_generate_pdf": kwargs.get("dont_generate_pdf"),
146 "dont_generate_png": kwargs.get("dont_generate_png"),
147 }
149 for key, value in render_overrides.items():
150 if value:
151 input_dict["settings"]["render_command"][key] = value
153 overrides = kwargs.get("overrides")
154 if overrides:
155 input_dict = apply_overrides_to_dictionary(input_dict, overrides)
157 return input_dict, overlay_sources
160def build_rendercv_model_from_commented_map(
161 commented_map: CommentedMap | dict[str, Any],
162 input_file_path: pathlib.Path | None = None,
163 overlay_sources: dict[str, CommentedMap] | None = None,
164) -> RenderCVModel:
165 """Validate merged dictionary and build Pydantic model with error mapping.
167 Args:
168 commented_map: Merged dictionary with line/column metadata.
169 input_file_path: Source file path for context and photo resolution.
170 overlay_sources: Per-section CommentedMaps from overlays (for correct error coordinates).
172 Returns:
173 Validated RenderCVModel instance.
174 """
175 try:
176 validation_context = {
177 "context": ValidationContext(
178 input_file_path=input_file_path,
179 current_date=commented_map.get("settings", {}).get(
180 "current_date", "today"
181 ),
182 )
183 }
184 model = RenderCVModel.model_validate(commented_map, context=validation_context)
185 except pydantic.ValidationError as e:
186 validation_errors = parse_validation_errors(e, commented_map, overlay_sources)
187 raise RenderCVUserValidationError(validation_errors) from e
189 return model
192def build_rendercv_dictionary_and_model(
193 main_yaml_file: str,
194 *,
195 input_file_path: pathlib.Path | None = None,
196 **kwargs: Unpack[BuildRendercvModelArguments],
197) -> tuple[CommentedMap, RenderCVModel]:
198 """Complete pipeline from raw YAML string to validated model.
200 Args:
201 main_yaml_file: Primary CV YAML content string.
202 input_file_path: Source file path for validation context (path resolution).
203 kwargs: Optional YAML overlay strings, output paths, generation flags, and CLI overrides.
205 Returns:
206 Tuple of merged dictionary and validated model.
207 """
208 d, overlay_sources = build_rendercv_dictionary(main_yaml_file, **kwargs)
209 m = build_rendercv_model_from_commented_map(d, input_file_path, overlay_sources)
210 return d, m