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