Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/nameparser/_policy.py: 40%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

274 statements  

1"""Immutable behavior configuration for the 2.0 API. 

2 

3Layering: imports nameparser._types only (enforced by 

4tests/v2/test_layering.py). 

5""" 

6from __future__ import annotations 

7 

8import dataclasses 

9import re 

10import unicodedata 

11from collections.abc import Callable, Iterable, Mapping 

12from dataclasses import dataclass, field 

13from enum import Enum, StrEnum, auto 

14from typing import Any 

15 

16from nameparser._types import ( 

17 ADDED_IN, FAMILY_FIRST, FAMILY_FIRST_GIVEN_LAST, GIVEN_FIRST, Role, 

18 _coerce_enum, 

19 _guarded_getstate, _guarded_setstate, _reject_bare_string_order, 

20 _require_iterable, _validated_order, 

21) 

22 

23 

24class PatronymicRule(StrEnum): 

25 """Stable rule names (API); implementations live in the pipeline. 

26 Enable via ``Policy(patronymic_rules={...})`` or, more commonly, a 

27 locale pack (:mod:`nameparser.locales`).""" 

28 

29 #: East Slavic formal order: "Sidorov Ivan Petrovich" 

30 #: (family, given, patronymic) is detected by the patronymic 

31 #: ending and reordered, and a middle word with the ending fills 

32 #: ParsedName.patronymic. Enabled by locales.RU. 

33 EAST_SLAVIC = "east-slavic" 

34 #: Turkic patronymic markers: a standalone "oglu"/"qizi"/"kyzy" 

35 #: (etc.) binds to the preceding name as a patronymic, the two 

36 #: filling ParsedName.patronymic. Enabled by locales.TR_AZ. 

37 TURKIC = "turkic" 

38 

39 

40class CapsSuffixes(StrEnum): 

41 """Where ``Policy.unlisted_caps_suffixes`` reads an unlisted 

42 all-caps word as a credential (#516, #564).""" 

43 

44 #: Nowhere: such a word is name material in every position, as 

45 #: 2.3 read it. 

46 OFF = "off" 

47 #: Only after a comma, behind two or more name words ("John Smith, 

48 #: XYZ" gives suffix ``XYZ``). The default: the all-caps SURNAME 

49 #: convention ("Jean DUPONT", "DUPONT, Jean") never writes the 

50 #: capitals there. 

51 AFTER_COMMA = "after-comma" 

52 #: After a comma and ending a name with no comma ("John Smith XYZ" 

53 #: gives suffix ``XYZ``), where the all-caps surname convention 

54 #: does write them -- "Jean Pierre DUPONT" then gives family 

55 #: ``Pierre``, suffix ``DUPONT``. 

56 EVERYWHERE = "everywhere" 

57 

58 

59class Script(StrEnum): 

60 """Writing systems the parser can key SCRIPT-CONDITIONAL behavior 

61 on: per-script name order (``Policy.script_orders``) and 

62 unspaced-name segmentation (``Policy.segment_scripts``). The rule 

63 that admits these (amendment 2026-07-27): script-conditional 

64 behavior only where the script itself determines the convention -- 

65 Latin-script input is never affected. The codepoint table backing 

66 these members is internal.""" 

67 

68 #: Chinese Hanzi -- and Japanese Kanji: a pure-Han string cannot 

69 #: say which language it is, which is fine for ORDER (both write 

70 #: family-first natively) and exactly why Han SEGMENTATION is 

71 #: opt-in, per language: locales.ZH brings the Chinese surname 

72 #: list, locales.JA activates the same stage for a pluggable 

73 #: segmenter to divide kanji names with. 

74 HAN = "han" 

75 #: Korean Hangul (precomposed syllables and the conjoining jamo). 

76 #: Unambiguously Korean. 

77 HANGUL = "hangul" 

78 #: Japanese hiragana. Never transcribes foreign names, so a mixed 

79 #: kanji+kana token (高橋みなみ) is Japanese and resolves HERE -- 

80 #: this member is the carrier key in script_orders/segment_scripts. 

81 HIRAGANA = "hiragana" 

82 #: Japanese katakana, full-width and halfwidth (タロウ) alike. A 

83 #: PURE-katakana token may be a transcribed foreign name in its 

84 #: original order (マイケル) or a Japanese name's reading written 

85 #: family-first (ヤマダ タロウ, and legacy halfwidth data), and the 

86 #: script cannot say which, so no default behavior keys on this 

87 #: member; it exists so the classifier can name what it 

88 #: deliberately declines. A caller who knows their katakana is 

89 #: Japanese maps it to FAMILY_FIRST in script_orders. 

90 KATAKANA = "katakana" 

91 

92 

93# Codepoint ranges per Script (#271). This integer table is the single 

94# source of truth for what a script covers; every matcher DERIVES from 

95# it -- _pipeline/_vocab.py compiles its per-script patterns from it, 

96# and it is importable from the pipeline and the locale packs alike, 

97# so the packs' predicates build on it too, through _script_matcher 

98# below (the table lives here rather than in the pipeline because 

99# packs must not import the pipeline). 

100# HAN: the ideographic iteration mark U+3005 and the shime mark 

101# U+3006, the URO plus Extension A, the compatibility block, and the 

102# supplementary-plane block 

103# (Ext B-I + CJK Compat Ideographs Supplement, 0x20000-0x323AF) -- 

104# rare surnames are the biggest real source of supplementary-plane 

105# hanzi in personal names (e.g. 𠮷田's 𠮷, U+20BB7), so leaving them 

106# out silently mis-orders those names; unassigned gaps inside the span 

107# are harmless, since no real name contains an unassigned codepoint. 

108# U+3005 々 is the block-vs-Script case, running the OPPOSITE way to 

109# U+30FB below: 々 already IS Script=Han under UAX #24 (Scripts.txt 

110# reads `3005 ; Han`), but it sits in CJK Symbols and Punctuation, 

111# outside every CJK ideograph block this table spans -- so a 

112# singleton entry was what a BLOCK table needed to reach a character 

113# the Script property would have classified correctly for free. It 

114# earns the reach: 々 repeats the preceding kanji and appears only 

115# inside Han-written names -- 佐々木 (Sasaki, a top-20 Japanese 

116# surname), 野々村, 奈々. Omitting it made 佐々木 a mixed-script token: 

117# the name reversed and never gated into segmentation. 

118# U+3006 〆 (the shime mark) extends that singleton to a two-codepoint 

119# span on a DIFFERENT justification: unlike 々, 〆 is Script=Common 

120# under UAX #24, so this is the table deliberately reaching PAST the 

121# Script property, not around a block boundary -- justified because 

122# within personal names 〆 appears solely in Japanese surnames (〆木 

123# Shimeki, 〆谷 Shimetani, 〆野) -- its other uses (the envelope 

124# closing mark, 〆切) never reach a name parser -- and it appears in 

125# no other script's names. 

126# HANGUL: the precomposed syllables and the three conjoining jamo 

127# blocks (U+1100-U+11FF, Extended-A U+A960-U+A97F, Extended-B 

128# U+D7B0-U+D7FF), the letters Unicode composes a syllable from (#1087). 

129# A syllable typed decomposed is jamo, and a reader of raw characters 

130# (tokenize's interpunct flank, rules/T3) must see it as hangul 

131# (decisions/nfd-shapes, #1087). All three blocks, though the extended 

132# ones hold only archaic jamo no modern name uses, the ground the kana 

133# entry below leaves out the supplementary kana on: the U+1100 block 

134# carries archaic jamo too, so no modern-only line can be drawn at 

135# block granularity. The compatibility jamo U+3131-U+318E are not 

136# conjoining and compose nothing; they stay out (rules/R7 initials 

137# 'ㄱㄴ 김' as 'ㄱ.'). 

138# HIRAGANA/KATAKANA (#272): the two kana blocks, each in full. There 

139# IS a supplementary-plane kana repertoire (Kana Supplement, Kana 

140# Extended-A/B, Small Kana Extension, U+1AFF0-U+1B16F, a few hundred 

141# assigned codepoints -- no exact count here, it moves with the 

142# Unicode version) but none of it is WORTH chasing the way Han's astral 

143# block is: those codepoints are hentaigana and other archaic/ 

144# phonetic-extension forms no modern Japanese name uses, unlike 

145# supplementary Han, which real surnames genuinely need. The Katakana 

146# Phonetic Extensions block (U+31F0-U+31FF, 16 small katakana for Ainu 

147# transcription) is excluded for the same reason -- no modern Japanese 

148# personal name uses them. Halfwidth kana (U+FF65-U+FF9F, #594) IS 

149# katakana here: legacy bank, payroll and CSV data written for JIS X 

150# 0201 systems spells names in it, and 山田 タロウ must read as 山田 タロウ 

151# does. Classifying it is a range, not a fold. rules/T's Background 

152# says "nothing rewrites the text before parsing", which rules out a 

153# fold at tokenize, and an NFKC fold at classification would reach far 

154# past the kana (fullwidth Latin, ㈱), and when #594 decided it a 

155# voicing mark with no precomposed form to join (ア゙) folded to the 

156# combining U+3099, which is in the HIRAGANA block -- so the folded 

157# token took the kana license and a wholly-katakana name turned 

158# family-first (decisions/W4; #596 has since given every voicing 

159# mark after a kana its base's script, see below). The span takes 

160# in the halfwidth nakaguro U+FF65, which tokenize turns into a separator, 

161# on the same direct-call grounds as U+30FB below, and the voicing 

162# marks U+FF9E/U+FF9F: they are spacing characters (category Lm) that 

163# NFC never composes into their base, there being no precomposed 

164# halfwidth voiced kana, so a span ending at U+FF9D would leave ダ 

165# unclassified. It stops short 

166# of U+FF61-U+FF64, the halfwidth CJK punctuation, whose U+FF61 is a 

167# full stop (_lexicon.FULL_STOPS). 

168# This table classifies by Unicode BLOCK, not the UAX #24 Script 

169# property: U+30A0, U+30FB (the middle dot), and U+30FC (the 

170# prolonged sound mark) all carry Script=Common under UAX #24, and the 

171# four kana voicing marks U+3099-U+309C split two and two -- U+3099 

172# and U+309A are the COMBINING forms (Script=Inherited), U+309B and 

173# U+309C the spacing ones (Script=Common) -- yet every one of them is 

174# needed here, and block membership, not the Script property, is what 

175# puts them in range. Those four are in range only for a mark that 

176# follows no kana: a mark voices the kana it follows, in either 

177# syllabary, so classification deletes every mark that follows a 

178# kana and the token takes its base's script (#596, 

179# _vocab._normalized_for_script) -- ア゙イ is katakana, not katakana 

180# plus hiragana. The katakana block's upper end (U+30FF) takes in 

181# the middle dot U+30FB, kept rather than carved out for a smaller 

182# reason than it looks: tokenize (#272 Task 2b) turns U+30FB into a 

183# token separator, so no real parse shows the classifier a string 

184# containing one. It is kept so that a DIRECT whole-string call -- 

185# effective_script (_pipeline/_vocab.py) on "マイケル・ジャクソン", which 

186# the unit tests (tests/v2/pipeline/test_vocab.py) make -- still 

187# classifies instead of returning None. The ranges below must 

188# stay mutually disjoint: single_script (_pipeline/_vocab.py) returns 

189# the FIRST covering entry (dict iteration order), so an overlapping 

190# future script would make the result order-dependent instead of 

191# well-defined. 

192#: The precomposed hangul syllables, and the three conjoining jamo 

193#: blocks HANGUL holds beside them (#1087), named so a test can state 

194#: the table before #1087 by subtracting them rather than copying spans. 

195_HANGUL_SYLLABLES = (0xAC00, 0xD7A3) 

196_CONJOINING_JAMO = ((0x1100, 0x11FF), (0xA960, 0xA97F), (0xD7B0, 0xD7FF)) 

197 

198_SCRIPT_RANGES: dict[Script, tuple[tuple[int, int], ...]] = { 

199 Script.HAN: ((0x3005, 0x3006), (0x3400, 0x4DBF), (0x4E00, 0x9FFF), 

200 (0xF900, 0xFAFF), (0x20000, 0x323AF)), 

201 Script.HANGUL: tuple(sorted((_HANGUL_SYLLABLES, *_CONJOINING_JAMO))), 

202 Script.HIRAGANA: ((0x3040, 0x309F),), 

203 Script.KATAKANA: ((0x30A0, 0x30FF), (0xFF65, 0xFF9F)), 

204} 

205 

206#: The Japanese repertoire: the three scripts Japanese names draw on. 

207#: The kana license (_pipeline/_vocab.py's effective_script), the ja 

208#: pack's DEVIATES, and the segmenter adapter's repertoire guard all 

209#: quantify over this one union (HANGUL simply omitted). 

210_JA_SCRIPTS = (Script.HAN, Script.HIRAGANA, Script.KATAKANA) 

211 

212#: Scripts whose characters cannot BE an initial. The criterion is 

213#: orthographic CONVENTION, not what a character is: does the writing 

214#: tradition abbreviate a given name to ONE character plus a period, 

215#: the way "J." stands in for "John"? Han, hangul and kana have no 

216#: such convention, so a lone punctuated 씨/様/김 is not a shortened 

217#: name and the veto has nothing to veto there. Do not restate that 

218#: phonologically ("letters, not syllables") -- Devanagari is an 

219#: abugida and Arabic an abjad, neither has letters in that sense, and 

220#: both abbreviate, so should Script.CYRILLIC or Script.DEVANAGARI 

221#: ever be added neither belongs here; their initials are real and 

222#: pinned as such ("А. С. Пушкин", "م. الفارسي"). 

223#: 

224#: Enumerated rather than derived from _SCRIPT_RANGES' keys: the 

225#: Script enum admits a member so that SOME behavior may key on it 

226#: (see Script), on assorted grounds -- KATAKANA is in it so the 

227#: classifier can name what it deliberately declines, and neither 

228#: DEFAULT_SCRIPT_ORDERS nor segment_scripts' default mentions it. 

229#: Membership therefore settles nothing about abbreviation: the four 

230#: coinciding today is what has been implemented, not a property of 

231#: the enum, and a new member must not inherit this answer. 

232#: 

233#: Decide it from CLDR rather than from the script's typology: count 

234#: the LOCALE-AUTHORED namePattern entries in common/main/<locale>.xml's 

235#: personNames block that produce an initial. ja and ko author 29 and 

236#: 32 patterns and use one in none of them; ru uses initials in 7 of 39 

237#: ("{given-initial} {given2-initial} {surname}"); zh abbreviates but 

238#: overrides initialPattern to "{0}", no period, which is why the 

239#: period is part of the test above. Do NOT read initialPattern alone 

240#: -- root defaults it to "{0}." and nearly every locale inherits it, 

241#: so it reports a period convention for locales that have none. 

242#: Measured 2026-08-02: th is 0 of 20, so Thai (#317) belongs here 

243#: once it earns a member. 

244_NO_INITIALS = (Script.HAN, Script.HANGUL, Script.HIRAGANA, 

245 Script.KATAKANA) 

246 

247 

248def _letter_units(text: str) -> list[int]: 

249 """How many letters each run of letters in `text` holds, a combining 

250 mark kept in the run of the letter it marks and every other 

251 character dividing two runs.""" 

252 units, letters, in_run = [], 0, False 

253 for ch in text: 

254 if ch.isalpha(): 

255 letters += 1 

256 in_run = True 

257 elif in_run and unicodedata.category(ch) in ("Mn", "Mc", "Me"): 

258 continue 

259 elif in_run: 

260 units.append(letters) 

261 letters, in_run = 0, False 

262 if in_run: 

263 units.append(letters) 

264 return units 

265 

266 

267def initials_as_family_may_move(text: str) -> bool: 

268 """Whether Policy.initials_as_family may move `text`: rules/O8 moves 

269 only letters standing two or more in a row, so a name it moves holds 

270 two runs in a row of one letter each, wherever they stand and 

271 whatever divides them. Every character but a letter and a combining 

272 mark divides a run here, wider than any separator the parse reads 

273 (a zero-width character, `・`, a glued comma or bracket), so the 

274 test over-declares, the safe direction, and never misses a name the 

275 rule moves. locales.NE's DEVIATES and Parser.suggest's letter runs 

276 both ask it.""" 

277 units = _letter_units(text) 

278 return any(a == 1 == b for a, b in zip(units, units[1:])) 

279 

280 

281def _script_matcher(*scripts: Script, 

282 whole: bool = False) -> Callable[[str], bool]: 

283 """A predicate over strings, compiled once from the union of the 

284 named scripts' spans in _SCRIPT_RANGES. whole=False: True when the 

285 string CONTAINS any such character -- DEVIATES' contract, where 

286 over-declaring is the gate's safe direction. whole=True: True when 

287 the string is non-empty and consists WHOLLY of such characters, 

288 as the ja adapter's repertoire guard and _vocab's script 

289 classifiers ask. Meant to be called at MODULE 

290 scope: "compiled once" is per matcher, and each call compiles a 

291 fresh pattern. The compiled pattern lives in the closure ON 

292 PURPOSE, and the compilation lives HERE rather than in a pack- 

293 local closure: tests/v2/test_locales.py classifies any pack module 

294 holding a module-level re.Pattern as a marker pack needing rotator 

295 branch coverage, and its registry gate goes further -- a pack that 

296 so much as IMPORTS re without exposing such a pattern fails 

297 "imports re but exposes no module-level pattern" -- so a 

298 range-declaring pack must not import re at all; predicates built 

299 here keep the packs invisible to that sweep by construction (and 

300 spare _vocab's derived matchers a declaration row in 

301 tests/v2/test_regex_sync.py's completeness sweep, which scans the 

302 pipeline modules (plus _render) for private module-level 

303 patterns).""" 

304 if not scripts: 

305 raise ValueError("_script_matcher needs at least one Script") 

306 cls = "".join(f"\\U{lo:08x}-\\U{hi:08x}" 

307 for script in scripts 

308 for lo, hi in _SCRIPT_RANGES[script]) 

309 # one pattern serves both modes: fullmatch of [cls]+ is wholly-of, 

310 # and search over [cls]+ is exactly contains-any 

311 pattern = re.compile(f"[{cls}]+") 

312 match = pattern.fullmatch if whole else pattern.search 

313 

314 def matcher(text: str) -> bool: 

315 return match(text) is not None 

316 return matcher 

317 

318 

319#: One character, or a string wholly, of a script the table classifies: 

320#: the question the interpunct's flank guard (rules/T3) and the 

321#: two-family-names declaration's run count (_locale) both ask, defined 

322#: once here because _locale may not import the pipeline (#1087). 

323_classified_char = _script_matcher(*_SCRIPT_RANGES, whole=True) 

324 

325 

326# Order-spec constants (#270), defined in _types beside Role since 

327# ParsedName.order holds one (3.0, #754) and _types imports nothing 

328# internal; re-exported here, where Policy validates name_order. 

329 

330_ORDER_CONSTANT_NAMES: dict[tuple[Role, ...], str] = { 

331 GIVEN_FIRST: "GIVEN_FIRST", 

332 FAMILY_FIRST: "FAMILY_FIRST", 

333 FAMILY_FIRST_GIVEN_LAST: "FAMILY_FIRST_GIVEN_LAST", 

334} 

335 

336 

337def _order_repr(value: tuple[Role, ...]) -> str: 

338 # Unreachable via Policy's constructor (its __post_init__ restricts 

339 # name_order to the three named orders) but REACHABLE via 

340 # PolicyPatch, which defers name_order validation to apply time by 

341 # design -- value may hold non-Role, even unhashable, elements. A 

342 # value smuggled in through __setstate__ (which validates layout, 

343 # not values) can also be a non-tuple container or not iterable at 

344 # all. repr must never raise, so the named-lookup path is taken 

345 # only for a TUPLE whose every element is confirmed a Role; 

346 # everything else renders via repr(value). (The annotation states 

347 # the Policy-side truth; the PolicyPatch call site passes 

348 # getattr-Any.) 

349 if isinstance(value, tuple) and all(isinstance(r, Role) for r in value): 

350 named = _ORDER_CONSTANT_NAMES.get(value) 

351 if named is not None: 

352 return named 

353 return "(" + ", ".join(r.name for r in value) + ")" 

354 return repr(value) 

355 

356 

357# Single source for the migration hint raised by both Policy and 

358# PolicyPatch when patronymic_rules gets a non-iterable (True is the 

359# likeliest wrong value -- v1's flag was a bool that enabled BOTH rules). 

360_PATRONYMIC_MIGRATION_HINT = ( 

361 "v1's patronymic_name_order=True enabled both rules -- " 

362 "patronymic_rules={PatronymicRule.EAST_SLAVIC, " 

363 "PatronymicRule.TURKIC} (or pick one via " 

364 "parser_for(locales.RU) / locales.TR_AZ)" 

365) 

366 

367#: Policy.script_orders' default: wholly-Han, wholly-Hangul, and 

368#: kana-licensed names read family-first. Public and named so opting 

369#: out or extending reads against a documented value (the 

370#: DEFAULT_NICKNAME_DELIMITERS precedent). The HAN entry is safe 

371#: WITHOUT knowing Chinese from Japanese: both write family-first in 

372#: native script -- the languages differ, the convention doesn't. 

373#: HIRAGANA joins by the same rule as HANGUL (the kana license, 

374#: amendment 2026-07-29): a mixed Han-and-kana token cannot be 

375#: Chinese (it contains kana) and is not a foreign transcription 

376#: (transcriptions are katakana-only), so it is Japanese, written 

377#: family-first -- another default change in a minor, release-log- 

378#: classified fix, #294's mechanism. KATAKANA is deliberately absent: 

379#: a PURE-katakana token may be a transcribed foreign name kept in its 

380#: source (usually given-first) order, or a Japanese reading written 

381#: family-first -- legacy halfwidth data (#594) spells Japanese names in 

382#: katakana too -- and the script cannot tell them apart, so nothing should 

383#: default on it. Canonical form: sorted (Script, order) pairs, 

384#: matching the field's storage. 

385DEFAULT_SCRIPT_ORDERS: tuple[ 

386 tuple[Script, tuple[Role, Role, Role]], ...] = ( 

387 (Script.HAN, FAMILY_FIRST), 

388 (Script.HANGUL, FAMILY_FIRST), 

389 (Script.HIRAGANA, FAMILY_FIRST), 

390) 

391 

392#: Policy.nickname_delimiters' default. Public and named so 

393#: customizations read as set math against a documented value -- e.g. 

394#: ``DEFAULT_NICKNAME_DELIMITERS | {("⦅", "⦆")}`` -- instead of a 

395#: rebuilt literal the user had to go discover. The v1 trio (straight 

396#: quotes + parentheses) plus the typographic conventions (#273): 

397#: smart quotes, low-high and right-right quotes, guillemets both 

398#: directions, CJK corner brackets (full-width and the halfwidth 

399#: 「」 legacy JIS X 0201 data carries, #597), fullwidth parentheses. 

400#: Curly SINGLE quotes are deliberately absent: U+2019 is the 

401#: typographic apostrophe ("O’Connor"). 

402DEFAULT_NICKNAME_DELIMITERS = frozenset({ 

403 ("'", "'"), ('"', '"'), ("(", ")"), # v1 trio 

404 ("“", "”"), # smart quotes (en, zh) 

405 ("„", "“"), # low-high (de, pl, cs, hu) 

406 ("”", "”"), # right-right (sv, fi) 

407 ("«", "»"), # guillemets (fr, ru, it, el) 

408 ("»", "«"), # reversed guillemets (de alt) 

409 ("「", "」"), ("『", "』"), # CJK corner brackets (ja) 

410 ("「", "」"), # halfwidth corner brackets 

411 ("(", ")"), # fullwidth parentheses (CJK) 

412}) 

413 

414 

415def _reject_str_and_mapping(value: object, field_name: str) -> None: 

416 """The two shapes that iterate into something plausible but wrong. 

417 

418 A bare string yields its characters (and '' yields nothing at all, 

419 so it silently stored an empty set); a Mapping yields only its keys. 

420 Both used to be accepted here, storing a value the caller never 

421 wrote. Lexicon._normset rejects the same two by name -- the wording 

422 is deliberately parallel, since a caller who hits one field's guard 

423 should recognize the other's. 

424 """ 

425 if isinstance(value, str): 

426 raise TypeError( 

427 f"{field_name} must be an iterable, not a bare string: " 

428 f"{value!r}" 

429 ) 

430 # bytes iterate to ints, so the entry check would report a byte 

431 # value and name neither the cause nor the fix; same decode hint 

432 # Lexicon and parse() give. 

433 if isinstance(value, (bytes, bytearray, memoryview)): 

434 raise TypeError( 

435 f"{field_name} must be an iterable of strings, not " 

436 f"{type(value).__name__} -- decode first, e.g. " 

437 f"raw.decode('utf-8')" 

438 ) 

439 if isinstance(value, Mapping): 

440 # Name the way out, not just the harm. "contributes only its 

441 # keys" is no help for the two mappings people actually pass: 

442 # {} (meant as an empty set -- Python's oldest trap, and the 

443 # keys story does not apply because there are none), and an 

444 # {open: close} dict, whose keys are only half the pair. 

445 raise TypeError( 

446 f"{field_name} must be an iterable, not a mapping: " 

447 f"{value!r}. A mapping yields only its keys -- write " 

448 f"frozenset() for an empty set, or .items() if this is an " 

449 f"{{open: close}} pair mapping" 

450 ) 

451 

452 

453def _validated_script(key: object) -> Script: 

454 """Coerce one Script key, with the message every script-keyed field 

455 shares. Single-sourced deliberately: script_orders and the 

456 script-keyed fields that follow it must not each grow their own 

457 wording for the same lookup.""" 

458 try: 

459 # Enum lookup accepts ANY value at runtime and answers a 

460 # non-member with ValueError -- which is the contract here, and 

461 # the taxonomy's rule for a failed enum lookup whatever the 

462 # input type was (stdlib EnumType precedent). 

463 return Script(key) # type: ignore[arg-type] 

464 except ValueError: 

465 valid = ", ".join(v.value for v in Script) 

466 raise ValueError( 

467 f"unknown script {key!r}; valid scripts: {valid}" 

468 ) from None 

469 

470 

471def _validated_script_orders( 

472 value: object) -> tuple[tuple[Script, tuple[Role, Role, Role]], ...]: 

473 """Policy.script_orders' whole check, from raw input to canonical 

474 storage. script_orders is the one MAPPING-shaped field, so the 

475 guards read inverted from every other one here: a Mapping is what 

476 the caller SHOULD pass, and a bare string is the shape that would 

477 otherwise iterate into plausible-looking garbage.""" 

478 if isinstance(value, str): 

479 raise TypeError( 

480 f"script_orders must be a mapping of Script to order, " 

481 f"not a bare string: {value!r}" 

482 ) 

483 if isinstance(value, (bytes, bytearray, memoryview)): 

484 raise TypeError( 

485 f"script_orders must be a mapping of Script to order, " 

486 f"not {type(value).__name__} -- decode first, " 

487 f"e.g. raw.decode('utf-8')" 

488 ) 

489 raw = value.items() if isinstance(value, Mapping) else value 

490 raw_iter = _require_iterable( 

491 raw, "script_orders", # type: ignore[arg-type] 

492 "a mapping of Script to order") 

493 canonical: dict[Script, tuple[Role, Role, Role]] = {} 

494 for entry in raw_iter: 

495 try: 

496 key, order = entry 

497 except (TypeError, ValueError): 

498 raise TypeError( 

499 f"script_orders entries must be (Script, order) " 

500 f"pairs, got {entry!r}" 

501 ) from None 

502 canonical[_validated_script(key)] = _validated_order( 

503 order, "script_orders") 

504 # Sorted pairs, not a dict: Policy is hashable, and two 

505 # differently-written but equivalent tables must converge (the 

506 # capitalization_exceptions precedent). 

507 return tuple(sorted(canonical.items())) 

508 

509 

510def _validated_segment_scripts(value: object) -> frozenset[Script]: 

511 """segment_scripts' check: an iterable of Script members (or 

512 their string values), coerced via _validated_script so the 

513 unknown-script wording stays single-sourced.""" 

514 _reject_str_and_mapping(value, "segment_scripts") 

515 script_iter = _require_iterable( 

516 value, "segment_scripts", # type: ignore[arg-type] 

517 "an iterable of Script members") 

518 return frozenset(_validated_script(s) for s in script_iter) 

519 

520 

521def _canonical_script_pair(pair: Iterable[Any]) -> tuple[Any, ...]: 

522 """Hashability-only canonicalization of one PolicyPatch 

523 script_orders entry: tuple-ize the pair AND the order value inside 

524 it. Shallow was not enough -- a {Script: [Role, ...]} patch stored 

525 the list, and hash() then raised far from the construction site, 

526 the same failure name_order's canonicalization exists to prevent. 

527 A malformed entry is still tuple-ized (that is the hashability 

528 floor); its CONTENTS are left exactly as written so Policy quotes 

529 the caller's own value when it raises at apply time.""" 

530 out = tuple(pair) 

531 if len(out) != 2: 

532 return out # not a (Script, order) pair 

533 key, value = out 

534 # str/bytes are iterable, so tuple() would shred exactly the two 

535 # values whose deferred errors ("not a bare string", the decode 

536 # hint) need to quote what the caller wrote. 

537 if isinstance(value, (str, bytes, bytearray, memoryview)): 

538 return out 

539 try: 

540 return (key, tuple(value)) 

541 except TypeError: 

542 return out # non-iterable order value 

543 

544 

545def _canonical_patch_script_orders(value: object) -> object: 

546 """Canonicalize a PolicyPatch.script_orders value for hashability 

547 without validating it: malformed shapes are stored so Policy can 

548 quote them at apply time; a caller-generator's own exception 

549 propagates from the UNGUARDED materialization below (deferring is 

550 impossible once a one-shot iterator is consumed).""" 

551 # Excluded HERE rather than delegated: a string's elements are 

552 # themselves tuple-izable, so no TypeError ever fires to signal 

553 # "leave this alone" -- "han" would shred to (("h",), ("a",), 

554 # ("n",)) and Policy's bare-string message would have nothing left 

555 # to quote. bytes shred the same way, into ints. 

556 if isinstance(value, (str, bytes, bytearray, memoryview)): 

557 return value # deferred whole: Policy's guards quote it 

558 # Any, not object: a patch defers validation, so anything a caller 

559 # wrote can arrive here, and the probe below is precisely the 

560 # runtime question mypy has no way to answer statically. 

561 pairs: Any = value.items() if isinstance(value, Mapping) else value 

562 try: 

563 pairs_iter = iter(pairs) # probe only 

564 except TypeError: 

565 return value # non-iterable: defer to apply 

566 items = tuple(pairs_iter) # UNGUARDED: caller errors propagate 

567 try: 

568 return tuple(map(_canonical_script_pair, items)) 

569 except TypeError: 

570 return items # malformed entry: materialized, so 

571 # Policy can still re-iterate + quote 

572 

573 

574@dataclass(frozen=True, slots=True) 

575class Policy: 

576 """The behavior switches a parser runs with: name order, 

577 patronymic rules, delimiter routing, input scrubbing. Immutable 

578 and hashable; every field has a safe default, so construct with 

579 only what you change -- ``Policy(maiden_delimiters=frozenset({("(", ")")}))`` 

580 -- and pass the result to ``Parser(policy=...)``.""" 

581 

582 #: How positional (no-comma) input maps onto given/middle/family 

583 #: (a name of one name word is placed by lone_name_word instead). 

584 #: Valid values are exactly the three exported 

585 #: :ref:`name-order constants <name-order-constants>` -- 

586 #: GIVEN_FIRST (the default), FAMILY_FIRST, and 

587 #: FAMILY_FIRST_GIVEN_LAST; any other tuple of Roles raises 

588 #: ValueError. Ignored when a comma separates family from given: 

589 #: "Thomas, John" puts the family name first no matter which words 

590 #: could otherwise be either ("Thomas" and "John" both work as 

591 #: given or family names). A comma that only sets off suffixes 

592 #: ("John Smith, Jr.") leaves name_order governing the name part. 

593 name_order: tuple[Role, Role, Role] = GIVEN_FIRST 

594 #: Per-script overrides of name_order (#271), consulted when every 

595 #: name piece is written wholly in one script, or in the 

596 #: Han/Hiragana/Katakana repertoire the #272 kana license shares 

597 #: across pieces: {Script: order} (constructor accepts a mapping; 

598 #: stored as sorted pairs). The default reads wholly-Han/Hangul 

599 #: names, and kana-licensed Japanese names, family-first -- see 

600 #: :data:`~nameparser.DEFAULT_SCRIPT_ORDERS`. Opt out with 

601 #: ``script_orders=()``. Latin-script and mixed-script input is 

602 #: never affected. Like name_order, ignored where a comma already 

603 #: decides the family name. 

604 script_orders: tuple[tuple[Script, tuple[Role, Role, Role]], ...] = ( 

605 DEFAULT_SCRIPT_ORDERS) 

606 #: Scripts for which the unspaced-name segmentation stage is 

607 #: active (#271): the first token written wholly in an activated 

608 #: script is split by longest surname match against 

609 #: :attr:`Lexicon.surnames <nameparser.Lexicon.surnames>`, and, 

610 #: where that vocabulary declines, by a 

611 #: :data:`~nameparser.Segmenter` if one was given to the parser 

612 #: (#272). Default: {Script.HANGUL} -- hangul is unambiguously 

613 #: Korean and Korean surnames are a closed default-shipped set. 

614 #: Han is NOT default: a zh surname list corrupts Japanese names 

615 #: (高橋一郎 must not split as 高+橋一郎), so it's opt-in via 

616 #: locales.ZH for Chinese and locales.JA -- which activates 

617 #: Script.HIRAGANA alongside it, the kana license's carrier key -- 

618 #: for Japanese. 

619 #: Opt out with ``segment_scripts=frozenset()``; note a PolicyPatch unions 

620 #: rather than replaces, so a pack can only add scripts, never 

621 #: disable one. 

622 segment_scripts: frozenset[Script] = frozenset({Script.HANGUL}) 

623 #: Opt-in detectors that reorder patronymic-shaped names 

624 #: (EAST_SLAVIC, TURKIC); usually set via a locale pack. A rotation 

625 #: restores the given-first reading a family-first listing hides, so 

626 #: under a declared FAMILY_FIRST or FAMILY_FIRST_GIVEN_LAST name_order 

627 #: it stands down and the declaration decides. 

628 patronymic_rules: frozenset[PatronymicRule] = frozenset() 

629 #: Folds middle into family instead of splitting them (v1's 

630 #: middle_name_as_last) -- for data where unrecognized interior 

631 #: words are surname parts, not middle names: multi-part surnames 

632 #: like Spanish/Portuguese dual surnames ("Gabriel García Márquez" 

633 #: -> family "García Márquez" instead of middle "García"). 

634 middle_as_family: bool = False # v1's middle_name_as_last 

635 #: (open, close) pairs whose enclosed content becomes the nickname 

636 #: field. Defaults to 

637 #: :data:`~nameparser.DEFAULT_NICKNAME_DELIMITERS` (#273). 

638 nickname_delimiters: frozenset[tuple[str, str]] = DEFAULT_NICKNAME_DELIMITERS 

639 #: (open, close) pairs whose enclosed content becomes the maiden 

640 #: field instead; a pair listed here is dropped from the effective 

641 #: nickname set (maiden wins, see __post_init__), so 

642 #: maiden_delimiters=frozenset({("(", ")")}) is the whole recipe (#274). 

643 #: Set this for a clause that says nothing about itself, which is 

644 #: two kinds and not one: content with no marker word in it, and a 

645 #: LONE marker word. Since #335 a clause that opens with a marker 

646 #: word AND has a word after it reads as the maiden name whatever 

647 #: pair encloses it, so "Jane Smith (née Jones)" needs no 

648 #: configuration -- unless S1 takes the clause first: a clause of 

649 #: credentials, generations or titles is set apart from the name, 

650 #: and other content ending in a period loses its brackets and is 

651 #: read as if written bare. A maiden_markers entry opening the enclosed 

652 #: content is dropped from the value -- all of it, an entry being 

653 #: allowed to span more than one word ("z domu") -- but only where 

654 #: a word stands past it: a clause of nothing but its marker keeps 

655 #: its words, since a lone "(Nee)" is a maiden NAME and not a 

656 #: marker (#329). 

657 maiden_delimiters: frozenset[tuple[str, str]] = frozenset() 

658 #: Additional separators that split suffix groups (e.g. " | " for 

659 #: "Jane Smith, RN | CRNA"). Additive only: the comma, the 

660 #: semicolon, and a hyphen or slash standing alone split suffix 

661 #: groups after a whole name and cannot be replaced -- comma handling is 

662 #: structural (the same comma reading that parses "Family, Given" 

663 #: input), not a configurable delimiter, the semicolon is built in 

664 #: since 2.6 and the spaced hyphen and slash since 2.7 (rules/C10). 

665 #: A declared separator glued to a suffix word after the first comma 

666 #: counts too, as the semicolon does: "John Smith, MD; PhD" reads 

667 #: suffix "MD; PhD", the word keeping its separator. The built-in 

668 #: hyphen and slash do not count glued, being a word's own there 

669 #: ("al-", "MD/PhD"); declaring " - " makes the hyphen count glued. 

670 extra_suffix_delimiters: frozenset[str] = frozenset() 

671 #: Governs "Family, Suffix"-shaped input where the suffix word is 

672 #: also initial-shaped (a single letter, bare or period-written -- 

673 #: of the default vocabulary that means the roman numerals "I" and 

674 #: "V"): "John Smith, V" reads as John Smith the fifth when True 

675 #: (the default, v1 behavior); False reads "V" as a given-name 

676 #: initial instead (family "John Smith", given "V"), and "john 

677 #: smith, v" the same way, a name written in one case reading as 

678 #: its capitals (rules/T4). Multi-letter suffixes ("III", "MD") 

679 #: parse the same either way. 

680 lenient_comma_suffixes: bool = True 

681 #: Excludes emoji from tokenization: they appear in no token, 

682 #: field, or rendered view. The original string keeps them (input 

683 #: is never modified -- spans stay true). 

684 strip_emoji: bool = True 

685 #: Excludes bidirectional control characters from tokenization: 

686 #: they appear in no token, field, or rendered view; the original 

687 #: string keeps them. 

688 strip_bidi: bool = True # =False replaces 1.x's opt-out CONSTANTS.regexes.bidi = False 

689 # -- fields added after 2.3 ------------------------------------------ 

690 # Policy is not kw_only, so a field's POSITION is API: a caller 

691 # writing Policy(GIVEN_FIRST, ..., True, False) binds by position. 

692 # The two 2.4 switches below first landed beside 

693 # lenient_comma_suffixes, which re-bound every positional argument 

694 # from `strip_emoji` on; they are appended here instead so 2.3's 

695 # eleven positions keep their meaning 

696 # (tests/v2/test_policy.py::test_the_2_3_positional_fields_did_not_move). 

697 # A field added in a later cycle goes at the END for the same 

698 # reason, whatever it is about. 

699 #: Reads an UNLISTED token of two or more period-separated chunks 

700 #: as a credential where the position allows it: "John Smith 

701 #: X.Y.Z." gives suffix ``X.Y.Z.`` and "Jack X.Y.Z." keeps family 

702 #: ``X.Y.Z.``, the same words-to-spare rule a listed ambiguous 

703 #: acronym takes, and either reading is reported. Case is 

704 #: irrelevant here -- the periods are the signal, so 

705 #: "john smith x.y.z." reads as the mixed-case spelling does. 

706 #: Whole-token vocabulary still wins ("M.A.", "Ph.D.", "A.B.C."), 

707 #: and a single trailing period is not this shape ("John Smith 

708 #: Xyz." keeps family ``Xyz.``), and nor are paired initials, two 

709 #: single letters each followed by a period in a script with 

710 #: capitals ("Ram Bahadur B.K." keeps family ``B.K.``) except 

711 #: right after a comma, where they are the given name unless 

712 #: something in the part speaks for them. ``False`` reads such a token as 

713 #: name material everywhere, as 2.3 did for a token no chunk 

714 #: claimed; the roman-chunk retirement (rules/S3) is not 

715 #: behind this switch, and still reports the fork. 

716 unlisted_dotted_suffixes: bool = True 

717 #: Where an UNLISTED all-caps word of two or more letters, with no 

718 #: period in it, reads as a credential (:class:`CapsSuffixes`). The 

719 #: name must contrast it: a word of the name holding a capital whose 

720 #: last letter is lowercase ("Smith", "DiCaprio") that the 

721 #: vocabulary does not claim as a title, particle 

722 #: or credential -- a record written wholly in capitals or wholly in 

723 #: lowercase keeps every word a name word. A listed member keeps its 

724 #: own case lean in every setting ("Jack MA" gives suffix ``MA``), 

725 #: and the roman-numeral fork claims a bare numeral first ("Jack 

726 #: VI" is unaffected). 

727 #: 

728 #: ``AFTER_COMMA``, the default, reads it only in the part after a 

729 #: comma with two or more name words before it: "John Smith, XYZ" 

730 #: gives suffix ``XYZ``, while "Smith, XYZ" keeps given ``XYZ`` (one 

731 #: name word) and "García Márquez, MJ" keeps given ``MJ`` -- a lone 

732 #: two-letter word is how a person's initials are written, and two 

733 #: words before the comma may be one surname, the case #563 decides 

734 #: for "M.J." too. The all-caps SURNAME convention ("Jean DUPONT", 

735 #: "DUPONT, Jean") writes the capitals at the end of a name or 

736 #: before a comma, never after one behind a full name, which is why 

737 #: this position is on by default and the others are not. 

738 #: 

739 #: ``EVERYWHERE`` adds the end of a name with no comma, the 

740 #: convention's own position: "John Smith XYZ" gives suffix ``XYZ``, 

741 #: and "Jean Pierre DUPONT" gives family ``Pierre``, suffix 

742 #: ``DUPONT`` -- the cost, a swallowed family name being the worse 

743 #: failure. The two-word "Jean DUPONT" and "Minjun KIM" keep their 

744 #: family even then (one word before the credential is never 

745 #: enough), but gain the fork's report. ``OFF`` reads no such word 

746 #: as a credential anywhere and reports nothing, as 2.3 did. A digit 

747 #: disqualifies the token and a single capital stays an initial. 

748 #: ``isupper()`` is script-agnostic, so the same holds in any script 

749 #: with a case contrast ("Иван ИВАНОВ", "Jean ÉCOLE"). 

750 unlisted_caps_suffixes: CapsSuffixes = CapsSuffixes.AFTER_COMMA 

751 #: Reads the last two name words of a name as the family, for data 

752 #: where a person carries two family names and writes both 

753 #: (rules/O6): "Gabriel García Márquez" gives given ``Gabriel``, 

754 #: family ``García Márquez``, and "Ana María García López" gives 

755 #: middle ``María``, family ``García López``. A particle counts with 

756 #: the word it attaches to ("Juan Pérez de la Vega" gives family 

757 #: ``Pérez de la Vega``), a family already holding two words keeps 

758 #: what it has ("José María Ortega y Gasset", "Ana María 

759 #: García-López", "Ana María de la Vega Rodríguez"), titles and 

760 #: suffixes count as no name word, and a name of two name words is 

761 #: unchanged. The word taken must be one name word a family name 

762 #: could be, so an initial written alone ("Ana M. García"), hyphenated 

763 #: initials ("Ana M.-J. López") and a bound given-name pair stay where 

764 #: they are; a hyphenated word counts as one when 

765 #: taken ("Andrés Sánchez-Cid Torres" gives family ``Sánchez-Cid 

766 #: Torres``), so a hyphenated given name is taken unless every part 

767 #: of it is a listed given name or an initial (``Lexicon.given_names``). Under a family-first 

768 #: name_order the FIRST two name words are the family; a name its 

769 #: script reads in an order other than name_order (a spaced Han or 

770 #: Hangul name under the default) is left alone, though under a 

771 #: declared FAMILY_FIRST, which the script agrees with, it moves. A 

772 #: family comma decides by itself, so "García López, Ana" is 

773 #: unaffected. A name with one family name behind two given names 

774 #: loses one to the family ("Juan Carlos Pérez" gives family 

775 #: ``Carlos Pérez``), which is most English and Argentine names of 

776 #: three words or more and many Portuguese and Brazilian ones as 

777 #: filed: switched on for a mixed population it costs more names 

778 #: than it mends, so it is off by default and reports nothing. Runs 

779 #: before middle_as_family, which then folds whatever middles are 

780 #: left. 

781 two_family_names: bool = False 

782 #: Reads the given name as two name units, for data where a person 

783 #: carries a compound given name and no middle name (rules/O7): 

784 #: "Juan Carlos Pérez" gives given ``Juan Carlos``, family 

785 #: ``Pérez``, and with ``two_family_names`` too, "María José García 

786 #: López" gives given ``María José``, family ``García López``. The 

787 #: middle unit beside the given name joins it -- the one after it, 

788 #: or the one before it under FAMILY_FIRST_GIVEN_LAST with no family 

789 #: comma, after which the part reads given-first -- and any 

790 #: middles past that stay middles; a particle chain the parse left 

791 #: in the middle counts with its word ("María del Carmen de la 

792 #: Vega" gives given ``María del Carmen``), and an initial is taken, 

793 #: a given name written short. A given name already holding two 

794 #: words keeps what it has (a hyphenated or bound pair), the word 

795 #: taken must be one name word, and a word an ending rule reads as a 

796 #: patronymic stays where it is. A family comma leaves the given part 

797 #: to this rule ("Pérez, Juan Carlos"); a name its script reads in an 

798 #: order other than name_order is left alone, as two_family_names 

799 #: leaves it. Runs after two_family_names, which takes its word 

800 #: first ("Gabriel García Márquez" keeps given ``Gabriel``), and 

801 #: before middle_as_family, which then folds whatever middles are 

802 #: left. A name whose second given word is a middle name gains it 

803 #: in the given name ("John Quincy Adams" gives given ``John 

804 #: Quincy``), so it is off by default and reports nothing. Added in 

805 #: 3.0. 

806 two_given_names: bool = field( 

807 default=False, metadata={ADDED_IN: "3.0.0"}) 

808 #: The field a name of one name word reads that word as, when 

809 #: nothing else decides it (rules/O5): ``Role.GIVEN`` (the default) 

810 #: or ``Role.FAMILY``, under every name_order. "Anh" gives given 

811 #: ``Anh``; with ``lone_name_word=Role.FAMILY`` it gives family 

812 #: ``Anh``. Which field a word written alone fills is a fact about 

813 #: where the data came from -- a list of surnames, a list of the 

814 #: names people go by -- not about the order its longer names are 

815 #: written in, so it does not follow name_order. A title, a 

816 #: nickname, a maiden name, a family comma or a script that settles 

817 #: the order still decides first ("Mr. Johnson" gives family 

818 #: ``Johnson``). The reading is reported as a ``given-or-family`` 

819 #: ambiguity either way, save a word its own reading claimed (a 

820 #: particle, a bound given name, an initial), which the switch 

821 #: places too and which reports nothing. Added in 3.0; through 2.x 

822 #: the field followed name_order. 

823 lone_name_word: Role = field( 

824 default=Role.GIVEN, metadata={ADDED_IN: "3.0.0"}) 

825 #: Reads two or more single letters ending a name as its family 

826 #: name, for data written in the South Asian convention where the 

827 #: letters after the given name stand for the father's or the 

828 #: family's name (rules/O8): "Lakshmi R S" gives given ``Lakshmi``, 

829 #: family ``R S``, and "Ram Bahadur B K" gives given ``Ram``, 

830 #: middle ``Bahadur``, family ``B K``. The letters are written as 

831 #: initials are, a bare Latin capital or a letter and its period, 

832 #: spaced and in a row, with a name word before them; one 

833 #: letter, letters before the name or inside it, and the given part 

834 #: after a family comma are left alone, and so is a name read in a 

835 #: family-first order, which already places those letters. Off by 

836 #: default: the same shape is a middle and a family initial in many 

837 #: lists ("Melanie J D") and a byline written surname first in a 

838 #: bibliography ("Huntley R P", for which name_order=FAMILY_FIRST 

839 #: is the setting). locales.NE turns it on, and Parser.suggest 

840 #: suggests it for a group whose names repeat one run of letters. 

841 #: Added in 3.1. 

842 initials_as_family: bool = field( 

843 default=False, metadata={ADDED_IN: "3.1.0"}) 

844 

845 # in the class body so @dataclass(slots=True) keeps them 

846 __getstate__ = _guarded_getstate 

847 __setstate__ = _guarded_setstate 

848 

849 # rules/D2: "configuration validation raises at construction 

850 # with the offending field and value named" 

851 def __post_init__(self) -> None: 

852 object.__setattr__( 

853 self, "name_order", _validated_order(self.name_order, 

854 "name_order")) 

855 object.__setattr__( 

856 self, "script_orders", 

857 _validated_script_orders(self.script_orders)) 

858 object.__setattr__( 

859 self, "segment_scripts", 

860 _validated_segment_scripts(self.segment_scripts)) 

861 _reject_str_and_mapping(self.patronymic_rules, "patronymic_rules") 

862 # Probe with iter() rather than wrapping tuple(): non-iterables 

863 # (True especially -- v1's patronymic_name_order was a bool flag, 

864 # so it's the likeliest wrong value here) get the migration- 

865 # pointing message, while an exception raised inside a caller's 

866 # generator still propagates untouched from the tuple() below 

867 # instead of being rewritten. Only the enum lookup itself gets 

868 # the unknown-rule message, naming the offender. 

869 try: 

870 rule_iter = iter(self.patronymic_rules) 

871 except TypeError: 

872 raise TypeError( 

873 f"patronymic_rules must be an iterable of PatronymicRule " 

874 f"names, got {self.patronymic_rules!r}; " 

875 f"{_PATRONYMIC_MIGRATION_HINT}" 

876 ) from None 

877 items = tuple(rule_iter) 

878 rules = set() 

879 for r in items: 

880 try: 

881 rules.add(PatronymicRule(r)) 

882 except ValueError: 

883 valid = ", ".join(v.value for v in PatronymicRule) 

884 raise ValueError( 

885 f"unknown patronymic rule {r!r}; valid rules: {valid}" 

886 ) from None 

887 object.__setattr__(self, "patronymic_rules", frozenset(rules)) 

888 for pairs_name in ("nickname_delimiters", "maiden_delimiters"): 

889 _reject_str_and_mapping(getattr(self, pairs_name), pairs_name) 

890 pairs = tuple(_require_iterable(getattr(self, pairs_name), pairs_name)) 

891 for pair in pairs: 

892 if (not isinstance(pair, tuple) or len(pair) != 2 

893 or not all(isinstance(s, str) for s in pair)): 

894 raise TypeError( 

895 f"{pairs_name} entries must be (open, close) tuples " 

896 f"of strings, got {pair!r}" 

897 ) 

898 if not all(pair): 

899 raise ValueError( 

900 f"{pairs_name} entries must be pairs of non-empty " 

901 f"strings, got {pair!r}" 

902 ) 

903 object.__setattr__(self, pairs_name, frozenset(pairs)) 

904 # Maiden wins: a pair can route to exactly one field, and listing 

905 # it in maiden_delimiters is the specific intent, so the effective 

906 # nickname set drops it. Canonicalization, not validation (the 

907 # name_order coercion precedent): differently-written but 

908 # equivalent Policies converge to equal values. 

909 object.__setattr__( 

910 self, "nickname_delimiters", 

911 self.nickname_delimiters - self.maiden_delimiters) 

912 _reject_str_and_mapping(self.extra_suffix_delimiters, 

913 "extra_suffix_delimiters") 

914 delimiters = tuple(_require_iterable( 

915 self.extra_suffix_delimiters, "extra_suffix_delimiters")) 

916 for d in delimiters: 

917 if not isinstance(d, str): 

918 raise TypeError( 

919 f"extra_suffix_delimiters entries must be strings, " 

920 f"got {d!r}" 

921 ) 

922 if not d: 

923 raise ValueError( 

924 "extra_suffix_delimiters entries must be non-empty strings" 

925 ) 

926 object.__setattr__( 

927 self, "extra_suffix_delimiters", frozenset(delimiters) 

928 ) 

929 # Truthy strings ("no", "false") would silently invert the 

930 # caller's intent downstream; bools are the one field kind the 

931 # coercing checks above can't cover. 

932 # 

933 # The roster is DERIVED from the dataclass rather than written 

934 # out: a hand-written list is a second place to remember, and 

935 # the twin loop in tests/v2/test_policy.py proved it -- it still 

936 # named 2.3's four flags after 2.4 added two, so the new 

937 # switches shipped with no "must be a bool" coverage while the 

938 # library validated them. `bool` is the whole test: every 

939 # bool-annotated Policy field is a flag, and the annotation is 

940 # a plain string here because of `from __future__ import 

941 # annotations` (AGENTS.md's guard-the-whole-family rule). 

942 for flag in _BOOL_FIELDS: 

943 value = getattr(self, flag) 

944 if not isinstance(value, bool): 

945 raise TypeError( 

946 f"{flag} must be a bool, got {value!r}" 

947 ) 

948 # A bool is the likeliest wrong value -- the sibling 

949 # unlisted_dotted_suffixes is one, and `True` is an `int` the 

950 # enum would read as no member -- so name every member. 

951 caps = self.unlisted_caps_suffixes 

952 if isinstance(caps, bool): 

953 raise TypeError( 

954 f"unlisted_caps_suffixes must be a CapsSuffixes, got " 

955 f"{caps!r}; use CapsSuffixes.OFF, CapsSuffixes.AFTER_COMMA " 

956 f"(the default) or CapsSuffixes.EVERYWHERE") 

957 object.__setattr__(self, "unlisted_caps_suffixes", _coerce_enum( 

958 caps, CapsSuffixes, "unlisted_caps_suffixes value", 

959 "values")) 

960 lone = self.lone_name_word 

961 if not isinstance(lone, str): 

962 raise TypeError( 

963 f"lone_name_word must be a Role, got {lone!r}; use " 

964 f"Role.GIVEN (the default) or Role.FAMILY") 

965 if lone not in _LONE_NAME_WORD_ROLES: 

966 raise ValueError( 

967 f"lone_name_word must be Role.GIVEN or Role.FAMILY, got " 

968 f"{lone!r}: a name of one word is read as a given or a " 

969 f"family name") 

970 object.__setattr__(self, "lone_name_word", Role(lone)) 

971 

972 def __repr__(self) -> str: 

973 # Bounded: only fields that deviate from the default are shown 

974 # (design rule, see nameparser._types module docstring). 

975 parts = [] 

976 for f in dataclasses.fields(self): 

977 value = getattr(self, f.name) 

978 if value == f.default: 

979 continue 

980 if f.name == "name_order": 

981 parts.append(f"name_order={_order_repr(value)}") 

982 else: 

983 parts.append(f"{f.name}={value!r}") 

984 return f"Policy({', '.join(parts)})" 

985 

986 # -- editing ------------------------------------------------------ 

987 

988 def patched(self, patch: PolicyPatch) -> Policy: 

989 """Fold a :class:`PolicyPatch` onto this Policy and return the 

990 combined Policy. Set-valued fields union with the patch's; 

991 scalar fields are overridden by the patch; UNSET fields are 

992 left alone. Patch VALUES are validated here (Policy's 

993 constructor re-runs on the result), not at patch construction 

994 -- see PolicyPatch. The maiden-wins canonicalization applies 

995 to the combined result exactly as if it had been constructed 

996 directly.""" 

997 if not isinstance(patch, PolicyPatch): 

998 raise TypeError(f"patched() takes a PolicyPatch, got {patch!r}") 

999 return apply_patch(self, patch) 

1000 

1001 

1002#: Every bool-valued Policy field, read off the dataclass rather than 

1003#: listed: `__post_init__`'s bool check sweeps this, so a flag added to 

1004#: the class above is validated the day it lands and cannot ship 

1005#: unchecked the way `unlisted_dotted_suffixes` and the then-bool 

1006#: `unlisted_caps_suffixes` did. `from __future__ import annotations` 

1007#: makes every annotation a string, so the comparison is against the 

1008#: SPELLING "bool" -- which is also what a reader of the class body 

1009#: sees, and a field annotated any other way is not a plain flag. 

1010_BOOL_FIELDS: tuple[str, ...] = tuple( 

1011 f.name for f in dataclasses.fields(Policy) if f.type == "bool") 

1012 

1013#: The two fields `Policy.lone_name_word` may name (rules/O5). 

1014_LONE_NAME_WORD_ROLES = frozenset({Role.GIVEN, Role.FAMILY}) 

1015 

1016 

1017class _Unset(Enum): 

1018 UNSET = auto() 

1019 

1020 

1021#: Sentinel for "this patch does not set this field" (picklable enum 

1022#: member, distinguishable from every real value including None/False). 

1023UNSET = _Unset.UNSET 

1024 

1025_UNION = {"compose": "union"} # field metadata: set-valued -> union 

1026 

1027 

1028@dataclass(frozen=True, slots=True) 

1029class PolicyPatch: 

1030 """A partial Policy: one field per Policy field, all defaulting to 

1031 UNSET. Composition per field is DECLARED via metadata -- set-valued 

1032 fields union, scalars override (later wins). Kept in lockstep with 

1033 Policy by the parity test in tests/v2/test_policy.py. 

1034 

1035 Values are validated when the patch is applied (Policy's constructor 

1036 re-runs), not at patch construction. 

1037 """ 

1038 

1039 name_order: tuple[Role, Role, Role] | _Unset = UNSET 

1040 #: Composes as a SCALAR (override, not merge) -- deliberate: nothing 

1041 #: shipped patches it today, so the simpler rule is the one to 

1042 #: defend; revisit if a pack ever needs to add one script's entry 

1043 #: without restating the rest. 

1044 script_orders: tuple[ 

1045 tuple[Script, tuple[Role, Role, Role]], ...] | _Unset = UNSET 

1046 segment_scripts: frozenset[Script] | _Unset = field( 

1047 default=UNSET, metadata=_UNION) 

1048 patronymic_rules: frozenset[PatronymicRule] | _Unset = field( 

1049 default=UNSET, metadata=_UNION) 

1050 middle_as_family: bool | _Unset = UNSET 

1051 nickname_delimiters: frozenset[tuple[str, str]] | _Unset = field( 

1052 default=UNSET, metadata=_UNION) 

1053 maiden_delimiters: frozenset[tuple[str, str]] | _Unset = field( 

1054 default=UNSET, metadata=_UNION) 

1055 extra_suffix_delimiters: frozenset[str] | _Unset = field( 

1056 default=UNSET, metadata=_UNION) 

1057 lenient_comma_suffixes: bool | _Unset = UNSET 

1058 strip_emoji: bool | _Unset = UNSET 

1059 strip_bidi: bool | _Unset = UNSET 

1060 # Appended, in Policy's order and for Policy's reason -- this class 

1061 # is positional too, and the parity test holds the two field 

1062 # sequences equal, so a field inserted on one side would have to be 

1063 # inserted on the other and both would re-bind together. 

1064 unlisted_dotted_suffixes: bool | _Unset = UNSET 

1065 unlisted_caps_suffixes: CapsSuffixes | _Unset = UNSET 

1066 two_family_names: bool | _Unset = UNSET 

1067 #: Patches :attr:`Policy.two_given_names`, a scalar (later wins). 

1068 #: Added in 3.0. 

1069 two_given_names: bool | _Unset = field( 

1070 default=UNSET, metadata={ADDED_IN: "3.0.0"}) 

1071 #: Patches :attr:`Policy.lone_name_word`, a scalar (later wins). 

1072 #: Added in 3.0. 

1073 lone_name_word: Role | _Unset = field( 

1074 default=UNSET, metadata={ADDED_IN: "3.0.0"}) 

1075 #: Patches :attr:`Policy.initials_as_family`, a scalar (later 

1076 #: wins). Added in 3.1. 

1077 initials_as_family: bool | _Unset = field( 

1078 default=UNSET, metadata={ADDED_IN: "3.1.0"}) 

1079 

1080 # in the class body so @dataclass(slots=True) keeps them 

1081 __getstate__ = _guarded_getstate 

1082 __setstate__ = _guarded_setstate 

1083 

1084 def __post_init__(self) -> None: 

1085 # Canonicalize (but do NOT validate) collection fields so a patch 

1086 # built from a set/list literal is hashable and unions cleanly in 

1087 # apply_patch. name_order needs the same treatment: Policy would 

1088 # coerce a list at apply time, but the patch itself (and any 

1089 # Locale holding it) must already be hashable. 

1090 if self.name_order is not UNSET: 

1091 _reject_bare_string_order(self.name_order, "name_order") 

1092 object.__setattr__(self, "name_order", tuple(self.name_order)) 

1093 # Same reason for script_orders, one level deeper: a patch built 

1094 # from a {Script: order} dict (or a list of pairs) must already 

1095 # be hashable, since a Locale holds it -- and hashable all the 

1096 # way down, hence _canonical_script_pair. Validation still 

1097 # belongs to Policy at apply time, so a shape the canonicalizer 

1098 # cannot digest is left for it to report; the one-shot and 

1099 # malformed-entry cases are _canonical_patch_script_orders'. 

1100 if self.script_orders is not UNSET: 

1101 object.__setattr__( 

1102 self, "script_orders", 

1103 _canonical_patch_script_orders(self.script_orders)) 

1104 for f in dataclasses.fields(self): 

1105 if f.metadata.get("compose") != "union": 

1106 continue 

1107 value = getattr(self, f.name) 

1108 if value is UNSET: 

1109 continue 

1110 # Shared with Policy, not re-implemented: this used to be an 

1111 # inline copy of the bare-string half only, so the mapping 

1112 # half never reached a patch -- and frozenset() below 

1113 # destroys the evidence, leaving nothing for Policy to catch 

1114 # at apply time. A Locale pack ships one of these. 

1115 _reject_str_and_mapping(value, f.name) 

1116 # same iter() probe as Policy: curated message for 

1117 # non-iterables (with the v1-flag hint where it applies), 

1118 # caller-generator exceptions propagate from frozenset() 

1119 try: 

1120 iter(value) 

1121 except TypeError: 

1122 hint = ("; " + _PATRONYMIC_MIGRATION_HINT 

1123 if f.name == "patronymic_rules" else "") 

1124 raise TypeError( 

1125 f"{f.name} must be an iterable, got {value!r}{hint}" 

1126 ) from None 

1127 object.__setattr__(self, f.name, frozenset(value)) 

1128 # Every other field (the scalars: the bools, 

1129 # unlisted_caps_suffixes and lone_name_word) DELIBERATELY gets no type check here, 

1130 # unlike name_order and the union fields above: a 

1131 # PolicyPatch(strip_emoji="off") is constructible, and only raises 

1132 # once apply_patch runs Policy.__post_init__'s checks. This is 

1133 # the one place the module's eager-validation ethos doesn't apply 

1134 # -- see the class docstring ("Values are validated when the patch 

1135 # is applied... not at patch construction"). 

1136 

1137 def __repr__(self) -> str: 

1138 # Bounded: only fields the patch actually sets are shown; UNSET 

1139 # fields are omitted (design rule, see nameparser._types module 

1140 # docstring -- the sibling of Policy's deviation-only repr). 

1141 parts = [] 

1142 for f in dataclasses.fields(self): 

1143 value = getattr(self, f.name) 

1144 if value is UNSET: 

1145 continue 

1146 if f.name == "name_order": 

1147 parts.append(f"name_order={_order_repr(value)}") 

1148 else: 

1149 parts.append(f"{f.name}={value!r}") 

1150 return f"PolicyPatch({', '.join(parts)})" 

1151 

1152 

1153def apply_patch(policy: Policy, patch: PolicyPatch) -> Policy: 

1154 """Fold a PolicyPatch onto a Policy. Policy.__post_init__ re-runs via 

1155 dataclasses.replace, so patched values are revalidated for free -- 

1156 including the maiden-wins canonicalization: a patch that adds a 

1157 maiden pair removes that pair from the base's effective nickname 

1158 set, exactly as if the combined Policy had been constructed 

1159 directly. Intended (decided 2026-07-19): maiden_delimiters 

1160 membership IS the routing decision, whoever contributes it.""" 

1161 updates: dict[str, object] = {} 

1162 for f in dataclasses.fields(PolicyPatch): 

1163 value = getattr(patch, f.name) 

1164 if value is UNSET: 

1165 continue 

1166 if f.metadata.get("compose") == "union": 

1167 value = getattr(policy, f.name) | value 

1168 updates[f.name] = value 

1169 if not updates: 

1170 return policy 

1171 # Known mypy limitation with **dict-unpacked replace; see the full 

1172 # explanation at Lexicon._edit in _lexicon.py. 

1173 return dataclasses.replace(policy, **updates) # type: ignore[arg-type]