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