ucdinfo 0.2__py3-none-any.whl
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.
- ucd/data/ucdata_pickle_0.2.bz2 +0 -0
- ucdinfo/__init__.py +4 -0
- ucdinfo/_initold.py +568 -0
- ucdinfo/cache.py +223 -0
- ucdinfo/ucd.py +580 -0
- ucdinfo-0.2.dist-info/METADATA +82 -0
- ucdinfo-0.2.dist-info/RECORD +11 -0
- ucdinfo-0.2.dist-info/WHEEL +5 -0
- ucdinfo-0.2.dist-info/entry_points.txt +2 -0
- ucdinfo-0.2.dist-info/licenses/LICENSE +21 -0
- ucdinfo-0.2.dist-info/top_level.txt +1 -0
|
Binary file
|
ucdinfo/__init__.py
ADDED
ucdinfo/_initold.py
ADDED
|
@@ -0,0 +1,568 @@
|
|
|
1
|
+
#!/usr/bin/python3
|
|
2
|
+
|
|
3
|
+
"""ucd
|
|
4
|
+
|
|
5
|
+
This module contains most of the ucd information for every character in Unicode.
|
|
6
|
+
|
|
7
|
+
SYNOPSIS:
|
|
8
|
+
|
|
9
|
+
from ucd import get_ucd, get_info, find_ucd, get_enums
|
|
10
|
+
print(get_ucd(0x0041, 'scx'))
|
|
11
|
+
print(get_info(0x0041))
|
|
12
|
+
print(get_enums("indic position"))
|
|
13
|
+
print(find_ucd("indic position", "Bottom")
|
|
14
|
+
|
|
15
|
+
Note cjk properties are not supported for space reasons.
|
|
16
|
+
|
|
17
|
+
If you want to use your own data file (perhaps the module data is stale) the use
|
|
18
|
+
the object interface:
|
|
19
|
+
|
|
20
|
+
from ucd import UCD
|
|
21
|
+
myucd = UCD(localfile="ucd.nounihan.flat.zip") # localfile falls back to bundled data
|
|
22
|
+
print(myucd.get(0x0041, 'scx'))
|
|
23
|
+
|
|
24
|
+
The second parameter specifies the property to be queried and can either be a property from
|
|
25
|
+
https://www.unicode.org/reports/tr42, especially section 4.4 Properties, or the start of a
|
|
26
|
+
full property name from that list.
|
|
27
|
+
|
|
28
|
+
For characters not yet in Unicode, data for additional characters can
|
|
29
|
+
be temporarily appended to the bundled data:
|
|
30
|
+
|
|
31
|
+
from ucd import get_ucd, loadxml
|
|
32
|
+
loadxml("extra-ucd.xml")
|
|
33
|
+
|
|
34
|
+
or, with the object interface:
|
|
35
|
+
|
|
36
|
+
from ucd import UCD
|
|
37
|
+
myucd = UCD().loadxml("extra-ucd.xml")
|
|
38
|
+
|
|
39
|
+
The named file must be coded in the same form as the "flat" UCD XML data, though the only
|
|
40
|
+
required character attributes are "cp" and anything needed by the calling process. For example:
|
|
41
|
+
|
|
42
|
+
<?xml version="1.0" encoding="utf-8" standalone="yes"?>
|
|
43
|
+
<ucd xmlns="http://www.unicode.org/ns/2003/ucd/1.0">
|
|
44
|
+
<description>Some additional characters</description>
|
|
45
|
+
<repertoire>
|
|
46
|
+
<char cp="10EC2" age="16.0" gc="Lo" bc="AL" na="ARABIC LETTER DAL WITH TWO DOTS VERTICALLY BELOW"></char>
|
|
47
|
+
<char cp="10EC3" age="16.0" gc="Lo" bc="AL" na="ARABIC LETTER TAH WITH TWO DOTS VERTICALLY BELOW"></char>
|
|
48
|
+
<char cp="10EC4" age="16.0" gc="Lo" bc="AL" na="ARABIC LETTER KAF WITH TWO DOTS VERTICALLY BELOW"></char>
|
|
49
|
+
</repertoire>
|
|
50
|
+
</ucd>
|
|
51
|
+
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
import array, pickle, pprint
|
|
55
|
+
import xml.etree.ElementTree as et
|
|
56
|
+
import os, bz2, zipfile, io, sys
|
|
57
|
+
import urllib.request
|
|
58
|
+
from datetime import datetime, timezone, timedelta
|
|
59
|
+
from email.utils import parsedate_to_datetime
|
|
60
|
+
import platformdirs
|
|
61
|
+
|
|
62
|
+
__all__ = ['get_ucd', 'find_ucd', 'get_enums', 'get_info']
|
|
63
|
+
|
|
64
|
+
FORMAT_VERSION = "1"
|
|
65
|
+
|
|
66
|
+
# Unicode data xml attributes
|
|
67
|
+
_binfieldnames = """AHex Alpha Bidi_C Bidi_M Cased CE CI Comp_Ex CWCF CWCM CWKCF CWL CWT CWU
|
|
68
|
+
Dash Dep DI Dia Ext Gr_Base Gr_Ext Gr_Link Hex Hyphen IDC Ideo IDS IDSB
|
|
69
|
+
IDST Join_C LOE Lower Math MCM NChar OAlpha ODI OGr_Ext OIDC OIDS OLower OMath
|
|
70
|
+
OUpper Pat_Syn Pat_WS PCM QMark Radical RI SD STerm Term UIdeo Upper VS
|
|
71
|
+
WSpace XIDC XIDS XO_NFC XO_NFD XO_NFKC XO_NFKD"""
|
|
72
|
+
_binmap = dict((x, i) for i, x in enumerate(_binfieldnames.split()))
|
|
73
|
+
_enumfieldnames = """age blk sc scx bc bpt ccc dt ea gc GCB hst InPC InSC jg jt lb
|
|
74
|
+
NFC_QC NFD_QC NFKC_QC NFKD_QC nt SB vo WB nv JSN"""
|
|
75
|
+
_cpfieldnames = """cf dm FC_NFKC lc NFKC_CF scf slc stc suc tc uc bmg bpb"""
|
|
76
|
+
_cpfields = set(_cpfieldnames.split())
|
|
77
|
+
_fields = ['_b0', 'age', 'na', 'JSN', 'gc', 'ccc', 'dt', 'dm', 'nt', 'nv',
|
|
78
|
+
'bc', 'bpt', 'bpb', 'bmg', 'suc', 'slc', 'stc', 'uc', 'lc', 'tc',
|
|
79
|
+
'scf', 'cf', 'jt', 'jg', 'ea', 'lb', 'sc', 'scx', 'NFKC_CF', 'FC_NFKC', 'InSC',
|
|
80
|
+
'InPC', 'vo', 'blk']
|
|
81
|
+
_fieldmap = dict((x, i) for i, x in enumerate(_fields))
|
|
82
|
+
|
|
83
|
+
_property_aliases = {
|
|
84
|
+
"Numeric_Value": "nv",
|
|
85
|
+
"Bidi_Mirroring_Glyph": "bmg",
|
|
86
|
+
"Bidi_Paired_Bracket": "bpb",
|
|
87
|
+
"Case_Folding": "cf",
|
|
88
|
+
"Decomposition_Mapping": "dm",
|
|
89
|
+
"FC_NFKC_Closure": "FC_NFKC",
|
|
90
|
+
"Lowercase_Mapping": "lc",
|
|
91
|
+
"NFKC_Casefold": "NFKC_CF",
|
|
92
|
+
"NFKC_Simple_Casefold": "NFKC_SCF",
|
|
93
|
+
"Simple_Case_Folding": "scf",
|
|
94
|
+
"Simple_Lowercase_Mapping": "slc",
|
|
95
|
+
"Simple_Titlecase_Mapping": "stc",
|
|
96
|
+
"Simple_Uppercase_Mapping": "suc",
|
|
97
|
+
"Titlecase_Mapping": "tc",
|
|
98
|
+
"Uppercase_Mapping": "uc",
|
|
99
|
+
"ISO_Comment": "isc",
|
|
100
|
+
"Jamo_Short_Name": "JSN",
|
|
101
|
+
"Name": "na",
|
|
102
|
+
"Script_Extensions": "scx",
|
|
103
|
+
"Age": "age",
|
|
104
|
+
"Block": "blk",
|
|
105
|
+
"Script": "sc",
|
|
106
|
+
"Bidi_Class": "bc",
|
|
107
|
+
"Bidi_Paired_Bracket_Type": "bpt",
|
|
108
|
+
"Canonical_Combining_Class": "ccc",
|
|
109
|
+
"Decomposition_Type": "dt",
|
|
110
|
+
"East_Asian_Width": "ea",
|
|
111
|
+
"General_Category": "gc",
|
|
112
|
+
"Grapheme_Cluster_Break": "GCB",
|
|
113
|
+
"Hangul_Syllable_Type": "hst",
|
|
114
|
+
"Indic_Conjunct_Break": "InCB",
|
|
115
|
+
"Indic_Positional_Category": "InPC",
|
|
116
|
+
"Indic_Syllabic_Category": "InSC",
|
|
117
|
+
"Joining_Group": "jg",
|
|
118
|
+
"Joining_Type": "jt",
|
|
119
|
+
"Line_Break": "lb",
|
|
120
|
+
"NFC_Quick_Check": "NFC_QC",
|
|
121
|
+
"NFD_Quick_Check": "NFD_QC",
|
|
122
|
+
"NFKC_Quick_Check": "NFKC_QC",
|
|
123
|
+
"NFKD_Quick_Check": "NFKD_QC",
|
|
124
|
+
"Numeric_Type": "nt",
|
|
125
|
+
"Sentence_Break": "SB",
|
|
126
|
+
"Vertical_Orientation": "vo",
|
|
127
|
+
"Word_Break": "WB",
|
|
128
|
+
"ASCII_Hex_Digit": "AHex",
|
|
129
|
+
"Alphabetic": "Alpha",
|
|
130
|
+
"Bidi_Control": "Bidi_C",
|
|
131
|
+
"Bidi_Mirrored": "Bidi_M",
|
|
132
|
+
"Cased": "Cased",
|
|
133
|
+
"Composition_Exclusion": "CE",
|
|
134
|
+
"Case_Ignorable": "CI",
|
|
135
|
+
"Full_Composition_Exclusion": "Comp_Ex",
|
|
136
|
+
"Changes_When_Casefolded": "CWCF",
|
|
137
|
+
"Changes_When_Casemapped": "CWCM",
|
|
138
|
+
"Changes_When_NFKC_Casefolded": "CWKCF",
|
|
139
|
+
"Changes_When_Lowercased": "CWL",
|
|
140
|
+
"Changes_When_Titlecased": "CWT",
|
|
141
|
+
"Changes_When_Uppercased": "CWU",
|
|
142
|
+
"Dash": "Dash",
|
|
143
|
+
"Deprecated": "Dep",
|
|
144
|
+
"Default_Ignorable_Code_Point": "DI",
|
|
145
|
+
"Diacritic": "Dia",
|
|
146
|
+
"Extender": "Ext",
|
|
147
|
+
"Grapheme_Base": "Gr_Base",
|
|
148
|
+
"Grapheme_Extend": "Gr_Ext",
|
|
149
|
+
"Grapheme_Link": "Gr_Link",
|
|
150
|
+
"Hex_Digit": "Hex",
|
|
151
|
+
"Hyphen": "Hyphen",
|
|
152
|
+
"ID_Continue": "IDC",
|
|
153
|
+
"Ideographic": "Ideo",
|
|
154
|
+
"ID_Start": "IDS",
|
|
155
|
+
"IDS_Binary_Operator": "IDSB",
|
|
156
|
+
"IDS_Trinary_Operator": "IDST",
|
|
157
|
+
"IDS_Unary_Operator": "IDSU",
|
|
158
|
+
"Join_Control": "Join_C",
|
|
159
|
+
"Logical_Order_Exception": "LOE",
|
|
160
|
+
"Lowercase": "Lower",
|
|
161
|
+
"Math": "Math",
|
|
162
|
+
"Modifier_Combining_Mark": "MCM",
|
|
163
|
+
"Noncharacter_Code_Point": "NChar",
|
|
164
|
+
"Other_Alphabetic": "OAlpha",
|
|
165
|
+
"Other_Default_Ignorable_Code_Point": "ODI",
|
|
166
|
+
"Other_Grapheme_Extend": "OGr_Ext",
|
|
167
|
+
"Other_ID_Continue": "OIDC",
|
|
168
|
+
"Other_ID_Start": "OIDS",
|
|
169
|
+
"Other_Lowercase": "OLower",
|
|
170
|
+
"Other_Math": "OMath",
|
|
171
|
+
"Other_Uppercase": "OUpper",
|
|
172
|
+
"Pattern_Syntax": "Pat_Syn",
|
|
173
|
+
"Pattern_White_Space": "Pat_WS",
|
|
174
|
+
"Prepended_Concatenation_Mark": "PCM",
|
|
175
|
+
"Quotation_Mark": "QMark",
|
|
176
|
+
"Radical": "Radical",
|
|
177
|
+
"Regional_Indicator": "RI",
|
|
178
|
+
"Soft_Dotted": "SD",
|
|
179
|
+
"Sentence_Terminal": "STerm",
|
|
180
|
+
"Terminal_Punctuation": "Term",
|
|
181
|
+
"Unified_Ideograph": "UIdeo",
|
|
182
|
+
"Uppercase": "Upper",
|
|
183
|
+
"Variation_Selector": "VS",
|
|
184
|
+
"White_Space": "WSpace",
|
|
185
|
+
"XID_Continue": "XIDC",
|
|
186
|
+
"XID_Start": "XIDS",
|
|
187
|
+
"Expands_On_NFC": "XO_NFC",
|
|
188
|
+
"Expands_On_NFD": "XO_NFD",
|
|
189
|
+
"Expands_On_NFKC": "XO_NFKC",
|
|
190
|
+
"Expands_On_NFKD": "XO_NFKD",
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
_property_extras = {
|
|
194
|
+
# -- hand-added informal aliases --
|
|
195
|
+
"category": "gc",
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
# normalized (lowercased) name -> canonical key, built once from the
|
|
199
|
+
# canonical field names, the binary field names, and _property_aliases
|
|
200
|
+
_key_lookup = {}
|
|
201
|
+
for _k in _fields:
|
|
202
|
+
if _k != '_b0':
|
|
203
|
+
_key_lookup[_k.lower()] = _k
|
|
204
|
+
for _k in _binmap:
|
|
205
|
+
_key_lookup[_k.lower()] = _k
|
|
206
|
+
for _alias, _canon in _property_aliases.items():
|
|
207
|
+
_key_lookup[_alias.lower()] = _canon
|
|
208
|
+
for _alias, _canon in _property_extras.items():
|
|
209
|
+
_key_lookup[_alias.lower()] = _canon
|
|
210
|
+
|
|
211
|
+
_fieldnames = {v:k.replace("_", " ") for k, v in _property_aliases.items()}
|
|
212
|
+
|
|
213
|
+
def _rebuild_ucd(items, enums, ucd_version=None):
|
|
214
|
+
obj = list.__new__(UCD)
|
|
215
|
+
obj.extend(items)
|
|
216
|
+
obj.enums = enums
|
|
217
|
+
obj.ucd_version = ucd_version
|
|
218
|
+
return obj
|
|
219
|
+
|
|
220
|
+
def _userroot():
|
|
221
|
+
if sys.platform == 'win32':
|
|
222
|
+
import ctypes
|
|
223
|
+
try:
|
|
224
|
+
return ctypes.windll.shell32.IsUserAdmin() != 0
|
|
225
|
+
except:
|
|
226
|
+
return False
|
|
227
|
+
else:
|
|
228
|
+
return os.geteuid() == 0
|
|
229
|
+
|
|
230
|
+
def _varcache():
|
|
231
|
+
if sys.platform == "win32":
|
|
232
|
+
return os.path.join(os.environ.get("ProgramData", 'C:\\ProgramData'), "python_ucd", "Cache")
|
|
233
|
+
elif sys.platform == "darwin":
|
|
234
|
+
return "/Library/Caches/python_ucd"
|
|
235
|
+
else:
|
|
236
|
+
return "/var/cache/python_ucd"
|
|
237
|
+
|
|
238
|
+
def resolve_key(name):
|
|
239
|
+
""" Translate the property name through the property aliases
|
|
240
|
+
using fuzzy matching. Return the name itself on failure or
|
|
241
|
+
ambiguous match. """
|
|
242
|
+
normalized = name.strip().lower().replace(" ", "_")
|
|
243
|
+
if normalized in _key_lookup:
|
|
244
|
+
return _key_lookup[normalized]
|
|
245
|
+
matches = {v for k, v in _key_lookup.items() if k.startswith(normalized)}
|
|
246
|
+
return matches.pop() if len(matches) == 1 else name
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
class _Codepoint(tuple):
|
|
250
|
+
"""Represents the complete information for a particular codepoint"""
|
|
251
|
+
def __new__(cls, *a, **kw):
|
|
252
|
+
if len(a) == 1 and len(a[0]) == len(_fields):
|
|
253
|
+
return tuple.__new__(cls, a[0])
|
|
254
|
+
if len(kw):
|
|
255
|
+
a = [0] * len(_fields)
|
|
256
|
+
for k, v in kw.items():
|
|
257
|
+
if k in _binmap and v == "Y":
|
|
258
|
+
#i = _fieldmap['_b'+str(_binmap[k][0])]
|
|
259
|
+
a[_fieldmap['_b0']] += (1 << _binmap[k])
|
|
260
|
+
elif k in _fieldmap:
|
|
261
|
+
a[_fieldmap[k]] = v
|
|
262
|
+
return tuple.__new__(cls, a)
|
|
263
|
+
|
|
264
|
+
def __getitem__(self, key):
|
|
265
|
+
if key in _fieldmap and key != "_b0":
|
|
266
|
+
return super(_Codepoint, self).__getitem__(_fieldmap[key])
|
|
267
|
+
elif key in _binmap:
|
|
268
|
+
return True if (super(_Codepoint, self).__getitem__(_fieldmap['_b0']) >> _binmap[key]) & 1 else False
|
|
269
|
+
else:
|
|
270
|
+
raise KeyError("Unknown key: {}".format(key))
|
|
271
|
+
|
|
272
|
+
def __contains__(self, key):
|
|
273
|
+
return key in _fieldmap or key in _binmap
|
|
274
|
+
|
|
275
|
+
def asdict(self, enums):
|
|
276
|
+
''' Returns a dictionary with nice keys and nice values '''
|
|
277
|
+
res = {}
|
|
278
|
+
for k, v in _fieldmap.items():
|
|
279
|
+
val = super().__getitem__(v)
|
|
280
|
+
if k == "_b0":
|
|
281
|
+
for bk, bv in _binmap.items():
|
|
282
|
+
if (val >> bv) & 1 != 0:
|
|
283
|
+
res[bk] = True
|
|
284
|
+
elif val:
|
|
285
|
+
res[_fieldnames[k]] = enums[k][val] if k in enums else val
|
|
286
|
+
return res
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
class UCD(list):
|
|
290
|
+
_remote_url = "http://www.unicode.org/Public/latest/ucdxml/ucd.all.flat.zip"
|
|
291
|
+
|
|
292
|
+
def save(self, localfile):
|
|
293
|
+
if localfile.endswith(".bz2"):
|
|
294
|
+
with bz2.open(localfile, "wb") as outf:
|
|
295
|
+
pickle.dump(self, outf, protocol=4)
|
|
296
|
+
elif localfile.endswith(".pickle"):
|
|
297
|
+
with open(localfile, "wb") as outf:
|
|
298
|
+
pickle.dump(self, outf, protocol=4)
|
|
299
|
+
else:
|
|
300
|
+
raise ValueError("localfile must end in .bz2 or .pickle")
|
|
301
|
+
|
|
302
|
+
@classmethod
|
|
303
|
+
def _cache_filename(cls):
|
|
304
|
+
return f"ucdata_pickle_{FORMAT}.bz2"
|
|
305
|
+
|
|
306
|
+
@classmethod
|
|
307
|
+
def _bundled_path(cls):
|
|
308
|
+
try:
|
|
309
|
+
from importlib.resources import files
|
|
310
|
+
p = files("ucd") / "data" / cls._cache_filename()
|
|
311
|
+
return str(p) if p.is_file() else None
|
|
312
|
+
except (ModuleNotFoundError, FileNotFoundError, TypeError):
|
|
313
|
+
return Noe
|
|
314
|
+
|
|
315
|
+
@classmethod
|
|
316
|
+
def _cache_path(cls):
|
|
317
|
+
fname = cls._cache_filename()
|
|
318
|
+
if _userroot():
|
|
319
|
+
cache_dirs = [_varcache()]
|
|
320
|
+
else:
|
|
321
|
+
cache_dirs = [platformdirs.user_cache_dir("python_ucd")]
|
|
322
|
+
cache_dirs.append("/var/cache/python_ucd")
|
|
323
|
+
read_path = None
|
|
324
|
+
for d in cache_dirs:
|
|
325
|
+
p = os.path.join(d, fname)
|
|
326
|
+
if os.path.exists(p):
|
|
327
|
+
read_path = p
|
|
328
|
+
break
|
|
329
|
+
write_dir = cache_dirs[0]
|
|
330
|
+
try:
|
|
331
|
+
os.makedirs(cache_dir, exist_ok=True)
|
|
332
|
+
except OSError:
|
|
333
|
+
pass
|
|
334
|
+
write_path = os.path.join(cache_dir, fname)
|
|
335
|
+
return read_path, write_path
|
|
336
|
+
|
|
337
|
+
@classmethod
|
|
338
|
+
def _cleanup_old_caches(cls, cache_dir):
|
|
339
|
+
keep = cls._cache_filename()
|
|
340
|
+
patterns = ["ucdata_pickle_*.bz2", "ucdata_pickle.bz2"]
|
|
341
|
+
for pat in patterns:
|
|
342
|
+
patpath = os.path.join(cache_dir, pat)
|
|
343
|
+
for p in glob.glob(patpath):
|
|
344
|
+
base = os.path.basename(p)
|
|
345
|
+
if base == keep:
|
|
346
|
+
continue
|
|
347
|
+
try:
|
|
348
|
+
os.remove(path)
|
|
349
|
+
except OSError:
|
|
350
|
+
pass
|
|
351
|
+
|
|
352
|
+
@classmethod
|
|
353
|
+
def test_update(cls, cache_period):
|
|
354
|
+
"""cache_period is in days. Returns True if the cache needs
|
|
355
|
+
updating. If the cache file is younger than cache_period, assumes
|
|
356
|
+
it's current (no network call). Otherwise does a HEAD request; if
|
|
357
|
+
the remote isn't newer, touches the cache file's mtime to reset
|
|
358
|
+
the clock and returns False."""
|
|
359
|
+
cache_path, _ = cls._cache_path()
|
|
360
|
+
if not os.path.exists(cache_path):
|
|
361
|
+
return True
|
|
362
|
+
|
|
363
|
+
mtime = datetime.fromtimestamp(os.path.getmtime(cache_path), tz=timezone.utc)
|
|
364
|
+
if datetime.now(timezone.utc) - mtime < timedelta(days=cache_period):
|
|
365
|
+
return False
|
|
366
|
+
|
|
367
|
+
req = urllib.request.Request(cls._remote_url, method="HEAD")
|
|
368
|
+
try:
|
|
369
|
+
with urllib.request.urlopen(req) as resp:
|
|
370
|
+
remote_lm = resp.headers.get("Last-Modified")
|
|
371
|
+
except urllib.error.URLError:
|
|
372
|
+
return False
|
|
373
|
+
|
|
374
|
+
if remote_lm and parsedate_to_datetime(remote_lm) > mtime:
|
|
375
|
+
return True
|
|
376
|
+
|
|
377
|
+
try:
|
|
378
|
+
os.utime(cache_path, None)
|
|
379
|
+
except PermissionError:
|
|
380
|
+
pass
|
|
381
|
+
return False
|
|
382
|
+
|
|
383
|
+
@classmethod
|
|
384
|
+
def force_update(cls):
|
|
385
|
+
"""Unconditionally fetch remote data, save to cache and clean up stale files"""
|
|
386
|
+
obj = cls.build_from_remote()
|
|
387
|
+
_, write_path = cls._cache_path()
|
|
388
|
+
try:
|
|
389
|
+
obj.save(write_path)
|
|
390
|
+
cls._cleanup_old_caches(os.path.direname(write_path))
|
|
391
|
+
except OSError:
|
|
392
|
+
pass
|
|
393
|
+
return obj
|
|
394
|
+
|
|
395
|
+
def _loadxml(self, fh, enums=None):
|
|
396
|
+
if enums is None:
|
|
397
|
+
enums = {}
|
|
398
|
+
for k, v in self.enums.items():
|
|
399
|
+
enums[k] = {x: i for i, x in enumerate(v)}
|
|
400
|
+
for (ev, e) in et.iterparse(fh, events=['start']):
|
|
401
|
+
if ev == 'start' and e.tag.endswith('char'):
|
|
402
|
+
d = dict(e.attrib)
|
|
403
|
+
if 'cp' in d:
|
|
404
|
+
firstcp = d.pop('cp')
|
|
405
|
+
lastcp = firstcp
|
|
406
|
+
elif 'first-cp' in d:
|
|
407
|
+
firstcp = d.pop('first-cp')
|
|
408
|
+
lastcp = d.pop('last-cp')
|
|
409
|
+
for n in _cpfields:
|
|
410
|
+
if n not in d or d[n] == "#":
|
|
411
|
+
d[n] = ""
|
|
412
|
+
d[n] = "".join(chr(int(x, 16)) for x in d[n].split())
|
|
413
|
+
for n, v in enums.items():
|
|
414
|
+
if n in d:
|
|
415
|
+
try:
|
|
416
|
+
d[n] = v[d[n]]
|
|
417
|
+
except KeyError:
|
|
418
|
+
# add new allowed value to field:
|
|
419
|
+
i = len(self.enums[n])
|
|
420
|
+
self.enums[n].append(d[n])
|
|
421
|
+
enums[n][d[n]] = i
|
|
422
|
+
d[n] = i
|
|
423
|
+
dat = _Codepoint(**d)
|
|
424
|
+
firsti = int(firstcp, 16)
|
|
425
|
+
lasti = int(lastcp, 16)
|
|
426
|
+
if lasti >= len(self):
|
|
427
|
+
self.extend([None] * (lasti - len(self) + 1))
|
|
428
|
+
for i in range(firsti, lasti+1):
|
|
429
|
+
self[i] = dat
|
|
430
|
+
return self
|
|
431
|
+
|
|
432
|
+
def _preproc(self, filename):
|
|
433
|
+
enums = {}
|
|
434
|
+
for e in _enumfieldnames.split():
|
|
435
|
+
enums[e] = {}
|
|
436
|
+
for (ev, e) in et.iterparse(filename, events=['start']):
|
|
437
|
+
if e.tag.endswith('char'):
|
|
438
|
+
for n, v in enums.items():
|
|
439
|
+
val = e.get(n, None)
|
|
440
|
+
if val is not None:
|
|
441
|
+
v.setdefault(val, len(v))
|
|
442
|
+
self.enums = {}
|
|
443
|
+
for k, v in enums.items():
|
|
444
|
+
self.enums[k] = sorted(v.keys(), key=lambda x:v[x])
|
|
445
|
+
return enums
|
|
446
|
+
|
|
447
|
+
def loadxml(self, filename):
|
|
448
|
+
""" Loads an additional UCDXML-formatted data file; commonly used for pipeline
|
|
449
|
+
characters prior to inclusion in a Unicode release """
|
|
450
|
+
with open(filename) as inf:
|
|
451
|
+
self._loadxml(inf)
|
|
452
|
+
return self
|
|
453
|
+
|
|
454
|
+
def get(self, cp, key):
|
|
455
|
+
""" Looks up a codepoint and returns the value for a given key. This
|
|
456
|
+
includes mapping enums back to their strings"""
|
|
457
|
+
v = self[cp]
|
|
458
|
+
if v is None:
|
|
459
|
+
raise KeyError("Undefined codepoint {:04X}".format(cp))
|
|
460
|
+
key = resolve_key(key)
|
|
461
|
+
if key not in v:
|
|
462
|
+
raise KeyError(f"Unknown or ambiguous property: {key}")
|
|
463
|
+
if key == "na":
|
|
464
|
+
return v[key].replace("#", "{:04X}".format(cp))
|
|
465
|
+
return self.enumstr(key, v[key])
|
|
466
|
+
|
|
467
|
+
def get_info(self, cp):
|
|
468
|
+
""" Returns a dictionary of nicely named properties and values where
|
|
469
|
+
the value is set """
|
|
470
|
+
v = self[cp]
|
|
471
|
+
res = v.asdict(self.enums)
|
|
472
|
+
return res
|
|
473
|
+
|
|
474
|
+
def enumstr(self, key, v):
|
|
475
|
+
""" Returns the string for an enum value given enum name and value """
|
|
476
|
+
key = resolve_key(key)
|
|
477
|
+
if key in self.enums:
|
|
478
|
+
m = self.enums[key]
|
|
479
|
+
return m[v] if v < len(m) else v
|
|
480
|
+
return v
|
|
481
|
+
|
|
482
|
+
def findall(self, key, val):
|
|
483
|
+
""" Returns a list of all the codepoints whose key value is value. Value
|
|
484
|
+
may be an enum name """
|
|
485
|
+
key = resolve_key(key)
|
|
486
|
+
if key in self.enums:
|
|
487
|
+
try:
|
|
488
|
+
enumval = self.enums[key].index(val)
|
|
489
|
+
except ValueError:
|
|
490
|
+
return []
|
|
491
|
+
else:
|
|
492
|
+
enumval = val
|
|
493
|
+
return [cp for cp in range(len(self)) if self[cp] is not None and key in self[cp] and self[cp][key] == enumval]
|
|
494
|
+
|
|
495
|
+
|
|
496
|
+
local_ucd = None
|
|
497
|
+
def _get_local_ucd():
|
|
498
|
+
global local_ucd
|
|
499
|
+
if local_ucd is None:
|
|
500
|
+
local_ucd = UCD()
|
|
501
|
+
return local_ucd
|
|
502
|
+
|
|
503
|
+
def loadxml(filename):
|
|
504
|
+
""" Ensures the global ucd is loaded into memory """
|
|
505
|
+
_get_local_ucd().loadxml(filename)
|
|
506
|
+
|
|
507
|
+
def get_ucd(cp, key):
|
|
508
|
+
""" Given codepoint and key, returns the property value. Key may be the
|
|
509
|
+
identifier or a partial full name from ucd/PropertyAliases.xml """
|
|
510
|
+
return _get_local_ucd().get(cp, key)
|
|
511
|
+
|
|
512
|
+
def get_info(cp):
|
|
513
|
+
""" Given a codepoint returns a nice dictionary of properties """
|
|
514
|
+
return _get_local_ucd().get_info(cp)
|
|
515
|
+
|
|
516
|
+
def find_ucd(key, val):
|
|
517
|
+
""" Returns a list of codepoints whose property key is the given val """
|
|
518
|
+
return _get_local_ucd().findall(key, val)
|
|
519
|
+
|
|
520
|
+
def get_enums(key):
|
|
521
|
+
""" Returns a list of property value names, suitable for passing to find_ucd,
|
|
522
|
+
for a given property key """
|
|
523
|
+
u = _get_local_ucd()
|
|
524
|
+
key = resolve_key(key)
|
|
525
|
+
if key in u.enums:
|
|
526
|
+
return u.enums[key]
|
|
527
|
+
else:
|
|
528
|
+
return []
|
|
529
|
+
|
|
530
|
+
def main():
|
|
531
|
+
import argparse
|
|
532
|
+
|
|
533
|
+
parser = argparse.ArgumentParser()
|
|
534
|
+
parser.add_argument("usv",nargs="?",help="USV")
|
|
535
|
+
parser.add_argument("-p","--property",help="property to query")
|
|
536
|
+
parser.add_argument("-v","--value",help="property value for find_ucd")
|
|
537
|
+
parser.add_argument("-e","--enum",help="Enum property to list values")
|
|
538
|
+
parser.add_argument("-x","--extend",help="XML file to extend properties")
|
|
539
|
+
parser.add_argument("-r","--reload",action='store_true',help="Forces an update of the database from Unicode")
|
|
540
|
+
args = parser.parse_args()
|
|
541
|
+
|
|
542
|
+
if args.reload:
|
|
543
|
+
UCD.force_update()
|
|
544
|
+
|
|
545
|
+
if args.extend:
|
|
546
|
+
loadxml(args.extend)
|
|
547
|
+
|
|
548
|
+
cp = None
|
|
549
|
+
if args.usv:
|
|
550
|
+
try:
|
|
551
|
+
cp = int(args.usv, 16)
|
|
552
|
+
except ValueError:
|
|
553
|
+
pass
|
|
554
|
+
|
|
555
|
+
if cp is not None:
|
|
556
|
+
if args.property:
|
|
557
|
+
print(get_ucd(cp, args.property))
|
|
558
|
+
else:
|
|
559
|
+
pprint.pprint(get_info(cp))
|
|
560
|
+
elif args.enum is not None:
|
|
561
|
+
print("\n".join(get_enums(args.enum)))
|
|
562
|
+
elif args.value and args.property:
|
|
563
|
+
print(" ".join("%04X" % x for x in find_ucd(args.property, args.value)))
|
|
564
|
+
else:
|
|
565
|
+
print("I don't know what to do. Try ucdinfo 0041 as a demo")
|
|
566
|
+
|
|
567
|
+
if __name__ == "__main__":
|
|
568
|
+
main()
|