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

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

2

3 import re

4 from copy import copy

5 from operator import itemgetter

6 from typing import Dict, List, MutableMapping

7

8 from bumpversion.exceptions import BumpVersionError, FormattingError

9 from bumpversion.ui import get_indented_logger

10 from bumpversion.utils import key_val_string, labels_for_format

11 from bumpversion.versioning.models import Version

12

13 logger = get_indented_logger(__name__)

14

15

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

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

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