backrefs 8.1__tar.gz → 8.2.1__tar.gz

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 (70) hide show
  1. {backrefs-8.1 → backrefs-8.2.1}/.pyspelling.yml +2 -3
  2. {backrefs-8.1 → backrefs-8.2.1}/PKG-INFO +2 -3
  3. {backrefs-8.1 → backrefs-8.2.1}/backrefs/__meta__.py +1 -1
  4. {backrefs-8.1 → backrefs-8.2.1}/backrefs/_bre_parse.py +169 -56
  5. {backrefs-8.1 → backrefs-8.2.1}/backrefs/_bregex_parse.py +213 -125
  6. {backrefs-8.1 → backrefs-8.2.1}/backrefs/bre.py +3 -2
  7. {backrefs-8.1 → backrefs-8.2.1}/backrefs/bregex.py +3 -2
  8. {backrefs-8.1 → backrefs-8.2.1}/backrefs/util.py +10 -0
  9. backrefs-8.2.1/docs/src/dictionary/en-custom.txt +67 -0
  10. backrefs-8.2.1/docs/src/dictionary/en-python.txt +15 -0
  11. {backrefs-8.1 → backrefs-8.2.1}/docs/src/markdown/about/changelog.md +38 -0
  12. {backrefs-8.1 → backrefs-8.2.1}/docs/src/markdown/refs.md +1 -1
  13. {backrefs-8.1 → backrefs-8.2.1}/hatch_build.py +0 -1
  14. {backrefs-8.1 → backrefs-8.2.1}/pyproject.toml +6 -7
  15. {backrefs-8.1 → backrefs-8.2.1}/tests/test_bidiclass.py +7 -0
  16. {backrefs-8.1 → backrefs-8.2.1}/tests/test_bre.py +240 -8
  17. {backrefs-8.1 → backrefs-8.2.1}/tests/test_bregex.py +169 -4
  18. {backrefs-8.1 → backrefs-8.2.1}/tests/test_generalcategory.py +8 -0
  19. {backrefs-8.1 → backrefs-8.2.1}/tools/unipropgen.py +18 -2
  20. backrefs-8.2.1/zensical.yml +178 -0
  21. backrefs-8.1/tools/unicodedata/13.0.0.zip +0 -0
  22. {backrefs-8.1 → backrefs-8.2.1}/.coveragerc +0 -0
  23. {backrefs-8.1 → backrefs-8.2.1}/.gitignore +0 -0
  24. {backrefs-8.1 → backrefs-8.2.1}/LICENSE.md +0 -0
  25. {backrefs-8.1 → backrefs-8.2.1}/README.md +0 -0
  26. {backrefs-8.1 → backrefs-8.2.1}/backrefs/__init__.py +0 -0
  27. {backrefs-8.1 → backrefs-8.2.1}/backrefs/_bregex_typing.py +0 -0
  28. {backrefs-8.1 → backrefs-8.2.1}/backrefs/py.typed +0 -0
  29. {backrefs-8.1 → backrefs-8.2.1}/backrefs/uniprops/__init__.py +0 -0
  30. {backrefs-8.1 → backrefs-8.2.1}/docs/src/markdown/.snippets/links.md +0 -0
  31. {backrefs-8.1 → backrefs-8.2.1}/docs/src/markdown/about/contributing.md +0 -0
  32. {backrefs-8.1 → backrefs-8.2.1}/docs/src/markdown/about/license.md +0 -0
  33. {backrefs-8.1 → backrefs-8.2.1}/docs/src/markdown/about/security.md +0 -0
  34. {backrefs-8.1 → backrefs-8.2.1}/docs/src/markdown/index.md +0 -0
  35. {backrefs-8.1 → backrefs-8.2.1}/docs/src/markdown/usage.md +0 -0
  36. {backrefs-8.1 → backrefs-8.2.1}/docs/theme/announce.html +0 -0
  37. {backrefs-8.1 → backrefs-8.2.1}/tests/__init__.py +0 -0
  38. {backrefs-8.1 → backrefs-8.2.1}/tests/test_age.py +0 -0
  39. {backrefs-8.1 → backrefs-8.2.1}/tests/test_bidipairedbrackettype.py +0 -0
  40. {backrefs-8.1 → backrefs-8.2.1}/tests/test_binary.py +0 -0
  41. {backrefs-8.1 → backrefs-8.2.1}/tests/test_block.py +0 -0
  42. {backrefs-8.1 → backrefs-8.2.1}/tests/test_canonicalcombiningclass.py +0 -0
  43. {backrefs-8.1 → backrefs-8.2.1}/tests/test_decompositiontype.py +0 -0
  44. {backrefs-8.1 → backrefs-8.2.1}/tests/test_eastasianwidth.py +0 -0
  45. {backrefs-8.1 → backrefs-8.2.1}/tests/test_graphemeclusterbreak.py +0 -0
  46. {backrefs-8.1 → backrefs-8.2.1}/tests/test_hangulsyllabletype.py +0 -0
  47. {backrefs-8.1 → backrefs-8.2.1}/tests/test_indicpositionalcategory.py +0 -0
  48. {backrefs-8.1 → backrefs-8.2.1}/tests/test_indicsylabiccategory.py +0 -0
  49. {backrefs-8.1 → backrefs-8.2.1}/tests/test_joininggroup.py +0 -0
  50. {backrefs-8.1 → backrefs-8.2.1}/tests/test_joiningtype.py +0 -0
  51. {backrefs-8.1 → backrefs-8.2.1}/tests/test_linebreak.py +0 -0
  52. {backrefs-8.1 → backrefs-8.2.1}/tests/test_numerictype.py +0 -0
  53. {backrefs-8.1 → backrefs-8.2.1}/tests/test_numericvalue.py +0 -0
  54. {backrefs-8.1 → backrefs-8.2.1}/tests/test_quickcheck.py +0 -0
  55. {backrefs-8.1 → backrefs-8.2.1}/tests/test_script.py +0 -0
  56. {backrefs-8.1 → backrefs-8.2.1}/tests/test_scriptextensions.py +0 -0
  57. {backrefs-8.1 → backrefs-8.2.1}/tests/test_sentencebreak.py +0 -0
  58. {backrefs-8.1 → backrefs-8.2.1}/tests/test_uniprops.py +0 -0
  59. {backrefs-8.1 → backrefs-8.2.1}/tests/test_versions.py +0 -0
  60. {backrefs-8.1 → backrefs-8.2.1}/tests/test_verticalorientation.py +0 -0
  61. {backrefs-8.1 → backrefs-8.2.1}/tests/test_wordbreak.py +0 -0
  62. {backrefs-8.1 → backrefs-8.2.1}/tools/__init__.py +0 -0
  63. {backrefs-8.1 → backrefs-8.2.1}/tools/pycon.py +0 -0
  64. {backrefs-8.1 → backrefs-8.2.1}/tools/unicodedata/14.0.0.zip +0 -0
  65. {backrefs-8.1 → backrefs-8.2.1}/tools/unicodedata/15.0.0.zip +0 -0
  66. {backrefs-8.1 → backrefs-8.2.1}/tools/unicodedata/15.1.0.zip +0 -0
  67. {backrefs-8.1 → backrefs-8.2.1}/tools/unicodedata/16.0.0.zip +0 -0
  68. {backrefs-8.1 → backrefs-8.2.1}/tools/unicodedata/17.0.0.zip +0 -0
  69. {backrefs-8.1 → backrefs-8.2.1}/tools/unicodedata/LICENSE +0 -0
  70. {backrefs-8.1 → backrefs-8.2.1}/tools/unidatadownload.py +0 -0
@@ -18,11 +18,10 @@ matrix:
18
18
  - alt
19
19
  ignores:
20
20
  - 'code, pre'
21
- - 'a:is(.magiclink-compare, .magiclink-commit, .magiclink-repository)'
22
21
  - 'span.keys'
23
22
  - '.MathJax_Preview, .md-nav__link, .md-footer-custom-text, .md-source__repository, .headerlink, .md-icon'
24
- - '.md-social__link, .magiclink-issue'
25
-
23
+ - '.md-social__link, .magiclink'
24
+
26
25
  - pyspelling.filters.url:
27
26
 
28
27
  - name: markdown
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: backrefs
3
- Version: 8.1
3
+ Version: 8.2.1
4
4
  Summary: A wrapper around re and regex that adds additional back references.
5
5
  Project-URL: Homepage, https://github.com/facelessuser/backrefs
6
6
  Author-email: Isaac Muse <Isaac.Muse@gmail.com>
@@ -13,7 +13,6 @@ Classifier: Intended Audience :: Developers
13
13
  Classifier: License :: OSI Approved :: MIT License
14
14
  Classifier: Operating System :: OS Independent
15
15
  Classifier: Programming Language :: Python :: 3
16
- Classifier: Programming Language :: Python :: 3.10
17
16
  Classifier: Programming Language :: Python :: 3.11
18
17
  Classifier: Programming Language :: Python :: 3.12
19
18
  Classifier: Programming Language :: Python :: 3.13
@@ -21,7 +20,7 @@ Classifier: Programming Language :: Python :: 3.14
21
20
  Classifier: Programming Language :: Python :: 3.15
22
21
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
23
22
  Classifier: Typing :: Typed
24
- Requires-Python: >=3.10
23
+ Requires-Python: >=3.11
25
24
  Provides-Extra: extras
26
25
  Requires-Dist: regex; extra == 'extras'
27
26
  Description-Content-Type: text/markdown
@@ -193,5 +193,5 @@ def parse_version(ver: str) -> Version:
193
193
  return Version(major, minor, micro, release, pre, post, dev)
194
194
 
195
195
 
196
- __version_info__ = Version(8, 1, 0, "final")
196
+ __version_info__ = Version(8, 2, 1, "final")
197
197
  __version__ = __version_info__._get_canonical()
@@ -6,19 +6,13 @@ Copyright (c) 2011 - 2020 Isaac Muse <isaacmuse@gmail.com>
6
6
  """
7
7
  from __future__ import annotations
8
8
  import re as _re
9
- import sys
9
+ import re._parser as _parser # type: ignore[import]
10
10
  import copyreg as _copyreg
11
11
  from . import util as _util
12
12
  from .util import PatternError
13
- import unicodedata as _unicodedata
14
13
  from . import uniprops as _uniprops
15
14
  from typing import Generic, AnyStr, Match, Any, Pattern, cast
16
15
 
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
16
  __all__ = ("ReplaceTemplate", "PatternError")
23
17
 
24
18
  _ASCII_LETTERS = frozenset(
@@ -37,7 +31,7 @@ _WORD = _LETTERS_UNDERSCORE | _DIGIT
37
31
  _STANDARD_ESCAPES = frozenset(('a', 'b', 'f', 'n', 'r', 't', 'v'))
38
32
  _CURLY_BRACKETS = frozenset(('{', '}'))
39
33
  _PROPERTY_STRIP = frozenset((' ', '-', '_'))
40
- _PROPERTY = _WORD | _DIGIT | _PROPERTY_STRIP
34
+ _PROPERTY = _WORD | _DIGIT | _PROPERTY_STRIP | frozenset(('.',))
41
35
  _SCOPED_FLAGS = frozenset(('a', 'i', 'L', 'm', 'u', 's', 'x'))
42
36
  _SCOPED_INV_FLAGS = frozenset(('i', 'm', 's', 'x'))
43
37
  _SCOPED_END = frozenset((':', ')'))
@@ -77,11 +71,12 @@ class _SearchParser(Generic[AnyStr]):
77
71
  _re_start_wb = r"\b(?=\w)"
78
72
  _re_end_wb = r"\b(?<=\w)"
79
73
  _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])'
74
+ _bytes_line_break = r'(?>\r\n|[\n\v\f\r\x85])'
81
75
  _grapheme_cluster = r'(?:{}{}*(?!{}))'
82
76
 
83
77
  verbose: bool
84
78
  unicode: bool
79
+ named_unicode: tuple[str, int]
85
80
  ascii: bool # noqa: A003
86
81
  is_bytes: bool
87
82
  search: AnyStr
@@ -176,7 +171,7 @@ class _SearchParser(Generic[AnyStr]):
176
171
  value = []
177
172
  try:
178
173
  c = next(i)
179
- if c.upper() in _ASCII_LETTERS:
174
+ if not brackets and c.upper() in _ASCII_LETTERS:
180
175
  prop.append(c)
181
176
  elif (not brackets and c != '{') or (brackets and c != ':'):
182
177
  raise PatternError(f"Unicode property missing '{{' at {i.index - 1}!")
@@ -284,6 +279,7 @@ class _SearchParser(Generic[AnyStr]):
284
279
  current.extend(self.unicode_name(text, in_group))
285
280
  if in_group:
286
281
  self.found_named_unicode = True
282
+ self.named_unicode = (f'\\N{{{text}}}', ord(_util.unicode_char(text)))
287
283
  else:
288
284
  current.extend(["\\", t])
289
285
  return current
@@ -419,27 +415,55 @@ class _SearchParser(Generic[AnyStr]):
419
415
  self.verbose = verbose
420
416
  self.unicode = unicode_flag
421
417
 
422
- if t == ")":
423
- current.append(t)
418
+ current.append(t)
424
419
  return current
425
420
 
426
421
  def char_groups(self, t: str, i: _util.StringIter) -> list[str]:
427
422
  """Handle character groups."""
428
423
 
429
- current = []
424
+ current = [] # type: list[str]
430
425
  pos = i.index - 1
431
426
  found = False
432
427
  escaped = False
428
+ escape_index = 0
433
429
  first = 0
434
430
  found_property = False
431
+ # Index of an unescaped `-` in `current` that is acting as a range operator.
432
+ range_dash = -1
433
+ # Whether the last member could start a range if followed by `-`.
434
+ range_start = False
435
+ # The last member's original text, position, and code point if it is a named character
436
+ # beyond the byte range of a byte string.
437
+ start = ('', 0, None) # type: tuple[str, int, int | None]
435
438
  self.found_property = False
436
439
  self.found_named_unicode = False
437
440
 
441
+ def add_member(raw: str, index: int, value: int | None) -> None:
442
+ """Track a member that was just added, which may start or end a range."""
443
+
444
+ nonlocal range_dash, range_start, start
445
+
446
+ if range_dash != -1:
447
+ # This member ends a range.
448
+ start_raw, start_index, start_value = start
449
+ if start_value is not None or value is not None:
450
+ end = self.get_named_byte_range(start_raw, raw, start_value, value, start_index)
451
+ if end is None:
452
+ current[range_dash] = ''
453
+ else:
454
+ current[-1] = end
455
+ range_dash = -1
456
+ range_start = False
457
+ else:
458
+ start = (raw, index, value)
459
+ range_start = True
460
+
438
461
  try:
439
462
  while True:
440
463
  # Prevent POSIX/Unicode class from being part of a range.
441
464
  if self.found_property and t == '-':
442
465
  current.append(_re.escape(t))
466
+ add_member(t, i.index - 1, None)
443
467
  pos += 1
444
468
  t = next(i)
445
469
  self.found_property = False
@@ -449,15 +473,26 @@ class _SearchParser(Generic[AnyStr]):
449
473
 
450
474
  if not escaped and t == "\\":
451
475
  escaped = True
476
+ escape_index = i.index - 1
452
477
  elif escaped:
453
478
  escaped = False
454
479
  idx = len(current) - 1
455
480
  current.extend(self.reference(t, i, True))
456
481
  if self.found_property:
457
482
  # Prevent Unicode class from being part of a range.
458
- if idx >= 0 and current[idx] == '-':
483
+ if idx == range_dash:
459
484
  current[idx] = _re.escape('-')
460
485
  found_property = True
486
+ range_dash = -1
487
+ range_start = False
488
+ elif t == 'N':
489
+ raw, code = self.named_unicode
490
+ add_member(raw, escape_index, code if self.is_bytes and code > 0xFF else None)
491
+ else:
492
+ digits = self.get_escape_digits(i, t)
493
+ if digits:
494
+ current.append(digits)
495
+ add_member(f'\\{t}{digits}', escape_index, None)
461
496
  elif t == "[" and not found:
462
497
  found = True
463
498
  first = pos
@@ -471,22 +506,43 @@ class _SearchParser(Generic[AnyStr]):
471
506
  i.rewind(i.index - index)
472
507
  if prop is not None:
473
508
  value = self.unicode_props(prop[0], prop[1], in_group=True)
474
- if current[-1] == '-':
509
+ # Prevent POSIX class from being part of a range.
510
+ if len(current) - 1 == range_dash:
475
511
  current[-1] = _re.escape('-')
476
512
  current.extend(value)
477
513
  found_property = True
514
+ self.found_property = True
478
515
  pos = i.index - 2
516
+ range_dash = -1
517
+ range_start = False
479
518
  else:
480
519
  current.append(t)
481
- elif t == "^" and found and (pos == first + 1):
520
+ add_member(t, index - 1, None)
521
+ elif t == "^" and found and (pos == first + 1) and len(current) == 1:
522
+ # Only a `^` directly after the opening `[` negates the set.
482
523
  first = pos
483
524
  current.append(t)
484
525
  elif t == "]" and found and (pos != first + 1):
485
526
  found = False
486
527
  current.append(t)
487
528
  break
488
- else:
529
+ elif t == '-' and range_start:
530
+ # A `-` after a member starts a range, unless it is followed by `]`.
489
531
  current.append(t)
532
+ range_dash = len(current) - 1
533
+ range_start = False
534
+ else:
535
+ if (
536
+ t == '^' and
537
+ (found_property or self.found_named_unicode) and
538
+ ''.join(current) in ('[', '[^')
539
+ ):
540
+ # Everything before this was a property that resolved to nothing,
541
+ # so escape `^` to prevent it from being treated as a negation.
542
+ current.append(_re.escape(t))
543
+ else:
544
+ current.append(t)
545
+ add_member(t, i.index - 1, None)
490
546
  pos += 1
491
547
  t = next(i)
492
548
  except StopIteration as e:
@@ -533,7 +589,7 @@ class _SearchParser(Generic[AnyStr]):
533
589
  def unicode_name(self, name: str, in_group: bool = False) -> list[str]:
534
590
  """Insert Unicode value by its name."""
535
591
 
536
- value = ord(_unicodedata.lookup(name))
592
+ value = ord(_util.unicode_char(name))
537
593
  if self.is_bytes and value > 0xFF:
538
594
  if not in_group:
539
595
  return ['(?!)']
@@ -541,6 +597,60 @@ class _SearchParser(Generic[AnyStr]):
541
597
  return ['']
542
598
  return [f'\\{value:03o}' if value <= 0xFF else chr(value)]
543
599
 
600
+ def get_escape_digits(self, i: _util.StringIter, e: str) -> str:
601
+ r"""
602
+ Get the digits that are part of an escape in a character set (`\x`, `\u`, `\U`, or octal).
603
+
604
+ This allows an escape to be treated as a single member of the character set.
605
+ """
606
+
607
+ if e == 'x':
608
+ count, valid = 2, _HEX
609
+ elif e == 'u' and not self.is_bytes:
610
+ count, valid = 4, _HEX
611
+ elif e == 'U' and not self.is_bytes:
612
+ count, valid = 8, _HEX
613
+ elif e in _OCTAL:
614
+ count, valid = 2, _OCTAL
615
+ else:
616
+ return ''
617
+
618
+ digits = []
619
+ while count:
620
+ try:
621
+ c = next(i)
622
+ except StopIteration:
623
+ break
624
+ if c.lower() not in valid:
625
+ i.rewind(1)
626
+ break
627
+ digits.append(c)
628
+ count -= 1
629
+ return ''.join(digits)
630
+
631
+ @staticmethod
632
+ def get_named_byte_range(
633
+ start: str,
634
+ end: str,
635
+ start_value: int | None,
636
+ end_value: int | None,
637
+ index: int
638
+ ) -> str | None:
639
+ """
640
+ Get the end of a range in a byte string where either end is a named character beyond the byte range.
641
+
642
+ `start_value` and `end_value` are the code points of the ends that are named characters beyond the byte
643
+ range, and `None` for ends within the byte range. `start` and `end` are the original text of each end,
644
+ and `index` is where the range starts, both used for errors.
645
+
646
+ A range that ends beyond the byte range includes everything up to the end of the byte range, so return
647
+ the end to use in its place. A range that starts beyond the byte range matches nothing, so return `None`.
648
+ """
649
+
650
+ if start_value is not None and (end_value is None or end_value < start_value):
651
+ raise PatternError(f'bad character range {start}-{end} at position {index}')
652
+ return None if start_value is not None else '\\377'
653
+
544
654
  def unicode_props(
545
655
  self,
546
656
  props: str,
@@ -755,7 +865,7 @@ class _ReplaceParser(Generic[AnyStr]):
755
865
  else:
756
866
  width.append(fill)
757
867
  fill = None
758
- else:
868
+ elif c != '}':
759
869
  fill = c
760
870
  c = self.format_next(i)
761
871
  if fill == 's' and c == '}':
@@ -852,6 +962,18 @@ class _ReplaceParser(Generic[AnyStr]):
852
962
 
853
963
  return ''.join(value) if value else None
854
964
 
965
+ def append_chars(self, text: str, i: _util.StringIter) -> None:
966
+ """Append each character of an escape value as a template escape."""
967
+
968
+ for c in text:
969
+ value = ord(c)
970
+ if self.use_format and value in _CURLY_BRACKETS_ORD:
971
+ self.handle_format(c, i)
972
+ elif value <= 0xFF:
973
+ self.result.append(f'\\{value:03o}')
974
+ else:
975
+ self.result.append(c)
976
+
855
977
  def parse_octal(self, text: str, i: _util.StringIter) -> None:
856
978
  """Parse octal value."""
857
979
 
@@ -863,15 +985,12 @@ class _ReplaceParser(Generic[AnyStr]):
863
985
  single = self.get_single_stack()
864
986
  if self.span_stack:
865
987
  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)
988
+ text = self.convert_case(text, single) if single is not None else text
867
989
  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}')
990
+ text = self.convert_case(chr(value), single)
873
991
  else:
874
- self.result.append(chr(value))
992
+ text = chr(value)
993
+ self.append_chars(text, i)
875
994
 
876
995
  def get_named_unicode(self, i: _util.StringIter) -> str:
877
996
  """Get named Unicode."""
@@ -893,19 +1012,16 @@ class _ReplaceParser(Generic[AnyStr]):
893
1012
  def parse_named_unicode(self, i: _util.StringIter) -> None:
894
1013
  """Parse named Unicode."""
895
1014
 
896
- value = ord(_unicodedata.lookup(self.get_named_unicode(i)))
1015
+ value = ord(_util.unicode_char(self.get_named_unicode(i)))
897
1016
  single = self.get_single_stack()
898
1017
  if self.span_stack:
899
1018
  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)
1019
+ text = self.convert_case(text, single) if single is not None else text
901
1020
  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}')
1021
+ text = self.convert_case(chr(value), single)
907
1022
  else:
908
- self.result.append(chr(value))
1023
+ text = chr(value)
1024
+ self.append_chars(text, i)
909
1025
 
910
1026
  def get_wide_unicode(self, i: _util.StringIter) -> str:
911
1027
  """Get narrow Unicode."""
@@ -952,15 +1068,12 @@ class _ReplaceParser(Generic[AnyStr]):
952
1068
  single = self.get_single_stack()
953
1069
  if self.span_stack:
954
1070
  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)
1071
+ text = self.convert_case(text, single) if single is not None else text
956
1072
  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}')
1073
+ text = self.convert_case(chr(value), single)
962
1074
  else:
963
- self.result.append(chr(value))
1075
+ text = chr(value)
1076
+ self.append_chars(text, i)
964
1077
 
965
1078
  def get_byte(self, i: _util.StringIter) -> str:
966
1079
  """Get byte."""
@@ -981,13 +1094,12 @@ class _ReplaceParser(Generic[AnyStr]):
981
1094
  single = self.get_single_stack()
982
1095
  if self.span_stack:
983
1096
  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)
1097
+ text = self.convert_case(text, single) if single is not None else text
985
1098
  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)
1099
+ text = self.convert_case(chr(value), single)
989
1100
  else:
990
- self.result.append(f'\\{value:03o}')
1101
+ text = chr(value)
1102
+ self.append_chars(text, i)
991
1103
 
992
1104
  def get_named_group(self, t: str, i: _util.StringIter) -> str:
993
1105
  """Get group number."""
@@ -1004,16 +1116,18 @@ class _ReplaceParser(Generic[AnyStr]):
1004
1116
  value.append(c)
1005
1117
  c = next(i)
1006
1118
  while c != '>':
1007
- if c in _DIGIT:
1008
- value.append(c)
1119
+ if c not in _DIGIT:
1120
+ raise PatternError(f"Bad character in group name at {i.index - 1}!")
1121
+ value.append(c)
1009
1122
  c = next(i)
1010
1123
  value.append(c)
1011
1124
  elif c in _LETTERS_UNDERSCORE:
1012
1125
  value.append(c)
1013
1126
  c = next(i)
1014
1127
  while c != '>':
1015
- if c in _WORD:
1016
- value.append(c)
1128
+ if c not in _WORD:
1129
+ raise PatternError(f"Bad character in group name at {i.index - 1}!")
1130
+ value.append(c)
1017
1131
  c = next(i)
1018
1132
  value.append(c)
1019
1133
  else:
@@ -1062,7 +1176,7 @@ class _ReplaceParser(Generic[AnyStr]):
1062
1176
  elif not self.is_bytes and t == "u":
1063
1177
  value = chr(int(self.get_narrow_unicode(i), 16))
1064
1178
  elif not self.is_bytes and t == "N":
1065
- value = _unicodedata.lookup(self.get_named_unicode(i))
1179
+ value = _util.unicode_char(self.get_named_unicode(i))
1066
1180
  elif t == "x":
1067
1181
  value = chr(int(self.get_byte(i), 16))
1068
1182
  else:
@@ -1180,7 +1294,7 @@ class _ReplaceParser(Generic[AnyStr]):
1180
1294
  count = len(self.span_stack)
1181
1295
  self.end_found = False
1182
1296
  try:
1183
- while not self.end_found:
1297
+ while True:
1184
1298
  t = next(i)
1185
1299
  if self.use_format and t in _CURLY_BRACKETS:
1186
1300
  self.handle_format(t, i)
@@ -1233,10 +1347,9 @@ class _ReplaceParser(Generic[AnyStr]):
1233
1347
  except StopIteration:
1234
1348
  self.result.append(t)
1235
1349
  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))
1350
+ else:
1351
+ # The stack always has the case just added.
1352
+ self.result.append(self.convert_case(t, cast(int, self.get_single_stack())))
1240
1353
  except StopIteration:
1241
1354
  pass
1242
1355