Coverage for /Users/OORDCOR/Documents/code/bump-my-version/bumpversion/utils.py: 78%

61 statements  

« prev     ^ index     » next       coverage.py v7.3.2, created at 2024-02-24 07:45 -0600

1"""General utilities.""" 

2 

3import datetime 

4import string 

5from collections import ChainMap 

6from dataclasses import asdict 

7from typing import TYPE_CHECKING, Any, List, Optional, Tuple 

8 

9if TYPE_CHECKING: # pragma: no-coverage 9 ↛ 10line 9 didn't jump to line 10, because the condition on line 9 was never true

10 from bumpversion.config import Config 

11 from bumpversion.scm import SCMInfo 

12 from bumpversion.versioning.models import Version 

13 

14 

15def extract_regex_flags(regex_pattern: str) -> Tuple[str, str]: 

16 """ 

17 Extract the regex flags from the regex pattern. 

18 

19 Args: 

20 regex_pattern: The pattern that might start with regex flags 

21 

22 Returns: 

23 A tuple of the regex pattern without the flag string and regex flag string 

24 """ 

25 import re 

26 

27 flag_pattern = r"^(\(\?[aiLmsux]+\))" 

28 bits = re.split(flag_pattern, regex_pattern) 

29 return (regex_pattern, "") if len(bits) == 1 else (bits[2], bits[1]) 

30 

31 

32def recursive_sort_dict(input_value: Any) -> Any: 

33 """Sort a dictionary recursively.""" 

34 if not isinstance(input_value, dict): 

35 return input_value 

36 

37 return {key: recursive_sort_dict(input_value[key]) for key in sorted(input_value.keys())} 

38 

39 

40def key_val_string(d: dict) -> str: 

41 """Render the dictionary as a comma-delimited key=value string.""" 

42 return ", ".join(f"{k}={v}" for k, v in sorted(d.items())) 

43 

44 

45def prefixed_environ() -> dict: 

46 """Return a dict of the environment with keys wrapped in `${}`.""" 

47 import os 

48 

49 return {f"${key}": value for key, value in os.environ.items()} 

50 

51 

52def labels_for_format(serialize_format: str) -> List[str]: 

53 """Return a list of labels for the given serialize_format.""" 

54 return [item[1] for item in string.Formatter().parse(serialize_format) if item[1]] 

55 

56 

57def base_context(scm_info: Optional["SCMInfo"] = None) -> ChainMap: 

58 """The default context for rendering messages and tags.""" 

59 from bumpversion.scm import SCMInfo # Including this here to avoid circular imports 

60 

61 scm = asdict(scm_info) if scm_info else asdict(SCMInfo()) 

62 

63 return ChainMap( 

64 { 

65 "now": datetime.datetime.now(), 

66 "utcnow": datetime.datetime.now(datetime.timezone.utc), 

67 }, 

68 prefixed_environ(), 

69 scm, 

70 {c: c for c in ("#", ";")}, 

71 ) 

72 

73 

74def get_context( 

75 config: "Config", current_version: Optional["Version"] = None, new_version: Optional["Version"] = None 

76) -> ChainMap: 

77 """Return the context for rendering messages and tags.""" 

78 ctx = base_context(config.scm_info) 

79 ctx = ctx.new_child({"current_version": config.current_version}) 

80 if current_version: 

81 ctx = ctx.new_child({f"current_{part}": current_version[part].value for part in current_version}) 

82 if new_version: 

83 ctx = ctx.new_child({f"new_{part}": new_version[part].value for part in new_version}) 

84 return ctx 

85 

86 

87def get_overrides(**kwargs) -> dict: 

88 """Return a dictionary containing only the overridden key-values.""" 

89 return {key: val for key, val in kwargs.items() if val is not None} 

90 

91 

92def get_nested_value(d: dict, path: str) -> Any: 

93 """ 

94 Retrieves the value of a nested key in a dictionary based on the given path. 

95 

96 Args: 

97 d: The dictionary to search. 

98 path: A string representing the path to the nested key, separated by periods. 

99 

100 Returns: 

101 The value of the nested key. 

102 

103 Raises: 

104 KeyError: If a key in the path does not exist. 

105 ValueError: If an element in the path is not a dictionary. 

106 """ 

107 keys = path.split(".") 

108 current_element = d 

109 

110 for key in keys: 

111 if not isinstance(current_element, dict): 111 ↛ 112line 111 didn't jump to line 112, because the condition on line 111 was never true

112 raise ValueError(f"Element at '{'.'.join(keys[:keys.index(key)])}' is not a dictionary") 

113 

114 if key not in current_element: 114 ↛ 115line 114 didn't jump to line 115, because the condition on line 114 was never true

115 raise KeyError(f"Key '{key}' not found at '{'.'.join(keys[:keys.index(key)])}'") 

116 

117 current_element = current_element[key] 

118 

119 return current_element 

120 

121 

122def set_nested_value(d: dict, value: Any, path: str) -> None: 

123 """ 

124 Sets the value of a nested key in a dictionary based on the given path. 

125 

126 Args: 

127 d: The dictionary to search. 

128 value: The value to set. 

129 path: A string representing the path to the nested key, separated by periods. 

130 

131 Raises: 

132 ValueError: If an element in the path is not a dictionary. 

133 """ 

134 keys = path.split(".") 

135 last_element = keys[-1] 

136 current_element = d 

137 

138 for i, key in enumerate(keys): 

139 if key == last_element: 

140 current_element[key] = value 

141 elif key not in current_element: 141 ↛ 142line 141 didn't jump to line 142, because the condition on line 141 was never true

142 raise KeyError(f"Key '{key}' not found at '{'.'.join(keys[:keys.index(key)])}'") 

143 elif not isinstance(current_element[key], dict): 143 ↛ 144line 143 didn't jump to line 144, because the condition on line 143 was never true

144 raise ValueError(f"Path '{'.'.join(keys[:i+1])}' does not lead to a dictionary.") 

145 else: 

146 current_element = current_element[key]