backrefs 8.2.1__tar.gz → 8.3__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.2.1 → backrefs-8.3}/PKG-INFO +1 -1
- {backrefs-8.2.1 → backrefs-8.3}/backrefs/__meta__.py +1 -1
- {backrefs-8.2.1 → backrefs-8.3}/backrefs/_bre_parse.py +50 -9
- {backrefs-8.2.1 → backrefs-8.3}/backrefs/uniprops/__init__.py +99 -62
- {backrefs-8.2.1 → backrefs-8.3}/docs/src/dictionary/en-custom.txt +3 -0
- {backrefs-8.2.1 → backrefs-8.3}/docs/src/markdown/about/changelog.md +13 -0
- {backrefs-8.2.1 → backrefs-8.3}/docs/src/markdown/refs.md +16 -3
- {backrefs-8.2.1 → backrefs-8.3}/tests/test_binary.py +41 -0
- {backrefs-8.2.1 → backrefs-8.3}/tests/test_block.py +53 -0
- {backrefs-8.2.1 → backrefs-8.3}/tests/test_bre.py +111 -0
- {backrefs-8.2.1 → backrefs-8.3}/tests/test_generalcategory.py +45 -0
- {backrefs-8.2.1 → backrefs-8.3}/tools/unipropgen.py +11 -0
- {backrefs-8.2.1 → backrefs-8.3}/.coveragerc +0 -0
- {backrefs-8.2.1 → backrefs-8.3}/.gitignore +0 -0
- {backrefs-8.2.1 → backrefs-8.3}/.pyspelling.yml +0 -0
- {backrefs-8.2.1 → backrefs-8.3}/LICENSE.md +0 -0
- {backrefs-8.2.1 → backrefs-8.3}/README.md +0 -0
- {backrefs-8.2.1 → backrefs-8.3}/backrefs/__init__.py +0 -0
- {backrefs-8.2.1 → backrefs-8.3}/backrefs/_bregex_parse.py +0 -0
- {backrefs-8.2.1 → backrefs-8.3}/backrefs/_bregex_typing.py +0 -0
- {backrefs-8.2.1 → backrefs-8.3}/backrefs/bre.py +0 -0
- {backrefs-8.2.1 → backrefs-8.3}/backrefs/bregex.py +0 -0
- {backrefs-8.2.1 → backrefs-8.3}/backrefs/py.typed +0 -0
- {backrefs-8.2.1 → backrefs-8.3}/backrefs/util.py +0 -0
- {backrefs-8.2.1 → backrefs-8.3}/docs/src/dictionary/en-python.txt +0 -0
- {backrefs-8.2.1 → backrefs-8.3}/docs/src/markdown/.snippets/links.md +0 -0
- {backrefs-8.2.1 → backrefs-8.3}/docs/src/markdown/about/contributing.md +0 -0
- {backrefs-8.2.1 → backrefs-8.3}/docs/src/markdown/about/license.md +0 -0
- {backrefs-8.2.1 → backrefs-8.3}/docs/src/markdown/about/security.md +0 -0
- {backrefs-8.2.1 → backrefs-8.3}/docs/src/markdown/index.md +0 -0
- {backrefs-8.2.1 → backrefs-8.3}/docs/src/markdown/usage.md +0 -0
- {backrefs-8.2.1 → backrefs-8.3}/docs/theme/announce.html +0 -0
- {backrefs-8.2.1 → backrefs-8.3}/hatch_build.py +0 -0
- {backrefs-8.2.1 → backrefs-8.3}/pyproject.toml +0 -0
- {backrefs-8.2.1 → backrefs-8.3}/tests/__init__.py +0 -0
- {backrefs-8.2.1 → backrefs-8.3}/tests/test_age.py +0 -0
- {backrefs-8.2.1 → backrefs-8.3}/tests/test_bidiclass.py +0 -0
- {backrefs-8.2.1 → backrefs-8.3}/tests/test_bidipairedbrackettype.py +0 -0
- {backrefs-8.2.1 → backrefs-8.3}/tests/test_bregex.py +0 -0
- {backrefs-8.2.1 → backrefs-8.3}/tests/test_canonicalcombiningclass.py +0 -0
- {backrefs-8.2.1 → backrefs-8.3}/tests/test_decompositiontype.py +0 -0
- {backrefs-8.2.1 → backrefs-8.3}/tests/test_eastasianwidth.py +0 -0
- {backrefs-8.2.1 → backrefs-8.3}/tests/test_graphemeclusterbreak.py +0 -0
- {backrefs-8.2.1 → backrefs-8.3}/tests/test_hangulsyllabletype.py +0 -0
- {backrefs-8.2.1 → backrefs-8.3}/tests/test_indicpositionalcategory.py +0 -0
- {backrefs-8.2.1 → backrefs-8.3}/tests/test_indicsylabiccategory.py +0 -0
- {backrefs-8.2.1 → backrefs-8.3}/tests/test_joininggroup.py +0 -0
- {backrefs-8.2.1 → backrefs-8.3}/tests/test_joiningtype.py +0 -0
- {backrefs-8.2.1 → backrefs-8.3}/tests/test_linebreak.py +0 -0
- {backrefs-8.2.1 → backrefs-8.3}/tests/test_numerictype.py +0 -0
- {backrefs-8.2.1 → backrefs-8.3}/tests/test_numericvalue.py +0 -0
- {backrefs-8.2.1 → backrefs-8.3}/tests/test_quickcheck.py +0 -0
- {backrefs-8.2.1 → backrefs-8.3}/tests/test_script.py +0 -0
- {backrefs-8.2.1 → backrefs-8.3}/tests/test_scriptextensions.py +0 -0
- {backrefs-8.2.1 → backrefs-8.3}/tests/test_sentencebreak.py +0 -0
- {backrefs-8.2.1 → backrefs-8.3}/tests/test_uniprops.py +0 -0
- {backrefs-8.2.1 → backrefs-8.3}/tests/test_versions.py +0 -0
- {backrefs-8.2.1 → backrefs-8.3}/tests/test_verticalorientation.py +0 -0
- {backrefs-8.2.1 → backrefs-8.3}/tests/test_wordbreak.py +0 -0
- {backrefs-8.2.1 → backrefs-8.3}/tools/__init__.py +0 -0
- {backrefs-8.2.1 → backrefs-8.3}/tools/pycon.py +0 -0
- {backrefs-8.2.1 → backrefs-8.3}/tools/unicodedata/14.0.0.zip +0 -0
- {backrefs-8.2.1 → backrefs-8.3}/tools/unicodedata/15.0.0.zip +0 -0
- {backrefs-8.2.1 → backrefs-8.3}/tools/unicodedata/15.1.0.zip +0 -0
- {backrefs-8.2.1 → backrefs-8.3}/tools/unicodedata/16.0.0.zip +0 -0
- {backrefs-8.2.1 → backrefs-8.3}/tools/unicodedata/17.0.0.zip +0 -0
- {backrefs-8.2.1 → backrefs-8.3}/tools/unicodedata/LICENSE +0 -0
- {backrefs-8.2.1 → backrefs-8.3}/tools/unidatadownload.py +0 -0
- {backrefs-8.2.1 → backrefs-8.3}/zensical.yml +0 -0
|
@@ -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, 3, 0, "final")
|
|
197
197
|
__version__ = __version_info__._get_canonical()
|
|
@@ -77,6 +77,7 @@ class _SearchParser(Generic[AnyStr]):
|
|
|
77
77
|
verbose: bool
|
|
78
78
|
unicode: bool
|
|
79
79
|
named_unicode: tuple[str, int]
|
|
80
|
+
negated_members: list[str]
|
|
80
81
|
ascii: bool # noqa: A003
|
|
81
82
|
is_bytes: bool
|
|
82
83
|
search: AnyStr
|
|
@@ -437,6 +438,8 @@ class _SearchParser(Generic[AnyStr]):
|
|
|
437
438
|
start = ('', 0, None) # type: tuple[str, int, int | None]
|
|
438
439
|
self.found_property = False
|
|
439
440
|
self.found_named_unicode = False
|
|
441
|
+
self.negated_members = []
|
|
442
|
+
negated_set = False
|
|
440
443
|
|
|
441
444
|
def add_member(raw: str, index: int, value: int | None) -> None:
|
|
442
445
|
"""Track a member that was just added, which may start or end a range."""
|
|
@@ -522,6 +525,7 @@ class _SearchParser(Generic[AnyStr]):
|
|
|
522
525
|
# Only a `^` directly after the opening `[` negates the set.
|
|
523
526
|
first = pos
|
|
524
527
|
current.append(t)
|
|
528
|
+
negated_set = True
|
|
525
529
|
elif t == "]" and found and (pos != first + 1):
|
|
526
530
|
found = False
|
|
527
531
|
current.append(t)
|
|
@@ -552,7 +556,9 @@ class _SearchParser(Generic[AnyStr]):
|
|
|
552
556
|
# This will occur when a property's values exceed
|
|
553
557
|
# either the Unicode char limit on a narrow system,
|
|
554
558
|
# or the ASCII limit in a byte string pattern.
|
|
555
|
-
if
|
|
559
|
+
if self.negated_members:
|
|
560
|
+
current = [self.negated_set_text(''.join(current), negated_set)]
|
|
561
|
+
elif found_property or self.found_named_unicode:
|
|
556
562
|
temp = "".join(current)
|
|
557
563
|
if temp == '[]':
|
|
558
564
|
# We specified some properties, but they are all
|
|
@@ -565,6 +571,32 @@ class _SearchParser(Generic[AnyStr]):
|
|
|
565
571
|
|
|
566
572
|
return current
|
|
567
573
|
|
|
574
|
+
def negated_set_text(self, text: str, negated: bool) -> str:
|
|
575
|
+
r"""
|
|
576
|
+
Get a character set that contains negated properties, such as `[a\P{Lu}]`.
|
|
577
|
+
|
|
578
|
+
Under case-insensitive matching, a negated property must be a negated set of the property's characters
|
|
579
|
+
(`[^...]`), as a set of the characters outside the property would also match the other case of the
|
|
580
|
+
characters in it. A set can't contain a negated set, so each part of the set is matched separately:
|
|
581
|
+
`[R\P{A}\P{B}]` becomes `(?:[R]|[^A]|[^B])`, and `[^R\P{A}\P{B}]` becomes `(?![R])(?=[A])[B]`.
|
|
582
|
+
"""
|
|
583
|
+
|
|
584
|
+
members = text[2 if negated else 1:-1]
|
|
585
|
+
if members.startswith('^'):
|
|
586
|
+
# A literal `^` after the negating `^` would negate the members' own set.
|
|
587
|
+
members = '\\' + members
|
|
588
|
+
|
|
589
|
+
if not negated:
|
|
590
|
+
alternatives = [f'[^{m}]' for m in self.negated_members]
|
|
591
|
+
if members:
|
|
592
|
+
alternatives.insert(0, f'[{members}]')
|
|
593
|
+
return alternatives[0] if len(alternatives) == 1 else f"(?:{'|'.join(alternatives)})"
|
|
594
|
+
|
|
595
|
+
conditions = [f'(?=[{m}])' for m in self.negated_members[:-1]]
|
|
596
|
+
if members:
|
|
597
|
+
conditions.insert(0, f'(?![{members}])')
|
|
598
|
+
return ''.join(conditions) + f'[{self.negated_members[-1]}]'
|
|
599
|
+
|
|
568
600
|
def normal(self, t: str, i: _util.StringIter) -> list[str]:
|
|
569
601
|
"""Handle normal chars."""
|
|
570
602
|
|
|
@@ -680,14 +712,23 @@ class _SearchParser(Generic[AnyStr]):
|
|
|
680
712
|
else:
|
|
681
713
|
mode = _uniprops.MODE_UNICODE
|
|
682
714
|
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
715
|
+
# Negated properties are expressed as a negated set of the property's characters, so that they work with
|
|
716
|
+
# case-insensitive matching (see `negated_set_text`).
|
|
717
|
+
v, negated = _uniprops.get_unicode_property_parts(props, prop_value, mode)
|
|
718
|
+
everything = _uniprops.ASCII_RANGE if self.is_bytes else _uniprops.UNICODE_RANGE
|
|
719
|
+
if in_group:
|
|
720
|
+
if negated:
|
|
721
|
+
if not v:
|
|
722
|
+
# The inverse of a property that matches nothing is everything, which case-insensitive
|
|
723
|
+
# matching can't extend, so it can be a member of the set.
|
|
724
|
+
return [everything]
|
|
725
|
+
# Added when the set is complete.
|
|
726
|
+
self.negated_members.append(v)
|
|
727
|
+
return ['']
|
|
728
|
+
return [v]
|
|
729
|
+
if negated:
|
|
730
|
+
return [f'[^{v}]' if v else f'[{everything}]']
|
|
731
|
+
return [f'[{v}]' if v else f'[^{everything}]']
|
|
691
732
|
|
|
692
733
|
def main_group(self, i: _util.StringIter) -> list[str]:
|
|
693
734
|
"""The main group: group 0."""
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"""Unicode Properties."""
|
|
2
2
|
from __future__ import annotations
|
|
3
|
+
from typing import Callable
|
|
3
4
|
from .unidata import alias
|
|
4
5
|
|
|
5
6
|
UNICODE_RANGE = '\u0000-\U0010ffff'
|
|
@@ -487,7 +488,7 @@ def get_vertical_orientation_property(value: str, mode: int = MODE_UNICODE) -> s
|
|
|
487
488
|
|
|
488
489
|
|
|
489
490
|
def get_is_property(value: str, mode: int = MODE_UNICODE) -> str:
|
|
490
|
-
"""Get shortcut for `SC
|
|
491
|
+
"""Get shortcut for `GC`, `SC`, or `Binary` property."""
|
|
491
492
|
|
|
492
493
|
from .unidata import scriptextensions as scx
|
|
493
494
|
from .unidata import binary
|
|
@@ -504,6 +505,10 @@ def get_is_property(value: str, mode: int = MODE_UNICODE) -> str:
|
|
|
504
505
|
if prefix != 'is':
|
|
505
506
|
raise ValueError("Does not start with 'is'!")
|
|
506
507
|
|
|
508
|
+
category = _general_category_name(temp)
|
|
509
|
+
if category is not None:
|
|
510
|
+
return get_gc_property(negate + category, mode)
|
|
511
|
+
|
|
507
512
|
script_obj = scx.ascii_script_extensions if mode != MODE_UNICODE else scx.unicode_script_extensions
|
|
508
513
|
bin_obj = binary.ascii_binary if mode != MODE_UNICODE else binary.unicode_binary
|
|
509
514
|
|
|
@@ -549,86 +554,100 @@ def _is_binary(name: str) -> bool:
|
|
|
549
554
|
return name in prop_table.unicode_binary or name in alias.unicode_alias['binary']
|
|
550
555
|
|
|
551
556
|
|
|
557
|
+
def _general_category_name(value: str) -> str | None:
|
|
558
|
+
"""Get the short name of a General Category value, or `None` if it isn't one."""
|
|
559
|
+
|
|
560
|
+
from .unidata import generalcategory as prop_table
|
|
561
|
+
|
|
562
|
+
name = alias.unicode_alias['generalcategory'].get(value, value)
|
|
563
|
+
table = prop_table.unicode_properties
|
|
564
|
+
if len(name) == 1:
|
|
565
|
+
valid = name in table
|
|
566
|
+
else:
|
|
567
|
+
valid = len(name) == 2 and name[0] in table and name[1] in table[name[0]]
|
|
568
|
+
return name if valid else None
|
|
569
|
+
|
|
570
|
+
|
|
571
|
+
def _strip_is(name: str, is_valid: Callable[[str], bool]) -> str:
|
|
572
|
+
"""
|
|
573
|
+
Remove an initial `is` from a name that isn't valid, if it is valid without it.
|
|
574
|
+
|
|
575
|
+
Unicode allows an initial `is` on property names and values (UAX #44, `UAX44-LM3`). The name as written is
|
|
576
|
+
always preferred, and `is` is only removed once. A negated name (`^name`) keeps its negation.
|
|
577
|
+
"""
|
|
578
|
+
|
|
579
|
+
negate = '^' if name.startswith('^') else ''
|
|
580
|
+
base = name[len(negate):]
|
|
581
|
+
if not is_valid(base) and base.startswith('is') and is_valid(base[2:]):
|
|
582
|
+
base = base[2:]
|
|
583
|
+
return negate + base
|
|
584
|
+
|
|
585
|
+
|
|
586
|
+
# Properties that have values, by their normalized name.
|
|
587
|
+
_ENUMERATED_PROPERTIES: dict[str, Callable[[str, int], str]] = {
|
|
588
|
+
'generalcategory': get_gc_property,
|
|
589
|
+
'script': get_script_property,
|
|
590
|
+
'scriptextensions': get_script_extension_property,
|
|
591
|
+
'block': get_block_property,
|
|
592
|
+
'bidiclass': get_bidi_property,
|
|
593
|
+
'bidipairedbrackettype': get_bidi_paired_bracket_type_property,
|
|
594
|
+
'age': get_age_property,
|
|
595
|
+
'eastasianwidth': get_east_asian_width_property,
|
|
596
|
+
'indicpositionalcategory': get_indic_positional_category_property,
|
|
597
|
+
'indicsyllabiccategory': get_indic_syllabic_category_property,
|
|
598
|
+
'hangulsyllabletype': get_hangul_syllable_type_property,
|
|
599
|
+
'decompositiontype': get_decomposition_type_property,
|
|
600
|
+
'canonicalcombiningclass': get_canonical_combining_class_property,
|
|
601
|
+
'numerictype': get_numeric_type_property,
|
|
602
|
+
'numericvalue': get_numeric_value_property,
|
|
603
|
+
'joiningtype': get_joining_type_property,
|
|
604
|
+
'joininggroup': get_joining_group_property,
|
|
605
|
+
'graphemeclusterbreak': get_grapheme_cluster_break_property,
|
|
606
|
+
'linebreak': get_line_break_property,
|
|
607
|
+
'sentencebreak': get_sentence_break_property,
|
|
608
|
+
'wordbreak': get_word_break_property,
|
|
609
|
+
'nfcquickcheck': get_nfc_quick_check_property,
|
|
610
|
+
'nfdquickcheck': get_nfd_quick_check_property,
|
|
611
|
+
'nfkcquickcheck': get_nfkc_quick_check_property,
|
|
612
|
+
'nfkdquickcheck': get_nfkd_quick_check_property,
|
|
613
|
+
'verticalorientation': get_vertical_orientation_property,
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
|
|
617
|
+
def _is_property_name(name: str) -> bool:
|
|
618
|
+
"""Check if a name is a property that can be given a value (`name=value`)."""
|
|
619
|
+
|
|
620
|
+
return _is_binary(name) or alias.unicode_alias['_'].get(name, name) in _ENUMERATED_PROPERTIES
|
|
621
|
+
|
|
622
|
+
|
|
552
623
|
def get_unicode_property(prop: str, value: str | None = None, mode: int = MODE_UNICODE) -> str:
|
|
553
624
|
"""Retrieve the Unicode category from the table."""
|
|
554
625
|
|
|
555
626
|
if value is not None:
|
|
556
627
|
|
|
557
628
|
negate = prop.startswith('^')
|
|
629
|
+
name = _strip_is(prop[1:] if negate else prop, _is_property_name)
|
|
558
630
|
|
|
559
631
|
# Normalize binary true/false input so we can handle it properly
|
|
560
|
-
if _is_binary(
|
|
561
|
-
name = prop[1:] if negate else prop
|
|
562
|
-
|
|
632
|
+
if _is_binary(name):
|
|
563
633
|
if value in ('n', 'no', 'f', 'false'):
|
|
564
634
|
negate = not negate
|
|
565
635
|
elif value not in ('y', 'yes', 't', 'true'):
|
|
566
636
|
raise ValueError(f"'{value}' is not a valid value for the binary property '{prop}'")
|
|
567
637
|
|
|
568
638
|
return get_binary_property('^' + name if negate else name, mode)
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
name = prop[1:]
|
|
573
|
-
else:
|
|
574
|
-
name = prop
|
|
639
|
+
|
|
640
|
+
if negate:
|
|
641
|
+
value = '^' + value
|
|
575
642
|
|
|
576
643
|
name = alias.unicode_alias['_'].get(name, name)
|
|
577
644
|
try:
|
|
578
645
|
if name == 'generalcategory':
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
elif name == 'scriptextensions':
|
|
583
|
-
return get_script_extension_property(value, mode)
|
|
584
|
-
elif name == 'block':
|
|
585
|
-
return get_block_property(value, mode)
|
|
586
|
-
elif name == 'bidiclass':
|
|
587
|
-
return get_bidi_property(value, mode)
|
|
588
|
-
elif name == 'bidipairedbrackettype':
|
|
589
|
-
return get_bidi_paired_bracket_type_property(value, mode)
|
|
590
|
-
elif name == 'age':
|
|
591
|
-
return get_age_property(value, mode)
|
|
592
|
-
elif name == 'eastasianwidth':
|
|
593
|
-
return get_east_asian_width_property(value, mode)
|
|
594
|
-
elif name == 'indicpositionalcategory':
|
|
595
|
-
return get_indic_positional_category_property(value, mode)
|
|
596
|
-
elif name == 'indicsyllabiccategory':
|
|
597
|
-
return get_indic_syllabic_category_property(value, mode)
|
|
598
|
-
elif name == 'hangulsyllabletype':
|
|
599
|
-
return get_hangul_syllable_type_property(value, mode)
|
|
600
|
-
elif name == 'decompositiontype':
|
|
601
|
-
return get_decomposition_type_property(value, mode)
|
|
602
|
-
elif name == 'canonicalcombiningclass':
|
|
603
|
-
return get_canonical_combining_class_property(value, mode)
|
|
604
|
-
elif name == 'numerictype':
|
|
605
|
-
return get_numeric_type_property(value, mode)
|
|
606
|
-
elif name == 'numericvalue':
|
|
607
|
-
return get_numeric_value_property(value, mode)
|
|
608
|
-
elif name == 'joiningtype':
|
|
609
|
-
return get_joining_type_property(value, mode)
|
|
610
|
-
elif name == 'joininggroup':
|
|
611
|
-
return get_joining_group_property(value, mode)
|
|
612
|
-
elif name == 'graphemeclusterbreak':
|
|
613
|
-
return get_grapheme_cluster_break_property(value, mode)
|
|
614
|
-
elif name == 'linebreak':
|
|
615
|
-
return get_line_break_property(value, mode)
|
|
616
|
-
elif name == 'sentencebreak':
|
|
617
|
-
return get_sentence_break_property(value, mode)
|
|
618
|
-
elif name == 'wordbreak':
|
|
619
|
-
return get_word_break_property(value, mode)
|
|
620
|
-
elif name == 'nfcquickcheck':
|
|
621
|
-
return get_nfc_quick_check_property(value, mode)
|
|
622
|
-
elif name == 'nfdquickcheck':
|
|
623
|
-
return get_nfd_quick_check_property(value, mode)
|
|
624
|
-
elif name == 'nfkcquickcheck':
|
|
625
|
-
return get_nfkc_quick_check_property(value, mode)
|
|
626
|
-
elif name == 'nfkdquickcheck':
|
|
627
|
-
return get_nfkd_quick_check_property(value, mode)
|
|
628
|
-
elif name == 'verticalorientation':
|
|
629
|
-
return get_vertical_orientation_property(value, mode)
|
|
630
|
-
else:
|
|
646
|
+
value = _strip_is(value, lambda v: _general_category_name(v) is not None)
|
|
647
|
+
get_property = _ENUMERATED_PROPERTIES.get(name)
|
|
648
|
+
if get_property is None:
|
|
631
649
|
raise ValueError(f"'{prop}={value}' does not have a valid property name")
|
|
650
|
+
return get_property(value, mode)
|
|
632
651
|
except Exception as e:
|
|
633
652
|
raise ValueError(f"'{prop}={value}' does not appear to be a valid property") from e
|
|
634
653
|
|
|
@@ -663,3 +682,21 @@ def get_unicode_property(prop: str, value: str | None = None, mode: int = MODE_U
|
|
|
663
682
|
pass
|
|
664
683
|
|
|
665
684
|
raise ValueError(f"'{prop}' does not appear to be a valid property")
|
|
685
|
+
|
|
686
|
+
|
|
687
|
+
def get_unicode_property_parts(prop: str, value: str | None = None, mode: int = MODE_UNICODE) -> tuple[str, bool]:
|
|
688
|
+
"""
|
|
689
|
+
Retrieve the characters of a Unicode property without its negation, and whether it is negated.
|
|
690
|
+
|
|
691
|
+
A property is negated when it is an inverse property (`^name`). With the characters of the property itself,
|
|
692
|
+
a negated property can be expressed as a negated character set (`[^...]`), which case-insensitive matching
|
|
693
|
+
handles correctly, unlike a set of the characters outside the property.
|
|
694
|
+
|
|
695
|
+
A binary property with a false value (`name=no`) is not a negation: it is the set of characters with that
|
|
696
|
+
value, as it is in Regex.
|
|
697
|
+
"""
|
|
698
|
+
|
|
699
|
+
negate = prop.startswith('^')
|
|
700
|
+
if negate:
|
|
701
|
+
prop = prop[1:]
|
|
702
|
+
return get_unicode_property(prop, value, mode), negate
|
|
@@ -3,6 +3,19 @@ icon: lucide/scroll-text
|
|
|
3
3
|
---
|
|
4
4
|
# Changelog
|
|
5
5
|
|
|
6
|
+
## 8.3
|
|
7
|
+
|
|
8
|
+
- **NEW**: `bre`: Add the binary properties `Any`, `Assigned`, and `ASCII`, which UTS \#18 requires but the Unicode
|
|
9
|
+
Character Database does not define. `ASCII` was previously only available as an alias of the `Basic_Latin` block.
|
|
10
|
+
- **NEW**: `bre`: Allow an initial `Is` on General Category values (`\p{IsLu}` or `\p{gc=IsLu}`) and on property names
|
|
11
|
+
(`\p{IsGC=Lu}` or `\p{IsAlphabetic=Yes}`), as Unicode allows, per UAX \#44.
|
|
12
|
+
- **FIX**: `bre`: Fix the inverse of the `No_Block` block property (`\P{No_Block}`) not including the
|
|
13
|
+
Supplementary Private Use Area blocks (`U+F0000` to `U+10FFFF`).
|
|
14
|
+
- **FIX**: `bre`: Fix negated Unicode properties and POSIX classes (`\P{Lu}`, `\p{^Lu}`, `[[:^upper:]]`) matching
|
|
15
|
+
both cases of the characters in the property when using `IGNORECASE`. Character sets that contain negated
|
|
16
|
+
properties are now expressed as alternatives (`[a\P{Lu}]` becomes `(?:[a]|[^...])`).
|
|
17
|
+
- **FIX**: `bre`: Fix an error when negating a binary property alias that has a value (`\P{Alpha=No}`).
|
|
18
|
+
|
|
6
19
|
## 8.2.1
|
|
7
20
|
|
|
8
21
|
- **FIX**: Reject compiled replace templates from another search pattern in compiled search objects,
|
|
@@ -174,6 +174,15 @@ example, to specify characters that are "alphabetic", we can use `\p{Alphabetic:
|
|
|
174
174
|
> [!new] New 5.4 Custom Binary properties
|
|
175
175
|
> In 5.4, the new custom binary properties `Vert_space` and `Horiz_Space` were added.
|
|
176
176
|
|
|
177
|
+
The binary properties `Any`, `Assigned`, and `ASCII` are also available. These are not in the Unicode Character
|
|
178
|
+
Database, but are required of regular expression engines by [UTS #18](https://www.unicode.org/reports/tr18/#RL1.2):
|
|
179
|
+
|
|
180
|
+
Property | Matches
|
|
181
|
+
---------- | -------
|
|
182
|
+
`Any` | Every character, even with the `ASCII` flag or in byte strings, as in Regex.
|
|
183
|
+
`Assigned` | Every character that is assigned (everything not in General Category `Cn`).
|
|
184
|
+
`ASCII` | Every ASCII character (`U+0000` to `U+007F`). This is the same as the `ASCII` alias of the `Basic_Latin` block.
|
|
185
|
+
|
|
177
186
|
### Property Short Names
|
|
178
187
|
|
|
179
188
|
General Category, Script Extensions, Blocks, and Binary all can be specified in a short form using just their name or
|
|
@@ -185,9 +194,13 @@ that is used in Script may be used in Blocks etc.
|
|
|
185
194
|
4. Binary
|
|
186
195
|
3. Blocks
|
|
187
196
|
|
|
188
|
-
Script Extensions and Binary properties can also be defined in the format `IsValue`. For instance,
|
|
189
|
-
match characters in the `Latin` script, we could use the syntax `\p{IsLatin}`, which would be the same as
|
|
190
|
-
`\p{scx: Latin}`. For Binary properties, `\p{IsAlphabetic}` is the same as `\p{Alphabetic: Y}` or
|
|
197
|
+
General Category, Script Extensions, and Binary properties can also be defined in the format `IsValue`. For instance,
|
|
198
|
+
if we wanted to match characters in the `Latin` script, we could use the syntax `\p{IsLatin}`, which would be the same as
|
|
199
|
+
`\p{Latin}` or `\p{scx: Latin}`. For Binary properties, `\p{IsAlphabetic}` is the same as `\p{Alphabetic: Y}` or
|
|
200
|
+
`\p{Alphabetic}`, and for General Category, `\p{IsLu}` is the same as `\p{Lu}`.
|
|
201
|
+
|
|
202
|
+
As Unicode allows, an initial `Is` is also accepted on property names (`\p{IsGeneral_Category: Lu}` or
|
|
203
|
+
`\p{IsAlphabetic: Yes}`) and on General Category values (`\p{gc: IsLu}`).
|
|
191
204
|
|
|
192
205
|
Block properties have a similar short form as Script and Binary properties. For Blocks you can use `InValue` to specify
|
|
193
206
|
a block. If we wanted to match characters in the `Basic_Latin` block, we could use the syntax `\p{InBasic_Latin}`. This
|
|
@@ -155,3 +155,44 @@ class TestBinary(unittest.TestCase):
|
|
|
155
155
|
result1 = uniprops.get_unicode_property(is_k)
|
|
156
156
|
result2 = uniprops.get_unicode_property(is_v)
|
|
157
157
|
self.assertEqual(result1, result2)
|
|
158
|
+
|
|
159
|
+
def test_uts18_properties(self):
|
|
160
|
+
"""Test the UTS #18 properties that are not in the Unicode Character Database: `Any`, `Assigned`, `ASCII`."""
|
|
161
|
+
|
|
162
|
+
import re._parser as parser
|
|
163
|
+
from backrefs.uniprops.unidata import generalcategory
|
|
164
|
+
|
|
165
|
+
def code_points(text):
|
|
166
|
+
if not text:
|
|
167
|
+
return set()
|
|
168
|
+
node = parser.parse(f'[{text}]')[0]
|
|
169
|
+
points = set()
|
|
170
|
+
for op, av in [node] if node[0] is parser.LITERAL else node[1]:
|
|
171
|
+
points.update([av] if op is parser.LITERAL else range(av[0], av[1] + 1))
|
|
172
|
+
return points
|
|
173
|
+
|
|
174
|
+
everything = set(range(0x110000))
|
|
175
|
+
ascii_range = set(range(0x80))
|
|
176
|
+
assigned = everything - code_points(generalcategory.unicode_properties['c']['n'])
|
|
177
|
+
|
|
178
|
+
for table, expected in (
|
|
179
|
+
(binary.unicode_binary, {'any': everything, 'assigned': assigned, 'ascii': ascii_range}),
|
|
180
|
+
# `Any` is everything even in ASCII mode, as it is in Regex.
|
|
181
|
+
(binary.ascii_binary, {'any': everything, 'assigned': assigned & ascii_range, 'ascii': ascii_range}),
|
|
182
|
+
):
|
|
183
|
+
for name, points in expected.items():
|
|
184
|
+
self.assertEqual(code_points(table[name]), points, name)
|
|
185
|
+
self.assertEqual(code_points(table['^' + name]), everything - points, name)
|
|
186
|
+
|
|
187
|
+
def test_uts18_property_forms(self):
|
|
188
|
+
"""Test that the UTS #18 properties can be used like other binary properties."""
|
|
189
|
+
|
|
190
|
+
for name in ('any', 'assigned', 'ascii'):
|
|
191
|
+
positive = uniprops.get_unicode_property(name)
|
|
192
|
+
negative = uniprops.get_unicode_property('^' + name)
|
|
193
|
+
for prop, value in (
|
|
194
|
+
(name, 'yes'), (name, 't'), ('^' + name, 'no'), ('is' + name, None), ('is' + name, 'y')
|
|
195
|
+
):
|
|
196
|
+
self.assertEqual(uniprops.get_unicode_property(prop, value), positive, (prop, value))
|
|
197
|
+
for prop, value in ((name, 'no'), ('^' + name, None), ('^' + name, 'yes'), ('^is' + name, None)):
|
|
198
|
+
self.assertEqual(uniprops.get_unicode_property(prop, value), negative, (prop, value))
|
|
@@ -31,6 +31,59 @@ class TestBlock(unittest.TestCase):
|
|
|
31
31
|
if not key.startswith('^'):
|
|
32
32
|
self.assertTrue('^' + key in keys1)
|
|
33
33
|
|
|
34
|
+
def test_inverse_is_complement(self):
|
|
35
|
+
"""Test that every inverse block is exactly the code points not in the block."""
|
|
36
|
+
|
|
37
|
+
import re._parser as parser
|
|
38
|
+
|
|
39
|
+
def ranges(text):
|
|
40
|
+
"""Get the sorted, merged ranges of a property's code points."""
|
|
41
|
+
|
|
42
|
+
if not text:
|
|
43
|
+
return []
|
|
44
|
+
node = parser.parse(f'[{text}]')[0]
|
|
45
|
+
items = []
|
|
46
|
+
for op, av in [node] if node[0] is parser.LITERAL else node[1]:
|
|
47
|
+
items.append((av, av) if op is parser.LITERAL else av)
|
|
48
|
+
merged = [] # type: list[tuple[int, int]]
|
|
49
|
+
for lo, hi in sorted(items):
|
|
50
|
+
if merged and lo <= merged[-1][1] + 1:
|
|
51
|
+
merged[-1] = (merged[-1][0], max(hi, merged[-1][1]))
|
|
52
|
+
else:
|
|
53
|
+
merged.append((lo, hi))
|
|
54
|
+
return merged
|
|
55
|
+
|
|
56
|
+
def complement(values):
|
|
57
|
+
"""Get the ranges of all code points not in the given ranges."""
|
|
58
|
+
|
|
59
|
+
result = []
|
|
60
|
+
start = 0
|
|
61
|
+
for lo, hi in values:
|
|
62
|
+
if lo > start:
|
|
63
|
+
result.append((start, lo - 1))
|
|
64
|
+
start = hi + 1
|
|
65
|
+
if start <= 0x10FFFF:
|
|
66
|
+
result.append((start, 0x10FFFF))
|
|
67
|
+
return result
|
|
68
|
+
|
|
69
|
+
for table in (block.unicode_blocks, block.ascii_blocks):
|
|
70
|
+
for key, value in table.items():
|
|
71
|
+
if not key.startswith('^'):
|
|
72
|
+
self.assertEqual(ranges(table['^' + key]), complement(ranges(value)), key)
|
|
73
|
+
|
|
74
|
+
def test_no_block_inverse(self):
|
|
75
|
+
"""Test that the inverse of `No_Block` includes the last blocks (Supplementary Private Use Areas)."""
|
|
76
|
+
|
|
77
|
+
from backrefs import bre
|
|
78
|
+
|
|
79
|
+
not_in_block = bre.compile(r'\p{No_Block}')
|
|
80
|
+
in_block = bre.compile(r'\P{No_Block}')
|
|
81
|
+
for c in (0xF0000, 0xFFFFD, 0x100000, 0x10FFFF):
|
|
82
|
+
self.assertTrue(in_block.fullmatch(chr(c)) is not None, f'U+{c:04X}')
|
|
83
|
+
self.assertTrue(not_in_block.fullmatch(chr(c)) is None, f'U+{c:04X}')
|
|
84
|
+
self.assertTrue(not_in_block.fullmatch('\U000effff') is not None)
|
|
85
|
+
self.assertTrue(in_block.fullmatch('\U000effff') is None)
|
|
86
|
+
|
|
34
87
|
def test_block(self):
|
|
35
88
|
"""Test `Block` properties."""
|
|
36
89
|
|
|
@@ -411,6 +411,117 @@ class TestSearchTemplate(unittest.TestCase):
|
|
|
411
411
|
with pytest.raises(PatternError, match='unterminated character set'):
|
|
412
412
|
bre.compile(pattern)
|
|
413
413
|
|
|
414
|
+
def test_uts18_properties(self):
|
|
415
|
+
"""Test `Any`, `Assigned`, and `ASCII`, which UTS #18 requires."""
|
|
416
|
+
|
|
417
|
+
unassigned = chr(0x0378)
|
|
418
|
+
for pattern, expected in (
|
|
419
|
+
(r'\p{Any}', ['A', 'é', unassigned]),
|
|
420
|
+
(r'\P{Any}', []),
|
|
421
|
+
(r'\p{Assigned}', ['A', 'é']),
|
|
422
|
+
(r'\P{Assigned}', [unassigned]),
|
|
423
|
+
(r'\p{ASCII}', ['A']),
|
|
424
|
+
(r'\p{ASCII=No}', ['é', unassigned]),
|
|
425
|
+
(r'[[:^ascii:]x]', ['é', unassigned]),
|
|
426
|
+
):
|
|
427
|
+
p = bre.compile(pattern)
|
|
428
|
+
self.assertEqual([c for c in ('A', 'é', unassigned) if p.fullmatch(c)], expected, pattern)
|
|
429
|
+
|
|
430
|
+
# `ASCII` is also an alias of the `Basic_Latin` block, with the same characters.
|
|
431
|
+
expected = [chr(c) for c in range(0x100) if c < 0x80]
|
|
432
|
+
for pattern in (r'\p{ASCII}', r'\p{InASCII}', r'\p{Block=ASCII}', r'\p{IsASCII}'):
|
|
433
|
+
p = bre.compile(pattern)
|
|
434
|
+
self.assertEqual([chr(c) for c in range(0x100) if p.fullmatch(chr(c))], expected, pattern)
|
|
435
|
+
|
|
436
|
+
# `Any` is every character, even in ASCII mode, as it is in Regex.
|
|
437
|
+
self.assertTrue(bre.fullmatch(r'\p{Any}', 'é', re.ASCII) is not None)
|
|
438
|
+
self.assertTrue(bre.fullmatch(r'\P{Any}', 'é', re.ASCII) is None)
|
|
439
|
+
self.assertTrue(bre.fullmatch(r'\p{Assigned}', 'é', re.ASCII) is None)
|
|
440
|
+
self.assertEqual(len([c for c in range(256) if bre.fullmatch(rb'\p{Any}', bytes([c]))]), 256)
|
|
441
|
+
self.assertEqual(len([c for c in range(256) if bre.fullmatch(rb'\p{ASCII}', bytes([c]))]), 128)
|
|
442
|
+
|
|
443
|
+
def test_is_prefix(self):
|
|
444
|
+
"""An initial `is` is allowed on property names and General Category values, as Unicode allows."""
|
|
445
|
+
|
|
446
|
+
for pattern, expected in (
|
|
447
|
+
(r'\p{IsLu}', ['A']),
|
|
448
|
+
(r'\p{Is_Uppercase_Letter}', ['A']),
|
|
449
|
+
(r'\P{IsLu}', ['a', '1']),
|
|
450
|
+
(r'\p{^IsLu}', ['a', '1']),
|
|
451
|
+
(r'[[:IsLu:]]', ['A']),
|
|
452
|
+
(r'[x\p{IsLu}]', ['A']),
|
|
453
|
+
(r'\p{gc=IsLu}', ['A']),
|
|
454
|
+
(r'\p{IsGC=Lu}', ['A']),
|
|
455
|
+
(r'\p{Is_General_Category=IsLu}', ['A']),
|
|
456
|
+
(r'\p{IsAlpha=Yes}', ['A', 'a']),
|
|
457
|
+
(r'\P{IsAlpha=No}', ['A', 'a']),
|
|
458
|
+
(r'\p{IsScript=Latin}', ['A', 'a']),
|
|
459
|
+
# Already supported
|
|
460
|
+
(r'\p{IsLatin}', ['A', 'a']),
|
|
461
|
+
(r'\p{IsAlpha}', ['A', 'a']),
|
|
462
|
+
):
|
|
463
|
+
p = bre.compile(pattern)
|
|
464
|
+
self.assertEqual([c for c in 'Aa1' if p.fullmatch(c)], expected, pattern)
|
|
465
|
+
|
|
466
|
+
# Only removed once
|
|
467
|
+
for pattern in (r'\p{IsIsLu}', r'\p{gc=IsIsLu}', r'\p{IsIsGC=Lu}'):
|
|
468
|
+
with pytest.raises(ValueError):
|
|
469
|
+
bre.compile(pattern)
|
|
470
|
+
|
|
471
|
+
# Values that begin with `is` are unchanged.
|
|
472
|
+
self.assertTrue(bre.fullmatch(r'\p{lb=IS}', ',') is not None)
|
|
473
|
+
self.assertTrue(bre.fullmatch(r'\p{ccc=IS}', chr(0x0345)) is not None)
|
|
474
|
+
self.assertTrue(bre.fullmatch(r'\p{dt=Iso}', chr(0xFE80)) is not None)
|
|
475
|
+
|
|
476
|
+
def test_negated_property_ignorecase(self):
|
|
477
|
+
"""Negated properties should not match the other case of characters in the property."""
|
|
478
|
+
|
|
479
|
+
def matches(pattern, chars='Aa1x', flags=re.IGNORECASE):
|
|
480
|
+
p = bre.compile(pattern, flags)
|
|
481
|
+
return [c for c in chars if p.fullmatch(c)]
|
|
482
|
+
|
|
483
|
+
# On their own
|
|
484
|
+
for pattern in (r'\P{Lu}', r'\p{^Lu}', r'\PL', r'[\P{Lu}]', r'[[:^upper:]]', r'[^\p{Lu}]'):
|
|
485
|
+
self.assertEqual(matches(pattern, 'Aa1'), ['1'], pattern)
|
|
486
|
+
self.assertEqual(matches(r'\P{Lu}', 'Aa1', 0), ['a', '1'])
|
|
487
|
+
|
|
488
|
+
# With other members of a set
|
|
489
|
+
self.assertEqual(matches(r'[\P{Lu}x]'), ['1', 'x'])
|
|
490
|
+
self.assertEqual(matches(r'[x\P{Lu}\d]'), ['1', 'x'])
|
|
491
|
+
self.assertEqual(matches(r'[\P{Lu}\P{Ll}]'), ['1'])
|
|
492
|
+
self.assertEqual(matches(r'[^\P{Lu}x]'), ['A', 'a'])
|
|
493
|
+
self.assertEqual(matches(r'[^\P{Lu}\P{Ll}]'), ['A', 'a', 'x'])
|
|
494
|
+
self.assertEqual(matches(r'[^\P{Lu}\d]'), ['A', 'a', 'x'])
|
|
495
|
+
self.assertEqual(matches(r'[\P{Lu}x]', flags=0), ['a', '1', 'x'])
|
|
496
|
+
self.assertEqual(matches(r'[^\P{Lu}x]', flags=0), ['A'])
|
|
497
|
+
|
|
498
|
+
# A literal `^` after the negating `^`
|
|
499
|
+
self.assertEqual(matches(r'[^^\P{Nd}]', '^1a'), ['1'])
|
|
500
|
+
|
|
501
|
+
# Quantifiers and lookbehinds
|
|
502
|
+
self.assertTrue(bre.fullmatch(r'[\P{Lu}x]{2}', 'X1', re.IGNORECASE) is not None)
|
|
503
|
+
self.assertTrue(bre.fullmatch(r'[\P{Lu}x]{2}', 'A1', re.IGNORECASE) is None)
|
|
504
|
+
self.assertTrue(bre.search(r'(?<=[^\P{Lu}x])b', 'ab', re.IGNORECASE) is not None)
|
|
505
|
+
self.assertTrue(bre.search(r'(?<=[^\P{Lu}x])b', 'xb', re.IGNORECASE) is None)
|
|
506
|
+
self.assertTrue(bre.search(r'(?<![\P{Lu}x])b', 'Ab', re.IGNORECASE) is not None)
|
|
507
|
+
|
|
508
|
+
# Byte strings
|
|
509
|
+
self.assertEqual([c for c in b'Aa1' if bre.fullmatch(rb'[\P{Lu}x]', bytes([c]), re.IGNORECASE)], [ord('1')])
|
|
510
|
+
|
|
511
|
+
def test_binary_property_false_value(self):
|
|
512
|
+
"""A binary property with a false value is the set of characters with that value, not a negation."""
|
|
513
|
+
|
|
514
|
+
# So case-insensitive matching applies to that set, as it does in Regex.
|
|
515
|
+
p = bre.compile(r'[\p{Lowercase=No}x]', re.IGNORECASE)
|
|
516
|
+
self.assertEqual([c for c in 'Aa1x' if p.fullmatch(c)], ['A', 'a', '1', 'x'])
|
|
517
|
+
|
|
518
|
+
# Negations of a property alias with a value
|
|
519
|
+
for pattern, expected in (
|
|
520
|
+
(r'\P{Alpha=No}', ['A', 'a']), (r'\p{^Alpha=No}', ['A', 'a']), (r'\P{Alpha=Yes}', ['1', ' ']),
|
|
521
|
+
(r'\P{Upper=F}', ['A']), (r'\P{WSpace=No}', [' '])
|
|
522
|
+
):
|
|
523
|
+
self.assertEqual([c for c in 'Aa1 ' if bre.fullmatch(pattern, c)], expected, pattern)
|
|
524
|
+
|
|
414
525
|
def test_caret_after_negation(self):
|
|
415
526
|
"""A `^` after the negating `^` is a literal."""
|
|
416
527
|
|
|
@@ -170,3 +170,48 @@ class TestGeneralCategory(unittest.TestCase):
|
|
|
170
170
|
result1 = uniprops.get_unicode_property(k)
|
|
171
171
|
result2 = uniprops.get_unicode_property(v)
|
|
172
172
|
self.assertEqual(result1, result2)
|
|
173
|
+
|
|
174
|
+
def test_is_prefix(self):
|
|
175
|
+
"""Test that an initial `is` is allowed on General Category values and on the property name (`UAX44-LM3`)."""
|
|
176
|
+
|
|
177
|
+
table = generalcategory.unicode_properties
|
|
178
|
+
names = set(table) | {a + b for a in table for b in table[a] if not b.startswith('^')}
|
|
179
|
+
names |= set(alias.unicode_alias['generalcategory'])
|
|
180
|
+
for name in names:
|
|
181
|
+
expected = uniprops.get_unicode_property(name)
|
|
182
|
+
negated = uniprops.get_unicode_property('^' + name)
|
|
183
|
+
self.assertEqual(uniprops.get_unicode_property('is' + name), expected, name)
|
|
184
|
+
self.assertEqual(uniprops.get_unicode_property('^is' + name), negated, name)
|
|
185
|
+
for prop in ('gc', 'generalcategory', 'isgc', 'isgeneralcategory'):
|
|
186
|
+
self.assertEqual(uniprops.get_unicode_property(prop, 'is' + name), expected, (prop, name))
|
|
187
|
+
self.assertEqual(uniprops.get_unicode_property(prop, name), expected, (prop, name))
|
|
188
|
+
self.assertEqual(uniprops.get_unicode_property('^' + prop, 'is' + name), negated, (prop, name))
|
|
189
|
+
|
|
190
|
+
# An initial `is` is only removed once.
|
|
191
|
+
for args in (('isislu',), ('gc', 'isislu'), ('isisgc', 'lu'), ('is',), ('gc', 'is')):
|
|
192
|
+
with self.assertRaises(ValueError):
|
|
193
|
+
uniprops.get_unicode_property(*args)
|
|
194
|
+
|
|
195
|
+
def test_is_prefix_unambiguous(self):
|
|
196
|
+
"""
|
|
197
|
+
Test that no name begins with `is`.
|
|
198
|
+
|
|
199
|
+
Removing an initial `is` relies on names being unique even when an initial `is` is ignored, which Unicode
|
|
200
|
+
guarantees. As no name begins with `is`, that holds for the names Backrefs supports. If a name ever begins
|
|
201
|
+
with `is`, check that it can't be confused with another name before allowing it here.
|
|
202
|
+
"""
|
|
203
|
+
|
|
204
|
+
from backrefs.uniprops.unidata import binary, block, scriptextensions
|
|
205
|
+
|
|
206
|
+
table = generalcategory.unicode_properties
|
|
207
|
+
namespaces = {
|
|
208
|
+
'General Category': (
|
|
209
|
+
set(table) | {a + b for a in table for b in table[a]} | set(alias.unicode_alias['generalcategory'])
|
|
210
|
+
),
|
|
211
|
+
'Script': set(scriptextensions.unicode_script_extensions) | set(alias.unicode_alias['script']),
|
|
212
|
+
'Binary': set(binary.unicode_binary) | set(alias.unicode_alias['binary']),
|
|
213
|
+
'Block': set(block.unicode_blocks) | set(alias.unicode_alias['block']),
|
|
214
|
+
'Property names': set(alias.unicode_alias['_']) | set(alias.unicode_alias['_'].values()),
|
|
215
|
+
}
|
|
216
|
+
for namespace, names in namespaces.items():
|
|
217
|
+
self.assertEqual([n for n in names if n.lstrip('^').startswith('is')], [], namespace)
|
|
@@ -316,6 +316,9 @@ def gen_blocks(output, ascii_props=False, append=False, prefix="", aliases=None)
|
|
|
316
316
|
if piece[0] > last + 1:
|
|
317
317
|
no_block_inverse.append((last + 1, piece[0] - 1))
|
|
318
318
|
last = piece[1]
|
|
319
|
+
# Include everything after the last range that is not in a block.
|
|
320
|
+
if last < max_range:
|
|
321
|
+
no_block_inverse.append((last + 1, max_range))
|
|
319
322
|
for block, name in ((no_block, 'noblock'), (no_block_inverse, '^noblock')):
|
|
320
323
|
f.write('\n "%s": "' % name)
|
|
321
324
|
for piece in block:
|
|
@@ -688,6 +691,14 @@ def gen_binary(table, output, ascii_props=False, append=False, prefix="", aliase
|
|
|
688
691
|
binary['horizspace'] = [x for x in binary['horizspace'] if x <= max_range]
|
|
689
692
|
binary['vertspace'] = [x for x in binary['vertspace'] if x <= max_range]
|
|
690
693
|
|
|
694
|
+
# Properties required by UTS #18 (`RL1.2`) that are not defined in the Unicode Character Database.
|
|
695
|
+
# `Any` is every character, even in ASCII mode, as it is in Regex. `Assigned` is everything that is not
|
|
696
|
+
# unassigned (General Category `Cn`).
|
|
697
|
+
unassigned = set(table['c']['n'])
|
|
698
|
+
binary['any'] = list(range(0, MAXUNICODE + 1))
|
|
699
|
+
binary['assigned'] = [x for x in range(0, max_range + 1) if x not in unassigned]
|
|
700
|
+
binary['ascii'] = list(range(0, min(MAXVALIDASCII, max_range) + 1))
|
|
701
|
+
|
|
691
702
|
for filename, include in binary_props:
|
|
692
703
|
with open(os.path.join(HOME, 'unicodedata', UNIVERSION, filename), 'r', encoding='utf-8') as uf:
|
|
693
704
|
for line in uf:
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|