mirror of
https://github.com/python/cpython.git
synced 2026-10-28 18:35:21 +08:00
* [3.11] gh-155292: Don't consider Unicode codepoint attributes outside RFC 3454 (GH-155293) (GH-156020) Due to a bug, some Unicode codepoint attributes were considered for characters not yet defined in Unicode 3.2.0 or attributes which changed in later Unicode versions. RFC 3454 (StringPrep) requires using Unicode 3.2.0 strictly. (cherry picked from commit7e109d084d) The cherry-pick needed reworking as GH-144815 wasn't backported to 3.14 and below, so unassigned characters don't have bidi values. Co-authored-by: Seth Larson <seth@python.org> Co-authored-by: Stan Ulbrych <89152624+stanfromireland@users.noreply.github.com> Co-authored-by: Petr Viktorin <encukou@gmail.com> (cherry picked from commit1e54caa096) * Add Unicode_3_2_0_FunctionsTest * Regenerate Unicode data --------- Co-authored-by: Seth Larson <seth@python.org> Co-authored-by: Stan Ulbrych <89152624+stanfromireland@users.noreply.github.com>
462 lines
11 KiB
Python
462 lines
11 KiB
Python
import re
|
|
import os
|
|
import unicodedata as unicodedata_current
|
|
from unicodedata import ucd_3_2_0 as unicodedata_320
|
|
|
|
FILENAME = "Tools/unicode/data/rfc3454.txt"
|
|
URL = "https://www.rfc-editor.org/rfc/rfc3454.txt"
|
|
|
|
def gen_category(cats):
|
|
for i in range(0, 0x110000):
|
|
if unicodedata_320.category(chr(i)) in cats:
|
|
yield i
|
|
|
|
def gen_bidirectional(cats):
|
|
for i in range(0, 0x110000):
|
|
if unicodedata_320.bidirectional(chr(i)) in cats:
|
|
yield i
|
|
|
|
def compact_set(l):
|
|
single = []
|
|
tuple = []
|
|
prev = None
|
|
span = 0
|
|
for e in l:
|
|
if prev is None:
|
|
prev = e
|
|
span = 0
|
|
continue
|
|
if prev+span+1 != e:
|
|
if span > 2:
|
|
tuple.append((prev,prev+span+1))
|
|
else:
|
|
for i in range(prev, prev+span+1):
|
|
single.append(i)
|
|
prev = e
|
|
span = 0
|
|
else:
|
|
span += 1
|
|
if span:
|
|
tuple.append((prev,prev+span+1))
|
|
else:
|
|
single.append(prev)
|
|
if not single and len(tuple) == 1:
|
|
tuple = "range(%d,%d)" % tuple[0]
|
|
else:
|
|
tuple = " + ".join("list(range(%d,%d))" % t for t in tuple)
|
|
if not single:
|
|
return "set(%s)" % tuple
|
|
if not tuple:
|
|
return "set(%r)" % (single,)
|
|
return "set(%r + %s)" % (single, tuple)
|
|
|
|
############## Read the tables in the RFC #######################
|
|
|
|
try:
|
|
data_file = open(FILENAME, encoding='utf-8')
|
|
except FileNotFoundError:
|
|
import urllib.request
|
|
os.makedirs(os.path.dirname(FILENAME), exist_ok=True)
|
|
urllib.request.urlretrieve(URL, filename=FILENAME)
|
|
data_file = open(FILENAME, encoding='utf-8')
|
|
|
|
with data_file:
|
|
data = data_file.readlines()
|
|
|
|
tables = []
|
|
curname = None
|
|
for l in data:
|
|
l = l.strip()
|
|
if not l:
|
|
continue
|
|
# Skip RFC page breaks
|
|
if l.startswith(("Hoffman & Blanchet", "RFC 3454")):
|
|
continue
|
|
# Find start/end lines
|
|
m = re.match("----- (Start|End) Table ([A-Z](.[0-9])+) -----", l)
|
|
if m:
|
|
if m.group(1) == "Start":
|
|
if curname:
|
|
raise RuntimeError("Double Start", (curname, l))
|
|
curname = m.group(2)
|
|
table = {}
|
|
tables.append((curname, table))
|
|
continue
|
|
else:
|
|
if not curname:
|
|
raise RuntimeError("End without start", l)
|
|
if curname != m.group(2):
|
|
raise RuntimeError("Unexpected end", l)
|
|
curname = None
|
|
continue
|
|
if not curname:
|
|
continue
|
|
# Now we are in a table
|
|
fields = l.split(";")
|
|
if len(fields) > 1:
|
|
# Drop comment field
|
|
fields = fields[:-1]
|
|
if len(fields) == 1:
|
|
fields = fields[0].split("-")
|
|
if len(fields) > 1:
|
|
# range
|
|
try:
|
|
start, end = fields
|
|
except ValueError:
|
|
raise RuntimeError("Unpacking problem", l)
|
|
else:
|
|
start = end = fields[0]
|
|
start = int(start, 16)
|
|
end = int(end, 16)
|
|
for i in range(start, end+1):
|
|
table[i] = i
|
|
else:
|
|
code, value = fields
|
|
value = value.strip()
|
|
if value:
|
|
value = [int(v, 16) for v in value.split(" ")]
|
|
else:
|
|
# table B.1
|
|
value = None
|
|
table[int(code, 16)] = value
|
|
|
|
########### Generate compact Python versions of the tables #############
|
|
|
|
print("""# This file is generated by mkstringprep.py. DO NOT EDIT.
|
|
\"\"\"Library that exposes various tables found in the StringPrep RFC 3454.
|
|
|
|
There are two kinds of tables: sets, for which a member test is provided,
|
|
and mappings, for which a mapping function is provided.
|
|
\"\"\"
|
|
|
|
# This check asserts that mkstringprep.py has been run
|
|
# when unicodedata is modified to ensure conformant behavior.
|
|
import unicodedata
|
|
""")
|
|
|
|
print("assert unicodedata.unidata_version == %r" % (unicodedata_current.unidata_version,))
|
|
|
|
print("""
|
|
from unicodedata import ucd_3_2_0 as unicodedata_320
|
|
""")
|
|
|
|
print("assert unicodedata_320.unidata_version == %r" % (unicodedata_320.unidata_version,))
|
|
|
|
# A.1 is the table of unassigned characters
|
|
# XXX Plane 15 PUA is listed as unassigned in Python.
|
|
name, table = tables[0]
|
|
del tables[0]
|
|
assert name == "A.1"
|
|
table = set(table.keys())
|
|
Cn = set(gen_category(["Cn"]))
|
|
|
|
# FDD0..FDEF are process internal codes
|
|
Cn -= set(range(0xFDD0, 0xFDF0))
|
|
# not a character
|
|
Cn -= set(range(0xFFFE, 0x110000, 0x10000))
|
|
Cn -= set(range(0xFFFF, 0x110000, 0x10000))
|
|
|
|
# assert table == Cn
|
|
|
|
print("""
|
|
def in_table_a1(code):
|
|
if unicodedata_320.category(code) != 'Cn': return False
|
|
c = ord(code)
|
|
if 0xFDD0 <= c < 0xFDF0: return False
|
|
return (c & 0xFFFF) not in (0xFFFE, 0xFFFF)
|
|
""")
|
|
|
|
# B.1 cannot easily be derived
|
|
name, table = tables[0]
|
|
del tables[0]
|
|
assert name == "B.1"
|
|
table = sorted(table.keys())
|
|
print("""
|
|
b1_set = """ + compact_set(table) + """
|
|
def in_table_b1(code):
|
|
return ord(code) in b1_set
|
|
""")
|
|
|
|
# B.2 and B.3 is case folding.
|
|
# It takes CaseFolding.txt into account, which is
|
|
# not available in the Python database. Since
|
|
# B.2 is derived from B.3, we process B.3 first.
|
|
# B.3 supposedly *is* CaseFolding-3.2.0.txt.
|
|
|
|
name, table_b2 = tables[0]
|
|
del tables[0]
|
|
assert name == "B.2"
|
|
|
|
name, table_b3 = tables[0]
|
|
del tables[0]
|
|
assert name == "B.3"
|
|
|
|
# B.3 is mostly Python's .lower, except for a number
|
|
# of special cases, e.g. considering canonical forms.
|
|
# To enforce Unicode 3.2.0 behavior of .lower instead of
|
|
# whatever Unicode version is included with Python we
|
|
# add unassigned or newly case-folding codepoints to
|
|
# the exception map, too.
|
|
|
|
b3_exceptions = {}
|
|
|
|
for k,v in table_b2.items():
|
|
if list(map(ord, chr(k).lower())) != v:
|
|
b3_exceptions[k] = "".join(map(chr,v))
|
|
for cp in range(0x110000):
|
|
ch = chr(cp)
|
|
# Assigned in current Unicode version
|
|
# and supports case folding, but not
|
|
# explicitly in B.2 or B.3 tables.
|
|
if (unicodedata_current.category(ch) != "Cn"
|
|
and ch.lower() != ch
|
|
and cp not in table_b2
|
|
and cp not in table_b3):
|
|
b3_exceptions[cp] = ch # Identity.
|
|
|
|
b3 = sorted(b3_exceptions.items())
|
|
|
|
print("""
|
|
b3_exceptions = {""")
|
|
for i, kv in enumerate(b3):
|
|
print("0x%x:%a," % kv, end='\n' if i % 4 == 3 else ' ')
|
|
print("}")
|
|
|
|
print("""
|
|
def map_table_b3(code):
|
|
r = b3_exceptions.get(ord(code))
|
|
if r is not None: return r
|
|
return code.lower()
|
|
""")
|
|
|
|
def map_table_b3(code):
|
|
r = b3_exceptions.get(ord(code))
|
|
if r is not None: return r
|
|
return code.lower()
|
|
|
|
# B.2 is case folding for NFKC. This is the same as B.3,
|
|
# except where NormalizeWithKC(Fold(a)) !=
|
|
# NormalizeWithKC(Fold(NormalizeWithKC(Fold(a))))
|
|
|
|
def map_table_b2(a):
|
|
al = map_table_b3(a)
|
|
b = unicodedata_320.normalize("NFKC", al)
|
|
bl = "".join([map_table_b3(ch) for ch in b])
|
|
c = unicodedata_320.normalize("NFKC", bl)
|
|
if b != c:
|
|
return c
|
|
else:
|
|
return al
|
|
|
|
specials = {}
|
|
for k,v in table_b2.items():
|
|
if list(map(ord, map_table_b2(chr(k)))) != v:
|
|
specials[k] = v
|
|
|
|
# B.3 should not add any additional special cases
|
|
assert specials == {}
|
|
|
|
print("""
|
|
def map_table_b2(a):
|
|
al = map_table_b3(a)
|
|
b = unicodedata_320.normalize("NFKC", al)
|
|
bl = "".join([map_table_b3(ch) for ch in b])
|
|
c = unicodedata_320.normalize("NFKC", bl)
|
|
if b != c:
|
|
return c
|
|
else:
|
|
return al
|
|
""")
|
|
|
|
# C.1.1 is a table with a single character
|
|
name, table = tables[0]
|
|
del tables[0]
|
|
assert name == "C.1.1"
|
|
assert table == {0x20:0x20}
|
|
|
|
print("""
|
|
def in_table_c11(code):
|
|
return code == " "
|
|
""")
|
|
|
|
# C.1.2 is the rest of all space characters
|
|
name, table = tables[0]
|
|
del tables[0]
|
|
assert name == "C.1.2"
|
|
|
|
table = set(table.keys())
|
|
Zs = set(gen_category(["Zs"])) - {0x20}
|
|
assert Zs == table
|
|
|
|
print("""
|
|
def in_table_c12(code):
|
|
return unicodedata_320.category(code) == "Zs" and code != " "
|
|
|
|
def in_table_c11_c12(code):
|
|
return unicodedata_320.category(code) == "Zs"
|
|
""")
|
|
|
|
# C.2.1 ASCII control characters
|
|
name, table_c21 = tables[0]
|
|
del tables[0]
|
|
assert name == "C.2.1"
|
|
|
|
Cc = set(gen_category(["Cc"]))
|
|
Cc_ascii = Cc & set(range(128))
|
|
table_c21 = set(table_c21.keys())
|
|
assert Cc_ascii == table_c21
|
|
|
|
print("""
|
|
def in_table_c21(code):
|
|
return ord(code) < 128 and unicodedata_320.category(code) == "Cc"
|
|
""")
|
|
|
|
# C.2.2 Non-ASCII control characters. It also includes
|
|
# a number of characters in category Cf.
|
|
name, table_c22 = tables[0]
|
|
del tables[0]
|
|
assert name == "C.2.2"
|
|
|
|
Cc_nonascii = Cc - Cc_ascii
|
|
table_c22 = set(table_c22.keys())
|
|
assert len(Cc_nonascii - table_c22) == 0
|
|
|
|
specials = list(table_c22 - Cc_nonascii)
|
|
specials.sort()
|
|
|
|
print("""c22_specials = """ + compact_set(specials) + """
|
|
def in_table_c22(code):
|
|
c = ord(code)
|
|
if c < 128: return False
|
|
if unicodedata_320.category(code) == "Cc": return True
|
|
return c in c22_specials
|
|
|
|
def in_table_c21_c22(code):
|
|
return unicodedata_320.category(code) == "Cc" or \\
|
|
ord(code) in c22_specials
|
|
""")
|
|
|
|
# C.3 Private use
|
|
name, table = tables[0]
|
|
del tables[0]
|
|
assert name == "C.3"
|
|
|
|
Co = set(gen_category(["Co"]))
|
|
assert set(table.keys()) == Co
|
|
|
|
print("""
|
|
def in_table_c3(code):
|
|
return unicodedata_320.category(code) == "Co"
|
|
""")
|
|
|
|
# C.4 Non-character code points, xFFFE, xFFFF
|
|
# plus process internal codes
|
|
name, table = tables[0]
|
|
del tables[0]
|
|
assert name == "C.4"
|
|
|
|
nonchar = set(range(0xFDD0,0xFDF0))
|
|
nonchar.update(range(0xFFFE,0x110000,0x10000))
|
|
nonchar.update(range(0xFFFF,0x110000,0x10000))
|
|
table = set(table.keys())
|
|
assert table == nonchar
|
|
|
|
print("""
|
|
def in_table_c4(code):
|
|
c = ord(code)
|
|
if c < 0xFDD0: return False
|
|
if c < 0xFDF0: return True
|
|
return (ord(code) & 0xFFFF) in (0xFFFE, 0xFFFF)
|
|
""")
|
|
|
|
# C.5 Surrogate codes
|
|
name, table = tables[0]
|
|
del tables[0]
|
|
assert name == "C.5"
|
|
|
|
Cs = set(gen_category(["Cs"]))
|
|
assert set(table.keys()) == Cs
|
|
|
|
print("""
|
|
def in_table_c5(code):
|
|
return unicodedata_320.category(code) == "Cs"
|
|
""")
|
|
|
|
# C.6 Inappropriate for plain text
|
|
name, table = tables[0]
|
|
del tables[0]
|
|
assert name == "C.6"
|
|
|
|
table = sorted(table.keys())
|
|
|
|
print("""
|
|
c6_set = """ + compact_set(table) + """
|
|
def in_table_c6(code):
|
|
return ord(code) in c6_set
|
|
""")
|
|
|
|
# C.7 Inappropriate for canonical representation
|
|
name, table = tables[0]
|
|
del tables[0]
|
|
assert name == "C.7"
|
|
|
|
table = sorted(table.keys())
|
|
|
|
print("""
|
|
c7_set = """ + compact_set(table) + """
|
|
def in_table_c7(code):
|
|
return ord(code) in c7_set
|
|
""")
|
|
|
|
# C.8 Change display properties or are deprecated
|
|
name, table = tables[0]
|
|
del tables[0]
|
|
assert name == "C.8"
|
|
|
|
table = sorted(table.keys())
|
|
|
|
print("""
|
|
c8_set = """ + compact_set(table) + """
|
|
def in_table_c8(code):
|
|
return ord(code) in c8_set
|
|
""")
|
|
|
|
# C.9 Tagging characters
|
|
name, table = tables[0]
|
|
del tables[0]
|
|
assert name == "C.9"
|
|
|
|
table = sorted(table.keys())
|
|
|
|
print("""
|
|
c9_set = """ + compact_set(table) + """
|
|
def in_table_c9(code):
|
|
return ord(code) in c9_set
|
|
""")
|
|
|
|
# D.1 Characters with bidirectional property "R" or "AL"
|
|
name, table = tables[0]
|
|
del tables[0]
|
|
assert name == "D.1"
|
|
|
|
RandAL = set(gen_bidirectional(["R","AL"]))
|
|
assert set(table.keys()) == RandAL
|
|
|
|
print("""
|
|
def in_table_d1(code):
|
|
return unicodedata_320.bidirectional(code) in ("R","AL")
|
|
""")
|
|
|
|
# D.2 Characters with bidirectional property "L"
|
|
name, table = tables[0]
|
|
del tables[0]
|
|
assert name == "D.2"
|
|
|
|
L = set(gen_bidirectional(["L"]))
|
|
assert set(table.keys()) == L
|
|
|
|
print("""
|
|
def in_table_d2(code):
|
|
return unicodedata_320.bidirectional(code) == "L"
|
|
""", end="")
|