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

1import copy 

2 

3from rendercv.exception import RenderCVUserError 

4 

5 

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. 

13 

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. 

19 

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 ``` 

31 

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. 

37 

38 Returns: 

39 Modified structure. 

40 """ 

41 keys = key.split(".") 

42 first_key: str | int = keys[0] 

43 remaining_key = ".".join(keys[1:]) 

44 

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 ) 

50 

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 

60 

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) 

72 

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] 

79 

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 ) 

87 

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] 

90 

91 return dict_or_list 

92 

93 

94def apply_overrides_to_dictionary[T: dict]( 

95 dictionary: T, 

96 overrides: dict[str, str], 

97) -> T: 

98 """Apply multiple CLI overrides to dictionary. 

99 

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. 

104 

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 ``` 

112 

113 Args: 

114 dictionary: Source dictionary to modify. 

115 overrides: Map of dotted paths to new values. 

116 

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) 

123 

124 return new_dictionary