Coverage for src / rendercv / schema / override_dictionary.py: 100%
32 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 copy
3from rendercv.exception import RenderCVUserError
6def update_value_by_location[T: dict | list](
7 dict_or_list: T,
8 key: str,
9 value: str,
10 full_key: str,
11) -> T:
12 """Navigate nested structure via dotted path and update value.
14 Why:
15 CLI overrides like `--cv.sections.education.0.institution MIT`
16 must modify deeply nested YAML values without requiring users
17 to edit files. Recursive traversal handles arbitrary nesting
18 with proper index validation and error context.
20 Example:
21 ```py
22 data = {"cv": {"sections": {"education": [{"institution": "A"}]}}}
23 result = update_value_by_location(
24 data,
25 "cv.sections.education.0.institution",
26 "MIT",
27 "cv.sections.education.0.institution",
28 )
29 assert result["cv"]["sections"]["education"][0]["institution"] == "MIT"
30 ```
32 Args:
33 dict_or_list: Target structure to modify.
34 key: Remaining path segments to traverse.
35 value: Replacement value.
36 full_key: Original full path for error messages.
38 Returns:
39 Modified structure.
40 """
41 keys = key.split(".")
42 first_key: str | int = keys[0]
43 remaining_key = ".".join(keys[1:])
45 # Calculate the parent path for error messages
46 processed_count = len(full_key.split(".")) - len(key.split("."))
47 previous_key = (
48 ".".join(full_key.split(".")[:processed_count]) if processed_count > 0 else ""
49 )
51 if isinstance(dict_or_list, list):
52 try:
53 first_key = int(first_key)
54 except ValueError as e:
55 message = (
56 f"`{previous_key}` corresponds to a list, but `{keys[0]}` is not an"
57 " integer."
58 )
59 raise RenderCVUserError(message) from e
61 if first_key >= len(dict_or_list):
62 message = (
63 f"Index {first_key} is out of range for the list `{previous_key}`."
64 )
65 raise RenderCVUserError(message)
66 elif not isinstance(dict_or_list, dict):
67 message = (
68 f"It seems like there's something wrong with `{full_key}`, but we don't"
69 " know what it is."
70 )
71 raise RenderCVUserError(message)
73 if len(keys) == 1:
74 new_value = value
75 else:
76 if isinstance(dict_or_list, dict) and first_key not in dict_or_list:
77 # Safe: isinstance above guarantees dict, but ty cannot narrow T:
78 dict_or_list[first_key] = {} # ty: ignore[invalid-assignment]
80 new_value = update_value_by_location(
81 # Safe: first_key is validated as int for list, str for dict above:
82 dict_or_list[first_key], # ty: ignore[invalid-argument-type]
83 remaining_key,
84 value,
85 full_key=full_key,
86 )
88 # Safe: first_key type matches dict_or_list indexing per isinstance checks:
89 dict_or_list[first_key] = new_value # ty: ignore[invalid-assignment]
91 return dict_or_list
94def apply_overrides_to_dictionary[T: dict](
95 dictionary: T,
96 overrides: dict[str, str],
97) -> T:
98 """Apply multiple CLI overrides to dictionary.
100 Why:
101 Users need to test configuration changes without editing YAML files.
102 Batching overrides ensures all modifications happen before validation,
103 preventing partial invalid states.
105 Example:
106 ```py
107 data = {"cv": {"name": "John", "phone": "123"}}
108 overrides = {"cv.name": "Jane", "cv.phone": "456"}
109 result = apply_overrides_to_dictionary(data, overrides)
110 assert result["cv"]["name"] == "Jane"
111 ```
113 Args:
114 dictionary: Source dictionary to modify.
115 overrides: Map of dotted paths to new values.
117 Returns:
118 Deep copy with all overrides applied.
119 """
120 new_dictionary = copy.deepcopy(dictionary)
121 for key, value in overrides.items():
122 new_dictionary = update_value_by_location(new_dictionary, key, value, key)
124 return new_dictionary