backrefs 8.1__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.
- {backrefs-8.1 → backrefs-8.2}/.pyspelling.yml +2 -3
- {backrefs-8.1 → backrefs-8.2}/PKG-INFO +2 -3
- {backrefs-8.1 → backrefs-8.2}/backrefs/__meta__.py +1 -1
- {backrefs-8.1 → backrefs-8.2}/backrefs/_bre_parse.py +40 -37
- {backrefs-8.1 → backrefs-8.2}/backrefs/_bregex_parse.py +37 -34
- {backrefs-8.1 → backrefs-8.2}/backrefs/util.py +10 -0
- backrefs-8.2/docs/src/dictionary/en-custom.txt +67 -0
- backrefs-8.2/docs/src/dictionary/en-python.txt +15 -0
- {backrefs-8.1 → backrefs-8.2}/docs/src/markdown/about/changelog.md +17 -0
- {backrefs-8.1 → backrefs-8.2}/docs/src/markdown/refs.md +1 -1
- {backrefs-8.1 → backrefs-8.2}/hatch_build.py +0 -1
- {backrefs-8.1 → backrefs-8.2}/pyproject.toml +6 -7
- {backrefs-8.1 → backrefs-8.2}/tests/test_bidiclass.py +7 -0
- {backrefs-8.1 → backrefs-8.2}/tests/test_bre.py +103 -0
- {backrefs-8.1 → backrefs-8.2}/tests/test_bregex.py +61 -0
- {backrefs-8.1 → backrefs-8.2}/tests/test_generalcategory.py +8 -0
- {backrefs-8.1 → backrefs-8.2}/tools/unipropgen.py +18 -2
- backrefs-8.2/zensical.yml +178 -0
- {backrefs-8.1 → backrefs-8.2}/.coveragerc +0 -0
- {backrefs-8.1 → backrefs-8.2}/.gitignore +0 -0
- {backrefs-8.1 → backrefs-8.2}/LICENSE.md +0 -0
- {backrefs-8.1 → backrefs-8.2}/README.md +0 -0
- {backrefs-8.1 → backrefs-8.2}/backrefs/__init__.py +0 -0
- {backrefs-8.1 → backrefs-8.2}/backrefs/_bregex_typing.py +0 -0
- {backrefs-8.1 → backrefs-8.2}/backrefs/bre.py +0 -0
- {backrefs-8.1 → backrefs-8.2}/backrefs/bregex.py +0 -0
- {backrefs-8.1 → backrefs-8.2}/backrefs/py.typed +0 -0
- {backrefs-8.1 → backrefs-8.2}/backrefs/uniprops/__init__.py +0 -0
- {backrefs-8.1 → backrefs-8.2}/docs/src/markdown/.snippets/links.md +0 -0
- {backrefs-8.1 → backrefs-8.2}/docs/src/markdown/about/contributing.md +0 -0
- {backrefs-8.1 → backrefs-8.2}/docs/src/markdown/about/license.md +0 -0
- {backrefs-8.1 → backrefs-8.2}/docs/src/markdown/about/security.md +0 -0
- {backrefs-8.1 → backrefs-8.2}/docs/src/markdown/index.md +0 -0
- {backrefs-8.1 → backrefs-8.2}/docs/src/markdown/usage.md +0 -0
- {backrefs-8.1 → backrefs-8.2}/docs/theme/announce.html +0 -0
- {backrefs-8.1 → backrefs-8.2}/tests/__init__.py +0 -0
- {backrefs-8.1 → backrefs-8.2}/tests/test_age.py +0 -0
- {backrefs-8.1 → backrefs-8.2}/tests/test_bidipairedbrackettype.py +0 -0
- {backrefs-8.1 → backrefs-8.2}/tests/test_binary.py +0 -0
- {backrefs-8.1 → backrefs-8.2}/tests/test_block.py +0 -0
- {backrefs-8.1 → backrefs-8.2}/tests/test_canonicalcombiningclass.py +0 -0
- {backrefs-8.1 → backrefs-8.2}/tests/test_decompositiontype.py +0 -0
- {backrefs-8.1 → backrefs-8.2}/tests/test_eastasianwidth.py +0 -0
- {backrefs-8.1 → backrefs-8.2}/tests/test_graphemeclusterbreak.py +0 -0
- {backrefs-8.1 → backrefs-8.2}/tests/test_hangulsyllabletype.py +0 -0
- {backrefs-8.1 → backrefs-8.2}/tests/test_indicpositionalcategory.py +0 -0
- {backrefs-8.1 → backrefs-8.2}/tests/test_indicsylabiccategory.py +0 -0
- {backrefs-8.1 → backrefs-8.2}/tests/test_joininggroup.py +0 -0
- {backrefs-8.1 → backrefs-8.2}/tests/test_joiningtype.py +0 -0
- {backrefs-8.1 → backrefs-8.2}/tests/test_linebreak.py +0 -0
- {backrefs-8.1 → backrefs-8.2}/tests/test_numerictype.py +0 -0
- {backrefs-8.1 → backrefs-8.2}/tests/test_numericvalue.py +0 -0
- {backrefs-8.1 → backrefs-8.2}/tests/test_quickcheck.py +0 -0
- {backrefs-8.1 → backrefs-8.2}/tests/test_script.py +0 -0
- {backrefs-8.1 → backrefs-8.2}/tests/test_scriptextensions.py +0 -0
- {backrefs-8.1 → backrefs-8.2}/tests/test_sentencebreak.py +0 -0
- {backrefs-8.1 → backrefs-8.2}/tests/test_uniprops.py +0 -0
- {backrefs-8.1 → backrefs-8.2}/tests/test_versions.py +0 -0
- {backrefs-8.1 → backrefs-8.2}/tests/test_verticalorientation.py +0 -0
- {backrefs-8.1 → backrefs-8.2}/tests/test_wordbreak.py +0 -0
- {backrefs-8.1 → backrefs-8.2}/tools/__init__.py +0 -0
- {backrefs-8.1 → backrefs-8.2}/tools/pycon.py +0 -0
- {backrefs-8.1 → backrefs-8.2}/tools/unicodedata/13.0.0.zip +0 -0
- {backrefs-8.1 → backrefs-8.2}/tools/unicodedata/14.0.0.zip +0 -0
- {backrefs-8.1 → backrefs-8.2}/tools/unicodedata/15.0.0.zip +0 -0
- {backrefs-8.1 → backrefs-8.2}/tools/unicodedata/15.1.0.zip +0 -0
- {backrefs-8.1 → backrefs-8.2}/tools/unicodedata/16.0.0.zip +0 -0
- {backrefs-8.1 → backrefs-8.2}/tools/unicodedata/17.0.0.zip +0 -0
- {backrefs-8.1 → backrefs-8.2}/tools/unicodedata/LICENSE +0 -0
- {backrefs-8.1 → 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
|
|
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.
|
|
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,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.
|
|
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,
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
877
|
+
text = self.convert_case(text, single) if single is not None else text
|
|
867
878
|
elif single:
|
|
868
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
908
|
+
text = self.convert_case(text, single) if single is not None else text
|
|
901
909
|
elif single:
|
|
902
|
-
|
|
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
|
-
|
|
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
|
-
|
|
960
|
+
text = self.convert_case(text, single) if single is not None else text
|
|
956
961
|
elif single:
|
|
957
|
-
|
|
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
|
-
|
|
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
|
-
|
|
986
|
+
text = self.convert_case(text, single) if single is not None else text
|
|
985
987
|
elif single:
|
|
986
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
721
|
+
text = self.convert_case(text, single) if single is not None else text
|
|
711
722
|
elif single:
|
|
712
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
752
|
+
text = self.convert_case(text, single) if single is not None else text
|
|
745
753
|
elif single:
|
|
746
|
-
|
|
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
|
-
|
|
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
|
-
|
|
804
|
+
text = self.convert_case(text, single) if single is not None else text
|
|
800
805
|
elif single:
|
|
801
|
-
|
|
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
|
-
|
|
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
|
-
|
|
830
|
+
text = self.convert_case(text, single) if single is not None else text
|
|
829
831
|
elif single:
|
|
830
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
|
@@ -3,6 +3,23 @@ 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
|
+
|
|
6
23
|
## 8.1
|
|
7
24
|
|
|
8
25
|
- **NEW**: Add official support for Python 3.15.
|
|
@@ -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
|
|
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
|
|
|
@@ -74,7 +74,6 @@ class CustomMetadataHook(MetadataHookInterface):
|
|
|
74
74
|
'License :: OSI Approved :: MIT License',
|
|
75
75
|
'Operating System :: OS Independent',
|
|
76
76
|
'Programming Language :: Python :: 3',
|
|
77
|
-
'Programming Language :: Python :: 3.10',
|
|
78
77
|
'Programming Language :: Python :: 3.11',
|
|
79
78
|
'Programming Language :: Python :: 3.12',
|
|
80
79
|
'Programming Language :: Python :: 3.13',
|
|
@@ -9,7 +9,7 @@ name = "backrefs"
|
|
|
9
9
|
description = "A wrapper around re and regex that adds additional back references."
|
|
10
10
|
readme = "README.md"
|
|
11
11
|
license = "MIT"
|
|
12
|
-
requires-python = ">=3.
|
|
12
|
+
requires-python = ">=3.11"
|
|
13
13
|
authors = [
|
|
14
14
|
{ name = "Isaac Muse", email = "Isaac.Muse@gmail.com" },
|
|
15
15
|
]
|
|
@@ -39,11 +39,10 @@ include = [
|
|
|
39
39
|
"/docs/src/markdown/**/*.md",
|
|
40
40
|
"/docs/src/markdown/**/*.gif",
|
|
41
41
|
"/docs/src/markdown/**/*.png",
|
|
42
|
-
"/docs/src/
|
|
42
|
+
"/docs/src/dictionary/*.txt",
|
|
43
43
|
"/docs/theme/**/*.css",
|
|
44
44
|
"/docs/theme/**/*.js",
|
|
45
45
|
"/docs/theme/**/*.html",
|
|
46
|
-
"/requirements/*.txt",
|
|
47
46
|
"/backrefs/**/*.py",
|
|
48
47
|
"/backrefs/py.typed",
|
|
49
48
|
"/tests/**/*.py",
|
|
@@ -52,7 +51,7 @@ include = [
|
|
|
52
51
|
"/tools/unicodedata/LICENSE",
|
|
53
52
|
"/.pyspelling.yml",
|
|
54
53
|
"/.coveragerc",
|
|
55
|
-
"/
|
|
54
|
+
"/zensical.yml"
|
|
56
55
|
]
|
|
57
56
|
|
|
58
57
|
exclude = [
|
|
@@ -155,10 +154,10 @@ dependencies = [
|
|
|
155
154
|
]
|
|
156
155
|
|
|
157
156
|
[[tool.hatch.envs.dev.matrix]]
|
|
158
|
-
python = ["3.
|
|
157
|
+
python = ["3.11", "3.12", "3.13", "3.14", "3.15"]
|
|
159
158
|
|
|
160
159
|
[[tool.hatch.envs.dev-stdlib.matrix]]
|
|
161
|
-
python = ["3.
|
|
160
|
+
python = ["3.11", "3.12", "3.13", "3.14", "3.15"]
|
|
162
161
|
|
|
163
162
|
[tool.hatch.envs.dev.scripts]
|
|
164
163
|
types = """
|
|
@@ -196,4 +195,4 @@ build = """
|
|
|
196
195
|
python tools/unidatadownload.py && \
|
|
197
196
|
python tools/unipropgen.py backrefs/uniprops/unidata && \
|
|
198
197
|
python -m zensical.main build -f zensical.yml --clean
|
|
199
|
-
"""
|
|
198
|
+
"""
|
|
@@ -50,6 +50,13 @@ class TestBidiClass(unittest.TestCase):
|
|
|
50
50
|
result = uniprops.get_unicode_property('bidiclass', k, mode=uniprops.MODE_ASCII)
|
|
51
51
|
self.assertEqual(result, uniprops.fmt_string(v, True))
|
|
52
52
|
|
|
53
|
+
def test_bidiclass_range(self):
|
|
54
|
+
"""Test characters `UnicodeData.txt` defines with a `First` and a `Last` line."""
|
|
55
|
+
|
|
56
|
+
for char in ('\uac00', '\u4e16', '\ud55c', '\ud7a3'):
|
|
57
|
+
result = re.match('[%s]' % uniprops.get_unicode_property('bidiclass', 'l'), char)
|
|
58
|
+
self.assertTrue(result is not None)
|
|
59
|
+
|
|
53
60
|
def test_bad_bidiclass(self):
|
|
54
61
|
"""Test `Bidi Class` property with bad value."""
|
|
55
62
|
|
|
@@ -161,6 +161,19 @@ class TestSearchTemplate(unittest.TestCase):
|
|
|
161
161
|
with pytest.warns(FutureWarning):
|
|
162
162
|
self.assertTrue(bre.compile(r'[[:graph:a]').match('a') is not None)
|
|
163
163
|
|
|
164
|
+
def test_bare_letter_after_inner_bracket_is_literal(self):
|
|
165
|
+
"""Test that a letter right after an inner `[` is a literal char, not a property."""
|
|
166
|
+
|
|
167
|
+
pattern = bre.compile(r'[x[l]]')
|
|
168
|
+
self.assertTrue(pattern.match('[]') is not None)
|
|
169
|
+
self.assertTrue(pattern.match('l]') is not None)
|
|
170
|
+
self.assertTrue(pattern.match('x]') is not None)
|
|
171
|
+
self.assertTrue(pattern.match('a]') is None)
|
|
172
|
+
pattern = bre.compile(r'[}[d]')
|
|
173
|
+
self.assertTrue(pattern.match('[') is not None)
|
|
174
|
+
self.assertTrue(pattern.match('d') is not None)
|
|
175
|
+
self.assertTrue(pattern.match('}') is not None)
|
|
176
|
+
|
|
164
177
|
def test_unicode_property_failures(self):
|
|
165
178
|
"""Test Unicode property."""
|
|
166
179
|
|
|
@@ -200,6 +213,12 @@ class TestSearchTemplate(unittest.TestCase):
|
|
|
200
213
|
with pytest.raises(PatternError):
|
|
201
214
|
bre.compile(r'\N{A')
|
|
202
215
|
|
|
216
|
+
with pytest.raises(PatternError):
|
|
217
|
+
bre.compile(r'\N{Undefined character}')
|
|
218
|
+
|
|
219
|
+
with pytest.raises(PatternError):
|
|
220
|
+
bre.compile_replace(re.compile(r'(a)'), r'\N{Undefined character}')
|
|
221
|
+
|
|
203
222
|
def test_word_boundary(self):
|
|
204
223
|
"""Test word boundary."""
|
|
205
224
|
|
|
@@ -484,6 +503,31 @@ class TestSearchTemplate(unittest.TestCase):
|
|
|
484
503
|
pattern2 = bre.compile_search('Test [!-\\~]', re.ASCII)
|
|
485
504
|
self.assertEqual(pattern.pattern, pattern2.pattern)
|
|
486
505
|
|
|
506
|
+
def test_age_canonical_value(self):
|
|
507
|
+
"""Test the `UCD` canonical age value form (`1.1`, not just the `V1_1` alias)."""
|
|
508
|
+
|
|
509
|
+
pattern = bre.compile_search(r'\p{Age=1.1}')
|
|
510
|
+
pattern2 = bre.compile_search(r'\p{Age=V1_1}')
|
|
511
|
+
self.assertEqual(pattern.pattern, pattern2.pattern)
|
|
512
|
+
|
|
513
|
+
pattern = bre.compile_search(r'\p{Age:1.1}')
|
|
514
|
+
pattern2 = bre.compile_search(r'\p{Age:V1_1}')
|
|
515
|
+
self.assertEqual(pattern.pattern, pattern2.pattern)
|
|
516
|
+
|
|
517
|
+
def test_age_canonical_value_in_group(self):
|
|
518
|
+
"""Test the canonical age value form inside a character group."""
|
|
519
|
+
|
|
520
|
+
pattern = bre.compile_search(r'[\p{Age=13.0}]')
|
|
521
|
+
pattern2 = bre.compile_search(r'[\p{Age=V13_0}]')
|
|
522
|
+
self.assertEqual(pattern.pattern, pattern2.pattern)
|
|
523
|
+
|
|
524
|
+
def test_age_canonical_value_negated(self):
|
|
525
|
+
"""Test the canonical age value form with a negated property."""
|
|
526
|
+
|
|
527
|
+
pattern = bre.compile_search(r'\p{^Age=1.1}')
|
|
528
|
+
pattern2 = bre.compile_search(r'\p{^Age=V1_1}')
|
|
529
|
+
self.assertEqual(pattern.pattern, pattern2.pattern)
|
|
530
|
+
|
|
487
531
|
def test_not_posix_at_start_group(self):
|
|
488
532
|
"""Test a situation that is not a POSIX at the start of a group."""
|
|
489
533
|
|
|
@@ -1857,6 +1901,8 @@ class TestReplaceTemplate(unittest.TestCase):
|
|
|
1857
1901
|
self.assertEqual(pattern.subf(r'{1:1^30}', 'Testing'), '11111111111111Te11111111111111')
|
|
1858
1902
|
self.assertEqual(pattern.subf(r'{1:<30s}', 'Testing'), 'Te ')
|
|
1859
1903
|
self.assertEqual(pattern.subf(r'{1:s}', 'Testing'), 'Te')
|
|
1904
|
+
self.assertEqual(pattern.subf(r'{1:}', 'Testing'), 'Te')
|
|
1905
|
+
self.assertEqual(pattern.subf(r'{1:}-x', 'Testing'), 'Te-x')
|
|
1860
1906
|
self.assertEqual(pattern.subf(r'{2!r}', 'Testing'), "'st'")
|
|
1861
1907
|
|
|
1862
1908
|
with pytest.raises(PatternError):
|
|
@@ -1889,6 +1935,8 @@ class TestReplaceTemplate(unittest.TestCase):
|
|
|
1889
1935
|
self.assertEqual(pattern.subf(br'{1:1^30}', b'Testing'), b'11111111111111Te11111111111111')
|
|
1890
1936
|
self.assertEqual(pattern.subf(br'{1:<30s}', b'Testing'), b'Te ')
|
|
1891
1937
|
self.assertEqual(pattern.subf(br'{1:s}', b'Testing'), b'Te')
|
|
1938
|
+
self.assertEqual(pattern.subf(br'{1:}', b'Testing'), b'Te')
|
|
1939
|
+
self.assertEqual(pattern.subf(br'{1:}-x', b'Testing'), b'Te-x')
|
|
1892
1940
|
self.assertEqual(pattern.subf(br'{2!r}', b'Testing'), b"b'st'")
|
|
1893
1941
|
|
|
1894
1942
|
with pytest.raises(PatternError):
|
|
@@ -1909,6 +1957,40 @@ class TestReplaceTemplate(unittest.TestCase):
|
|
|
1909
1957
|
with pytest.raises(PatternError):
|
|
1910
1958
|
pattern.subf(br'{:030}', b'Testing')
|
|
1911
1959
|
|
|
1960
|
+
def test_case_hex_above_byte_range(self):
|
|
1961
|
+
"""Test case conversion of hex escapes beyond the byte range."""
|
|
1962
|
+
|
|
1963
|
+
pattern = re.compile('Test')
|
|
1964
|
+
for template, expected in (
|
|
1965
|
+
(r'\c\xb5', '\u039c'),
|
|
1966
|
+
(r'\C\xff\E', '\u0178'),
|
|
1967
|
+
(r'\C\l\xb5\E', '\u03bc'),
|
|
1968
|
+
(r'\C\x5c\E', '\\')
|
|
1969
|
+
):
|
|
1970
|
+
with self.subTest(template=template):
|
|
1971
|
+
expand = bre.compile_replace(pattern, template)
|
|
1972
|
+
results = expand(pattern.match('Test'))
|
|
1973
|
+
self.assertEqual(expected, results)
|
|
1974
|
+
|
|
1975
|
+
def test_case_escape_multiple_chars(self):
|
|
1976
|
+
"""Test case conversion of escapes that give more than one character."""
|
|
1977
|
+
|
|
1978
|
+
pattern = re.compile('Test')
|
|
1979
|
+
for template, expected in (
|
|
1980
|
+
(r'\C\xdf\E', 'SS'),
|
|
1981
|
+
(r'\c\337', 'SS'),
|
|
1982
|
+
(r'\C\N{LATIN SMALL LETTER SHARP S}\E', 'SS'),
|
|
1983
|
+
(r'\C\u00df\E', 'SS'),
|
|
1984
|
+
(r'\L\u0130\E', 'i\u0307'),
|
|
1985
|
+
(r'\C\l\460\E', 'i\u0307'),
|
|
1986
|
+
(r'\C\l\N{LATIN CAPITAL LETTER I WITH DOT ABOVE}\E', 'i\u0307'),
|
|
1987
|
+
(r'\C\l\u0130\E', 'i\u0307')
|
|
1988
|
+
):
|
|
1989
|
+
with self.subTest(template=template):
|
|
1990
|
+
expand = bre.compile_replace(pattern, template)
|
|
1991
|
+
results = expand(pattern.match('Test'))
|
|
1992
|
+
self.assertEqual(expected, results)
|
|
1993
|
+
|
|
1912
1994
|
def test_dont_case_special_refs(self):
|
|
1913
1995
|
"""Test that we don't case Unicode and bytes tokens, but case the character."""
|
|
1914
1996
|
|
|
@@ -1979,6 +2061,27 @@ class TestReplaceTemplate(unittest.TestCase):
|
|
|
1979
2061
|
class TestExceptions(unittest.TestCase):
|
|
1980
2062
|
"""Test Exceptions."""
|
|
1981
2063
|
|
|
2064
|
+
def test_bad_char_in_numeric_group_name(self):
|
|
2065
|
+
"""Test bad character in a numeric group name."""
|
|
2066
|
+
|
|
2067
|
+
p = bre.compile_search(r'(a)(b)')
|
|
2068
|
+
with pytest.raises(PatternError):
|
|
2069
|
+
bre.compile_replace(p, r'Replace \g<1X>')
|
|
2070
|
+
|
|
2071
|
+
def test_bad_char_in_group_name(self):
|
|
2072
|
+
"""Test bad character in a named group reference."""
|
|
2073
|
+
|
|
2074
|
+
p = bre.compile_search(r'(?P<word>a)(b)')
|
|
2075
|
+
with pytest.raises(PatternError):
|
|
2076
|
+
bre.compile_replace(p, r'Replace \g<w-ord>')
|
|
2077
|
+
|
|
2078
|
+
def test_bad_char_in_group_name_spacing(self):
|
|
2079
|
+
"""Test spacing in a group name reference."""
|
|
2080
|
+
|
|
2081
|
+
p = bre.compile_search(r'(?P<word>a)(b)')
|
|
2082
|
+
with pytest.raises(PatternError):
|
|
2083
|
+
bre.compile_replace(p, r'Replace \g<word >')
|
|
2084
|
+
|
|
1982
2085
|
def test_bad_flag(self):
|
|
1983
2086
|
"""Test bad flag."""
|
|
1984
2087
|
|
|
@@ -722,6 +722,9 @@ class TestReplaceTemplate(unittest.TestCase):
|
|
|
722
722
|
with pytest.raises(PatternError):
|
|
723
723
|
bregex.sub('test', r'\N{A', 'test')
|
|
724
724
|
|
|
725
|
+
with pytest.raises(PatternError):
|
|
726
|
+
bregex.sub('test', r'\N{Undefined character}', 'test')
|
|
727
|
+
|
|
725
728
|
def test_group_failures(self):
|
|
726
729
|
"""Test group failures."""
|
|
727
730
|
|
|
@@ -1538,6 +1541,8 @@ class TestReplaceTemplate(unittest.TestCase):
|
|
|
1538
1541
|
self.assertEqual(pattern.subf(r'{1:1^30}', 'Testing'), '11111111111111Te11111111111111')
|
|
1539
1542
|
self.assertEqual(pattern.subf(r'{1:<30s}', 'Testing'), 'Te ')
|
|
1540
1543
|
self.assertEqual(pattern.subf(r'{1:s}', 'Testing'), 'Te')
|
|
1544
|
+
self.assertEqual(pattern.subf(r'{1:}', 'Testing'), 'Te')
|
|
1545
|
+
self.assertEqual(pattern.subf(r'{1:}-x', 'Testing'), 'Te-x')
|
|
1541
1546
|
self.assertEqual(pattern.subf(r'{2!r}', 'Testing'), "'st'")
|
|
1542
1547
|
|
|
1543
1548
|
with pytest.raises(PatternError):
|
|
@@ -1570,6 +1575,8 @@ class TestReplaceTemplate(unittest.TestCase):
|
|
|
1570
1575
|
self.assertEqual(pattern.subf(br'{1:1^30}', b'Testing'), b'11111111111111Te11111111111111')
|
|
1571
1576
|
self.assertEqual(pattern.subf(br'{1:<30s}', b'Testing'), b'Te ')
|
|
1572
1577
|
self.assertEqual(pattern.subf(br'{1:s}', b'Testing'), b'Te')
|
|
1578
|
+
self.assertEqual(pattern.subf(br'{1:}', b'Testing'), b'Te')
|
|
1579
|
+
self.assertEqual(pattern.subf(br'{1:}-x', b'Testing'), b'Te-x')
|
|
1573
1580
|
self.assertEqual(pattern.subf(br'{2!r}', b'Testing'), b"b'st'")
|
|
1574
1581
|
|
|
1575
1582
|
with pytest.raises(PatternError):
|
|
@@ -1590,6 +1597,39 @@ class TestReplaceTemplate(unittest.TestCase):
|
|
|
1590
1597
|
with pytest.raises(ValueError):
|
|
1591
1598
|
pattern.subf(br'{:030}', b'Testing')
|
|
1592
1599
|
|
|
1600
|
+
def test_case_hex_above_byte_range(self):
|
|
1601
|
+
"""Test case conversion of hex escapes beyond the byte range."""
|
|
1602
|
+
|
|
1603
|
+
pattern = regex.compile('Test')
|
|
1604
|
+
for template, expected in (
|
|
1605
|
+
(r'\c\xb5', '\u039c'),
|
|
1606
|
+
(r'\C\l\xb5\E', '\u03bc'),
|
|
1607
|
+
(r'\C\x5c\E', '\\')
|
|
1608
|
+
):
|
|
1609
|
+
with self.subTest(template=template):
|
|
1610
|
+
expand = bregex.compile_replace(pattern, template)
|
|
1611
|
+
results = expand(pattern.match('Test'))
|
|
1612
|
+
self.assertEqual(expected, results)
|
|
1613
|
+
|
|
1614
|
+
def test_case_escape_multiple_chars(self):
|
|
1615
|
+
"""Test case conversion of escapes that give more than one character."""
|
|
1616
|
+
|
|
1617
|
+
pattern = regex.compile('Test')
|
|
1618
|
+
for template, expected in (
|
|
1619
|
+
(r'\C\xdf\E', 'SS'),
|
|
1620
|
+
(r'\c\337', 'SS'),
|
|
1621
|
+
(r'\C\N{LATIN SMALL LETTER SHARP S}\E', 'SS'),
|
|
1622
|
+
(r'\C\u00df\E', 'SS'),
|
|
1623
|
+
(r'\L\u0130\E', 'i\u0307'),
|
|
1624
|
+
(r'\C\l\460\E', 'i\u0307'),
|
|
1625
|
+
(r'\C\l\N{LATIN CAPITAL LETTER I WITH DOT ABOVE}\E', 'i\u0307'),
|
|
1626
|
+
(r'\C\l\u0130\E', 'i\u0307')
|
|
1627
|
+
):
|
|
1628
|
+
with self.subTest(template=template):
|
|
1629
|
+
expand = bregex.compile_replace(pattern, template)
|
|
1630
|
+
results = expand(pattern.match('Test'))
|
|
1631
|
+
self.assertEqual(expected, results)
|
|
1632
|
+
|
|
1593
1633
|
def test_dont_case_special_refs(self):
|
|
1594
1634
|
"""Test that we don't case Unicode and bytes tokens, but case the character."""
|
|
1595
1635
|
|
|
@@ -1660,6 +1700,27 @@ class TestReplaceTemplate(unittest.TestCase):
|
|
|
1660
1700
|
class TestExceptions(unittest.TestCase):
|
|
1661
1701
|
"""Test Exceptions."""
|
|
1662
1702
|
|
|
1703
|
+
def test_bad_char_in_numeric_group_name(self):
|
|
1704
|
+
"""Test bad character in a numeric group name."""
|
|
1705
|
+
|
|
1706
|
+
p = bregex.compile_search(r'(a)(b)')
|
|
1707
|
+
with pytest.raises(PatternError):
|
|
1708
|
+
bregex.compile_replace(p, r'Replace \g<1X>')
|
|
1709
|
+
|
|
1710
|
+
def test_bad_char_in_group_name(self):
|
|
1711
|
+
"""Test bad character in a named group reference."""
|
|
1712
|
+
|
|
1713
|
+
p = bregex.compile_search(r'(?P<word>a)(b)')
|
|
1714
|
+
with pytest.raises(PatternError):
|
|
1715
|
+
bregex.compile_replace(p, r'Replace \g<w-ord>')
|
|
1716
|
+
|
|
1717
|
+
def test_bad_char_in_group_name_spacing(self):
|
|
1718
|
+
"""Test spacing in a group name reference."""
|
|
1719
|
+
|
|
1720
|
+
p = bregex.compile_search(r'(?P<word>a)(b)')
|
|
1721
|
+
with pytest.raises(PatternError):
|
|
1722
|
+
bregex.compile_replace(p, r'Replace \g<word >')
|
|
1723
|
+
|
|
1663
1724
|
def test_format_existing_group_no_match_with_index(self):
|
|
1664
1725
|
"""Test format group with no match and attempt at indexing."""
|
|
1665
1726
|
|
|
@@ -125,6 +125,14 @@ class TestGeneralCategory(unittest.TestCase):
|
|
|
125
125
|
result = uniprops.get_unicode_property(category, mode=uniprops.MODE_ASCII)
|
|
126
126
|
self.assertEqual(result, uniprops.fmt_string(v2, True))
|
|
127
127
|
|
|
128
|
+
def test_gc_range(self):
|
|
129
|
+
"""Test characters `UnicodeData.txt` defines with a `First` and a `Last` line."""
|
|
130
|
+
|
|
131
|
+
for char in ('\uac00', '\u4e16', '\ud55c', '\ud7a3'):
|
|
132
|
+
for category, match in (('lo', True), ('l', True), ('cn', False), ('^cn', True)):
|
|
133
|
+
result = re.match('[%s]' % uniprops.get_unicode_property(category), char)
|
|
134
|
+
self.assertEqual(result is not None, match)
|
|
135
|
+
|
|
128
136
|
def test_bad_single_subcategory(self):
|
|
129
137
|
"""Test `Joining Group` property with bad value."""
|
|
130
138
|
|
|
@@ -60,6 +60,22 @@ def create_span(unirange, is_bytes=False):
|
|
|
60
60
|
return list(range(unirange[0], unirange[1] + 1))
|
|
61
61
|
|
|
62
62
|
|
|
63
|
+
def expand_ranges(lines):
|
|
64
|
+
"""Give each code point in a `First`/`Last` range of `UnicodeData.txt` a line of its own."""
|
|
65
|
+
|
|
66
|
+
first = None
|
|
67
|
+
for line in lines:
|
|
68
|
+
code, name, fields = line.split(';', 2)
|
|
69
|
+
if name.endswith(', First>'):
|
|
70
|
+
first = int(code, 16)
|
|
71
|
+
elif first is None:
|
|
72
|
+
yield line
|
|
73
|
+
else:
|
|
74
|
+
for i in range(first, int(code, 16) + 1):
|
|
75
|
+
yield '%04X;%s;%s' % (i, name, fields)
|
|
76
|
+
first = None
|
|
77
|
+
|
|
78
|
+
|
|
63
79
|
def not_explicitly_defined(table, name, is_bytes=False):
|
|
64
80
|
"""Compose a table with the specified entry name of values not explicitly defined."""
|
|
65
81
|
|
|
@@ -780,7 +796,7 @@ def gen_bidi(output, ascii_props=False, append=False, prefix="", aliases=None):
|
|
|
780
796
|
|
|
781
797
|
max_range = MAXVALIDASCII if ascii_props else MAXUNICODE
|
|
782
798
|
with open(os.path.join(HOME, 'unicodedata', UNIVERSION, 'UnicodeData.txt'), 'r', encoding='utf-8') as uf:
|
|
783
|
-
for line in uf:
|
|
799
|
+
for line in expand_ranges(uf):
|
|
784
800
|
data = line.strip().split(';')
|
|
785
801
|
if data:
|
|
786
802
|
bidi = data[4].strip().lower()
|
|
@@ -1042,7 +1058,7 @@ def gen_properties(output, files, aliases, ascii_props=False, append=False):
|
|
|
1042
1058
|
table = {'l': {'c': []}}
|
|
1043
1059
|
itable = {'l': {}}
|
|
1044
1060
|
with open(os.path.join(HOME, 'unicodedata', UNIVERSION, 'UnicodeData.txt'), 'r', encoding='utf-8') as uf:
|
|
1045
|
-
for line in uf:
|
|
1061
|
+
for line in expand_ranges(uf):
|
|
1046
1062
|
data = line.strip().split(';')
|
|
1047
1063
|
if data:
|
|
1048
1064
|
i = int(data[0], 16)
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
site_name: Backrefs Documentation
|
|
2
|
+
site_url: https://facelessuser.github.io/backrefs
|
|
3
|
+
repo_url: https://github.com/facelessuser/backrefs
|
|
4
|
+
edit_uri: tree/master/docs/src/markdown
|
|
5
|
+
site_description: A library to add additional back references to regular expressions.
|
|
6
|
+
copyright: |
|
|
7
|
+
Copyright © 2015 - 2026 <a href="https://github.com/facelessuser" target="_blank" rel="noopener">Isaac Muse</a>
|
|
8
|
+
|
|
9
|
+
docs_dir: docs/src/markdown
|
|
10
|
+
theme:
|
|
11
|
+
name: zensical_pymdownx
|
|
12
|
+
custom_dir: docs/theme
|
|
13
|
+
features:
|
|
14
|
+
- announce.dismiss
|
|
15
|
+
- navigation.tabs
|
|
16
|
+
- navigation.top
|
|
17
|
+
- navigation.instant
|
|
18
|
+
- navigation.indexes
|
|
19
|
+
- navigation.path
|
|
20
|
+
- navigation.footer
|
|
21
|
+
- toc.follow
|
|
22
|
+
- content.tabs.link
|
|
23
|
+
- search.share
|
|
24
|
+
- search.highlight
|
|
25
|
+
- search.suggest
|
|
26
|
+
- content.code.copy
|
|
27
|
+
- content.tabs.link
|
|
28
|
+
- content.code.annotations
|
|
29
|
+
|
|
30
|
+
nav:
|
|
31
|
+
- Getting Started:
|
|
32
|
+
- Installation: index.md
|
|
33
|
+
- Usage: usage.md
|
|
34
|
+
- Supported References: refs.md
|
|
35
|
+
- About:
|
|
36
|
+
- Contributing & Support: about/contributing.md
|
|
37
|
+
- Security Vulnerabilities: about/security.md
|
|
38
|
+
- Changelog: about/changelog.md
|
|
39
|
+
- License: about/license.md
|
|
40
|
+
|
|
41
|
+
markdown_extensions:
|
|
42
|
+
- markdown.extensions.toc:
|
|
43
|
+
slugify: !!python/object/apply:pymdownx.slugs.slugify {kwds: {case: lower}}
|
|
44
|
+
permalink: ""
|
|
45
|
+
- markdown.extensions.smarty:
|
|
46
|
+
smart_quotes: false
|
|
47
|
+
- pymdownx.betterem:
|
|
48
|
+
- markdown.extensions.attr_list:
|
|
49
|
+
- markdown.extensions.tables:
|
|
50
|
+
- markdown.extensions.abbr:
|
|
51
|
+
- markdown.extensions.md_in_html:
|
|
52
|
+
- pymdownx.superfences:
|
|
53
|
+
custom_fences:
|
|
54
|
+
- name: py
|
|
55
|
+
class: 'highlight'
|
|
56
|
+
format: !!python/object/apply:tools.pycon.py_command_formatter
|
|
57
|
+
kwds:
|
|
58
|
+
init: |
|
|
59
|
+
import backrefs
|
|
60
|
+
from backrefs import bre, bregex
|
|
61
|
+
import re
|
|
62
|
+
import regex
|
|
63
|
+
validator: !!python/name:tools.pycon.py_command_validator
|
|
64
|
+
reset: !!python/name:tools.pycon.py_command_reset
|
|
65
|
+
- name: python
|
|
66
|
+
class: 'highlight'
|
|
67
|
+
format: !!python/object/apply:tools.pycon.py_command_formatter
|
|
68
|
+
kwds:
|
|
69
|
+
init: |
|
|
70
|
+
import backrefs
|
|
71
|
+
from backrefs import bre, bregex
|
|
72
|
+
import re
|
|
73
|
+
import regex
|
|
74
|
+
validator: !!python/name:tools.pycon.py_command_validator
|
|
75
|
+
reset: !!python/name:tools.pycon.py_command_reset
|
|
76
|
+
- pymdownx.highlight:
|
|
77
|
+
line_spans: __codeline
|
|
78
|
+
line_anchors: __codelineno
|
|
79
|
+
anchor_linenums: True
|
|
80
|
+
extend_pygments_lang:
|
|
81
|
+
- name: php-inline
|
|
82
|
+
lang: php
|
|
83
|
+
options:
|
|
84
|
+
startinline: true
|
|
85
|
+
- name: pycon3
|
|
86
|
+
lang: pycon
|
|
87
|
+
options:
|
|
88
|
+
python3: true
|
|
89
|
+
- pymdownx.inlinehilite:
|
|
90
|
+
- pymdownx.magiclink:
|
|
91
|
+
repo_url_shortener: true
|
|
92
|
+
repo_url_shorthand: true
|
|
93
|
+
social_url_shorthand: true
|
|
94
|
+
user: facelessuser
|
|
95
|
+
repo: backrefs
|
|
96
|
+
- pymdownx.tilde:
|
|
97
|
+
- pymdownx.caret:
|
|
98
|
+
- pymdownx.smartsymbols:
|
|
99
|
+
- pymdownx.emoji:
|
|
100
|
+
emoji_index: !!python/name:material.extensions.emoji.twemoji
|
|
101
|
+
emoji_generator: !!python/name:material.extensions.emoji.to_svg
|
|
102
|
+
- pymdownx.escapeall:
|
|
103
|
+
hardbreak: True
|
|
104
|
+
nbsp: True
|
|
105
|
+
- pymdownx.tasklist:
|
|
106
|
+
custom_checkbox: true
|
|
107
|
+
- pymdownx.progressbar:
|
|
108
|
+
- pymdownx.mark:
|
|
109
|
+
- pymdownx.striphtml:
|
|
110
|
+
- pymdownx.snippets:
|
|
111
|
+
base_path:
|
|
112
|
+
- docs/src/markdown/.snippets
|
|
113
|
+
- LICENSE.md
|
|
114
|
+
- SECURITY.md
|
|
115
|
+
auto_append:
|
|
116
|
+
- links.md
|
|
117
|
+
- pymdownx.keys:
|
|
118
|
+
separator: "\uff0b"
|
|
119
|
+
- pymdownx.saneheaders:
|
|
120
|
+
- pymdownx.blocks.admonition:
|
|
121
|
+
types:
|
|
122
|
+
- new
|
|
123
|
+
- settings
|
|
124
|
+
- note
|
|
125
|
+
- abstract
|
|
126
|
+
- info
|
|
127
|
+
- tip
|
|
128
|
+
- success
|
|
129
|
+
- question
|
|
130
|
+
- warning
|
|
131
|
+
- failure
|
|
132
|
+
- danger
|
|
133
|
+
- bug
|
|
134
|
+
- example
|
|
135
|
+
- quote
|
|
136
|
+
- pymdownx.blocks.details:
|
|
137
|
+
types:
|
|
138
|
+
- name: details-new
|
|
139
|
+
class: new
|
|
140
|
+
- name: details-settings
|
|
141
|
+
class: settings
|
|
142
|
+
- name: details-note
|
|
143
|
+
class: note
|
|
144
|
+
- name: details-abstract
|
|
145
|
+
class: abstract
|
|
146
|
+
- name: details-info
|
|
147
|
+
class: info
|
|
148
|
+
- name: details-tip
|
|
149
|
+
class: tip
|
|
150
|
+
- name: details-success
|
|
151
|
+
class: success
|
|
152
|
+
- name: details-question
|
|
153
|
+
class: question
|
|
154
|
+
- name: details-warning
|
|
155
|
+
class: warning
|
|
156
|
+
- name: details-failure
|
|
157
|
+
class: failure
|
|
158
|
+
- name: details-danger
|
|
159
|
+
class: danger
|
|
160
|
+
- name: details-bug
|
|
161
|
+
class: bug
|
|
162
|
+
- name: details-example
|
|
163
|
+
class: example
|
|
164
|
+
- name: details-quote
|
|
165
|
+
class: quote
|
|
166
|
+
- pymdownx.blocks.html:
|
|
167
|
+
- pymdownx.blocks.definition:
|
|
168
|
+
- pymdownx.blocks.tab:
|
|
169
|
+
alternate_style: True
|
|
170
|
+
- pymdownx.quotes:
|
|
171
|
+
callouts: true
|
|
172
|
+
|
|
173
|
+
extra:
|
|
174
|
+
social:
|
|
175
|
+
- icon: fontawesome/brands/github
|
|
176
|
+
link: https://github.com/facelessuser
|
|
177
|
+
zensical_pymdownx:
|
|
178
|
+
sponsor: https://github.com/sponsors/facelessuser
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|