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

1"""General utilities."""

2

3 import datetime

4 import string

5 from collections import ChainMap

6 from dataclasses import asdict

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

8

9 if 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

15 def 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

32 def 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

40 def 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

45 def 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

52 def 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

57 def 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

74 def 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

87 def 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

92 def 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

122 def 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]