Coverage for app/venv/lib/python3.14/site-packages/weblate/utils/inv_regex.py: 32%

138 statements  

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

1# Copyright © 2008, Paul McGuire 

2# Copyright © Michal Čihař <michal@weblate.org> 

3# 

4# SPDX-License-Identifier: MIT 

5# 

6# Based on https://github.com/pyparsing/pyparsing/blob/master/examples/inv_regex.py 

7 

8import string 

9 

10from pyparsing import ( 

11 Combine, 

12 Empty, 

13 Literal, 

14 OpAssoc, 

15 ParseException, 

16 ParseFatalException, 

17 ParserElement, 

18 ParseResults, 

19 Regex, 

20 SkipTo, 

21 Suppress, 

22 Word, 

23 infix_notation, 

24 nums, 

25 one_of, 

26 printables, 

27 srange, 

28) 

29 

30 

31class CharacterRangeEmitter: 

32 def __init__(self, chars) -> None: 

33 # remove duplicate chars in character range, but preserve original order, 

34 # this is based on dict being ordered 

35 self.charset = "".join(dict.fromkeys(chars).keys()) 

36 

37 def __str__(self) -> str: 

38 return "[" + self.charset + "]" 

39 

40 def __repr__(self) -> str: 

41 return "[" + self.charset + "]" 

42 

43 def make_generator(self): 

44 def gen_chars(): 

45 yield self.charset[0] 

46 

47 return gen_chars 

48 

49 

50class OptionalEmitter: 

51 def __init__(self, expr) -> None: 

52 self.expr = expr 

53 

54 def make_generator(self): 

55 def optional_gen(): 

56 yield "" 

57 

58 return optional_gen 

59 

60 

61class DotEmitter: 

62 def make_generator(self): 

63 def dot_gen(): 

64 yield "." 

65 

66 return dot_gen 

67 

68 

69class GroupEmitter: 

70 def __init__(self, exprs) -> None: 

71 self.exprs = ParseResults(exprs) 

72 

73 def make_generator(self): 

74 def group_gen(): 

75 def recurse_list(elist): 

76 if len(elist) == 1: 

77 yield from elist[0].make_generator()() 

78 else: 

79 for s in elist[0].make_generator()(): 

80 for s2 in recurse_list(elist[1:]): 

81 yield s + s2 

82 

83 if self.exprs: 

84 yield from recurse_list(self.exprs) 

85 

86 return group_gen 

87 

88 

89class AlternativeEmitter: 

90 def __init__(self, exprs) -> None: 

91 self.exprs = exprs 

92 

93 def make_generator(self): 

94 def alt_gen(): 

95 for e in self.exprs: 

96 yield from e.make_generator()() 

97 

98 return alt_gen 

99 

100 

101class LiteralEmitter: 

102 def __init__(self, lit) -> None: 

103 self.lit = lit 

104 

105 def __str__(self) -> str: 

106 return "Lit:" + self.lit 

107 

108 def __repr__(self) -> str: 

109 return "Lit:" + self.lit 

110 

111 def make_generator(self): 

112 def lit_gen(): 

113 yield self.lit 

114 

115 return lit_gen 

116 

117 

118def handle_range(toks): 

119 return CharacterRangeEmitter(srange(toks[0])) 

120 

121 

122def handle_repetition(toks): 

123 toks = toks[0] 

124 if toks[1] == "+": 

125 return GroupEmitter([toks[0]]) 

126 if toks[1] in "*?": 

127 return OptionalEmitter(toks[0]) 

128 if "count" in toks: 

129 return GroupEmitter([toks[0]] * int(toks.count)) 

130 if "minCount" in toks: 

131 mincount = int(toks.minCount) 

132 maxcount = int(toks.maxCount) 

133 optcount = maxcount - mincount 

134 if optcount: 

135 opt = OptionalEmitter(toks[0]) 

136 for _i in range(1, optcount): 

137 opt = OptionalEmitter(GroupEmitter([toks[0], opt])) 

138 return GroupEmitter([toks[0]] * mincount + [opt]) 

139 return [toks[0]] * mincount 

140 msg = "" 

141 raise ParseFatalException(msg, 0, f"Unsupported repetition {toks!r}") 

142 

143 

144def handle_literal(toks): 

145 lit = "" 

146 for t in toks: 

147 if t[0] == "\\": 

148 if t[1] == "t": 

149 lit += "\t" 

150 else: 

151 lit += t[1] 

152 else: 

153 lit += t 

154 return LiteralEmitter(lit) 

155 

156 

157def handle_macro(toks): 

158 macro_char = toks[0][1] 

159 if macro_char == "d": 

160 return CharacterRangeEmitter(string.digits) 

161 if macro_char == "w": 

162 return CharacterRangeEmitter(srange("[A-Za-z0-9_]")) 

163 if macro_char in {"s", "W"}: 

164 return LiteralEmitter(" ") 

165 msg = "" 

166 raise ParseFatalException(msg, 0, f"unsupported macro character ({macro_char})") 

167 

168 

169def handle_boundary(toks): 

170 return LiteralEmitter("") 

171 

172 

173def handle_sequence(toks): 

174 return GroupEmitter(toks[0]) 

175 

176 

177def handle_dot(): 

178 return CharacterRangeEmitter(printables) 

179 

180 

181def handle_alternative(toks): 

182 return AlternativeEmitter(toks[0]) 

183 

184 

185def get_parser(): 

186 orig_whitespace = ParserElement.DEFAULT_WHITE_CHARS 

187 ParserElement.set_default_whitespace_chars("") 

188 ( 

189 lbrack, 

190 rbrack, 

191 lbrace, 

192 rbrace, 

193 _lparen, 

194 _rparen, 

195 _colon, 

196 _qmark, 

197 dollar, 

198 cflex, 

199 ) = map(Literal, "[]{}():?$^") 

200 

201 re_macro = Combine("\\" + one_of(list("dwsW"))) 

202 escaped_char = ~re_macro + Combine("\\" + one_of(list(printables))) 

203 re_literal_char = ( 

204 "".join(c for c in printables if c not in r"\[]{}().*?+|$^") + " \t" 

205 ) 

206 

207 re_range = Combine(lbrack + SkipTo(rbrack, ignore=escaped_char) + rbrack) 

208 re_literal = escaped_char | one_of(list(re_literal_char)) 

209 re_non_capture_group = Suppress(Regex(r"\?[aiLmsux:-]")) 

210 re_dot = Literal(".") 

211 re_boundary = cflex | dollar 

212 repetition = ( 

213 (lbrace + Word(nums)("count") + rbrace) 

214 | (lbrace + Word(nums)("minCount") + "," + Word(nums)("maxCount") + rbrace) 

215 | one_of(list("*+?")) 

216 ) 

217 

218 re_range.setParseAction(handle_range) 

219 re_literal.setParseAction(handle_literal) 

220 re_macro.setParseAction(handle_macro) 

221 re_dot.setParseAction(handle_dot) 

222 re_boundary.setParseAction(handle_boundary) 

223 

224 re_term = ( 

225 re_boundary | re_literal | re_range | re_macro | re_dot | re_non_capture_group 

226 ) 

227 re_expr = infix_notation( 

228 re_term, 

229 [ 

230 (repetition, 1, OpAssoc.LEFT, handle_repetition), 

231 (Empty(), 2, OpAssoc.LEFT, handle_sequence), 

232 (Suppress("|"), 2, OpAssoc.LEFT, handle_alternative), 

233 ], 

234 ) 

235 ParserElement.set_default_whitespace_chars(orig_whitespace) 

236 return re_expr 

237 

238 

239RE_PARSER = get_parser() 

240 

241 

242def invert_re(regex): 

243 """ 

244 Return a list of examples of minimal strings that match the expression. 

245 

246 This is a single purpose generator to optimize database queries in Weblate. 

247 """ 

248 from weblate.utils.errors import report_error 

249 

250 try: 

251 invre = GroupEmitter(RE_PARSER.parse_string(regex)).make_generator() 

252 except ParseException: 

253 report_error("Regexp parser") 

254 return [] 

255 return invre()