Coverage for src / rendercv / schema / yaml_reader.py: 100%
30 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
3import ruamel.yaml
4from ruamel.yaml.comments import CommentedMap
5from ruamel.yaml.scanner import RoundTripScanner
7from rendercv.exception import RenderCVInternalError, RenderCVUserError
10def read_yaml(file_path_or_contents: pathlib.Path | str) -> CommentedMap:
11 """Parse YAML/JSON content from file path or string.
13 Why:
14 Validation errors must point to exact YAML locations. CommentedMap
15 preserves source coordinates that map Pydantic errors back to input
16 lines, enabling user-friendly error tables showing exactly where
17 mistakes occur in the input file.
19 Example:
20 ```py
21 data = read_yaml(pathlib.Path("cv.yaml"))
22 name = data["cv"]["name"] # Regular dict access
23 # Line info also available: data.lc.data["cv"][0] = (line, col)
24 ```
26 Args:
27 file_path_or_contents: File path or raw YAML string.
29 Returns:
30 Dictionary with line/column metadata for error reporting.
31 """
32 if isinstance(file_path_or_contents, pathlib.Path):
33 # Check if the file exists:
34 if not file_path_or_contents.exists():
35 message = f"The input file `{file_path_or_contents}` doesn't exist!"
36 raise RenderCVUserError(message)
38 # Check the file extension:
39 accepted_extensions = [".yaml", ".yml", ".json", ".json5"]
40 if file_path_or_contents.suffix not in accepted_extensions:
41 message = (
42 "The input file should have one of the following extensions:"
43 f" {', '.join(accepted_extensions)}. The input file is"
44 f" {file_path_or_contents.name}."
45 )
46 raise RenderCVUserError(message)
48 file_content = file_path_or_contents.read_text(encoding="utf-8")
49 else:
50 file_content = file_path_or_contents
52 yaml_as_dictionary: CommentedMap = yaml.load(file_content)
54 if yaml_as_dictionary is None:
55 message = "The input file is empty!"
56 raise RenderCVUserError(message)
58 if isinstance(yaml_as_dictionary, str):
59 message = (
60 "You probably meant to pass a path to the YAML file, but you passed as a"
61 " string and RenderCV interpreted it as the contents of the YAML file."
62 f" Pass the path using `pathlib.Path({file_path_or_contents})`."
63 )
64 raise RenderCVInternalError(message)
66 return yaml_as_dictionary
69class ScannerNoAlias(RoundTripScanner):
70 """Custom Scanner that treats * as a regular character instead of alias syntax.
72 Why:
73 CV content frequently contains literal * characters (e.g., in Markdown bold
74 syntax). Standard YAML interprets * as an alias indicator, causing parse
75 errors. This subclass overrides alias handling to treat * as plain text.
76 """
78 def fetch_alias(self) -> None:
79 """Treat * as a plain scalar character instead of alias syntax."""
80 self.fetch_plain()
83yaml = ruamel.yaml.YAML()
84yaml.Scanner = ScannerNoAlias
86# Disable ISO date parsing, keep it as a string:
87yaml.constructor.yaml_constructors["tag:yaml.org,2002:timestamp"] = (
88 lambda loader, node: loader.construct_scalar(node)
89)