Download and parse Unicode DerivedNormalizationProps.txt, caching in temp directory. Args: version: Unicode version string (e.g., "17.0.0") Returns: Dict mapping codepoints to their normalization properties. Properties include: NFC_QC, NFD_QC, NFKC_QC, NFKD_QC (Quic
(version: str = UNICODE_VERSION)
| 175 | |
| 176 | |
| 177 | def get_normalization_props(version: str = UNICODE_VERSION) -> Dict[int, Dict[str, str]]: |
| 178 | """Download and parse Unicode DerivedNormalizationProps.txt, caching in temp directory. |
| 179 | |
| 180 | Args: |
| 181 | version: Unicode version string (e.g., "17.0.0") |
| 182 | |
| 183 | Returns: |
| 184 | Dict mapping codepoints to their normalization properties. |
| 185 | Properties include: NFC_QC, NFD_QC, NFKC_QC, NFKD_QC (Quick_Check values) |
| 186 | """ |
| 187 | cache_path = os.path.join( |
| 188 | tempfile.gettempdir(), f"DerivedNormalizationProps-{version}.txt" |
| 189 | ) |
| 190 | |
| 191 | if not os.path.exists(cache_path): |
| 192 | url = f"https://www.unicode.org/Public/{version}/ucd/DerivedNormalizationProps.txt" |
| 193 | print(f"Downloading Unicode {version} DerivedNormalizationProps.txt from {url}...") |
| 194 | try: |
| 195 | urllib.request.urlretrieve(url, cache_path) |
| 196 | print(f"Cached to {cache_path}") |
| 197 | except Exception as e: |
| 198 | raise UnicodeDataDownloadError( |
| 199 | f"Could not download DerivedNormalizationProps.txt from {url}: {e}" |
| 200 | ) |
| 201 | else: |
| 202 | print(f"Using cached Unicode {version} DerivedNormalizationProps.txt: {cache_path}") |
| 203 | |
| 204 | props: Dict[int, Dict[str, str]] = {} |
| 205 | with open(cache_path, "r", encoding="utf-8") as f: |
| 206 | for line in f: |
| 207 | line = line.split("#")[0].strip() |
| 208 | if not line: |
| 209 | continue |
| 210 | parts = line.split(";") |
| 211 | if len(parts) < 2: |
| 212 | continue |
| 213 | try: |
| 214 | cp_range = parts[0].strip() |
| 215 | prop_value = parts[1].strip() |
| 216 | |
| 217 | # Parse property name and value (e.g., "NFC_QC" or "NFC_QC; N") |
| 218 | if len(parts) >= 3: |
| 219 | prop_name = prop_value |
| 220 | prop_val = parts[2].strip() |
| 221 | else: |
| 222 | # Properties like "Full_Composition_Exclusion" are boolean |
| 223 | prop_name = prop_value |
| 224 | prop_val = "Y" |
| 225 | |
| 226 | # Parse codepoint range |
| 227 | if ".." in cp_range: |
| 228 | start, end = cp_range.split("..") |
| 229 | start_cp = int(start, 16) |
| 230 | end_cp = int(end, 16) |
| 231 | else: |
| 232 | start_cp = end_cp = int(cp_range, 16) |
| 233 | |
| 234 | for cp in range(start_cp, end_cp + 1): |
nothing calls this directly
no test coverage detected
searching dependent graphs…