backrefs 8.0__tar.gz → 8.2__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 (76) hide show
  1. {backrefs-8.0 → backrefs-8.2}/.pyspelling.yml +2 -3
  2. {backrefs-8.0 → backrefs-8.2}/PKG-INFO +4 -4
  3. {backrefs-8.0 → backrefs-8.2}/backrefs/__meta__.py +1 -1
  4. {backrefs-8.0 → backrefs-8.2}/backrefs/_bre_parse.py +40 -37
  5. {backrefs-8.0 → backrefs-8.2}/backrefs/_bregex_parse.py +37 -34
  6. {backrefs-8.0 → backrefs-8.2}/backrefs/util.py +10 -0
  7. backrefs-8.2/docs/src/dictionary/en-custom.txt +67 -0
  8. backrefs-8.2/docs/src/dictionary/en-python.txt +15 -0
  9. {backrefs-8.0 → backrefs-8.2}/docs/src/markdown/about/changelog.md +22 -1
  10. {backrefs-8.0 → backrefs-8.2}/docs/src/markdown/about/contributing.md +42 -15
  11. {backrefs-8.0 → backrefs-8.2}/docs/src/markdown/refs.md +2 -2
  12. {backrefs-8.0 → backrefs-8.2}/docs/src/markdown/usage.md +55 -78
  13. {backrefs-8.0 → backrefs-8.2}/hatch_build.py +1 -1
  14. {backrefs-8.0 → backrefs-8.2}/pyproject.toml +69 -41
  15. {backrefs-8.0 → backrefs-8.2}/tests/test_bidiclass.py +7 -0
  16. {backrefs-8.0 → backrefs-8.2}/tests/test_bre.py +103 -0
  17. {backrefs-8.0 → backrefs-8.2}/tests/test_bregex.py +61 -0
  18. {backrefs-8.0 → backrefs-8.2}/tests/test_generalcategory.py +8 -0
  19. backrefs-8.2/tools/pycon.py +283 -0
  20. {backrefs-8.0 → backrefs-8.2}/tools/unipropgen.py +18 -2
  21. backrefs-8.2/zensical.yml +178 -0
  22. backrefs-8.0/docs/theme/assets/pymdownx-extras/extra-95634471d6.css +0 -2
  23. backrefs-8.0/docs/theme/assets/pymdownx-extras/extra-loader-Ccztcqfq.js +0 -4
  24. backrefs-8.0/docs/theme/main.html +0 -17
  25. backrefs-8.0/requirements/docs.txt +0 -4
  26. backrefs-8.0/requirements/lint.txt +0 -1
  27. backrefs-8.0/requirements/test.txt +0 -4
  28. {backrefs-8.0 → backrefs-8.2}/.coveragerc +0 -0
  29. {backrefs-8.0 → backrefs-8.2}/.gitignore +0 -0
  30. {backrefs-8.0 → backrefs-8.2}/LICENSE.md +0 -0
  31. {backrefs-8.0 → backrefs-8.2}/README.md +0 -0
  32. {backrefs-8.0 → backrefs-8.2}/backrefs/__init__.py +0 -0
  33. {backrefs-8.0 → backrefs-8.2}/backrefs/_bregex_typing.py +0 -0
  34. {backrefs-8.0 → backrefs-8.2}/backrefs/bre.py +0 -0
  35. {backrefs-8.0 → backrefs-8.2}/backrefs/bregex.py +0 -0
  36. {backrefs-8.0 → backrefs-8.2}/backrefs/py.typed +0 -0
  37. {backrefs-8.0 → backrefs-8.2}/backrefs/uniprops/__init__.py +0 -0
  38. {backrefs-8.0 → backrefs-8.2}/docs/src/markdown/.snippets/links.md +0 -0
  39. {backrefs-8.0 → backrefs-8.2}/docs/src/markdown/about/license.md +0 -0
  40. {backrefs-8.0 → backrefs-8.2}/docs/src/markdown/about/security.md +0 -0
  41. {backrefs-8.0 → backrefs-8.2}/docs/src/markdown/index.md +0 -0
  42. {backrefs-8.0 → backrefs-8.2}/docs/theme/announce.html +0 -0
  43. {backrefs-8.0 → backrefs-8.2}/tests/__init__.py +0 -0
  44. {backrefs-8.0 → backrefs-8.2}/tests/test_age.py +0 -0
  45. {backrefs-8.0 → backrefs-8.2}/tests/test_bidipairedbrackettype.py +0 -0
  46. {backrefs-8.0 → backrefs-8.2}/tests/test_binary.py +0 -0
  47. {backrefs-8.0 → backrefs-8.2}/tests/test_block.py +0 -0
  48. {backrefs-8.0 → backrefs-8.2}/tests/test_canonicalcombiningclass.py +0 -0
  49. {backrefs-8.0 → backrefs-8.2}/tests/test_decompositiontype.py +0 -0
  50. {backrefs-8.0 → backrefs-8.2}/tests/test_eastasianwidth.py +0 -0
  51. {backrefs-8.0 → backrefs-8.2}/tests/test_graphemeclusterbreak.py +0 -0
  52. {backrefs-8.0 → backrefs-8.2}/tests/test_hangulsyllabletype.py +0 -0
  53. {backrefs-8.0 → backrefs-8.2}/tests/test_indicpositionalcategory.py +0 -0
  54. {backrefs-8.0 → backrefs-8.2}/tests/test_indicsylabiccategory.py +0 -0
  55. {backrefs-8.0 → backrefs-8.2}/tests/test_joininggroup.py +0 -0
  56. {backrefs-8.0 → backrefs-8.2}/tests/test_joiningtype.py +0 -0
  57. {backrefs-8.0 → backrefs-8.2}/tests/test_linebreak.py +0 -0
  58. {backrefs-8.0 → backrefs-8.2}/tests/test_numerictype.py +0 -0
  59. {backrefs-8.0 → backrefs-8.2}/tests/test_numericvalue.py +0 -0
  60. {backrefs-8.0 → backrefs-8.2}/tests/test_quickcheck.py +0 -0
  61. {backrefs-8.0 → backrefs-8.2}/tests/test_script.py +0 -0
  62. {backrefs-8.0 → backrefs-8.2}/tests/test_scriptextensions.py +0 -0
  63. {backrefs-8.0 → backrefs-8.2}/tests/test_sentencebreak.py +0 -0
  64. {backrefs-8.0 → backrefs-8.2}/tests/test_uniprops.py +0 -0
  65. {backrefs-8.0 → backrefs-8.2}/tests/test_versions.py +0 -0
  66. {backrefs-8.0 → backrefs-8.2}/tests/test_verticalorientation.py +0 -0
  67. {backrefs-8.0 → backrefs-8.2}/tests/test_wordbreak.py +0 -0
  68. {backrefs-8.0 → backrefs-8.2}/tools/__init__.py +0 -0
  69. {backrefs-8.0 → backrefs-8.2}/tools/unicodedata/13.0.0.zip +0 -0
  70. {backrefs-8.0 → backrefs-8.2}/tools/unicodedata/14.0.0.zip +0 -0
  71. {backrefs-8.0 → backrefs-8.2}/tools/unicodedata/15.0.0.zip +0 -0
  72. {backrefs-8.0 → backrefs-8.2}/tools/unicodedata/15.1.0.zip +0 -0
  73. {backrefs-8.0 → backrefs-8.2}/tools/unicodedata/16.0.0.zip +0 -0
  74. {backrefs-8.0 → backrefs-8.2}/tools/unicodedata/17.0.0.zip +0 -0
  75. {backrefs-8.0 → backrefs-8.2}/tools/unicodedata/LICENSE +0 -0
  76. {backrefs-8.0 → backrefs-8.2}/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
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: backrefs
3
- Version: 8.0
3
+ Version: 8.2
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,14 +13,14 @@ 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
20
19
  Classifier: Programming Language :: Python :: 3.14
20
+ Classifier: Programming Language :: Python :: 3.15
21
21
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
22
22
  Classifier: Typing :: Typed
23
- Requires-Python: >=3.10
23
+ Requires-Python: >=3.11
24
24
  Provides-Extra: extras
25
25
  Requires-Dist: regex; extra == 'extras'
26
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, 0, 0, "final")
196
+ __version_info__ = Version(8, 2, 0, "final")
197
197
  __version__ = __version_info__._get_canonical()
@@ -10,7 +10,6 @@ import sys
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
 
@@ -37,7 +36,7 @@ _WORD = _LETTERS_UNDERSCORE | _DIGIT
37
36
  _STANDARD_ESCAPES = frozenset(('a', 'b', 'f', 'n', 'r', 't', 'v'))
38
37
  _CURLY_BRACKETS = frozenset(('{', '}'))
39
38
  _PROPERTY_STRIP = frozenset((' ', '-', '_'))
40
- _PROPERTY = _WORD | _DIGIT | _PROPERTY_STRIP
39
+ _PROPERTY = _WORD | _DIGIT | _PROPERTY_STRIP | frozenset(('.',))
41
40
  _SCOPED_FLAGS = frozenset(('a', 'i', 'L', 'm', 'u', 's', 'x'))
42
41
  _SCOPED_INV_FLAGS = frozenset(('i', 'm', 's', 'x'))
43
42
  _SCOPED_END = frozenset((':', ')'))
@@ -176,7 +175,7 @@ class _SearchParser(Generic[AnyStr]):
176
175
  value = []
177
176
  try:
178
177
  c = next(i)
179
- if c.upper() in _ASCII_LETTERS:
178
+ if not brackets and c.upper() in _ASCII_LETTERS:
180
179
  prop.append(c)
181
180
  elif (not brackets and c != '{') or (brackets and c != ':'):
182
181
  raise PatternError(f"Unicode property missing '{{' at {i.index - 1}!")
@@ -533,7 +532,7 @@ class _SearchParser(Generic[AnyStr]):
533
532
  def unicode_name(self, name: str, in_group: bool = False) -> list[str]:
534
533
  """Insert Unicode value by its name."""
535
534
 
536
- value = ord(_unicodedata.lookup(name))
535
+ value = ord(_util.unicode_char(name))
537
536
  if self.is_bytes and value > 0xFF:
538
537
  if not in_group:
539
538
  return ['(?!)']
@@ -755,7 +754,7 @@ class _ReplaceParser(Generic[AnyStr]):
755
754
  else:
756
755
  width.append(fill)
757
756
  fill = None
758
- else:
757
+ elif c != '}':
759
758
  fill = c
760
759
  c = self.format_next(i)
761
760
  if fill == 's' and c == '}':
@@ -852,6 +851,18 @@ class _ReplaceParser(Generic[AnyStr]):
852
851
 
853
852
  return ''.join(value) if value else None
854
853
 
854
+ def append_chars(self, text: str, i: _util.StringIter) -> None:
855
+ """Append each character of an escape value as a template escape."""
856
+
857
+ for c in text:
858
+ value = ord(c)
859
+ if self.use_format and value in _CURLY_BRACKETS_ORD:
860
+ self.handle_format(c, i)
861
+ elif value <= 0xFF:
862
+ self.result.append(f'\\{value:03o}')
863
+ else:
864
+ self.result.append(c)
865
+
855
866
  def parse_octal(self, text: str, i: _util.StringIter) -> None:
856
867
  """Parse octal value."""
857
868
 
@@ -863,15 +874,12 @@ class _ReplaceParser(Generic[AnyStr]):
863
874
  single = self.get_single_stack()
864
875
  if self.span_stack:
865
876
  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)
877
+ text = self.convert_case(text, single) if single is not None else text
867
878
  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}')
879
+ text = self.convert_case(chr(value), single)
873
880
  else:
874
- self.result.append(chr(value))
881
+ text = chr(value)
882
+ self.append_chars(text, i)
875
883
 
876
884
  def get_named_unicode(self, i: _util.StringIter) -> str:
877
885
  """Get named Unicode."""
@@ -893,19 +901,16 @@ class _ReplaceParser(Generic[AnyStr]):
893
901
  def parse_named_unicode(self, i: _util.StringIter) -> None:
894
902
  """Parse named Unicode."""
895
903
 
896
- value = ord(_unicodedata.lookup(self.get_named_unicode(i)))
904
+ value = ord(_util.unicode_char(self.get_named_unicode(i)))
897
905
  single = self.get_single_stack()
898
906
  if self.span_stack:
899
907
  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)
908
+ text = self.convert_case(text, single) if single is not None else text
901
909
  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}')
910
+ text = self.convert_case(chr(value), single)
907
911
  else:
908
- self.result.append(chr(value))
912
+ text = chr(value)
913
+ self.append_chars(text, i)
909
914
 
910
915
  def get_wide_unicode(self, i: _util.StringIter) -> str:
911
916
  """Get narrow Unicode."""
@@ -952,15 +957,12 @@ class _ReplaceParser(Generic[AnyStr]):
952
957
  single = self.get_single_stack()
953
958
  if self.span_stack:
954
959
  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)
960
+ text = self.convert_case(text, single) if single is not None else text
956
961
  elif single:
957
- value = ord(self.convert_case(chr(value), single))
958
- if self.use_format and value in _CURLY_BRACKETS_ORD:
959
- self.handle_format(chr(value), i)
960
- elif value <= 0xFF:
961
- self.result.append(f'\\{value:03o}')
962
+ text = self.convert_case(chr(value), single)
962
963
  else:
963
- self.result.append(chr(value))
964
+ text = chr(value)
965
+ self.append_chars(text, i)
964
966
 
965
967
  def get_byte(self, i: _util.StringIter) -> str:
966
968
  """Get byte."""
@@ -981,13 +983,12 @@ class _ReplaceParser(Generic[AnyStr]):
981
983
  single = self.get_single_stack()
982
984
  if self.span_stack:
983
985
  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)
986
+ text = self.convert_case(text, single) if single is not None else text
985
987
  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)
988
+ text = self.convert_case(chr(value), single)
989
989
  else:
990
- self.result.append(f'\\{value:03o}')
990
+ text = chr(value)
991
+ self.append_chars(text, i)
991
992
 
992
993
  def get_named_group(self, t: str, i: _util.StringIter) -> str:
993
994
  """Get group number."""
@@ -1004,16 +1005,18 @@ class _ReplaceParser(Generic[AnyStr]):
1004
1005
  value.append(c)
1005
1006
  c = next(i)
1006
1007
  while c != '>':
1007
- if c in _DIGIT:
1008
- value.append(c)
1008
+ if c not in _DIGIT:
1009
+ raise PatternError(f"Bad character in group name at {i.index - 1}!")
1010
+ value.append(c)
1009
1011
  c = next(i)
1010
1012
  value.append(c)
1011
1013
  elif c in _LETTERS_UNDERSCORE:
1012
1014
  value.append(c)
1013
1015
  c = next(i)
1014
1016
  while c != '>':
1015
- if c in _WORD:
1016
- value.append(c)
1017
+ if c not in _WORD:
1018
+ raise PatternError(f"Bad character in group name at {i.index - 1}!")
1019
+ value.append(c)
1017
1020
  c = next(i)
1018
1021
  value.append(c)
1019
1022
  else:
@@ -1062,7 +1065,7 @@ class _ReplaceParser(Generic[AnyStr]):
1062
1065
  elif not self.is_bytes and t == "u":
1063
1066
  value = chr(int(self.get_narrow_unicode(i), 16))
1064
1067
  elif not self.is_bytes and t == "N":
1065
- value = _unicodedata.lookup(self.get_named_unicode(i))
1068
+ value = _util.unicode_char(self.get_named_unicode(i))
1066
1069
  elif t == "x":
1067
1070
  value = chr(int(self.get_byte(i), 16))
1068
1071
  else:
@@ -5,7 +5,6 @@ Licensed under MIT
5
5
  Copyright (c) 2011 - 2020 Isaac Muse <isaacmuse@gmail.com>
6
6
  """
7
7
  from __future__ import annotations
8
- import unicodedata as _unicodedata
9
8
  import copyreg as _copyreg
10
9
  from . import util as _util
11
10
  from .util import PatternError
@@ -599,7 +598,7 @@ class _ReplaceParser(Generic[AnyStr]):
599
598
  else:
600
599
  width.append(fill)
601
600
  fill = None
602
- else:
601
+ elif c != '}':
603
602
  fill = c
604
603
  c = self.format_next(i)
605
604
  if fill == 's' and c == '}':
@@ -696,6 +695,18 @@ class _ReplaceParser(Generic[AnyStr]):
696
695
 
697
696
  return ''.join(value) if value else None
698
697
 
698
+ def append_chars(self, text: str, i: _util.StringIter) -> None:
699
+ """Append each character of an escape value as a template escape."""
700
+
701
+ for c in text:
702
+ value = ord(c)
703
+ if self.use_format and value in _CURLY_BRACKETS_ORD:
704
+ self.handle_format(c, i)
705
+ elif value <= 0xFF:
706
+ self.result.append(f'\\{value:03o}')
707
+ else:
708
+ self.result.append(c)
709
+
699
710
  def parse_octal(self, text: str, i: _util.StringIter) -> None:
700
711
  """Parse octal value."""
701
712
 
@@ -707,15 +718,12 @@ class _ReplaceParser(Generic[AnyStr]):
707
718
  single = self.get_single_stack()
708
719
  if self.span_stack:
709
720
  text = self.convert_case(chr(value), self.span_stack[-1])
710
- value = ord(self.convert_case(text, single)) if single is not None else ord(text)
721
+ text = self.convert_case(text, single) if single is not None else text
711
722
  elif single:
712
- value = ord(self.convert_case(chr(value), single))
713
- if self.use_format and value in _CURLY_BRACKETS_ORD:
714
- self.handle_format(chr(value), i)
715
- elif value <= 0xFF:
716
- self.result.append(f'\\{value:03o}')
723
+ text = self.convert_case(chr(value), single)
717
724
  else:
718
- self.result.append(chr(value))
725
+ text = chr(value)
726
+ self.append_chars(text, i)
719
727
 
720
728
  def get_named_unicode(self, i: _util.StringIter) -> str:
721
729
  """Get named Unicode."""
@@ -737,19 +745,16 @@ class _ReplaceParser(Generic[AnyStr]):
737
745
  def parse_named_unicode(self, i: _util.StringIter) -> None:
738
746
  """Parse named Unicode."""
739
747
 
740
- value = ord(_unicodedata.lookup(self.get_named_unicode(i)))
748
+ value = ord(_util.unicode_char(self.get_named_unicode(i)))
741
749
  single = self.get_single_stack()
742
750
  if self.span_stack:
743
751
  text = self.convert_case(chr(value), self.span_stack[-1])
744
- value = ord(self.convert_case(text, single)) if single is not None else ord(text)
752
+ text = self.convert_case(text, single) if single is not None else text
745
753
  elif single:
746
- value = ord(self.convert_case(chr(value), single))
747
- if self.use_format and value in _CURLY_BRACKETS_ORD:
748
- self.handle_format(chr(value), i)
749
- elif value <= 0xFF:
750
- self.result.append(f'\\{value:03o}')
754
+ text = self.convert_case(chr(value), single)
751
755
  else:
752
- self.result.append(chr(value))
756
+ text = chr(value)
757
+ self.append_chars(text, i)
753
758
 
754
759
  def get_wide_unicode(self, i: _util.StringIter) -> str:
755
760
  """Get narrow Unicode."""
@@ -796,15 +801,12 @@ class _ReplaceParser(Generic[AnyStr]):
796
801
  single = self.get_single_stack()
797
802
  if self.span_stack:
798
803
  text = self.convert_case(chr(value), self.span_stack[-1])
799
- value = ord(self.convert_case(text, single)) if single is not None else ord(text)
804
+ text = self.convert_case(text, single) if single is not None else text
800
805
  elif single:
801
- value = ord(self.convert_case(chr(value), single))
802
- if self.use_format and value in _CURLY_BRACKETS_ORD:
803
- self.handle_format(chr(value), i)
804
- elif value <= 0xFF:
805
- self.result.append(f'\\{value:03o}')
806
+ text = self.convert_case(chr(value), single)
806
807
  else:
807
- self.result.append(chr(value))
808
+ text = chr(value)
809
+ self.append_chars(text, i)
808
810
 
809
811
  def get_byte(self, i: _util.StringIter) -> str:
810
812
  """Get byte."""
@@ -825,13 +827,12 @@ class _ReplaceParser(Generic[AnyStr]):
825
827
  single = self.get_single_stack()
826
828
  if self.span_stack:
827
829
  text = self.convert_case(chr(value), self.span_stack[-1])
828
- value = ord(self.convert_case(text, single)) if single is not None else ord(text)
830
+ text = self.convert_case(text, single) if single is not None else text
829
831
  elif single:
830
- value = ord(self.convert_case(chr(value), single))
831
- if self.use_format and value in _CURLY_BRACKETS_ORD:
832
- self.handle_format(chr(value), i)
832
+ text = self.convert_case(chr(value), single)
833
833
  else:
834
- self.result.append(f'\\{value:03o}')
834
+ text = chr(value)
835
+ self.append_chars(text, i)
835
836
 
836
837
  def get_named_group(self, t: str, i: _util.StringIter) -> str:
837
838
  """Get group number."""
@@ -848,16 +849,18 @@ class _ReplaceParser(Generic[AnyStr]):
848
849
  value.append(c)
849
850
  c = next(i)
850
851
  while c != '>':
851
- if c in _DIGIT:
852
- value.append(c)
852
+ if c not in _DIGIT:
853
+ raise PatternError(f"Bad character in group name at {i.index - 1}!")
854
+ value.append(c)
853
855
  c = next(i)
854
856
  value.append(c)
855
857
  elif c in _LETTERS_UNDERSCORE:
856
858
  value.append(c)
857
859
  c = next(i)
858
860
  while c != '>':
859
- if c in _WORD:
860
- value.append(c)
861
+ if c not in _WORD:
862
+ raise PatternError(f"Bad character in group name at {i.index - 1}!")
863
+ value.append(c)
861
864
  c = next(i)
862
865
  value.append(c)
863
866
  else:
@@ -906,7 +909,7 @@ class _ReplaceParser(Generic[AnyStr]):
906
909
  elif not self.is_bytes and t == "u":
907
910
  value = chr(int(self.get_narrow_unicode(i), 16))
908
911
  elif not self.is_bytes and t == "N":
909
- value = _unicodedata.lookup(self.get_named_unicode(i))
912
+ value = _util.unicode_char(self.get_named_unicode(i))
910
913
  elif t == "x":
911
914
  value = chr(int(self.get_byte(i), 16))
912
915
  else:
@@ -7,6 +7,7 @@ Copyright (c) 2015 - 2020 Isaac Muse <isaacmuse@gmail.com>
7
7
  from __future__ import annotations
8
8
  import warnings
9
9
  import sys
10
+ import unicodedata as _unicodedata
10
11
  from typing import Any, Callable, AnyStr
11
12
 
12
13
  PY311 = (3, 11) <= sys.version_info
@@ -23,6 +24,15 @@ class PatternError(Exception):
23
24
  """Pattern error."""
24
25
 
25
26
 
27
+ def unicode_char(name: str) -> str:
28
+ """Unicode character by its official name, raising our pattern error when no character has it."""
29
+
30
+ try:
31
+ return _unicodedata.lookup(name)
32
+ except KeyError as e:
33
+ raise PatternError(f"Undefined Unicode character name {name!r}!") from e
34
+
35
+
26
36
  class StringIter:
27
37
  """Preprocess replace tokens."""
28
38
 
@@ -0,0 +1,67 @@
1
+ API
2
+ Accessors
3
+ Backport
4
+ Backrefs
5
+ Bre
6
+ Bregex
7
+ CVE
8
+ Changelog
9
+ EmojiOne
10
+ Feb
11
+ Formatter
12
+ GitHub
13
+ Gitter
14
+ Jan
15
+ Jun
16
+ MERCHANTABILITY
17
+ Mar
18
+ MkDocs
19
+ NONINFRINGEMENT
20
+ Posix
21
+ PyMdown
22
+ PyPI
23
+ PyPI's
24
+ Regex
25
+ Regex's
26
+ Sep
27
+ Tidelift
28
+ Tox
29
+ Twemoji
30
+ Zensical
31
+ accessor
32
+ backend
33
+ boolean
34
+ decrement
35
+ deprecations
36
+ emoji
37
+ enablement
38
+ formatter
39
+ grapheme
40
+ hashable
41
+ incrementing
42
+ namespaced
43
+ natively
44
+ parsers
45
+ plugin
46
+ posix
47
+ postfix
48
+ pre
49
+ preprocess
50
+ preprocesses
51
+ preprocessing
52
+ preprocessor
53
+ prerelease
54
+ prereleases
55
+ refactored
56
+ reinstalling
57
+ reparsing
58
+ requesters
59
+ sublicense
60
+ subpattern
61
+ subpatterns
62
+ tuple
63
+ uncompiled
64
+ unescaped
65
+ unterminated
66
+ validator
67
+ whitespace
@@ -0,0 +1,15 @@
1
+ BOM
2
+ BOMs
3
+ Bre
4
+ Preprocess
5
+ UTF
6
+ backref
7
+ backrefs
8
+ bidi
9
+ bregex
10
+ builtin
11
+ enum
12
+ expander
13
+ regex
14
+ sortable
15
+ whitespace
@@ -3,6 +3,27 @@ icon: lucide/scroll-text
3
3
  ---
4
4
  # Changelog
5
5
 
6
+ ## 8.2
7
+
8
+ - **NEW**: Drop Python 3.10.
9
+ - **FIX**: Fix error in replace templates when casing a character escape gives two characters (`\C\xdf\E`).
10
+ - **FIX**: Fix wrong text or errors when case conversion of a `\x` escape exceeds the byte range in replace templates.
11
+ - **FIX**: Fix an empty format specification (`{1:}`) raising an error in format replace templates.
12
+ - **FIX**: Fix Unicode properties, such as general category `\p{L}` and `\p{Bidi_Class=L}`, not matching ideographs,
13
+ Hangul syllables, and other characters Unicode defines as ranges.
14
+ - **FIX**: Fix `\g<...>` references in replace templates silently dropping invalid characters in a group name
15
+ instead of raising an error.
16
+ - **FIX**: Fix `\p{...}` Unicode properties rejecting the canonical dotted value form (e.g. `Age=1.1`),
17
+ which previously only worked through the alias form (e.g. `V1_1`).
18
+ - **FIX**: Fix issue where Unicode properties within character classes (e.g. `[x[:property:]]`) were incorrectly
19
+ detected with `[x[l]]` (@rahul05ranjan).
20
+ - **FIX**: Fix an undefined Unicode name in `\N{...}` raising a `KeyError` instead of `PatternError`
21
+ (@rahul05ranjan).
22
+
23
+ ## 8.1
24
+
25
+ - **NEW**: Add official support for Python 3.15.
26
+
6
27
  ## 8.0
7
28
 
8
29
  - **NEW**: Backrefs will preemptively fail, raising a `PatternError`, if parsing and finding an unterminated character
@@ -132,7 +153,7 @@ icon: lucide/scroll-text
132
153
  to improve result.
133
154
  - **NEW**: POSIX style properties now handle all existing Unicode properties.
134
155
  - **NEW**: POSIX properties now follow the [Unicode specification for POSIX compatibility][unicode-posix].
135
- Read the [documentation](../refs.md#posix-style-properties) to learn more.
156
+ Read the [documentation](../refs.md#posix-character-classes) to learn more.
136
157
  - **NEW**: Unicode properties are now sensitive to the `ASCII` flag and will properly restrict the range of properties
137
158
  to the ASCII range even in Unicode strings.
138
159
  - **NEW**: Removed the old deprecated search references: `\l`, `\L`, `\c`, and `\C`. These are available in various
@@ -45,6 +45,12 @@ any tier you feel comfortable with. No amount is too little. We also accept one
45
45
  Take part in reviewing pull requests and/or reviewing direct commits. Make suggestions to improve the code and discuss
46
46
  solutions to overcome weakness in the algorithm.
47
47
 
48
+ ## Answer Questions in Issues
49
+
50
+ Take time and answer questions and offer suggestions to people who've created issues in the issue tracker. Often people
51
+ will have questions that you might have an answer for. Or maybe you know how to help them accomplish a specific task
52
+ they are asking about. Feel free to share your experience to help others.
53
+
48
54
  ## Pull Requests
49
55
 
50
56
  Pull requests are welcome, and if you plan on contributing directly to the code, there are a couple of things to be
@@ -55,23 +61,28 @@ tests will automatically be run, and the request must pass to be accepted. You
55
61
  pull requesting. If it is not possible to run these tests locally, they will be run when the pull request is made, but
56
62
  it is strongly suggested that requesters make an effort to verify before requesting to allow for a quick, smooth merge.
57
63
 
58
- Feel free to use a virtual environment if you are concerned about installing any of the Python packages.
59
-
60
64
  ### Running Validation Tests
61
65
 
62
- 1. Make sure that [Tox][tox] is installed:
66
+ In order to preserve good code health, a test suite has been put together with `pytest` (@pytest-dev/pytest). There are
67
+ currently two kinds of tests: syntax and targeted. To run these tests, you can use the following command:
63
68
 
64
- ```
65
- pip install tox
66
- ```
69
+ If you wish to run the tests locally, just run:
67
70
 
68
- 2. Run Tox:
71
+ ```console
72
+ $ hatch run +py=3.14 dev:tests
73
+ ```
69
74
 
70
- ```
71
- tox
72
- ```
75
+ Coding standards are enforced using @astral-sh/ruff. The environment can be setup and run as shown below.
73
76
 
74
- Tox should install necessary dependencies and run the tests.
77
+ ```console
78
+ $ hatch run +py=3.14 dev:lint
79
+ ```
80
+
81
+ We use @python/mypy to enforce typing. It can be run as shown below.
82
+
83
+ ```console
84
+ $ hatch run +py=3.14 dev:mypy
85
+ ```
75
86
 
76
87
  ## Documentation Improvements
77
88
 
@@ -80,7 +91,23 @@ you feel it is still lacking, show your appreciation for the plugin by helping t
80
91
  documentation is always appreciated and can be done via pull requests. There shouldn't be any need to run validation
81
92
  tests if only updating documentation.
82
93
 
83
- You don't have to render the docs locally before pull requesting, but if you wish to, I currently use a combination of
84
- [MkDocs][mkdocs], the [Material theme][mkdocs-material], and [PyMdown Extensions][pymdown-extensions] to render the
85
- docs. You can preview the docs if you install these two packages. The command for previewing the docs is `mkdocs serve`
86
- from the root directory. You can then view the documents at `localhost:8000`.
94
+ Documents are in Markdown (with some additional syntax) and converted to HTML via Python Markdown and this extension
95
+ bundle. The documentation site is built with @zensical/zensical.
96
+
97
+ To build docs:
98
+
99
+ ```console
100
+ $ hatch run docs:build
101
+ ```
102
+
103
+ To serve docs and to live preview in a browser:
104
+
105
+ ```console
106
+ $ hatch run docs:serve
107
+ ```
108
+
109
+ To clean the documents:
110
+
111
+ ```console
112
+ $ hatch run docs:clean
113
+ ```
@@ -23,7 +23,7 @@ Back\ References | Description
23
23
  `\pX` | Unicode property character class where `X` is the uppercase letter that represents the General Category property. For instance, `\pL` would be equivalent to `\p{L}` or `\p{Letter}`.
24
24
  `\P{UnicodeProperty}` | Inverse Unicode property character class. Can be used in character classes `[]`. See [Unicode Properties](#unicode-properties) for more info.
25
25
  `\PX` | Inverse Unicode property character class where `X` is the uppercase letter that represents the General Category property. For instance, `\PL` would be equivalent to `\P{L}` or `\P{Letter}`.
26
- `[[:alnum:]]` | Though not really a back reference, support for POSIX style character classes is available. See [POSIX Style Properties](#posix-style-properties) for more info.
26
+ `[[:alnum:]]` | Though not really a back reference, support for POSIX style character classes is available. See [POSIX Style Properties](#posix-character-classes) for more info.
27
27
  `\N{UnicodeName}` | Named characters are normally ignored in Re, but Backrefs adds support for them.
28
28
  `\m` | Start word boundary. Translates to `\b(?=\w)`.
29
29
  `\M` | End word boundary. Translates to `\b(?<=\w)`.
@@ -92,7 +92,7 @@ implementation of Unicode properties. Some properties may not be available on ce
92
92
  included Unicode build.
93
93
 
94
94
  It is important to note that Backrefs handles Unicode properties by transforming them to character classes with all the
95
- associated characters: `\p{Cs}` --> `[\ud800\udb7f-\udb80\udbff-\udc00\udfff]`. Because of this, Backrefs can create
95
+ associated characters: `\p{Cs}` --> `[\ud800-\udfff]`. Because of this, Backrefs can create
96
96
  really large regular expressions that the underlying engine must walk through. In short, Re with Backrefs will never be
97
97
  as efficient or fast as using Regex's Unicode properties, but it is very useful when you need or want to use Re.
98
98