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
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
1"""Immutable behavior configuration for the 2.0 API.
3Layering: imports nameparser._types only (enforced by
4tests/v2/test_layering.py).
5"""
6from __future__ import annotations
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
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)
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`)."""
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"
40class CapsSuffixes(StrEnum):
41 """Where ``Policy.unlisted_caps_suffixes`` reads an unlisted
42 all-caps word as a credential (#516, #564)."""
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"
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."""
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"
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))
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}
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)
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)
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
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:]))
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
314 def matcher(text: str) -> bool:
315 return match(text) is not None
316 return matcher
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)
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.
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}
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)
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)
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)
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})
415def _reject_str_and_mapping(value: object, field_name: str) -> None:
416 """The two shapes that iterate into something plausible but wrong.
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 )
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
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()))
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)
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
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
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=...)``."""
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"})
845 # in the class body so @dataclass(slots=True) keeps them
846 __getstate__ = _guarded_getstate
847 __setstate__ = _guarded_setstate
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))
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)})"
986 # -- editing ------------------------------------------------------
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)
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")
1013#: The two fields `Policy.lone_name_word` may name (rules/O5).
1014_LONE_NAME_WORD_ROLES = frozenset({Role.GIVEN, Role.FAMILY})
1017class _Unset(Enum):
1018 UNSET = auto()
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
1025_UNION = {"compose": "union"} # field metadata: set-valued -> union
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.
1035 Values are validated when the patch is applied (Policy's constructor
1036 re-runs), not at patch construction.
1037 """
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"})
1080 # in the class body so @dataclass(slots=True) keeps them
1081 __getstate__ = _guarded_getstate
1082 __setstate__ = _guarded_setstate
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").
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)})"
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]