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
« prev ^ index » next coverage.py v7.3.2, created at 2024-02-24 07:45 -0600
1"""General utilities."""
3import datetime
4import string
5from collections import ChainMap
6from dataclasses import asdict
7from typing import TYPE_CHECKING, Any, List, Optional, Tuple
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
15def extract_regex_flags(regex_pattern: str) -> Tuple[str, str]:
16 """
17 Extract the regex flags from the regex pattern.
19 Args:
20 regex_pattern: The pattern that might start with regex flags
22 Returns:
23 A tuple of the regex pattern without the flag string and regex flag string
24 """
25 import re
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])
32def recursive_sort_dict(input_value: Any) -> Any:
33 """Sort a dictionary recursively."""
34 if not isinstance(input_value, dict):
35 return input_value
37 return {key: recursive_sort_dict(input_value[key]) for key in sorted(input_value.keys())}
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()))
45def prefixed_environ() -> dict:
46 """Return a dict of the environment with keys wrapped in `${}`."""
47 import os
49 return {f"${key}": value for key, value in os.environ.items()}
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]]
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
61 scm = asdict(scm_info) if scm_info else asdict(SCMInfo())
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 )
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
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}
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.
96 Args:
97 d: The dictionary to search.
98 path: A string representing the path to the nested key, separated by periods.
100 Returns:
101 The value of the nested key.
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
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")
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)])}'")
117 current_element = current_element[key]
119 return current_element
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.
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.
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
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]