Coverage for /Users/OORDCOR/Documents/code/bump-my-version/bumpversion/versioning/serialization.py: 81%

55 statements  

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

1"""Functions for serializing and deserializing version objects.""" 

2 

3import re 

4from copy import copy 

5from operator import itemgetter 

6from typing import Dict, List, MutableMapping 

7 

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 

12 

13logger = get_indented_logger(__name__) 

14 

15 

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. 

19 

20 Args: 

21 version_string: Version string to parse 

22 parse_pattern: The regular expression pattern to use for parsing 

23 

24 Returns: 

25 A dictionary of version part labels and their values, or an empty dictionary 

26 if the version string doesn't match. 

27 

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 {} 

37 

38 logger.debug("Parsing version '%s' using regexp '%s'", version_string, parse_pattern) 

39 logger.indent() 

40 

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 

45 

46 match = re.search(pattern, version_string) 

47 

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 {} 

55 

56 parsed = match.groupdict(default="") 

57 logger.debug("Parsed the following values: %s", key_val_string(parsed)) 

58 logger.dedent() 

59 

60 return parsed 

61 

62 

63def multisort(xs: list, specs: tuple) -> list: 

64 """ 

65 Sort a list of dictionaries by multiple keys. 

66 

67 From https://docs.python.org/3/howto/sorting.html#sort-stability-and-complex-sorts 

68 

69 Args: 

70 xs: The list of dictionaries to sort 

71 specs: A tuple of (key, reverse) pairs 

72 

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 

79 

80 

81def serialize(version: Version, serialize_patterns: List[str], context: MutableMapping) -> str: 

82 """ 

83 Attempts to serialize a version with the given serialization format. 

84 

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 

90 

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 

95 

96 Raises: 

97 FormattingError: if a serialization pattern 

98 

99 Returns: 

100 The serialized version as a string 

101 """ 

102 logger.debug("Serializing version '%s'", version) 

103 logger.indent() 

104 

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()) 

109 

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 ) 

123 

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 ) 

128 

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}") 

131 

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() 

137 

138 return serialized