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
« 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
8import string
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)
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())
37 def __str__(self) -> str:
38 return "[" + self.charset + "]"
40 def __repr__(self) -> str:
41 return "[" + self.charset + "]"
43 def make_generator(self):
44 def gen_chars():
45 yield self.charset[0]
47 return gen_chars
50class OptionalEmitter:
51 def __init__(self, expr) -> None:
52 self.expr = expr
54 def make_generator(self):
55 def optional_gen():
56 yield ""
58 return optional_gen
61class DotEmitter:
62 def make_generator(self):
63 def dot_gen():
64 yield "."
66 return dot_gen
69class GroupEmitter:
70 def __init__(self, exprs) -> None:
71 self.exprs = ParseResults(exprs)
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
83 if self.exprs:
84 yield from recurse_list(self.exprs)
86 return group_gen
89class AlternativeEmitter:
90 def __init__(self, exprs) -> None:
91 self.exprs = exprs
93 def make_generator(self):
94 def alt_gen():
95 for e in self.exprs:
96 yield from e.make_generator()()
98 return alt_gen
101class LiteralEmitter:
102 def __init__(self, lit) -> None:
103 self.lit = lit
105 def __str__(self) -> str:
106 return "Lit:" + self.lit
108 def __repr__(self) -> str:
109 return "Lit:" + self.lit
111 def make_generator(self):
112 def lit_gen():
113 yield self.lit
115 return lit_gen
118def handle_range(toks):
119 return CharacterRangeEmitter(srange(toks[0]))
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}")
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)
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})")
169def handle_boundary(toks):
170 return LiteralEmitter("")
173def handle_sequence(toks):
174 return GroupEmitter(toks[0])
177def handle_dot():
178 return CharacterRangeEmitter(printables)
181def handle_alternative(toks):
182 return AlternativeEmitter(toks[0])
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, "[]{}():?$^")
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 )
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 )
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)
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
239RE_PARSER = get_parser()
242def invert_re(regex):
243 """
244 Return a list of examples of minimal strings that match the expression.
246 This is a single purpose generator to optimize database queries in Weblate.
247 """
248 from weblate.utils.errors import report_error
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()