Coverage for src/backend/InvenTree/InvenTree/format.py: 84%

49 statements  

« prev     ^ index     » next       coverage.py v7.15.2, created at 2026-10-07 17:47 +0000

1"""Custom string formatting functions and helpers.""" 

2 

3import re 

4import string 

5 

6from django.utils.translation import gettext_lazy as _ 

7 

8 

9def parse_format_string(fmt_string: str) -> dict: 

10 """Extract formatting information from the provided format string. 

11 

12 Returns a dict object which contains structured information about the format groups 

13 """ 

14 groups = string.Formatter().parse(fmt_string) 

15 

16 info = {} 

17 

18 seen_groups = set() 

19 

20 for group in groups: 

21 # Skip any group which does not have a named value 

22 if not group[1]: 22 ↛ 23line 22 didn't jump to line 23 because the condition on line 22 was never true

23 continue 

24 

25 name = group[1] 

26 

27 # Check for duplicate named groups 

28 if name in seen_groups: 28 ↛ 29line 28 didn't jump to line 29 because the condition on line 28 was never true

29 raise ValueError(f"Duplicate group '{name}'") 

30 else: 

31 seen_groups.add(name) 

32 

33 info[group[1]] = {'format': group[1], 'prefix': group[0]} 

34 

35 return info 

36 

37 

38def construct_format_regex(fmt_string: str) -> str: 

39 r"""Construct a regular expression based on a provided format string. 

40 

41 This function turns a python format string into a regular expression, 

42 which can be used for two purposes: 

43 

44 - Ensure that a particular string matches the specified format 

45 - Extract named variables from a matching string 

46 

47 This function also provides support for wildcard characters: 

48 

49 - '?' provides single character matching; is converted to a '.' (period) for regex 

50 - '#' provides single digit matching; is converted to '\d' 

51 

52 Args: 

53 fmt_string: A typical format string e.g. "PO-???-{ref:04d}" 

54 

55 Returns: 

56 str: A regular expression pattern e.g. ^PO\-...\-(?P<ref>.*)$ 

57 

58 Raises: 

59 ValueError: Format string is invalid 

60 """ 

61 pattern = '^' 

62 

63 for group in string.Formatter().parse(fmt_string): 

64 prefix = group[0] # Prefix (literal text appearing before this group) 

65 name = group[1] # Name of this format variable 

66 _fmt = group[2] # Format specifier e.g :04d 

67 

68 rep = [ 

69 '+', 

70 '-', 

71 '.', 

72 '{', 

73 '}', 

74 '(', 

75 ')', 

76 '^', 

77 '$', 

78 '~', 

79 '!', 

80 '@', 

81 ':', 

82 ';', 

83 '|', 

84 "'", 

85 '"', 

86 ] 

87 

88 # Escape any special regex characters 

89 for ch in rep: 

90 prefix = prefix.replace(ch, '\\' + ch) 

91 

92 # Replace ? with single-character match 

93 prefix = prefix.replace('?', '.') 

94 

95 # Replace # with single-digit match 

96 prefix = prefix.replace('#', r'\d') 

97 

98 pattern += prefix 

99 

100 # Add a named capture group for the format entry 

101 if name: 101 ↛ 63line 101 didn't jump to line 63 because the condition on line 101 was always true

102 # Check if integer values are required 

103 c = '\\d' if _fmt and _fmt.endswith('d') else '.' 

104 

105 # Specify width 

106 # TODO: Introspect required width 

107 w = '+' 

108 

109 # replace invalid regex group name '?' with a valid name 

110 if name == '?': 110 ↛ 111line 110 didn't jump to line 111 because the condition on line 110 was never true

111 name = 'wild' 

112 

113 pattern += f'(?P<{name}>{c}{w})' 

114 

115 pattern += '$' 

116 

117 return pattern 

118 

119 

120def validate_string(value: str, fmt_string: str) -> bool: 

121 """Validate that the provided string matches the specified format. 

122 

123 Args: 

124 value: The string to be tested e.g. 'SO-1234-ABC', 

125 fmt_string: The required format e.g. 'SO-{ref}-???', 

126 

127 Returns: 

128 bool: True if the value matches the required format, else False 

129 

130 Raises: 

131 ValueError: The provided format string is invalid 

132 """ 

133 pattern = construct_format_regex(fmt_string) 

134 

135 result = re.match(pattern, value) 

136 

137 return result is not None 

138 

139 

140def extract_named_group(name: str, value: str, fmt_string: str) -> str: 

141 """Extract a named value from the provided string, given the provided format string. 

142 

143 Args: 

144 name: Name of group to extract e.g. 'ref' 

145 value: Raw string e.g. 'PO-ABC-1234' 

146 fmt_string: Format pattern e.g. 'PO-???-{ref} 

147 

148 Returns: 

149 str: String value of the named group 

150 

151 Raises: 

152 ValueError: format string is incorrectly specified, or provided value does not match format string 

153 NameError: named value does not exist in the format string 

154 IndexError: named value could not be found in the provided entry 

155 """ 

156 info = parse_format_string(fmt_string) 

157 

158 if name not in info: 158 ↛ 159line 158 didn't jump to line 159 because the condition on line 158 was never true

159 raise NameError(_(f"Value '{name}' does not appear in pattern format")) 

160 

161 # Construct a regular expression for matching against the provided format string 

162 # Note: This will raise a ValueError if 'fmt_string' is incorrectly specified 

163 pattern = construct_format_regex(fmt_string) 

164 

165 # Run the regex matcher against the raw string 

166 result = re.match(pattern, value) 

167 

168 if not result: 168 ↛ 169line 168 didn't jump to line 169 because the condition on line 168 was never true

169 raise ValueError( 

170 _('Provided value does not match required pattern: ') + fmt_string 

171 ) 

172 

173 # And return the value we are interested in 

174 # Note: This will raise an IndexError if the named group was not matched 

175 return result.group(name)