Coverage for /Users/coordt/Documents/code/bump-my-version/bumpversion/versioning/serialization.py: 81%
55 statements
« prev ^ index » next coverage.py v7.4.4, created at 2024-06-11 14:29 -0500
« prev ^ index » next coverage.py v7.4.4, created at 2024-06-11 14:29 -0500
1"""Functions for serializing and deserializing version objects."""
3import re
4from copy import copy
5from operator import itemgetter
6from typing import Dict, List, MutableMapping
8from bumpversion.exceptions import BumpVersionError, FormattingError
9from bumpversion.ui import get_indented_logger
10from bumpversion.utils import key_val_string, labels_for_format
11from bumpversion.versioning.models import Version
13logger = get_indented_logger(__name__)
16def parse_version(version_string: str, parse_pattern: str) -> Dict[str, str]:
17 """
18 Parse a version string into a dictionary of the parts and values using a regular expression.
20 Args:
21 version_string: Version string to parse
22 parse_pattern: The regular expression pattern to use for parsing
24 Returns:
25 A dictionary of version part labels and their values, or an empty dictionary
26 if the version string doesn't match.
28 Raises:
29 BumpVersionError: If the parse_pattern is not a valid regular expression
30 """
31 if not version_string: 31 ↛ 32line 31 didn't jump to line 32, because the condition on line 31 was never true
32 logger.debug("Version string is empty, returning empty dict")
33 return {}
34 elif not parse_pattern: 34 ↛ 35line 34 didn't jump to line 35, because the condition on line 34 was never true
35 logger.debug("Parse pattern is empty, returning empty dict")
36 return {}
38 logger.debug("Parsing version '%s' using regexp '%s'", version_string, parse_pattern)
39 logger.indent()
41 try:
42 pattern = re.compile(parse_pattern, re.VERBOSE)
43 except re.error as e:
44 raise BumpVersionError(f"'{parse_pattern}' is not a valid regular expression.") from e
46 match = re.search(pattern, version_string)
48 if not match: 48 ↛ 49line 48 didn't jump to line 49, because the condition on line 48 was never true
49 logger.debug(
50 "'%s' does not parse current version '%s'",
51 parse_pattern,
52 version_string,
53 )
54 return {}
56 parsed = match.groupdict(default="")
57 logger.debug("Parsed the following values: %s", key_val_string(parsed))
58 logger.dedent()
60 return parsed
63def multisort(xs: list, specs: tuple) -> list:
64 """
65 Sort a list of dictionaries by multiple keys.
67 From https://docs.python.org/3/howto/sorting.html#sort-stability-and-complex-sorts
69 Args:
70 xs: The list of dictionaries to sort
71 specs: A tuple of (key, reverse) pairs
73 Returns:
74 The sorted list
75 """
76 for key, reverse in reversed(specs):
77 xs.sort(key=itemgetter(key), reverse=reverse)
78 return xs
81def serialize(version: Version, serialize_patterns: List[str], context: MutableMapping) -> str:
82 """
83 Attempts to serialize a version with the given serialization format.
85 - valid serialization patterns are those that are renderable with the given context
86 - formats that contain all required components are preferred
87 - the shortest valid serialization pattern is used
88 - if two patterns are equally short, the first one is used
89 - if no valid serialization pattern is found, an error is raised
91 Args:
92 version: The version to serialize
93 serialize_patterns: The serialization format to use, using Python's format string syntax
94 context: The context to use when serializing the version
96 Raises:
97 FormattingError: if a serialization pattern
99 Returns:
100 The serialized version as a string
101 """
102 logger.debug("Serializing version '%s'", version)
103 logger.indent()
105 local_context = copy(context)
106 local_context.update(version.values())
107 local_context_keys = set(local_context.keys())
108 required_component_labels = set(version.required_components())
110 patterns = []
111 for index, pattern in enumerate(serialize_patterns):
112 labels = set(labels_for_format(pattern))
113 patterns.append(
114 {
115 "pattern": pattern,
116 "labels": labels,
117 "order": index,
118 "num_labels": len(labels),
119 "renderable": local_context_keys >= labels,
120 "has_required_components": required_component_labels <= labels,
121 }
122 )
124 valid_patterns = filter(itemgetter("renderable"), patterns)
125 sorted_patterns = multisort(
126 list(valid_patterns), (("has_required_components", True), ("num_labels", False), ("order", False))
127 )
129 if not sorted_patterns: 129 ↛ 130line 129 didn't jump to line 130, because the condition on line 129 was never true
130 raise FormattingError(f"Could not find a valid serialization format in {serialize_patterns!r} for {version!r}")
132 chosen_pattern = sorted_patterns[0]["pattern"]
133 logger.debug("Using serialization format '%s'", chosen_pattern)
134 serialized = chosen_pattern.format(**local_context)
135 logger.debug("Serialized to '%s'", serialized)
136 logger.dedent()
138 return serialized