backrefs 8.1__py315-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- backrefs/__init__.py +2 -0
- backrefs/__meta__.py +197 -0
- backrefs/_bre_parse.py +1494 -0
- backrefs/_bregex_parse.py +1332 -0
- backrefs/_bregex_typing.py +6 -0
- backrefs/bre.py +656 -0
- backrefs/bregex.py +730 -0
- backrefs/py.typed +0 -0
- backrefs/uniprops/__init__.py +665 -0
- backrefs/uniprops/unidata/__init__.py +1 -0
- backrefs/uniprops/unidata/age.py +123 -0
- backrefs/uniprops/unidata/alias.py +796 -0
- backrefs/uniprops/unidata/bidiclass.py +99 -0
- backrefs/uniprops/unidata/bidipairedbrackettype.py +19 -0
- backrefs/uniprops/unidata/binary.py +331 -0
- backrefs/uniprops/unidata/block.py +1395 -0
- backrefs/uniprops/unidata/canonicalcombiningclass.py +1031 -0
- backrefs/uniprops/unidata/decompositiontype.py +79 -0
- backrefs/uniprops/unidata/eastasianwidth.py +31 -0
- backrefs/uniprops/unidata/generalcategory.py +173 -0
- backrefs/uniprops/unidata/graphemeclusterbreak.py +79 -0
- backrefs/uniprops/unidata/hangulsyllabletype.py +31 -0
- backrefs/uniprops/unidata/indicpositionalcategory.py +71 -0
- backrefs/uniprops/unidata/indicsyllabiccategory.py +155 -0
- backrefs/uniprops/unidata/joininggroup.py +431 -0
- backrefs/uniprops/unidata/joiningtype.py +31 -0
- backrefs/uniprops/unidata/linebreak.py +203 -0
- backrefs/uniprops/unidata/numerictype.py +23 -0
- backrefs/uniprops/unidata/numericvalue.py +583 -0
- backrefs/uniprops/unidata/quickcheck.py +59 -0
- backrefs/uniprops/unidata/script.py +711 -0
- backrefs/uniprops/unidata/scriptextensions.py +711 -0
- backrefs/uniprops/unidata/sentencebreak.py +67 -0
- backrefs/uniprops/unidata/verticalorientation.py +23 -0
- backrefs/uniprops/unidata/wordbreak.py +99 -0
- backrefs/util.py +167 -0
- backrefs-8.1.dist-info/METADATA +70 -0
- backrefs-8.1.dist-info/RECORD +40 -0
- backrefs-8.1.dist-info/WHEEL +4 -0
- backrefs-8.1.dist-info/licenses/LICENSE.md +21 -0
backrefs/_bre_parse.py
ADDED
|
@@ -0,0 +1,1494 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Backrefs Re parser.
|
|
3
|
+
|
|
4
|
+
Licensed under MIT
|
|
5
|
+
Copyright (c) 2011 - 2020 Isaac Muse <isaacmuse@gmail.com>
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
import re as _re
|
|
9
|
+
import sys
|
|
10
|
+
import copyreg as _copyreg
|
|
11
|
+
from . import util as _util
|
|
12
|
+
from .util import PatternError
|
|
13
|
+
import unicodedata as _unicodedata
|
|
14
|
+
from . import uniprops as _uniprops
|
|
15
|
+
from typing import Generic, AnyStr, Match, Any, Pattern, cast
|
|
16
|
+
|
|
17
|
+
if sys.version_info >= (3, 11):
|
|
18
|
+
import re._parser as _parser # type: ignore[import]
|
|
19
|
+
else:
|
|
20
|
+
import sre_parse as _parser
|
|
21
|
+
|
|
22
|
+
__all__ = ("ReplaceTemplate", "PatternError")
|
|
23
|
+
|
|
24
|
+
_ASCII_LETTERS = frozenset(
|
|
25
|
+
(
|
|
26
|
+
'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm',
|
|
27
|
+
'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z',
|
|
28
|
+
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M',
|
|
29
|
+
'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z'
|
|
30
|
+
)
|
|
31
|
+
)
|
|
32
|
+
_DIGIT = frozenset(('0', '1', '2', '3', '4', '5', '6', '7', '8', '9'))
|
|
33
|
+
_OCTAL = frozenset(('0', '1', '2', '3', '4', '5', '6', '7'))
|
|
34
|
+
_HEX = frozenset(('a', 'b', 'c', 'd', 'e', 'f', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9'))
|
|
35
|
+
_LETTERS_UNDERSCORE = _ASCII_LETTERS | frozenset(('_',))
|
|
36
|
+
_WORD = _LETTERS_UNDERSCORE | _DIGIT
|
|
37
|
+
_STANDARD_ESCAPES = frozenset(('a', 'b', 'f', 'n', 'r', 't', 'v'))
|
|
38
|
+
_CURLY_BRACKETS = frozenset(('{', '}'))
|
|
39
|
+
_PROPERTY_STRIP = frozenset((' ', '-', '_'))
|
|
40
|
+
_PROPERTY = _WORD | _DIGIT | _PROPERTY_STRIP
|
|
41
|
+
_SCOPED_FLAGS = frozenset(('a', 'i', 'L', 'm', 'u', 's', 'x'))
|
|
42
|
+
_SCOPED_INV_FLAGS = frozenset(('i', 'm', 's', 'x'))
|
|
43
|
+
_SCOPED_END = frozenset((':', ')'))
|
|
44
|
+
|
|
45
|
+
_CURLY_BRACKETS_ORD = frozenset((0x7b, 0x7d))
|
|
46
|
+
|
|
47
|
+
_COMPATIBILITY_PROPERTIES = frozenset(
|
|
48
|
+
(
|
|
49
|
+
'alpha', 'lower', 'upper', 'punct', 'digit', 'xdigit', 'alnum',
|
|
50
|
+
'space', 'blank', 'cntrl', 'graph', 'print', 'word'
|
|
51
|
+
)
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
# Case upper or lower
|
|
55
|
+
_UPPER = 1
|
|
56
|
+
_LOWER = 2
|
|
57
|
+
|
|
58
|
+
# Format Constants
|
|
59
|
+
_BACK_SLASH_TRANSLATION = {
|
|
60
|
+
"\\a": '\a',
|
|
61
|
+
"\\b": '\b',
|
|
62
|
+
"\\f": '\f',
|
|
63
|
+
"\\r": '\r',
|
|
64
|
+
"\\t": '\t',
|
|
65
|
+
"\\n": '\n',
|
|
66
|
+
"\\v": '\v',
|
|
67
|
+
"\\\\": '\\'
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
_FMT_CONV_TYPE = ('a', 'r', 's')
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class _SearchParser(Generic[AnyStr]):
|
|
74
|
+
"""Search Template."""
|
|
75
|
+
|
|
76
|
+
_new_refs = ("c", "C", "e", "E", "h", "l", "L", "m", "M", "N", "p", "P", "Q", "R", "X")
|
|
77
|
+
_re_start_wb = r"\b(?=\w)"
|
|
78
|
+
_re_end_wb = r"\b(?<=\w)"
|
|
79
|
+
_line_break = r'(?:\r\n|(?!\r\n)[\n\v\f\r\x85\u2028\u2029])'
|
|
80
|
+
_bytes_line_break = r'(?>\r\n|[\n\v\f\r\x85])' if _util.PY311 else r'(?:\r\n|(?!\r\n)[\n\v\f\r\x85])'
|
|
81
|
+
_grapheme_cluster = r'(?:{}{}*(?!{}))'
|
|
82
|
+
|
|
83
|
+
verbose: bool
|
|
84
|
+
unicode: bool
|
|
85
|
+
ascii: bool # noqa: A003
|
|
86
|
+
is_bytes: bool
|
|
87
|
+
search: AnyStr
|
|
88
|
+
|
|
89
|
+
def __init__(self, search: AnyStr, re_verbose: bool = False, re_unicode: bool | None = None) -> None:
|
|
90
|
+
"""Initialize."""
|
|
91
|
+
|
|
92
|
+
if isinstance(search, bytes):
|
|
93
|
+
self.is_bytes = True
|
|
94
|
+
else:
|
|
95
|
+
self.is_bytes = False
|
|
96
|
+
|
|
97
|
+
if self.is_bytes:
|
|
98
|
+
self._re_line_break = self._bytes_line_break
|
|
99
|
+
else:
|
|
100
|
+
self._re_line_break = self._line_break
|
|
101
|
+
self.search = search
|
|
102
|
+
self.re_verbose = re_verbose
|
|
103
|
+
self.re_unicode = re_unicode
|
|
104
|
+
|
|
105
|
+
def process_quotes(self, text: str) -> str:
|
|
106
|
+
"""Process quotes."""
|
|
107
|
+
|
|
108
|
+
escaped = False
|
|
109
|
+
in_quotes = False
|
|
110
|
+
current = []
|
|
111
|
+
quoted = [] # type: list[str]
|
|
112
|
+
i = _util.StringIter(text)
|
|
113
|
+
|
|
114
|
+
for t in i:
|
|
115
|
+
if not escaped and t == "\\":
|
|
116
|
+
escaped = True
|
|
117
|
+
elif escaped:
|
|
118
|
+
escaped = False
|
|
119
|
+
if t == "E":
|
|
120
|
+
if in_quotes:
|
|
121
|
+
current.append(_re.escape("".join(quoted)))
|
|
122
|
+
quoted = []
|
|
123
|
+
in_quotes = False
|
|
124
|
+
elif t == "Q" and not in_quotes:
|
|
125
|
+
in_quotes = True
|
|
126
|
+
elif in_quotes:
|
|
127
|
+
quoted.extend(["\\", t])
|
|
128
|
+
else:
|
|
129
|
+
current.extend(["\\", t])
|
|
130
|
+
elif in_quotes:
|
|
131
|
+
quoted.extend(t)
|
|
132
|
+
else:
|
|
133
|
+
current.append(t)
|
|
134
|
+
|
|
135
|
+
if in_quotes and escaped:
|
|
136
|
+
quoted.append("\\")
|
|
137
|
+
elif escaped:
|
|
138
|
+
current.append("\\")
|
|
139
|
+
|
|
140
|
+
if quoted:
|
|
141
|
+
current.append(_re.escape("".join(quoted)))
|
|
142
|
+
|
|
143
|
+
return "".join(current)
|
|
144
|
+
|
|
145
|
+
def verbose_comment(self, t: str, i: _util.StringIter) -> list[str]:
|
|
146
|
+
"""Handle verbose comments."""
|
|
147
|
+
|
|
148
|
+
current = []
|
|
149
|
+
escaped = False
|
|
150
|
+
|
|
151
|
+
try:
|
|
152
|
+
while t != "\n":
|
|
153
|
+
if not escaped and t == "\\":
|
|
154
|
+
escaped = True
|
|
155
|
+
current.append(t)
|
|
156
|
+
elif escaped:
|
|
157
|
+
escaped = False
|
|
158
|
+
if t in self._new_refs:
|
|
159
|
+
current.append("\\")
|
|
160
|
+
current.append(t)
|
|
161
|
+
else:
|
|
162
|
+
current.append(t)
|
|
163
|
+
t = next(i)
|
|
164
|
+
except StopIteration:
|
|
165
|
+
pass
|
|
166
|
+
|
|
167
|
+
if t == "\n":
|
|
168
|
+
current.append(t)
|
|
169
|
+
return current
|
|
170
|
+
|
|
171
|
+
def get_unicode_property(self, i: _util.StringIter, brackets: bool = False) -> tuple[str, str]:
|
|
172
|
+
"""Get Unicode property."""
|
|
173
|
+
|
|
174
|
+
index = i.index
|
|
175
|
+
prop = []
|
|
176
|
+
value = []
|
|
177
|
+
try:
|
|
178
|
+
c = next(i)
|
|
179
|
+
if c.upper() in _ASCII_LETTERS:
|
|
180
|
+
prop.append(c)
|
|
181
|
+
elif (not brackets and c != '{') or (brackets and c != ':'):
|
|
182
|
+
raise PatternError(f"Unicode property missing '{{' at {i.index - 1}!")
|
|
183
|
+
else:
|
|
184
|
+
c = next(i)
|
|
185
|
+
if c == '^':
|
|
186
|
+
prop.append(c)
|
|
187
|
+
c = next(i)
|
|
188
|
+
|
|
189
|
+
while c not in (':', '=', '}'):
|
|
190
|
+
if c not in _PROPERTY:
|
|
191
|
+
raise PatternError(f'Invalid Unicode property character at {i.index - 1}!')
|
|
192
|
+
if c not in _PROPERTY_STRIP:
|
|
193
|
+
prop.append(c)
|
|
194
|
+
c = next(i)
|
|
195
|
+
|
|
196
|
+
if c in (':', '='):
|
|
197
|
+
skip = False
|
|
198
|
+
if brackets:
|
|
199
|
+
is_colon = c == ':'
|
|
200
|
+
c = next(i)
|
|
201
|
+
if is_colon and c == ']':
|
|
202
|
+
# That's the end of the property
|
|
203
|
+
skip = True
|
|
204
|
+
end = ':'
|
|
205
|
+
else:
|
|
206
|
+
c = next(i)
|
|
207
|
+
end = '}'
|
|
208
|
+
|
|
209
|
+
# Get the property value
|
|
210
|
+
if not skip:
|
|
211
|
+
while c != end:
|
|
212
|
+
if c not in _PROPERTY:
|
|
213
|
+
raise PatternError(f'Invalid Unicode property character at {i.index - 1}!')
|
|
214
|
+
if c not in _PROPERTY_STRIP:
|
|
215
|
+
value.append(c)
|
|
216
|
+
c = next(i)
|
|
217
|
+
if brackets and c == ':':
|
|
218
|
+
c = next(i)
|
|
219
|
+
if c != ']':
|
|
220
|
+
raise PatternError(f'Invalid Unicode property character at {i.index - 1}!')
|
|
221
|
+
if not value:
|
|
222
|
+
raise PatternError('Invalid Unicode property!')
|
|
223
|
+
|
|
224
|
+
except StopIteration as e:
|
|
225
|
+
if brackets:
|
|
226
|
+
raise PatternError(f"Missing or unmatched ':]' at {index}!") from e
|
|
227
|
+
else:
|
|
228
|
+
raise PatternError(f"Missing or unmatched '{{' at {index}!") from e
|
|
229
|
+
|
|
230
|
+
p = ''.join(prop).lower()
|
|
231
|
+
v = ''.join(value).lower()
|
|
232
|
+
|
|
233
|
+
# Ensure when using POSIX form, that any property considered a compatibility property uses the POSIX form.
|
|
234
|
+
# POSIX form is not guaranteed to be different from standard form and sometimes is just an alias for standard.
|
|
235
|
+
if brackets and p in _COMPATIBILITY_PROPERTIES:
|
|
236
|
+
p = 'posix' + p
|
|
237
|
+
|
|
238
|
+
return p, v
|
|
239
|
+
|
|
240
|
+
def get_named_unicode(self, i: _util.StringIter) -> str:
|
|
241
|
+
"""Get Unicode name."""
|
|
242
|
+
|
|
243
|
+
index = i.index
|
|
244
|
+
value = []
|
|
245
|
+
try:
|
|
246
|
+
if next(i) != '{':
|
|
247
|
+
raise PatternError(f"Named Unicode missing '{{' {i.index - 1}!")
|
|
248
|
+
c = next(i)
|
|
249
|
+
while c != '}':
|
|
250
|
+
value.append(c)
|
|
251
|
+
c = next(i)
|
|
252
|
+
except Exception as e:
|
|
253
|
+
raise PatternError(f"Unmatched '{{' at {index}!") from e
|
|
254
|
+
|
|
255
|
+
return ''.join(value)
|
|
256
|
+
|
|
257
|
+
def reference(self, t: str, i: _util.StringIter, in_group: bool = False) -> list[str]:
|
|
258
|
+
"""Handle references."""
|
|
259
|
+
|
|
260
|
+
current = []
|
|
261
|
+
|
|
262
|
+
if not in_group and t == "m":
|
|
263
|
+
current.append(self._re_start_wb)
|
|
264
|
+
elif not in_group and t == "M":
|
|
265
|
+
current.append(self._re_end_wb)
|
|
266
|
+
elif not in_group and t == "R":
|
|
267
|
+
current.append(self._re_line_break)
|
|
268
|
+
elif not in_group and t == "X":
|
|
269
|
+
no_mark = self.unicode_props("^m", None, in_group=False)[0]
|
|
270
|
+
mark = self.unicode_props("m", None, in_group=False)[0]
|
|
271
|
+
current.extend(self._grapheme_cluster.format(no_mark, mark, mark))
|
|
272
|
+
elif t == 'p':
|
|
273
|
+
prop = self.get_unicode_property(i)
|
|
274
|
+
current.extend(self.unicode_props(prop[0], prop[1], in_group=in_group))
|
|
275
|
+
if in_group:
|
|
276
|
+
self.found_property = True
|
|
277
|
+
elif t == 'P':
|
|
278
|
+
prop = self.get_unicode_property(i)
|
|
279
|
+
current.extend(self.unicode_props(prop[0], prop[1], in_group=in_group, negate=True))
|
|
280
|
+
if in_group:
|
|
281
|
+
self.found_property = True
|
|
282
|
+
elif t == "N":
|
|
283
|
+
text = self.get_named_unicode(i)
|
|
284
|
+
current.extend(self.unicode_name(text, in_group))
|
|
285
|
+
if in_group:
|
|
286
|
+
self.found_named_unicode = True
|
|
287
|
+
else:
|
|
288
|
+
current.extend(["\\", t])
|
|
289
|
+
return current
|
|
290
|
+
|
|
291
|
+
def get_comments(self, i: _util.StringIter) -> str | None:
|
|
292
|
+
"""Get comments."""
|
|
293
|
+
|
|
294
|
+
index = i.index
|
|
295
|
+
value = ['(']
|
|
296
|
+
escaped = False
|
|
297
|
+
try:
|
|
298
|
+
c = next(i)
|
|
299
|
+
if c != '?':
|
|
300
|
+
i.rewind(1)
|
|
301
|
+
return None
|
|
302
|
+
value.append(c)
|
|
303
|
+
c = next(i)
|
|
304
|
+
if c != '#':
|
|
305
|
+
i.rewind(2)
|
|
306
|
+
return None
|
|
307
|
+
value.append(c)
|
|
308
|
+
c = next(i)
|
|
309
|
+
while c != ')' or escaped is True:
|
|
310
|
+
if escaped:
|
|
311
|
+
escaped = False
|
|
312
|
+
elif c == '\\':
|
|
313
|
+
escaped = True
|
|
314
|
+
value.append(c)
|
|
315
|
+
c = next(i)
|
|
316
|
+
value.append(c)
|
|
317
|
+
except StopIteration as e:
|
|
318
|
+
raise PatternError(f"Unmatched '(' at {index - 1}!") from e
|
|
319
|
+
|
|
320
|
+
return ''.join(value)
|
|
321
|
+
|
|
322
|
+
def flags(self, text: str, scoped: bool = False) -> None:
|
|
323
|
+
"""Analyze flags."""
|
|
324
|
+
|
|
325
|
+
flags = text.split('-')
|
|
326
|
+
enable = flags[0]
|
|
327
|
+
disable = flags[1] if len(flags) > 1 else ''
|
|
328
|
+
|
|
329
|
+
if ('a' in enable or 'L' in enable) and self.unicode:
|
|
330
|
+
self.unicode = False
|
|
331
|
+
elif 'u' in enable and not self.unicode and not self.is_bytes:
|
|
332
|
+
self.unicode = True
|
|
333
|
+
if 'x' in disable and self.verbose:
|
|
334
|
+
self.verbose = False
|
|
335
|
+
elif 'x' in enable and not self.verbose:
|
|
336
|
+
self.verbose = True
|
|
337
|
+
|
|
338
|
+
def get_flags(self, i: _util.StringIter) -> tuple[str | None, bool]:
|
|
339
|
+
"""
|
|
340
|
+
Get flags.
|
|
341
|
+
|
|
342
|
+
In Re, flags are quite predictable when global or scoped.
|
|
343
|
+
Global can never be disabled with minus, and never have a `:` after them.
|
|
344
|
+
The global flag set is also very specific, but can be used as enablers in scoped.
|
|
345
|
+
|
|
346
|
+
The returned scoped status will indicate whether flags are generally considered
|
|
347
|
+
scoped flags or global flags.
|
|
348
|
+
"""
|
|
349
|
+
|
|
350
|
+
index = i.index
|
|
351
|
+
value = ['(']
|
|
352
|
+
smells_scoped = False
|
|
353
|
+
try:
|
|
354
|
+
c = next(i)
|
|
355
|
+
if c != '?':
|
|
356
|
+
i.rewind(1)
|
|
357
|
+
return None, False
|
|
358
|
+
value.append(c)
|
|
359
|
+
c = next(i)
|
|
360
|
+
while c not in _SCOPED_END:
|
|
361
|
+
if c == '-':
|
|
362
|
+
c2 = next(i)
|
|
363
|
+
if c2 not in _SCOPED_INV_FLAGS:
|
|
364
|
+
raise PatternError('Bad scope')
|
|
365
|
+
c += c2
|
|
366
|
+
smells_scoped = True
|
|
367
|
+
elif c not in _SCOPED_FLAGS:
|
|
368
|
+
raise PatternError("Bad flag")
|
|
369
|
+
value.append(c)
|
|
370
|
+
c = next(i)
|
|
371
|
+
if smells_scoped and c != ':':
|
|
372
|
+
raise PatternError("Bad flag")
|
|
373
|
+
elif c == ':':
|
|
374
|
+
smells_scoped = True
|
|
375
|
+
value.append(c)
|
|
376
|
+
except Exception:
|
|
377
|
+
i.rewind(i.index - index)
|
|
378
|
+
value = []
|
|
379
|
+
|
|
380
|
+
return ''.join(value) if value else None, smells_scoped
|
|
381
|
+
|
|
382
|
+
def subgroup(self, t: str, i: _util.StringIter) -> list[str]:
|
|
383
|
+
"""Handle parenthesis."""
|
|
384
|
+
|
|
385
|
+
current = [] # type: list[str]
|
|
386
|
+
|
|
387
|
+
# (?#comment)
|
|
388
|
+
comments = self.get_comments(i)
|
|
389
|
+
if comments:
|
|
390
|
+
return [comments]
|
|
391
|
+
|
|
392
|
+
verbose = self.verbose
|
|
393
|
+
unicode_flag = self.unicode
|
|
394
|
+
pos = i.index - 1
|
|
395
|
+
|
|
396
|
+
# (?flags:pattern) or (?flags)
|
|
397
|
+
flags, scoped = self.get_flags(i)
|
|
398
|
+
if flags and flags != '(?)':
|
|
399
|
+
t = flags
|
|
400
|
+
self.flags(flags[2:-1], scoped=scoped)
|
|
401
|
+
if not scoped:
|
|
402
|
+
if pos != 0:
|
|
403
|
+
raise PatternError(f'Global flags not at the start of the expression at position {pos}')
|
|
404
|
+
return [flags]
|
|
405
|
+
|
|
406
|
+
current = []
|
|
407
|
+
try:
|
|
408
|
+
while t != ')':
|
|
409
|
+
if not current:
|
|
410
|
+
current.append(t)
|
|
411
|
+
else:
|
|
412
|
+
current.extend(self.normal(t, i))
|
|
413
|
+
|
|
414
|
+
t = next(i)
|
|
415
|
+
except StopIteration as e:
|
|
416
|
+
raise PatternError(f"Missing ')', unterminated subpattern at position {pos}") from e
|
|
417
|
+
|
|
418
|
+
# Restore flags after group
|
|
419
|
+
self.verbose = verbose
|
|
420
|
+
self.unicode = unicode_flag
|
|
421
|
+
|
|
422
|
+
if t == ")":
|
|
423
|
+
current.append(t)
|
|
424
|
+
return current
|
|
425
|
+
|
|
426
|
+
def char_groups(self, t: str, i: _util.StringIter) -> list[str]:
|
|
427
|
+
"""Handle character groups."""
|
|
428
|
+
|
|
429
|
+
current = []
|
|
430
|
+
pos = i.index - 1
|
|
431
|
+
found = False
|
|
432
|
+
escaped = False
|
|
433
|
+
first = 0
|
|
434
|
+
found_property = False
|
|
435
|
+
self.found_property = False
|
|
436
|
+
self.found_named_unicode = False
|
|
437
|
+
|
|
438
|
+
try:
|
|
439
|
+
while True:
|
|
440
|
+
# Prevent POSIX/Unicode class from being part of a range.
|
|
441
|
+
if self.found_property and t == '-':
|
|
442
|
+
current.append(_re.escape(t))
|
|
443
|
+
pos += 1
|
|
444
|
+
t = next(i)
|
|
445
|
+
self.found_property = False
|
|
446
|
+
continue
|
|
447
|
+
else:
|
|
448
|
+
self.found_property = False
|
|
449
|
+
|
|
450
|
+
if not escaped and t == "\\":
|
|
451
|
+
escaped = True
|
|
452
|
+
elif escaped:
|
|
453
|
+
escaped = False
|
|
454
|
+
idx = len(current) - 1
|
|
455
|
+
current.extend(self.reference(t, i, True))
|
|
456
|
+
if self.found_property:
|
|
457
|
+
# Prevent Unicode class from being part of a range.
|
|
458
|
+
if idx >= 0 and current[idx] == '-':
|
|
459
|
+
current[idx] = _re.escape('-')
|
|
460
|
+
found_property = True
|
|
461
|
+
elif t == "[" and not found:
|
|
462
|
+
found = True
|
|
463
|
+
first = pos
|
|
464
|
+
current.append(t)
|
|
465
|
+
elif t == "[":
|
|
466
|
+
index = i.index
|
|
467
|
+
try:
|
|
468
|
+
prop = self.get_unicode_property(i, True) # type: tuple[str, str] | None
|
|
469
|
+
except Exception:
|
|
470
|
+
prop = None
|
|
471
|
+
i.rewind(i.index - index)
|
|
472
|
+
if prop is not None:
|
|
473
|
+
value = self.unicode_props(prop[0], prop[1], in_group=True)
|
|
474
|
+
if current[-1] == '-':
|
|
475
|
+
current[-1] = _re.escape('-')
|
|
476
|
+
current.extend(value)
|
|
477
|
+
found_property = True
|
|
478
|
+
pos = i.index - 2
|
|
479
|
+
else:
|
|
480
|
+
current.append(t)
|
|
481
|
+
elif t == "^" and found and (pos == first + 1):
|
|
482
|
+
first = pos
|
|
483
|
+
current.append(t)
|
|
484
|
+
elif t == "]" and found and (pos != first + 1):
|
|
485
|
+
found = False
|
|
486
|
+
current.append(t)
|
|
487
|
+
break
|
|
488
|
+
else:
|
|
489
|
+
current.append(t)
|
|
490
|
+
pos += 1
|
|
491
|
+
t = next(i)
|
|
492
|
+
except StopIteration as e:
|
|
493
|
+
raise PatternError(f"Missing ']', unterminated character set at position {pos}") from e
|
|
494
|
+
|
|
495
|
+
# Handle properties that return an empty string.
|
|
496
|
+
# This will occur when a property's values exceed
|
|
497
|
+
# either the Unicode char limit on a narrow system,
|
|
498
|
+
# or the ASCII limit in a byte string pattern.
|
|
499
|
+
if found_property or self.found_named_unicode:
|
|
500
|
+
temp = "".join(current)
|
|
501
|
+
if temp == '[]':
|
|
502
|
+
# We specified some properties, but they are all
|
|
503
|
+
# out of reach. Therefore we can match nothing.
|
|
504
|
+
current = ['(?!)']
|
|
505
|
+
elif temp == '[^]':
|
|
506
|
+
current = [f'[{_uniprops.ASCII_RANGE if self.is_bytes else _uniprops.UNICODE_RANGE}]']
|
|
507
|
+
else:
|
|
508
|
+
current = [temp]
|
|
509
|
+
|
|
510
|
+
return current
|
|
511
|
+
|
|
512
|
+
def normal(self, t: str, i: _util.StringIter) -> list[str]:
|
|
513
|
+
"""Handle normal chars."""
|
|
514
|
+
|
|
515
|
+
current = []
|
|
516
|
+
|
|
517
|
+
if t == "\\":
|
|
518
|
+
try:
|
|
519
|
+
t = next(i)
|
|
520
|
+
current.extend(self.reference(t, i))
|
|
521
|
+
except StopIteration:
|
|
522
|
+
current.append(t)
|
|
523
|
+
elif t == "(":
|
|
524
|
+
current.extend(self.subgroup(t, i))
|
|
525
|
+
elif self.verbose and t == "#":
|
|
526
|
+
current.extend(self.verbose_comment(t, i))
|
|
527
|
+
elif t == "[":
|
|
528
|
+
current.extend(self.char_groups(t, i))
|
|
529
|
+
else:
|
|
530
|
+
current.append(t)
|
|
531
|
+
return current
|
|
532
|
+
|
|
533
|
+
def unicode_name(self, name: str, in_group: bool = False) -> list[str]:
|
|
534
|
+
"""Insert Unicode value by its name."""
|
|
535
|
+
|
|
536
|
+
value = ord(_unicodedata.lookup(name))
|
|
537
|
+
if self.is_bytes and value > 0xFF:
|
|
538
|
+
if not in_group:
|
|
539
|
+
return ['(?!)']
|
|
540
|
+
else:
|
|
541
|
+
return ['']
|
|
542
|
+
return [f'\\{value:03o}' if value <= 0xFF else chr(value)]
|
|
543
|
+
|
|
544
|
+
def unicode_props(
|
|
545
|
+
self,
|
|
546
|
+
props: str,
|
|
547
|
+
prop_value: str | None,
|
|
548
|
+
in_group: bool = False,
|
|
549
|
+
negate: bool = False
|
|
550
|
+
) -> list[str]:
|
|
551
|
+
"""
|
|
552
|
+
Insert Unicode properties.
|
|
553
|
+
|
|
554
|
+
Unicode properties are very forgiving.
|
|
555
|
+
Case doesn't matter and `[ -_]` will be stripped out.
|
|
556
|
+
"""
|
|
557
|
+
|
|
558
|
+
if props.startswith("^"):
|
|
559
|
+
if negate:
|
|
560
|
+
props = props[1:]
|
|
561
|
+
elif negate:
|
|
562
|
+
props = '^' + props
|
|
563
|
+
if not prop_value and prop_value is not None:
|
|
564
|
+
prop_value = None
|
|
565
|
+
|
|
566
|
+
if self.is_bytes:
|
|
567
|
+
mode = _uniprops.MODE_ASCII
|
|
568
|
+
elif not self.unicode:
|
|
569
|
+
mode = _uniprops.MODE_NORMAL
|
|
570
|
+
else:
|
|
571
|
+
mode = _uniprops.MODE_UNICODE
|
|
572
|
+
|
|
573
|
+
v = _uniprops.get_unicode_property(props, prop_value, mode)
|
|
574
|
+
if not in_group:
|
|
575
|
+
if not v:
|
|
576
|
+
v = f'^{_uniprops.ASCII_RANGE if self.is_bytes else _uniprops.UNICODE_RANGE}'
|
|
577
|
+
v = f"[{v}]"
|
|
578
|
+
properties = [v]
|
|
579
|
+
|
|
580
|
+
return properties
|
|
581
|
+
|
|
582
|
+
def main_group(self, i: _util.StringIter) -> list[str]:
|
|
583
|
+
"""The main group: group 0."""
|
|
584
|
+
|
|
585
|
+
current = []
|
|
586
|
+
try:
|
|
587
|
+
while True:
|
|
588
|
+
t = next(i)
|
|
589
|
+
current.extend(self.normal(t, i))
|
|
590
|
+
except StopIteration:
|
|
591
|
+
pass
|
|
592
|
+
return current
|
|
593
|
+
|
|
594
|
+
def _parse(self, search: str) -> str:
|
|
595
|
+
"""Begin parsing."""
|
|
596
|
+
|
|
597
|
+
self.verbose = bool(self.re_verbose)
|
|
598
|
+
self.unicode = bool(self.re_unicode)
|
|
599
|
+
self.ascii = self.re_unicode is not None and not self.re_unicode
|
|
600
|
+
if not self.unicode and not self.ascii:
|
|
601
|
+
self.unicode = True
|
|
602
|
+
|
|
603
|
+
new_pattern = []
|
|
604
|
+
i = _util.StringIter(self.process_quotes(search))
|
|
605
|
+
|
|
606
|
+
new_pattern = self.main_group(i)
|
|
607
|
+
return "".join(new_pattern)
|
|
608
|
+
|
|
609
|
+
def parse(self) -> AnyStr:
|
|
610
|
+
"""Apply search template."""
|
|
611
|
+
|
|
612
|
+
if isinstance(self.search, bytes):
|
|
613
|
+
return self._parse(self.search.decode('latin-1')).encode('latin-1')
|
|
614
|
+
else:
|
|
615
|
+
return self._parse(self.search)
|
|
616
|
+
|
|
617
|
+
|
|
618
|
+
class _ReplaceParser(Generic[AnyStr]):
|
|
619
|
+
"""Pre-replace template."""
|
|
620
|
+
|
|
621
|
+
def __init__(self, pattern: Pattern[AnyStr], template: AnyStr, use_format: bool = False) -> None:
|
|
622
|
+
"""Initialize."""
|
|
623
|
+
|
|
624
|
+
self.pattern = pattern # type: Pattern[AnyStr]
|
|
625
|
+
self._original = template # type: AnyStr
|
|
626
|
+
self._template = template # type: AnyStr
|
|
627
|
+
self.use_format = use_format
|
|
628
|
+
self.end_found = False
|
|
629
|
+
self.group_slots = [] # type: list[tuple[int, tuple[int | None, int | None, Any]]]
|
|
630
|
+
self.literal_slots = [] # type: list[str]
|
|
631
|
+
self.result = [] # type: list[str]
|
|
632
|
+
self.span_stack = [] # type: list[int]
|
|
633
|
+
self.single_stack = [] # type: list[int]
|
|
634
|
+
self.literals = [] # type: list[AnyStr | None]
|
|
635
|
+
self.groups = [] # type: list[tuple[int, int]]
|
|
636
|
+
self.slot = 0
|
|
637
|
+
self.manual = False
|
|
638
|
+
self.auto = False
|
|
639
|
+
self.auto_index = 0
|
|
640
|
+
self.is_bytes = isinstance(self._original, bytes)
|
|
641
|
+
|
|
642
|
+
def parse_format_index(self, text: str) -> int | str:
|
|
643
|
+
"""Parse format index."""
|
|
644
|
+
|
|
645
|
+
base = 10
|
|
646
|
+
prefix = text[1:3] if text[0] == "-" else text[:2]
|
|
647
|
+
if prefix[0:1] == "0":
|
|
648
|
+
char = prefix[-1]
|
|
649
|
+
if char == "b":
|
|
650
|
+
base = 2
|
|
651
|
+
elif char == "o":
|
|
652
|
+
base = 8
|
|
653
|
+
elif char == "x":
|
|
654
|
+
base = 16
|
|
655
|
+
try:
|
|
656
|
+
idx = int(text, base) # type: int | str
|
|
657
|
+
except Exception:
|
|
658
|
+
idx = text
|
|
659
|
+
return idx
|
|
660
|
+
|
|
661
|
+
def get_format(self, c: str, i: _util.StringIter) -> tuple[str, list[tuple[int, Any]]]:
|
|
662
|
+
"""Get format group."""
|
|
663
|
+
|
|
664
|
+
index = i.index
|
|
665
|
+
field = ''
|
|
666
|
+
value = [] # type: list[tuple[int, Any]]
|
|
667
|
+
|
|
668
|
+
try:
|
|
669
|
+
if c == '}':
|
|
670
|
+
value.append((_util.FMT_FIELD, ''))
|
|
671
|
+
value.append((_util.FMT_INDEX, -1))
|
|
672
|
+
else:
|
|
673
|
+
# Field
|
|
674
|
+
temp = [] # type: list[str]
|
|
675
|
+
if c in _LETTERS_UNDERSCORE:
|
|
676
|
+
# Handle name
|
|
677
|
+
temp.append(c)
|
|
678
|
+
c = self.format_next(i)
|
|
679
|
+
while c in _WORD:
|
|
680
|
+
temp.append(c)
|
|
681
|
+
c = self.format_next(i)
|
|
682
|
+
elif c in _DIGIT:
|
|
683
|
+
# Handle group number
|
|
684
|
+
temp.append(c)
|
|
685
|
+
c = self.format_next(i)
|
|
686
|
+
while c in _DIGIT:
|
|
687
|
+
temp.append(c)
|
|
688
|
+
c = self.format_next(i)
|
|
689
|
+
|
|
690
|
+
# Try and covert to integer index
|
|
691
|
+
field = ''.join(temp).strip()
|
|
692
|
+
try:
|
|
693
|
+
value = [(_util.FMT_FIELD, str(int(field, 10)))]
|
|
694
|
+
except ValueError:
|
|
695
|
+
value = [(_util.FMT_FIELD, field)]
|
|
696
|
+
pass
|
|
697
|
+
|
|
698
|
+
if c != '[':
|
|
699
|
+
value.append((_util.FMT_INDEX, None))
|
|
700
|
+
|
|
701
|
+
# Attributes and indexes
|
|
702
|
+
while c in ('[', '.'):
|
|
703
|
+
if c == '[':
|
|
704
|
+
findex = []
|
|
705
|
+
sindex = i.index - 1
|
|
706
|
+
c = self.format_next(i)
|
|
707
|
+
try:
|
|
708
|
+
while c != ']':
|
|
709
|
+
findex.append(c)
|
|
710
|
+
c = self.format_next(i)
|
|
711
|
+
except StopIteration as e:
|
|
712
|
+
raise PatternError(f"Unmatched '[' at {sindex - 1}") from e
|
|
713
|
+
idx = self.parse_format_index(''.join(findex))
|
|
714
|
+
value.append((_util.FMT_INDEX, idx))
|
|
715
|
+
c = self.format_next(i)
|
|
716
|
+
else:
|
|
717
|
+
findex = []
|
|
718
|
+
c = self.format_next(i)
|
|
719
|
+
while c in _WORD:
|
|
720
|
+
findex.append(c)
|
|
721
|
+
c = self.format_next(i)
|
|
722
|
+
value.append((_util.FMT_ATTR, ''.join(findex)))
|
|
723
|
+
|
|
724
|
+
# Conversion
|
|
725
|
+
if c == '!':
|
|
726
|
+
c = self.format_next(i)
|
|
727
|
+
if c not in _FMT_CONV_TYPE:
|
|
728
|
+
raise PatternError(f"Invalid conversion type at {i.index - 1}!")
|
|
729
|
+
value.append((_util.FMT_CONV, c))
|
|
730
|
+
c = self.format_next(i)
|
|
731
|
+
|
|
732
|
+
# Format spec
|
|
733
|
+
if c == ':':
|
|
734
|
+
fill = None # type: str | None
|
|
735
|
+
width = []
|
|
736
|
+
align = None
|
|
737
|
+
convert = None
|
|
738
|
+
c = self.format_next(i)
|
|
739
|
+
|
|
740
|
+
if c in ('<', '>', '^'):
|
|
741
|
+
# Get fill and alignment
|
|
742
|
+
align = c
|
|
743
|
+
c = self.format_next(i)
|
|
744
|
+
if c in ('<', '>', '^'):
|
|
745
|
+
fill = align
|
|
746
|
+
align = c
|
|
747
|
+
c = self.format_next(i)
|
|
748
|
+
elif c in _DIGIT:
|
|
749
|
+
# Get Width
|
|
750
|
+
fill = c
|
|
751
|
+
c = self.format_next(i)
|
|
752
|
+
if c in ('<', '>', '^'):
|
|
753
|
+
align = c
|
|
754
|
+
c = self.format_next(i)
|
|
755
|
+
else:
|
|
756
|
+
width.append(fill)
|
|
757
|
+
fill = None
|
|
758
|
+
else:
|
|
759
|
+
fill = c
|
|
760
|
+
c = self.format_next(i)
|
|
761
|
+
if fill == 's' and c == '}':
|
|
762
|
+
convert = fill
|
|
763
|
+
fill = None
|
|
764
|
+
if fill is not None:
|
|
765
|
+
if c not in ('<', '>', '^'):
|
|
766
|
+
raise PatternError(f'Invalid format spec char at {i.index - 1}!')
|
|
767
|
+
align = c
|
|
768
|
+
c = self.format_next(i)
|
|
769
|
+
|
|
770
|
+
while c in _DIGIT:
|
|
771
|
+
width.append(c)
|
|
772
|
+
c = self.format_next(i)
|
|
773
|
+
|
|
774
|
+
if not align and len(width) and width[0] == '0':
|
|
775
|
+
raise PatternError("'=' alignment is not supported!")
|
|
776
|
+
if align and not fill and len(width) and width[0] == '0':
|
|
777
|
+
fill = '0'
|
|
778
|
+
|
|
779
|
+
if c == 's':
|
|
780
|
+
convert = c
|
|
781
|
+
c = self.format_next(i)
|
|
782
|
+
|
|
783
|
+
if not fill:
|
|
784
|
+
fill = ' '
|
|
785
|
+
|
|
786
|
+
value.append(
|
|
787
|
+
(
|
|
788
|
+
_util.FMT_SPEC,
|
|
789
|
+
(
|
|
790
|
+
fill.encode('latin-1') if self.is_bytes else fill,
|
|
791
|
+
align,
|
|
792
|
+
(int(''.join(width)) if width else 0),
|
|
793
|
+
convert
|
|
794
|
+
)
|
|
795
|
+
)
|
|
796
|
+
)
|
|
797
|
+
|
|
798
|
+
if c != '}':
|
|
799
|
+
raise PatternError(f"Unmatched '{{' at {index - 1}")
|
|
800
|
+
except StopIteration as e:
|
|
801
|
+
raise PatternError(f"Unmatched '{{' at {index - 1}!") from e
|
|
802
|
+
|
|
803
|
+
return field, value
|
|
804
|
+
|
|
805
|
+
def handle_format(self, t: str, i: _util.StringIter) -> None:
|
|
806
|
+
"""Handle format."""
|
|
807
|
+
|
|
808
|
+
if t == '{':
|
|
809
|
+
t = self.format_next(i)
|
|
810
|
+
if t == '{':
|
|
811
|
+
self.get_single_stack()
|
|
812
|
+
self.result.append(t)
|
|
813
|
+
else:
|
|
814
|
+
field, text = self.get_format(t, i)
|
|
815
|
+
self.handle_format_group(field, text)
|
|
816
|
+
else:
|
|
817
|
+
t = self.format_next(i)
|
|
818
|
+
if t == '}':
|
|
819
|
+
self.get_single_stack()
|
|
820
|
+
self.result.append(t)
|
|
821
|
+
else:
|
|
822
|
+
raise PatternError(f"Unmatched '}}' at {i.index - 2}!")
|
|
823
|
+
|
|
824
|
+
def get_octal(self, c: str, i: _util.StringIter) -> str | None:
|
|
825
|
+
"""Get octal."""
|
|
826
|
+
|
|
827
|
+
index = i.index
|
|
828
|
+
value = []
|
|
829
|
+
zero_count = 0
|
|
830
|
+
try:
|
|
831
|
+
if c == '0':
|
|
832
|
+
for _ in range(3):
|
|
833
|
+
if c != '0':
|
|
834
|
+
break
|
|
835
|
+
value.append(c)
|
|
836
|
+
c = next(i)
|
|
837
|
+
zero_count = len(value)
|
|
838
|
+
if zero_count < 3:
|
|
839
|
+
for _ in range(3 - zero_count):
|
|
840
|
+
if c not in _OCTAL:
|
|
841
|
+
break
|
|
842
|
+
value.append(c)
|
|
843
|
+
c = next(i)
|
|
844
|
+
i.rewind(1)
|
|
845
|
+
except StopIteration:
|
|
846
|
+
pass
|
|
847
|
+
|
|
848
|
+
octal_count = len(value)
|
|
849
|
+
if not (self.use_format and octal_count) and not (zero_count and octal_count < 3) and octal_count != 3:
|
|
850
|
+
i.rewind(i.index - index)
|
|
851
|
+
value = []
|
|
852
|
+
|
|
853
|
+
return ''.join(value) if value else None
|
|
854
|
+
|
|
855
|
+
def parse_octal(self, text: str, i: _util.StringIter) -> None:
|
|
856
|
+
"""Parse octal value."""
|
|
857
|
+
|
|
858
|
+
value = int(text, 8)
|
|
859
|
+
if value > 0xFF and self.is_bytes:
|
|
860
|
+
# Re fails on octal greater than `0o377` or `0xFF`
|
|
861
|
+
raise PatternError("Octal escape value outside of range 0-0o377!")
|
|
862
|
+
else:
|
|
863
|
+
single = self.get_single_stack()
|
|
864
|
+
if self.span_stack:
|
|
865
|
+
text = self.convert_case(chr(value), self.span_stack[-1])
|
|
866
|
+
value = ord(self.convert_case(text, single)) if single is not None else ord(text)
|
|
867
|
+
elif single:
|
|
868
|
+
value = ord(self.convert_case(chr(value), single))
|
|
869
|
+
if self.use_format and value in _CURLY_BRACKETS_ORD:
|
|
870
|
+
self.handle_format(chr(value), i)
|
|
871
|
+
elif value <= 0xFF:
|
|
872
|
+
self.result.append(f'\\{value:03o}')
|
|
873
|
+
else:
|
|
874
|
+
self.result.append(chr(value))
|
|
875
|
+
|
|
876
|
+
def get_named_unicode(self, i: _util.StringIter) -> str:
|
|
877
|
+
"""Get named Unicode."""
|
|
878
|
+
|
|
879
|
+
index = i.index
|
|
880
|
+
value = []
|
|
881
|
+
try:
|
|
882
|
+
if next(i) != '{':
|
|
883
|
+
raise PatternError(f"Named Unicode missing '{{' at {i.index - 1}!")
|
|
884
|
+
c = next(i)
|
|
885
|
+
while c != '}':
|
|
886
|
+
value.append(c)
|
|
887
|
+
c = next(i)
|
|
888
|
+
except StopIteration as e:
|
|
889
|
+
raise PatternError(f"Unmatched '}}' at {index}!") from e
|
|
890
|
+
|
|
891
|
+
return ''.join(value)
|
|
892
|
+
|
|
893
|
+
def parse_named_unicode(self, i: _util.StringIter) -> None:
|
|
894
|
+
"""Parse named Unicode."""
|
|
895
|
+
|
|
896
|
+
value = ord(_unicodedata.lookup(self.get_named_unicode(i)))
|
|
897
|
+
single = self.get_single_stack()
|
|
898
|
+
if self.span_stack:
|
|
899
|
+
text = self.convert_case(chr(value), self.span_stack[-1])
|
|
900
|
+
value = ord(self.convert_case(text, single)) if single is not None else ord(text)
|
|
901
|
+
elif single:
|
|
902
|
+
value = ord(self.convert_case(chr(value), single))
|
|
903
|
+
if self.use_format and value in _CURLY_BRACKETS_ORD:
|
|
904
|
+
self.handle_format(chr(value), i)
|
|
905
|
+
elif value <= 0xFF:
|
|
906
|
+
self.result.append(f'\\{value:03o}')
|
|
907
|
+
else:
|
|
908
|
+
self.result.append(chr(value))
|
|
909
|
+
|
|
910
|
+
def get_wide_unicode(self, i: _util.StringIter) -> str:
|
|
911
|
+
"""Get narrow Unicode."""
|
|
912
|
+
|
|
913
|
+
value = []
|
|
914
|
+
for _ in range(3):
|
|
915
|
+
c = next(i)
|
|
916
|
+
if c == '0':
|
|
917
|
+
value.append(c)
|
|
918
|
+
else: # pragma: no cover
|
|
919
|
+
raise PatternError(f'Invalid wide Unicode character at {i.index - 1}!')
|
|
920
|
+
|
|
921
|
+
c = next(i)
|
|
922
|
+
if c in ('0', '1'):
|
|
923
|
+
value.append(c)
|
|
924
|
+
else: # pragma: no cover
|
|
925
|
+
raise PatternError(f'Invalid wide Unicode character at {i.index - 1}!')
|
|
926
|
+
|
|
927
|
+
for _ in range(4):
|
|
928
|
+
c = next(i)
|
|
929
|
+
if c.lower() in _HEX:
|
|
930
|
+
value.append(c)
|
|
931
|
+
else: # pragma: no cover
|
|
932
|
+
raise PatternError(f'Invalid wide Unicode character at {i.index - 1}!')
|
|
933
|
+
return ''.join(value)
|
|
934
|
+
|
|
935
|
+
def get_narrow_unicode(self, i: _util.StringIter) -> str:
|
|
936
|
+
"""Get narrow Unicode."""
|
|
937
|
+
|
|
938
|
+
value = []
|
|
939
|
+
for _ in range(4):
|
|
940
|
+
c = next(i)
|
|
941
|
+
if c.lower() in _HEX:
|
|
942
|
+
value.append(c)
|
|
943
|
+
else: # pragma: no cover
|
|
944
|
+
raise PatternError(f'Invalid Unicode character at {i.index - 1}!')
|
|
945
|
+
return ''.join(value)
|
|
946
|
+
|
|
947
|
+
def parse_unicode(self, i: _util.StringIter, wide: bool = False) -> None:
|
|
948
|
+
"""Parse Unicode."""
|
|
949
|
+
|
|
950
|
+
text = self.get_wide_unicode(i) if wide else self.get_narrow_unicode(i)
|
|
951
|
+
value = int(text, 16)
|
|
952
|
+
single = self.get_single_stack()
|
|
953
|
+
if self.span_stack:
|
|
954
|
+
text = self.convert_case(chr(value), self.span_stack[-1])
|
|
955
|
+
value = ord(self.convert_case(text, single)) if single is not None else ord(text)
|
|
956
|
+
elif single:
|
|
957
|
+
value = ord(self.convert_case(chr(value), single))
|
|
958
|
+
if self.use_format and value in _CURLY_BRACKETS_ORD:
|
|
959
|
+
self.handle_format(chr(value), i)
|
|
960
|
+
elif value <= 0xFF:
|
|
961
|
+
self.result.append(f'\\{value:03o}')
|
|
962
|
+
else:
|
|
963
|
+
self.result.append(chr(value))
|
|
964
|
+
|
|
965
|
+
def get_byte(self, i: _util.StringIter) -> str:
|
|
966
|
+
"""Get byte."""
|
|
967
|
+
|
|
968
|
+
value = []
|
|
969
|
+
for _x in range(2):
|
|
970
|
+
c = next(i)
|
|
971
|
+
if c.lower() in _HEX:
|
|
972
|
+
value.append(c)
|
|
973
|
+
else: # pragma: no cover
|
|
974
|
+
raise PatternError(f'Invalid byte character at {i.index - 1}!')
|
|
975
|
+
return ''.join(value)
|
|
976
|
+
|
|
977
|
+
def parse_bytes(self, i: _util.StringIter) -> None:
|
|
978
|
+
"""Parse byte."""
|
|
979
|
+
|
|
980
|
+
value = int(self.get_byte(i), 16)
|
|
981
|
+
single = self.get_single_stack()
|
|
982
|
+
if self.span_stack:
|
|
983
|
+
text = self.convert_case(chr(value), self.span_stack[-1])
|
|
984
|
+
value = ord(self.convert_case(text, single)) if single is not None else ord(text)
|
|
985
|
+
elif single:
|
|
986
|
+
value = ord(self.convert_case(chr(value), single))
|
|
987
|
+
if self.use_format and value in _CURLY_BRACKETS_ORD:
|
|
988
|
+
self.handle_format(chr(value), i)
|
|
989
|
+
else:
|
|
990
|
+
self.result.append(f'\\{value:03o}')
|
|
991
|
+
|
|
992
|
+
def get_named_group(self, t: str, i: _util.StringIter) -> str:
|
|
993
|
+
"""Get group number."""
|
|
994
|
+
|
|
995
|
+
index = i.index
|
|
996
|
+
value = [t]
|
|
997
|
+
try:
|
|
998
|
+
c = next(i)
|
|
999
|
+
if c != "<":
|
|
1000
|
+
raise PatternError(f"Group missing '<' at {i.index - 1}!")
|
|
1001
|
+
value.append(c)
|
|
1002
|
+
c = next(i)
|
|
1003
|
+
if c in _DIGIT:
|
|
1004
|
+
value.append(c)
|
|
1005
|
+
c = next(i)
|
|
1006
|
+
while c != '>':
|
|
1007
|
+
if c in _DIGIT:
|
|
1008
|
+
value.append(c)
|
|
1009
|
+
c = next(i)
|
|
1010
|
+
value.append(c)
|
|
1011
|
+
elif c in _LETTERS_UNDERSCORE:
|
|
1012
|
+
value.append(c)
|
|
1013
|
+
c = next(i)
|
|
1014
|
+
while c != '>':
|
|
1015
|
+
if c in _WORD:
|
|
1016
|
+
value.append(c)
|
|
1017
|
+
c = next(i)
|
|
1018
|
+
value.append(c)
|
|
1019
|
+
else:
|
|
1020
|
+
raise PatternError(f"Invalid group character at {i.index - 1}!")
|
|
1021
|
+
except StopIteration as e:
|
|
1022
|
+
raise PatternError(f"Unmatched '<' at {index}!") from e
|
|
1023
|
+
|
|
1024
|
+
return ''.join(value)
|
|
1025
|
+
|
|
1026
|
+
def get_group(self, t: str, i: _util.StringIter) -> str | None:
|
|
1027
|
+
"""Get group number."""
|
|
1028
|
+
|
|
1029
|
+
value = []
|
|
1030
|
+
try:
|
|
1031
|
+
if t in _DIGIT and t != '0':
|
|
1032
|
+
value.append(t)
|
|
1033
|
+
t = next(i)
|
|
1034
|
+
if t in _DIGIT:
|
|
1035
|
+
value.append(t)
|
|
1036
|
+
else:
|
|
1037
|
+
i.rewind(1)
|
|
1038
|
+
except StopIteration:
|
|
1039
|
+
pass
|
|
1040
|
+
return ''.join(value) if value else None
|
|
1041
|
+
|
|
1042
|
+
def format_next(self, i: _util.StringIter) -> str:
|
|
1043
|
+
"""Get next format char."""
|
|
1044
|
+
|
|
1045
|
+
c = next(i)
|
|
1046
|
+
return self.format_references(next(i), i) if c == '\\' else c
|
|
1047
|
+
|
|
1048
|
+
def format_references(self, t: str, i: _util.StringIter) -> str:
|
|
1049
|
+
"""Handle format references."""
|
|
1050
|
+
|
|
1051
|
+
octal = self.get_octal(t, i)
|
|
1052
|
+
if octal:
|
|
1053
|
+
o = int(octal, 8)
|
|
1054
|
+
if o > 0xFF and self.is_bytes:
|
|
1055
|
+
# Re fails on octal greater than `0o377` or `0xFF`
|
|
1056
|
+
raise PatternError("Octal escape value outside of range 0-0o377!")
|
|
1057
|
+
value = chr(o)
|
|
1058
|
+
elif t in _STANDARD_ESCAPES or t == '\\':
|
|
1059
|
+
value = _BACK_SLASH_TRANSLATION['\\' + t]
|
|
1060
|
+
elif not self.is_bytes and t == "U":
|
|
1061
|
+
value = chr(int(self.get_wide_unicode(i), 16))
|
|
1062
|
+
elif not self.is_bytes and t == "u":
|
|
1063
|
+
value = chr(int(self.get_narrow_unicode(i), 16))
|
|
1064
|
+
elif not self.is_bytes and t == "N":
|
|
1065
|
+
value = _unicodedata.lookup(self.get_named_unicode(i))
|
|
1066
|
+
elif t == "x":
|
|
1067
|
+
value = chr(int(self.get_byte(i), 16))
|
|
1068
|
+
else:
|
|
1069
|
+
i.rewind(1)
|
|
1070
|
+
value = '\\'
|
|
1071
|
+
return value
|
|
1072
|
+
|
|
1073
|
+
def reference(self, t: str, i: _util.StringIter) -> None:
|
|
1074
|
+
"""Handle references."""
|
|
1075
|
+
octal = self.get_octal(t, i)
|
|
1076
|
+
if t in _OCTAL and octal:
|
|
1077
|
+
self.parse_octal(octal, i)
|
|
1078
|
+
elif (t in _DIGIT or t == 'g') and not self.use_format:
|
|
1079
|
+
group = self.get_group(t, i)
|
|
1080
|
+
if not group:
|
|
1081
|
+
group = self.get_named_group(t, i)
|
|
1082
|
+
self.handle_group('\\' + group)
|
|
1083
|
+
elif t in _STANDARD_ESCAPES:
|
|
1084
|
+
self.get_single_stack()
|
|
1085
|
+
self.result.append('\\' + t)
|
|
1086
|
+
elif t == "l":
|
|
1087
|
+
self.single_case(i, _LOWER)
|
|
1088
|
+
elif t == "L":
|
|
1089
|
+
self.span_case(i, _LOWER)
|
|
1090
|
+
elif t == "c":
|
|
1091
|
+
self.single_case(i, _UPPER)
|
|
1092
|
+
elif t == "C":
|
|
1093
|
+
self.span_case(i, _UPPER)
|
|
1094
|
+
elif t == "E":
|
|
1095
|
+
self.end_found = True
|
|
1096
|
+
elif not self.is_bytes and t == "U":
|
|
1097
|
+
self.parse_unicode(i, True)
|
|
1098
|
+
elif not self.is_bytes and t == "u":
|
|
1099
|
+
self.parse_unicode(i)
|
|
1100
|
+
elif not self.is_bytes and t == "N":
|
|
1101
|
+
self.parse_named_unicode(i)
|
|
1102
|
+
elif t == "x":
|
|
1103
|
+
self.parse_bytes(i)
|
|
1104
|
+
elif self.use_format and t in _CURLY_BRACKETS:
|
|
1105
|
+
self.result.append('\\\\')
|
|
1106
|
+
self.handle_format(t, i)
|
|
1107
|
+
elif self.use_format and t == 'g':
|
|
1108
|
+
self.result.append('\\\\')
|
|
1109
|
+
self.result.append(t)
|
|
1110
|
+
else:
|
|
1111
|
+
value = '\\' + t
|
|
1112
|
+
self.get_single_stack()
|
|
1113
|
+
if self.span_stack:
|
|
1114
|
+
value = self.convert_case(value, self.span_stack[-1])
|
|
1115
|
+
self.result.append(value)
|
|
1116
|
+
|
|
1117
|
+
def _parse_template(self, template: str) -> str:
|
|
1118
|
+
"""Parse template."""
|
|
1119
|
+
|
|
1120
|
+
self.result = [""]
|
|
1121
|
+
|
|
1122
|
+
i = _util.StringIter(template)
|
|
1123
|
+
|
|
1124
|
+
try:
|
|
1125
|
+
while True:
|
|
1126
|
+
t = next(i)
|
|
1127
|
+
if self.use_format and t in _CURLY_BRACKETS:
|
|
1128
|
+
self.handle_format(t, i)
|
|
1129
|
+
elif t == '\\':
|
|
1130
|
+
try:
|
|
1131
|
+
t = next(i)
|
|
1132
|
+
self.reference(t, i)
|
|
1133
|
+
except StopIteration:
|
|
1134
|
+
self.result.append(t)
|
|
1135
|
+
raise
|
|
1136
|
+
else:
|
|
1137
|
+
self.result.append(t)
|
|
1138
|
+
except StopIteration:
|
|
1139
|
+
pass
|
|
1140
|
+
|
|
1141
|
+
if len(self.result) > 1:
|
|
1142
|
+
self.literal_slots.append("".join(self.result))
|
|
1143
|
+
del self.result[:]
|
|
1144
|
+
self.result.append("")
|
|
1145
|
+
self.slot += 1
|
|
1146
|
+
|
|
1147
|
+
return "".join(self.literal_slots)
|
|
1148
|
+
|
|
1149
|
+
def parse_template(self) -> None:
|
|
1150
|
+
"""Parse template."""
|
|
1151
|
+
|
|
1152
|
+
if isinstance(self._original, bytes):
|
|
1153
|
+
self._template = self._parse_template(self._original.decode('latin-1')).encode('latin-1')
|
|
1154
|
+
else:
|
|
1155
|
+
self._template = self._parse_template(self._original)
|
|
1156
|
+
|
|
1157
|
+
if _util.PY312:
|
|
1158
|
+
count = 0
|
|
1159
|
+
for part in _parser.parse_template(self._template, self.pattern):
|
|
1160
|
+
if isinstance(part, int):
|
|
1161
|
+
self.groups.append((count, part))
|
|
1162
|
+
self.literals.append(None)
|
|
1163
|
+
elif part:
|
|
1164
|
+
self.literals.append(cast(AnyStr, part))
|
|
1165
|
+
else:
|
|
1166
|
+
continue
|
|
1167
|
+
count += 1
|
|
1168
|
+
else:
|
|
1169
|
+
self.groups, self.literals = _parser.parse_template(self._template, self.pattern)
|
|
1170
|
+
|
|
1171
|
+
def span_case(self, i: _util.StringIter, case: int) -> None:
|
|
1172
|
+
"""Uppercase or lowercase the next range of characters until end marker is found."""
|
|
1173
|
+
|
|
1174
|
+
# A new \L, \C or \E should pop the last in the stack.
|
|
1175
|
+
if self.span_stack:
|
|
1176
|
+
self.span_stack.pop()
|
|
1177
|
+
if self.single_stack:
|
|
1178
|
+
self.single_stack.pop()
|
|
1179
|
+
self.span_stack.append(case)
|
|
1180
|
+
count = len(self.span_stack)
|
|
1181
|
+
self.end_found = False
|
|
1182
|
+
try:
|
|
1183
|
+
while not self.end_found:
|
|
1184
|
+
t = next(i)
|
|
1185
|
+
if self.use_format and t in _CURLY_BRACKETS:
|
|
1186
|
+
self.handle_format(t, i)
|
|
1187
|
+
elif t == '\\':
|
|
1188
|
+
try:
|
|
1189
|
+
t = next(i)
|
|
1190
|
+
self.reference(t, i)
|
|
1191
|
+
except StopIteration:
|
|
1192
|
+
self.result.append(t)
|
|
1193
|
+
raise
|
|
1194
|
+
else:
|
|
1195
|
+
self.result.append(self.convert_case(t, case))
|
|
1196
|
+
if self.end_found or count > len(self.span_stack):
|
|
1197
|
+
self.end_found = False
|
|
1198
|
+
break
|
|
1199
|
+
except StopIteration:
|
|
1200
|
+
pass
|
|
1201
|
+
if count == len(self.span_stack):
|
|
1202
|
+
self.span_stack.pop()
|
|
1203
|
+
|
|
1204
|
+
def convert_case(self, value: str, case: int) -> str:
|
|
1205
|
+
"""Convert case."""
|
|
1206
|
+
|
|
1207
|
+
if self.is_bytes:
|
|
1208
|
+
cased = []
|
|
1209
|
+
for c in value:
|
|
1210
|
+
if c in _ASCII_LETTERS:
|
|
1211
|
+
cased.append(c.lower() if case == _LOWER else c.upper())
|
|
1212
|
+
else:
|
|
1213
|
+
cased.append(c)
|
|
1214
|
+
return "".join(cased)
|
|
1215
|
+
else:
|
|
1216
|
+
return value.lower() if case == _LOWER else value.upper()
|
|
1217
|
+
|
|
1218
|
+
def single_case(self, i: _util.StringIter, case: int) -> None:
|
|
1219
|
+
"""Uppercase or lowercase the next character."""
|
|
1220
|
+
|
|
1221
|
+
# Pop a previous case if we have consecutive ones.
|
|
1222
|
+
if self.single_stack:
|
|
1223
|
+
self.single_stack.pop()
|
|
1224
|
+
self.single_stack.append(case)
|
|
1225
|
+
try:
|
|
1226
|
+
t = next(i)
|
|
1227
|
+
if self.use_format and t in _CURLY_BRACKETS:
|
|
1228
|
+
self.handle_format(t, i)
|
|
1229
|
+
elif t == '\\':
|
|
1230
|
+
try:
|
|
1231
|
+
t = next(i)
|
|
1232
|
+
self.reference(t, i)
|
|
1233
|
+
except StopIteration:
|
|
1234
|
+
self.result.append(t)
|
|
1235
|
+
raise
|
|
1236
|
+
elif self.single_stack:
|
|
1237
|
+
this_case = self.get_single_stack()
|
|
1238
|
+
if this_case is not None:
|
|
1239
|
+
self.result.append(self.convert_case(t, this_case))
|
|
1240
|
+
except StopIteration:
|
|
1241
|
+
pass
|
|
1242
|
+
|
|
1243
|
+
def get_single_stack(self) -> int | None:
|
|
1244
|
+
"""Get the correct single stack item to use."""
|
|
1245
|
+
|
|
1246
|
+
single = None
|
|
1247
|
+
while self.single_stack:
|
|
1248
|
+
single = self.single_stack.pop()
|
|
1249
|
+
return single
|
|
1250
|
+
|
|
1251
|
+
def handle_format_group(self, field: str, text: list[tuple[int, Any]]) -> None:
|
|
1252
|
+
"""Handle format group."""
|
|
1253
|
+
|
|
1254
|
+
# Handle auto incrementing group indexes
|
|
1255
|
+
if field == '':
|
|
1256
|
+
if self.auto:
|
|
1257
|
+
field = str(self.auto_index)
|
|
1258
|
+
text[0] = (_util.FMT_FIELD, field)
|
|
1259
|
+
self.auto_index += 1
|
|
1260
|
+
elif not self.manual and not self.auto:
|
|
1261
|
+
self.auto = True
|
|
1262
|
+
field = str(self.auto_index)
|
|
1263
|
+
text[0] = (_util.FMT_FIELD, field)
|
|
1264
|
+
self.auto_index += 1
|
|
1265
|
+
else:
|
|
1266
|
+
raise PatternError("Cannot switch to auto format during manual format!")
|
|
1267
|
+
elif not self.manual and not self.auto:
|
|
1268
|
+
self.manual = True
|
|
1269
|
+
elif not self.manual:
|
|
1270
|
+
raise PatternError("Cannot switch to manual format during auto format!")
|
|
1271
|
+
|
|
1272
|
+
self.handle_group(field, tuple(text), True)
|
|
1273
|
+
|
|
1274
|
+
def handle_group(
|
|
1275
|
+
self,
|
|
1276
|
+
text: str,
|
|
1277
|
+
capture: tuple[tuple[int, Any], ...] | None = None,
|
|
1278
|
+
is_format: bool = False
|
|
1279
|
+
) -> None:
|
|
1280
|
+
"""Handle groups."""
|
|
1281
|
+
|
|
1282
|
+
if len(self.result) > 1:
|
|
1283
|
+
self.literal_slots.append("".join(self.result))
|
|
1284
|
+
if is_format:
|
|
1285
|
+
self.literal_slots.extend(["\\g<", text, ">"])
|
|
1286
|
+
else:
|
|
1287
|
+
self.literal_slots.append(text)
|
|
1288
|
+
del self.result[:]
|
|
1289
|
+
self.result.append("")
|
|
1290
|
+
self.slot += 1
|
|
1291
|
+
elif is_format:
|
|
1292
|
+
self.literal_slots.extend(["\\g<", text, ">"])
|
|
1293
|
+
else:
|
|
1294
|
+
self.literal_slots.append(text)
|
|
1295
|
+
|
|
1296
|
+
self.group_slots.append(
|
|
1297
|
+
(
|
|
1298
|
+
self.slot,
|
|
1299
|
+
(
|
|
1300
|
+
(self.span_stack[-1] if self.span_stack else None),
|
|
1301
|
+
self.get_single_stack(),
|
|
1302
|
+
(() if self.is_bytes else '') if capture is None else capture
|
|
1303
|
+
)
|
|
1304
|
+
)
|
|
1305
|
+
)
|
|
1306
|
+
self.slot += 1
|
|
1307
|
+
|
|
1308
|
+
def get_base_template(self) -> AnyStr:
|
|
1309
|
+
"""Return the unmodified template before expansion."""
|
|
1310
|
+
|
|
1311
|
+
return self._original
|
|
1312
|
+
|
|
1313
|
+
def parse(self) -> ReplaceTemplate[AnyStr]:
|
|
1314
|
+
"""Parse template."""
|
|
1315
|
+
|
|
1316
|
+
if not isinstance(self.pattern.pattern, type(self._original)):
|
|
1317
|
+
raise TypeError('Pattern string type must match replace template string type!')
|
|
1318
|
+
|
|
1319
|
+
self.parse_template()
|
|
1320
|
+
|
|
1321
|
+
return ReplaceTemplate(
|
|
1322
|
+
tuple(self.groups),
|
|
1323
|
+
tuple(self.group_slots),
|
|
1324
|
+
tuple(self.literals),
|
|
1325
|
+
hash(self.pattern),
|
|
1326
|
+
self.use_format,
|
|
1327
|
+
self.is_bytes
|
|
1328
|
+
)
|
|
1329
|
+
|
|
1330
|
+
|
|
1331
|
+
class ReplaceTemplate(_util.Immutable, Generic[AnyStr]):
|
|
1332
|
+
"""Replacement template expander."""
|
|
1333
|
+
|
|
1334
|
+
__slots__ = ("groups", "group_slots", "literals", "pattern_hash", "use_format", "_hash", "_bytes")
|
|
1335
|
+
|
|
1336
|
+
groups: tuple[tuple[int, int], ...]
|
|
1337
|
+
group_slots: tuple[tuple[int, tuple[int | None, int | None, Any]], ...]
|
|
1338
|
+
literals: tuple[AnyStr | None, ...]
|
|
1339
|
+
pattern_hash: int
|
|
1340
|
+
use_format: bool
|
|
1341
|
+
_hash: int
|
|
1342
|
+
_bytes: bool
|
|
1343
|
+
|
|
1344
|
+
def __init__(
|
|
1345
|
+
self,
|
|
1346
|
+
groups: tuple[tuple[int, int], ...],
|
|
1347
|
+
group_slots: tuple[tuple[int, tuple[int | None, int | None, Any]], ...],
|
|
1348
|
+
literals: tuple[AnyStr | None, ...],
|
|
1349
|
+
pattern_hash: int,
|
|
1350
|
+
use_format: bool,
|
|
1351
|
+
is_bytes: bool
|
|
1352
|
+
) -> None:
|
|
1353
|
+
"""Initialize."""
|
|
1354
|
+
|
|
1355
|
+
super().__init__(
|
|
1356
|
+
use_format=use_format,
|
|
1357
|
+
groups=groups,
|
|
1358
|
+
group_slots=group_slots,
|
|
1359
|
+
literals=literals,
|
|
1360
|
+
pattern_hash=pattern_hash,
|
|
1361
|
+
_bytes=is_bytes,
|
|
1362
|
+
_hash=hash(
|
|
1363
|
+
(
|
|
1364
|
+
type(self),
|
|
1365
|
+
groups, group_slots, literals,
|
|
1366
|
+
pattern_hash, use_format, is_bytes
|
|
1367
|
+
)
|
|
1368
|
+
)
|
|
1369
|
+
)
|
|
1370
|
+
|
|
1371
|
+
def __call__(self, m: Match[AnyStr] | None) -> AnyStr:
|
|
1372
|
+
"""Call."""
|
|
1373
|
+
|
|
1374
|
+
return self.expand(m)
|
|
1375
|
+
|
|
1376
|
+
def __hash__(self) -> int:
|
|
1377
|
+
"""Hash."""
|
|
1378
|
+
|
|
1379
|
+
return self._hash
|
|
1380
|
+
|
|
1381
|
+
def __eq__(self, other: Any) -> bool:
|
|
1382
|
+
"""Equal."""
|
|
1383
|
+
|
|
1384
|
+
return (
|
|
1385
|
+
isinstance(other, ReplaceTemplate) and
|
|
1386
|
+
self.groups == other.groups and
|
|
1387
|
+
self.group_slots == other.group_slots and
|
|
1388
|
+
self.literals == other.literals and
|
|
1389
|
+
self.pattern_hash == other.pattern_hash and
|
|
1390
|
+
self.use_format == other.use_format and
|
|
1391
|
+
self._bytes == other._bytes
|
|
1392
|
+
)
|
|
1393
|
+
|
|
1394
|
+
def __ne__(self, other: Any) -> bool:
|
|
1395
|
+
"""Equal."""
|
|
1396
|
+
|
|
1397
|
+
return (
|
|
1398
|
+
not isinstance(other, ReplaceTemplate) or
|
|
1399
|
+
self.groups != other.groups or
|
|
1400
|
+
self.group_slots != other.group_slots or
|
|
1401
|
+
self.literals != other.literals or
|
|
1402
|
+
self.pattern_hash != other.pattern_hash or
|
|
1403
|
+
self.use_format != other.use_format or
|
|
1404
|
+
self._bytes != self._bytes
|
|
1405
|
+
)
|
|
1406
|
+
|
|
1407
|
+
def __repr__(self) -> str: # pragma: no cover
|
|
1408
|
+
"""Representation."""
|
|
1409
|
+
|
|
1410
|
+
return "{}.{}({!r}, {!r}, {!r}, {!r}, {!r})".format(
|
|
1411
|
+
self.__module__, self.__class__.__name__,
|
|
1412
|
+
self.groups, self.group_slots, self.literals,
|
|
1413
|
+
self.pattern_hash, self.use_format
|
|
1414
|
+
)
|
|
1415
|
+
|
|
1416
|
+
def _get_group_index(self, index: int) -> int:
|
|
1417
|
+
"""Find and return the appropriate group index."""
|
|
1418
|
+
|
|
1419
|
+
g_index = 0
|
|
1420
|
+
for group in self.groups:
|
|
1421
|
+
if group[0] == index:
|
|
1422
|
+
g_index = group[1]
|
|
1423
|
+
break
|
|
1424
|
+
return g_index
|
|
1425
|
+
|
|
1426
|
+
def _get_group_attributes(self, index: int) -> tuple[int | None, int | None, Any]:
|
|
1427
|
+
"""Find and return the appropriate group case."""
|
|
1428
|
+
|
|
1429
|
+
g_case = (None, None, -1) # type: tuple[int | None, int | None, Any]
|
|
1430
|
+
for group in self.group_slots:
|
|
1431
|
+
if group[0] == index:
|
|
1432
|
+
g_case = group[1]
|
|
1433
|
+
break
|
|
1434
|
+
return g_case
|
|
1435
|
+
|
|
1436
|
+
def expand(self, m: Match[AnyStr] | None) -> AnyStr:
|
|
1437
|
+
"""Using the template, expand the string."""
|
|
1438
|
+
|
|
1439
|
+
if m is None:
|
|
1440
|
+
raise ValueError("Match is None!")
|
|
1441
|
+
|
|
1442
|
+
sep = m.string[:0]
|
|
1443
|
+
if not isinstance(sep, bytes if self._bytes else str):
|
|
1444
|
+
raise TypeError('Match string type does not match expander string type!')
|
|
1445
|
+
text = []
|
|
1446
|
+
# Expand string
|
|
1447
|
+
for x in range(0, len(self.literals)):
|
|
1448
|
+
index = x
|
|
1449
|
+
l = self.literals[x]
|
|
1450
|
+
if l is None:
|
|
1451
|
+
g_index = self._get_group_index(index)
|
|
1452
|
+
span_case, single_case, capture = self._get_group_attributes(index)
|
|
1453
|
+
if not self.use_format:
|
|
1454
|
+
# Non format replace
|
|
1455
|
+
try:
|
|
1456
|
+
l = m.group(g_index)
|
|
1457
|
+
if l is None:
|
|
1458
|
+
l = sep
|
|
1459
|
+
except IndexError as e: # pragma: no cover
|
|
1460
|
+
raise IndexError(f"'{g_index}' is out of range!") from e
|
|
1461
|
+
else:
|
|
1462
|
+
# String format replace
|
|
1463
|
+
try:
|
|
1464
|
+
obj = m.group(g_index)
|
|
1465
|
+
except IndexError as e: # pragma: no cover
|
|
1466
|
+
raise IndexError(f"'{g_index}' is out of range!") from e
|
|
1467
|
+
l = _util.format_captures(
|
|
1468
|
+
[] if obj is None else [obj],
|
|
1469
|
+
capture,
|
|
1470
|
+
_util._to_bstr if isinstance(sep, bytes) else _util._to_str,
|
|
1471
|
+
sep
|
|
1472
|
+
)
|
|
1473
|
+
if span_case is not None:
|
|
1474
|
+
if span_case == _LOWER:
|
|
1475
|
+
l = l.lower()
|
|
1476
|
+
else:
|
|
1477
|
+
l = l.upper()
|
|
1478
|
+
if single_case is not None:
|
|
1479
|
+
if single_case == _LOWER:
|
|
1480
|
+
l = l[0:1].lower() + l[1:]
|
|
1481
|
+
else:
|
|
1482
|
+
l = l[0:1].upper() + l[1:]
|
|
1483
|
+
text.append(l)
|
|
1484
|
+
|
|
1485
|
+
return sep.join(text)
|
|
1486
|
+
|
|
1487
|
+
|
|
1488
|
+
def _pickle(r): # type: ignore[no-untyped-def]
|
|
1489
|
+
"""Pickle."""
|
|
1490
|
+
|
|
1491
|
+
return ReplaceTemplate, (r.groups, r.group_slots, r.literals, r.pattern_hash, r.use_format, r._bytes)
|
|
1492
|
+
|
|
1493
|
+
|
|
1494
|
+
_copyreg.pickle(ReplaceTemplate, _pickle)
|