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

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

434 statements  

1"""v1 ``Constants`` compatibility shim over Lexicon/Policy 

2(mechanisms.md#CONFIG-SHIM-SNAPSHOT). ``nameparser.config`` 

3re-exports these names from the swap 

4commit onward; the whole module is deleted in 3.0 with the facade. 

5 

6Layering: facade layer -- may import anything public; here that's 

7``nameparser.util`` for ``lc()``, ``nameparser.config.regexes`` for 

8the read-only regexes proxy's underlying compiled patterns, and 

9``nameparser._lexicon``/``_policy``/``_parser`` for the ``_snapshot()`` 

10translation and the shared parser cache. 

11 

12All ``nameparser.config.<data module>`` imports below are deferred 

13(function-local), never module-level: ``nameparser/config/__init__.py`` 

14itself imports ``CONSTANTS``/``Constants``/etc. from this module, so a 

15module-level ``from nameparser.config.regexes import REGEXES`` here 

16would need the ``nameparser.config`` package's ``__init__.py`` to run 

17to completion first -- which needs this module to already be fully 

18initialized. Importing the data submodules lazily, on first use, breaks 

19that cycle. 

20""" 

21from __future__ import annotations 

22 

23import functools 

24import warnings 

25from collections.abc import ( 

26 Callable, ItemsView, Iterable, Iterator, KeysView, Mapping, Set, 

27 ValuesView, 

28) 

29from typing import NamedTuple, Self 

30 

31from nameparser._lexicon import Lexicon, _title_key 

32from nameparser._parser import Parser 

33from nameparser._policy import PatronymicRule, Policy 

34from nameparser.util import lc 

35 

36 

37#: The eight multi-word entries the pre-2.0 DEFAULT vocabulary shipped. 

38#: Provably inert in every release (they can never match; see the 

39#: 2.0.0 release log), so dropping them from a restored legacy pickle 

40#: changes no parse -- and keeps the multi-word warning from firing 

41#: eight times, with wrong advice, at library-internal lines, on the 

42#: first parse after a supported 1.3/1.4 pickle upgrade. 

43#: 

44#: Gated on ALL EIGHT being present across the two fields -- the 

45#: signature of a pre-2.0 blob, which froze the complete shipped set. 

46#: A round-trip of 2.0-era state that carries FEWER than all eight 

47#: keeps them (a user who deliberately re-added one or two does not 

48#: match the signature, and .copy() never subtracts either). The trade: 

49#: a 2.0 user who re-adds ALL eight exact strings is indistinguishable 

50#: from a legacy blob and loses them on the next unpickle. 

51_LEGACY_DEAD_ENTRIES = { 

52 "titles": frozenset({"chargé d'affaires"}), 

53 "suffix_acronyms": frozenset({ 

54 "leed ap", "nicet i", "nicet ii", "nicet iii", "nicet iv", 

55 "psm i", "psm ii"}), 

56} 

57 

58 

59def _reject_bare_str_or_bytes(value: object, expected: str) -> None: 

60 # A bare string is an iterable of its characters, so e.g. SetManager('dr') 

61 # would silently shred it into {'d', 'r'} instead of raising -- shared by 

62 # SetManager's constructor/operands (#238/#241) and TupleManager's 

63 # constructor (#242). Ported from v1's `_reject_bare_str_or_bytes`. 

64 if isinstance(value, bytes): 

65 raise TypeError( 

66 f"expected {expected}, got a single bytes; decode it first: " 

67 f"{value!r}.decode()" 

68 ) 

69 if isinstance(value, str): 

70 raise TypeError( 

71 f"expected {expected}, got a single str; wrap it in a list: " 

72 f"[{value!r}]" 

73 ) 

74 

75 

76def _lc_validated(s: object) -> str: 

77 # Validates and lc()-normalizes a single element -- shared by bulk 

78 # iterable normalization (constructor/operands) and add()'s per-string 

79 # loop, so every path raises the same #238/#245-shaped TypeError instead 

80 # of lc() crashing cryptically (bytes) or silently transmuting (None). 

81 if isinstance(s, bytes): 

82 raise TypeError( 

83 f"expected a str, got bytes; decode it first: {s!r}.decode()" 

84 ) 

85 if not isinstance(s, str): 

86 raise TypeError(f"expected a str, got {type(s).__name__}: {s!r}") 

87 return lc(s) 

88 

89 

90def _normalize_iterable_of_strings( 

91 elements: object, expected: str = "an iterable of strings") -> set[str]: 

92 # a SetManager's elements were already validated/normalized when it was 

93 # built, so copy them instead of re-validating (v1's fast path) 

94 if isinstance(elements, SetManager): 

95 return set(elements._elements) 

96 _reject_bare_str_or_bytes(elements, expected) 

97 return {_lc_validated(s) for s in elements} # type: ignore[attr-defined] 

98 

99 

100class SetManager: 

101 """v1 ``SetManager`` surface over a plain set of ``lc()``-normalized 

102 strings. Mutations call ``_on_change`` (the owning Constants' 

103 generation bump, wired by the facade). ``__call__`` and the 

104 missing-member-tolerant ``remove()`` are gone per the #243 schedule 

105 (warned 1.3.0, removed 2.0): ``remove()`` of a missing member raises 

106 ``KeyError``, matching ``set.remove``. 

107 """ 

108 

109 _elements: set[str] 

110 _on_change: Callable[[], None] | None 

111 

112 def __init__(self, elements: Iterable[str] = (), 

113 _on_change: Callable[[], None] | None = None, 

114 _field: str | None = None) -> None: 

115 # _field carries a Constants field name (e.g. "titles") through to 

116 # the bare-str/bytes guard's message, when this SetManager is being 

117 # built on behalf of a named Constants field (constructor kwarg or 

118 # __setattr__ auto-wrap); a direct SetManager(...) call gets the 

119 # generic v1 message instead. 

120 expected = f"{_field} to be an iterable of strings" if _field \ 

121 else "an iterable of strings" 

122 self._elements = _normalize_iterable_of_strings(elements, expected) 

123 self._on_change = _on_change 

124 

125 def _changed(self) -> None: 

126 if self._on_change is not None: 

127 self._on_change() 

128 

129 def add(self, *strings: str) -> SetManager: 

130 """Add the normalized string arguments to the set. Returns 

131 ``self`` for chaining.""" 

132 # notify only on real change (v1 parity): a no-op add must not 

133 # bump the owner's generation 

134 changed = False 

135 try: 

136 for s in strings: 

137 normalized = _lc_validated(s) # TypeError on bytes (#245) 

138 if normalized not in self._elements: 

139 self._elements.add(normalized) 

140 changed = True 

141 finally: 

142 # a TypeError mid-list still leaves earlier additions 

143 # applied, so the owner must hear about them or its cache 

144 # goes stale (same rule as remove() below) 

145 if changed: 

146 self._changed() 

147 return self 

148 

149 def remove(self, *strings: str) -> SetManager: 

150 """Remove the normalized string arguments from the set. 

151 Raises ``KeyError`` if any argument is not a member. Returns 

152 ``self`` for chaining.""" 

153 changed = False 

154 try: 

155 for s in strings: 

156 # _lc_validated: bytes get the same decode-hint TypeError 

157 # as add() (#245) 

158 normalized = _lc_validated(s) 

159 self._elements.remove(normalized) # KeyError on missing (#243) 

160 changed = True 

161 finally: 

162 # a KeyError mid-list still leaves earlier removals applied, 

163 # so the owner must hear about them or its cache goes stale 

164 if changed: 

165 self._changed() 

166 return self 

167 

168 def discard(self, *strings: str) -> SetManager: 

169 """Remove the normalized string arguments from the set if 

170 present; missing members are ignored, like ``set.discard``. 

171 Returns ``self`` for chaining.""" 

172 changed = False 

173 for s in strings: 

174 normalized = _lc_validated(s) # bytes decode hint (#245) 

175 if normalized in self._elements: 

176 self._elements.discard(normalized) 

177 changed = True 

178 if changed: 

179 self._changed() 

180 return self 

181 

182 def clear(self) -> SetManager: 

183 """Remove all entries from the set. Returns ``self`` for 

184 chaining.""" 

185 if self._elements: 

186 self._elements.clear() 

187 self._changed() 

188 return self 

189 

190 def __contains__(self, item: object) -> bool: 

191 return isinstance(item, str) and lc(item) in self._elements 

192 

193 def __iter__(self) -> Iterator[str]: 

194 return iter(self._elements) 

195 

196 def __len__(self) -> int: 

197 return len(self._elements) 

198 

199 def __eq__(self, other: object) -> bool: 

200 if isinstance(other, SetManager): 

201 return self._elements == other._elements 

202 if isinstance(other, (set, frozenset)): 

203 return self._elements == other 

204 return NotImplemented 

205 

206 __hash__ = None # type: ignore[assignment] # mutable; v1 parity 

207 

208 # -- set operators: accept ANY iterable (v1.3 normalize-everywhere) ----- 

209 # A bare str/bytes operand raises TypeError via _normalize_iterable_of_ 

210 # strings rather than iterating its characters (#238/#241); everything 

211 # else (list, generator, set, another SetManager, ...) is normalized 

212 # through lc() before the plain set op runs. 

213 

214 def __or__(self, other: object) -> set[str]: 

215 return self._elements | _normalize_iterable_of_strings(other) 

216 

217 __ror__ = __or__ 

218 

219 def __and__(self, other: object) -> set[str]: 

220 return self._elements & _normalize_iterable_of_strings(other) 

221 

222 __rand__ = __and__ 

223 

224 def __sub__(self, other: object) -> set[str]: 

225 return self._elements - _normalize_iterable_of_strings(other) 

226 

227 def __rsub__(self, other: object) -> set[str]: 

228 return _normalize_iterable_of_strings(other) - self._elements 

229 

230 def __xor__(self, other: object) -> set[str]: 

231 return self._elements ^ _normalize_iterable_of_strings(other) 

232 

233 __rxor__ = __xor__ # symmetric difference is commutative, like v1 

234 

235 # -- comparisons: v1 subclassed collections.abc.Set, whose __le__/__lt__/ 

236 # __ge__/__gt__ mixins only accept another Set-registered operand (set, 

237 # frozenset, or another Set subclass) -- NOT an arbitrary iterable like 

238 # list, unlike the operators above. Mirrored here since this SetManager 

239 # doesn't itself subclass the ABC. 

240 

241 def __le__(self, other: object) -> bool: 

242 if not isinstance(other, (SetManager, Set)): 

243 return NotImplemented 

244 if len(self) > len(other): 

245 return False 

246 return all(elem in other for elem in self) 

247 

248 def __lt__(self, other: object) -> bool: 

249 if not isinstance(other, (SetManager, Set)): 

250 return NotImplemented 

251 return len(self) < len(other) and self.__le__(other) 

252 

253 def __ge__(self, other: object) -> bool: 

254 if not isinstance(other, (SetManager, Set)): 

255 return NotImplemented 

256 if len(self) < len(other): 

257 return False 

258 return all(elem in self for elem in other) 

259 

260 def __gt__(self, other: object) -> bool: 

261 if not isinstance(other, (SetManager, Set)): 

262 return NotImplemented 

263 return len(self) > len(other) and self.__ge__(other) 

264 

265 def __repr__(self) -> str: 

266 # Sorted so repr is stable across runs -- set() iteration order 

267 # depends on per-process string hash randomization. 

268 elements = ", ".join(repr(e) for e in sorted(self._elements)) 

269 return f"SetManager({{{elements}}})" if self._elements else "SetManager(set())" 

270 

271 # -- pickle interop with v1 blobs --------------------------------------- 

272 

273 def __getstate__(self) -> dict[str, object]: 

274 return {"_elements": set(self._elements)} 

275 

276 def __setstate__(self, state: dict[str, object]) -> None: 

277 # v1 SetManager stored its set under `elements` (plain __dict__ 

278 # pickling); the shim stores `_elements`. Accept both, so a 

279 # v1.3/1.4 Constants blob's embedded managers unpickle straight 

280 # into shim instances. Re-normalize: nothing guarantees an 

281 # incoming blob's elements passed through lc(). 

282 elements: Iterable[str] = state.get( # type: ignore[assignment] 

283 "_elements", state.get("elements", ())) 

284 self._elements = {lc(e) for e in elements} 

285 self._on_change = None # rewired by the owning Constants 

286 

287 

288def _validated_mapping_args(args: tuple[object, ...]) -> tuple[object, ...]: 

289 """The #242 constructor guard, shared by TupleManager and 

290 _DelimiterManager: a bare str/bytes silently shreds into a garbage 

291 mapping (dict's own error), and an iterable of short strings 

292 silently splits each one into a (key, value) pair -- ported from 

293 v1's TupleManager.__init__ guard.""" 

294 if not args: 

295 return args 

296 arg = args[0] 

297 _reject_bare_str_or_bytes( 

298 arg, "a mapping or iterable of (key, value) pairs") 

299 if not isinstance(arg, Mapping): 

300 checked = [] 

301 for item in arg: # type: ignore[attr-defined] 

302 if isinstance(item, (str, bytes)): 

303 kind = "bytes" if isinstance(item, bytes) else "str" 

304 raise TypeError( 

305 f"expected (key, value) pairs, got a {kind} " 

306 f"element {item!r}; a 2-character string " 

307 "silently splits into a key and a value" 

308 ) 

309 checked.append(item) 

310 args = (checked, *args[1:]) 

311 return args 

312 

313 

314class TupleManager(dict[str, object]): 

315 """v1 ``TupleManager``: a dict with dot-notation access. Backs 

316 ``capitalization_exceptions``. Unknown-key attribute access raises 

317 ``AttributeError`` naming the key (#256, warned 1.4, enforced 2.0 -- 

318 the v1 ``DeprecationWarning`` is gone, this shim only speaks 2.0). 

319 Mutations call ``_on_change`` (the owning Constants' generation 

320 bump, wired by the facade). 

321 """ 

322 

323 _on_change: Callable[[], None] | None 

324 

325 def __init__(self, *args: object, 

326 _on_change: Callable[[], None] | None = None, 

327 **kwargs: object) -> None: 

328 args = _validated_mapping_args(args) 

329 super().__init__(*args, **kwargs) 

330 self._on_change = _on_change 

331 

332 def _changed(self) -> None: 

333 if self._on_change is not None: 

334 self._on_change() 

335 

336 def __getattr__(self, name: str) -> object: 

337 # Only reached for a missing attribute -- real instance attrs 

338 # (_on_change) and dict methods (keys, get, ...) resolve first 

339 # without ever hitting this. Dunder/underscore probes (pickling, 

340 # copy.deepcopy, IPython's _repr_html_) are never config keys. 

341 if name.startswith("_"): 

342 raise AttributeError(name) 

343 try: 

344 return self[name] 

345 except KeyError: 

346 # #256: name the known keys, like v1's 1.4 deprecation warning 

347 # did -- this shim only speaks 2.0, so what was a warning there 

348 # is a hard AttributeError here. 

349 raise AttributeError( 

350 f"{name!r} is not a known key " 

351 f"({', '.join(sorted(self))}); use .get() for intentional " 

352 "soft access." 

353 ) from None 

354 

355 def __setattr__(self, name: str, value: object) -> None: 

356 # v1 parity: dunder probes (typing's __orig_class__, etc.) and this 

357 # shim's own _on_change hook get real object-attribute storage; 

358 # every other name -- including single-underscore ones, per v1 -- 

359 # routes to the dict so `t.mcdonald = 'x'` and `t['mcdonald'] = 'x'` 

360 # are the same operation. 

361 if name == "_on_change" or (name.startswith("__") and name.endswith("__")): 

362 object.__setattr__(self, name, value) 

363 else: 

364 self[name] = value 

365 

366 def __delattr__(self, name: str) -> None: 

367 if name == "_on_change" or (name.startswith("__") and name.endswith("__")): 

368 object.__delattr__(self, name) 

369 else: 

370 del self[name] 

371 

372 def __setitem__(self, key: str, value: object) -> None: 

373 super().__setitem__(key, value) 

374 self._changed() 

375 

376 def __delitem__(self, key: str) -> None: 

377 super().__delitem__(key) 

378 self._changed() 

379 

380 def pop(self, *args: object) -> object: 

381 # bump only on a real removal -- pop(key, default) on a missing 

382 # key is a no-op read, not a mutation, same rule as SetManager's 

383 # no-op add() 

384 present = bool(args) and args[0] in self 

385 result = super().pop(*args) # type: ignore[call-overload] 

386 if present: 

387 self._changed() 

388 return result 

389 

390 def popitem(self) -> tuple[str, object]: 

391 result = super().popitem() # KeyError when empty: no bump 

392 self._changed() 

393 return result 

394 

395 def clear(self) -> None: 

396 had_items = bool(self) 

397 super().clear() 

398 if had_items: # clearing an empty dict is a no-op, not a change 

399 self._changed() 

400 

401 def update(self, *args: object, **kwargs: object) -> None: 

402 # dict.update's C path skips a subclass __setitem__; route every 

403 # item through it so subclass validation (_DelimiterManager's 

404 # sentinel rule) and the owner notification hold here too 

405 for key, value in dict(*args, **kwargs).items(): 

406 self[key] = value 

407 

408 def setdefault(self, key: str, default: object = None) -> object: 

409 if key in self: 

410 return self[key] # existing key: a read, not a mutation 

411 self[key] = default # validated + notifying path 

412 return default 

413 

414 # in-place |= must validate/notify like update; dict's C path would 

415 # skip both. mypy flags any non-overloaded __ior__ as inconsistent 

416 # with dict.__or__'s overloads -- the runtime behavior is the plain 

417 # dict |= contract, so the ignore is about typeshed shape only. 

418 def __ior__(self, other: object) -> Self: # type: ignore[override, misc] 

419 self.update(other) 

420 return self 

421 

422 # -- pickle interop ------------------------------------------------- 

423 

424 def __reduce__(self) -> tuple[type[TupleManager], tuple[()], dict[str, object]]: 

425 return (type(self), (), dict(self)) 

426 

427 def __setstate__(self, state: dict[str, object]) -> None: 

428 # routes through __setitem__ (validated for _DelimiterManager); 

429 # _on_change is still None here, so no spurious bumps 

430 self.update(state) 

431 self._on_change = None # rewired by the owning Constants 

432 

433 

434#: The named delimiter buckets, translated to the ``Policy`` 

435#: (open, close) pairs they stand for. The first three are 

436#: v1's; the rest are the #273 typographic conventions, named so the 

437#: v1 keyed idioms (pop/move/del) work on them like the originals. 

438#: Keep in sync with DEFAULT_NICKNAME_DELIMITERS in _policy.py (pinned 

439#: by the default-Constants equality test). 

440_SENTINEL_PAIRS = { 

441 "quoted_word": ("'", "'"), 

442 "double_quotes": ('"', '"'), 

443 "parenthesis": ("(", ")"), 

444 "smart_double_quotes": ("“", "”"), 

445 "low_high_quotes": ("„", "“"), 

446 "right_double_quotes": ("”", "”"), 

447 "guillemets": ("«", "»"), 

448 "reversed_guillemets": ("»", "«"), 

449 "corner_brackets": ("「", "」"), 

450 "white_corner_brackets": ("『", "』"), 

451 "fullwidth_parenthesis": ("(", ")"), 

452} 

453 

454#: derived, so the manager's accepted keys and _snapshot()'s 

455#: translation table can never drift apart 

456_DELIMITER_SENTINELS = tuple(_SENTINEL_PAIRS) 

457 

458 

459class RegexTupleManager(TupleManager): # pickle-compat: do NOT delete 

460 """Pickle-compat alias only: v1.4's ``Constants.regexes`` field was a 

461 ``nameparser.config.RegexTupleManager`` instance (a ``TupleManager`` 

462 subclass whose ``__getattr__`` fell back to ``EMPTY_REGEX`` for an 

463 unknown key). Unpickling a v1.4 blob resolves and constructs this 

464 class -- via ``TupleManager.__reduce__``'s ``(type(self), (), state)`` 

465 -- before ``Constants.__setstate__`` runs, so the name must exist 

466 here even though the shim's ``Constants._snapshot()`` never reads it: 

467 ``regexes`` is a read-only ``_RegexesProxy`` in 2.0 (see above), and 

468 ``Constants.__setstate__`` below deliberately ignores an incoming 

469 ``regexes`` key rather than restoring it from this reconstructed 

470 (and otherwise unused) instance. 

471 """ 

472 

473 

474class _DelimiterManager(TupleManager): 

475 """v1 ``nickname_delimiters``/``maiden_delimiters`` bucket. In 2.0 

476 only the named sentinels in ``_DELIMITER_SENTINELS`` exist (the v1 

477 trio plus the #273 typographic pairs) -- assigning any 

478 other key raises so a caller reaches for a custom-delimiter Policy 

479 kwarg instead of a dict entry that silently does nothing. ``pop()``/ 

480 ``__setitem__``/``__delitem__`` stay open (inherited) for the 

481 documented bucket-move idiom, e.g. 

482 ``maiden_delimiters['parenthesis'] = nickname_delimiters.pop('parenthesis')``. 

483 """ 

484 

485 def __init__(self, *args: object, 

486 _on_change: Callable[[], None] | None = None, 

487 **kwargs: object) -> None: 

488 # dict's C-level __init__ never calls a subclass __setitem__, so 

489 # collect and validate the initial items here -- BEFORE any item 

490 # lands -- or the sentinel rule silently misses the constructor. 

491 # The parent's #242 guard runs first: bare str/bytes gets the 

492 # friendly TypeError, not dict's cryptic one. 

493 args = _validated_mapping_args(args) 

494 items: dict[str, object] = dict(*args, **kwargs) 

495 for key in items: 

496 self._reject_non_sentinel(key) 

497 super().__init__(items, _on_change=_on_change) 

498 

499 @staticmethod 

500 def _reject_non_sentinel(key: str) -> None: 

501 if key not in _DELIMITER_SENTINELS: 

502 raise TypeError( 

503 f"2.0 delimiter managers accept only the named sentinels " 

504 f"{_DELIMITER_SENTINELS}; for custom delimiter pairs use " 

505 f"Policy(nickname_delimiters=...) / maiden_delimiters" 

506 ) 

507 

508 def __setitem__(self, key: str, value: object) -> None: 

509 self._reject_non_sentinel(key) 

510 super().__setitem__(key, value) 

511 # update/setdefault/|= inherit TupleManager's routing through 

512 # __setitem__, so they validate (and notify) for free 

513 

514 

515class _RegexesProxy: 

516 """Read-only view over the v1 compiled patterns 

517 (``nameparser.config.regexes.REGEXES``). Reads keep working -- 

518 ``CONSTANTS.regexes.word`` stays informational -- but 2.0 configures 

519 parsing behavior through named ``Policy`` flags, not by mutating a 

520 regex, so any attribute *or* item assignment raises ``TypeError`` 

521 (the shim's uniform read-only rule). 

522 """ 

523 

524 @staticmethod 

525 def _regexes() -> Mapping[str, object]: 

526 # deferred import: see the module docstring's note on why every 

527 # nameparser.config.<data module> import in this file is lazy 

528 from nameparser.config.regexes import REGEXES 

529 return REGEXES 

530 

531 #: dict methods this read-only proxy deliberately does not carry. 

532 #: Without this, __getattr__ reports them as missing regexes and 

533 #: sends the reader hunting for a vocabulary entry. 

534 _DICT_ONLY = frozenset({ 

535 "pop", "popitem", "setdefault", "update", "clear", "fromkeys", 

536 }) 

537 

538 def __getattr__(self, name: str) -> object: 

539 if name.startswith("_"): 

540 raise AttributeError(name) 

541 if name in _RegexesProxy._DICT_ONLY: 

542 raise AttributeError( 

543 f"{name!r} is not supported on CONSTANTS.regexes: it is a " 

544 f"read-only view in 2.0, and parsing behavior is configured " 

545 f"through named Policy flags" 

546 ) 

547 try: 

548 return self._regexes()[name] 

549 except KeyError: 

550 raise AttributeError(f"no regex named {name!r}") from None 

551 

552 def __getitem__(self, name: str) -> object: 

553 return self._regexes()[name] 

554 

555 def __contains__(self, name: object) -> bool: 

556 return name in self._regexes() 

557 

558 def __iter__(self) -> Iterator[str]: 

559 return iter(self._regexes()) 

560 

561 def keys(self) -> KeysView[str]: 

562 return self._regexes().keys() 

563 

564 # Defined explicitly because __getattr__ would otherwise claim each 

565 # of these names as a regex lookup and raise "no regex named 

566 # 'items'". The sibling managers inherit the whole surface from 

567 # dict; this proxy has to spell out the read half it supports. 

568 # #256's deprecation text promised .get() on both managers. 

569 

570 def get(self, name: str, default: object = None) -> object: 

571 return self._regexes().get(name, default) 

572 

573 def items(self) -> ItemsView[str, object]: 

574 return self._regexes().items() 

575 

576 def values(self) -> ValuesView[object]: 

577 return self._regexes().values() 

578 

579 def __len__(self) -> int: 

580 return len(self._regexes()) 

581 

582 def copy(self) -> dict[str, object]: 

583 # v1's regexes was a dict subclass, so copy() returned a plain 

584 # mutable dict. Mutating the copy never affected parsing there 

585 # either, so this is a faithful read-only carry-over. 

586 return dict(self._regexes()) 

587 

588 def __setattr__(self, name: str, value: object) -> None: 

589 self._raise_readonly(name) 

590 

591 def __setitem__(self, name: str, value: object) -> None: 

592 self._raise_readonly(name) 

593 

594 @staticmethod 

595 def _raise_readonly(name: str) -> None: 

596 # bidi/emoji are the two regexes v1 code toggled directly 

597 # (`CONSTANTS.regexes.bidi = False`) to opt out of stripping; 

598 # point those two at their named Policy replacement, everything 

599 # else gets the generic pointer. 

600 hints = { 

601 "bidi": "use Policy(strip_bidi=False) to keep bidi marks", 

602 "emoji": "use Policy(strip_emoji=False) to keep emoji", 

603 } 

604 hint = hints.get( 

605 name, "parsing behavior is configured through named Policy " 

606 "flags in 2.0; if none fits, open an issue") 

607 raise TypeError( 

608 f"assigning CONSTANTS.regexes.{name} is not supported in " 

609 f"2.0: {hint}" 

610 ) 

611 

612 

613_SET_FIELDS = ( 

614 "prefixes", "suffix_acronyms", "suffix_not_acronyms", 

615 "suffix_acronyms_ambiguous", "titles", "first_name_titles", 

616 "conjunctions", "bound_first_names", "non_first_name_prefixes", 

617) 

618_MANAGER_FIELDS = _SET_FIELDS + ( 

619 "capitalization_exceptions", "nickname_delimiters", "maiden_delimiters", 

620) 

621 

622#: v1's Constants.__repr__ field order (#221) -- kept as its own tuple 

623#: rather than reusing _SET_FIELDS, whose order differs (v1 lists 

624#: suffix_acronyms_ambiguous last, not fourth). 

625_REPR_COLLECTION_ATTRS = ( 

626 "prefixes", "suffix_acronyms", "suffix_not_acronyms", "titles", 

627 "first_name_titles", "conjunctions", "bound_first_names", 

628 "non_first_name_prefixes", "suffix_acronyms_ambiguous", 

629) 

630#: v1's repr scalar order, minus empty_attribute_default -- removed in 2.0 

631#: (#255), so there's no such attribute on this shim's Constants to show. 

632_REPR_SCALAR_ATTRS = ( 

633 "string_format", "initials_format", "initials_delimiter", 

634 "initials_separator", "suffix_delimiter", 

635 "capitalize_name", "force_mixed_case_capitalization", 

636 "patronymic_name_order", "middle_name_as_last", 

637) 

638 

639_SCALAR_DEFAULTS: dict[str, object] = { 

640 "patronymic_name_order": False, 

641 "middle_name_as_last": False, 

642 "capitalize_name": False, 

643 "force_mixed_case_capitalization": False, 

644 "string_format": "{title} {first} {middle} {last} {suffix} ({nickname})", 

645 "initials_format": "{first} {middle} {last}", 

646 "initials_delimiter": ".", 

647 "initials_separator": " ", 

648 "suffix_delimiter": None, 

649} 

650 

651# distinguishes "attribute not set yet" from any real scalar value 

652# (None is a legitimate value for string_format/suffix_delimiter) 

653_UNSET = object() 

654 

655# distinguishes "kwarg not passed to Constants()" (use library defaults) 

656# from any real value a caller might pass, including a falsy one like "" 

657_UNSET_KWARG = object() 

658 

659 

660_SHARED_MUTATION_MESSAGE = ( 

661 "mutating the shared CONSTANTS singleton is deprecated and will be " 

662 "removed in 3.0; build a Lexicon/Policy (or a private Constants " 

663 "passed as HumanName(constants=...)) instead. See the migration " 

664 "guide." 

665) 

666 

667 

668def _default_vocab() -> dict[str, frozenset[str]]: 

669 # v1 data modules stay the single vocabulary source through 2.x 

670 # (same rule as Lexicon.default()). 

671 from nameparser.config.bound_given_names import BOUND_GIVEN_NAMES 

672 from nameparser.config.conjunctions import CONJUNCTIONS 

673 from nameparser.config.particles import ( 

674 NON_GIVEN_NAME_PARTICLES, PARTICLES, 

675 ) 

676 from nameparser.config.suffixes import ( 

677 SUFFIX_ACRONYMS, SUFFIX_ACRONYMS_AMBIGUOUS, SUFFIX_WORDS, 

678 ) 

679 from nameparser.config.titles import GIVEN_NAME_TITLES, TITLES 

680 return { 

681 "prefixes": PARTICLES, 

682 "suffix_acronyms": SUFFIX_ACRONYMS, 

683 "suffix_not_acronyms": SUFFIX_WORDS, 

684 "suffix_acronyms_ambiguous": SUFFIX_ACRONYMS_AMBIGUOUS, 

685 "titles": TITLES, 

686 "first_name_titles": GIVEN_NAME_TITLES, 

687 "conjunctions": CONJUNCTIONS, 

688 "bound_first_names": BOUND_GIVEN_NAMES, 

689 "non_first_name_prefixes": NON_GIVEN_NAME_PARTICLES, 

690 } 

691 

692 

693class _RenderDefaults(NamedTuple): 

694 """v1 scalar rendering knobs that have no home on ``Policy`` 

695 (mechanisms.md#CONFIG-SHIM-SNAPSHOT): ``__str__``/initials 

696 formatting and capitalization stay 

697 per-Constants defaults, layered onto a shared ``Parser`` by the 

698 facade (nameparser/_facade.py) rather than folded into the cache 

699 key.""" 

700 

701 string_format: str | None 

702 initials_format: str 

703 initials_delimiter: str 

704 initials_separator: str 

705 suffix_delimiter: str | None 

706 capitalize_name: bool 

707 force_mixed_case_capitalization: bool 

708 

709 

710@functools.lru_cache(maxsize=64) 

711def _cached_parser(lexicon: Lexicon, policy: Policy) -> Parser: 

712 # keyed on hashable value objects: shared across every facade whose 

713 # Constants resolve to the same snapshot 

714 # (mechanisms.md#CONFIG-SHIM-SNAPSHOT) 

715 return Parser(lexicon=lexicon, policy=policy) 

716 

717 

718class Constants: 

719 """v1 ``Constants`` shim: a mutable container whose state resolves to 

720 a frozen ``(Lexicon, Policy, _RenderDefaults)`` snapshot via 

721 ``_snapshot()``. ``_generation`` increments on every mutation; 

722 facades compare it against a cached value to decide whether their 

723 snapshot is stale (dirty-tracking -- the facade side lives in 

724 nameparser/_facade.py). 

725 

726 The module-level ``CONSTANTS`` singleton (below) has ``_shared`` 

727 flipped to ``True``: any mutation reached through it emits 

728 ``DeprecationWarning`` pointing at ``Lexicon``/``Policy`` and 

729 ``HumanName(constants=...)``. A private ``Constants()`` never 

730 warns -- only the shared instance is on the 3.0 removal path. 

731 """ 

732 

733 _shared = False # the CONSTANTS singleton flips this to True 

734 _generation: int 

735 

736 prefixes: SetManager 

737 suffix_acronyms: SetManager 

738 suffix_not_acronyms: SetManager 

739 suffix_acronyms_ambiguous: SetManager 

740 titles: SetManager 

741 first_name_titles: SetManager 

742 conjunctions: SetManager 

743 bound_first_names: SetManager 

744 non_first_name_prefixes: SetManager 

745 capitalization_exceptions: TupleManager 

746 nickname_delimiters: _DelimiterManager 

747 maiden_delimiters: _DelimiterManager 

748 regexes: _RegexesProxy 

749 

750 patronymic_name_order: bool 

751 middle_name_as_last: bool 

752 capitalize_name: bool 

753 force_mixed_case_capitalization: bool 

754 string_format: str | None 

755 initials_format: str 

756 initials_delimiter: str 

757 initials_separator: str 

758 suffix_delimiter: str | None 

759 

760 def __init__( 

761 self, 

762 *, 

763 prefixes: Iterable[str] | object = _UNSET_KWARG, 

764 suffix_acronyms: Iterable[str] | object = _UNSET_KWARG, 

765 suffix_not_acronyms: Iterable[str] | object = _UNSET_KWARG, 

766 suffix_acronyms_ambiguous: Iterable[str] | object = _UNSET_KWARG, 

767 titles: Iterable[str] | object = _UNSET_KWARG, 

768 first_name_titles: Iterable[str] | object = _UNSET_KWARG, 

769 conjunctions: Iterable[str] | object = _UNSET_KWARG, 

770 bound_first_names: Iterable[str] | object = _UNSET_KWARG, 

771 non_first_name_prefixes: Iterable[str] | object = _UNSET_KWARG, 

772 capitalization_exceptions: 

773 Mapping[str, str] | Iterable[tuple[str, str]] | object 

774 = _UNSET_KWARG, 

775 regexes: object = _UNSET_KWARG, 

776 patronymic_name_order: bool = False, 

777 middle_name_as_last: bool = False, 

778 ) -> None: 

779 # v1.4 parity constructor kwargs (#238/#242/#244 hardening); the 

780 # signature is spelled out rather than **kwargs so an unknown 

781 # keyword raises Python's own TypeError with no help from here. 

782 # `regexes` is the one deliberate 2.0 divergence: v1.4 accepted it 

783 # (RegexTupleManager(regexes)), but constructor injection is 

784 # assignment by another door, and __setattr__ above already 

785 # forbids `c.regexes = ...` post-construction -- the uniform 2.0 

786 # rule is that parsing behavior is configured through Policy, not 

787 # by handing Constants a compiled-pattern mapping either way. 

788 if regexes is not _UNSET_KWARG: 

789 raise TypeError( 

790 "Constants(regexes=...) is not supported in 2.0; parsing " 

791 "behavior is configured through named Policy flags, not " 

792 "by constructing Constants with a regex mapping. See the " 

793 "migration guide." 

794 ) 

795 overrides = { 

796 "prefixes": prefixes, 

797 "suffix_acronyms": suffix_acronyms, 

798 "suffix_not_acronyms": suffix_not_acronyms, 

799 "suffix_acronyms_ambiguous": suffix_acronyms_ambiguous, 

800 "titles": titles, 

801 "first_name_titles": first_name_titles, 

802 "conjunctions": conjunctions, 

803 "bound_first_names": bound_first_names, 

804 "non_first_name_prefixes": non_first_name_prefixes, 

805 } 

806 vocab = _default_vocab() 

807 object.__setattr__(self, "_generation", 0) 

808 for name in _SET_FIELDS: 

809 value = overrides[name] 

810 if value is _UNSET_KWARG: 

811 value = vocab[name] 

812 # a caller-supplied value REPLACES that field's default 

813 # vocabulary wholesale (v1 parity); SetManager itself validates/ 

814 # normalizes and rejects a bare str/bytes (#238/#241), naming 

815 # this field in the message via _field= 

816 object.__setattr__( 

817 self, name, 

818 SetManager(value, _on_change=self._bump, _field=name)) # type: ignore[arg-type] 

819 if capitalization_exceptions is _UNSET_KWARG: 

820 from nameparser.config.capitalization import ( 

821 CAPITALIZATION_EXCEPTIONS, 

822 ) 

823 capitalization_exceptions = CAPITALIZATION_EXCEPTIONS 

824 object.__setattr__(self, "capitalization_exceptions", TupleManager( 

825 capitalization_exceptions, # type: ignore[arg-type] 

826 _on_change=self._bump)) 

827 object.__setattr__(self, "nickname_delimiters", _DelimiterManager( 

828 {name: name for name in _DELIMITER_SENTINELS}, 

829 _on_change=self._bump)) 

830 object.__setattr__(self, "maiden_delimiters", _DelimiterManager( 

831 _on_change=self._bump)) 

832 object.__setattr__(self, "regexes", _RegexesProxy()) 

833 for name, value in _SCALAR_DEFAULTS.items(): 

834 object.__setattr__(self, name, value) 

835 # the two behavior bools were v1.4 constructor kwargs too 

836 # (docs/customize.rst doctests use them); truthiness matches v1, 

837 # storage goes through the plain scalar slot 

838 if patronymic_name_order: 

839 object.__setattr__(self, "patronymic_name_order", True) 

840 if middle_name_as_last: 

841 object.__setattr__(self, "middle_name_as_last", True) 

842 

843 def _invalidate_pst(self) -> None: 

844 """Pickle-compat alias only, never called at runtime: v1's four 

845 cached-union ``SetManager`` fields (``prefixes``, 

846 ``suffix_acronyms``, ``suffix_not_acronyms``, ``titles``) stored 

847 their ``_on_change`` as the bound method 

848 ``Constants._invalidate_pst``. A pickled bound method serializes 

849 as a back-reference to its ``__self__`` (this same ``Constants`` 

850 instance, mid-unpickle) plus the method NAME -- reconstructed via 

851 ``getattr(constants_obj, '_invalidate_pst')`` before 

852 ``Constants.__setstate__`` ever runs (same two-phase-unpickling 

853 story as ``RegexTupleManager`` above). Without this name, loading 

854 a v1.4 blob raises ``AttributeError`` looking up the method. 

855 ``SetManager.__setstate__`` (shim) never restores ``_on_change`` 

856 from pickled state regardless -- it always resets to ``None`` and 

857 is rewired by ``Constants.__setstate__`` below -- so this bound 

858 method value is reconstructed only to satisfy the pickle format; 

859 it is discarded immediately and never invoked. 

860 """ 

861 

862 def _bump(self) -> None: 

863 # stacklevel=3 is exact for the direct scalar-assignment path 

864 # (user code -> Constants.__setattr__ -> here) and lands one 

865 # frame short -- inside the manager's own add()/remove()/ 

866 # __setitem__ -- for the indirect manager-mutation path (user 

867 # code -> manager method -> _changed() -> here), since a 

868 # single warn() call can't be exact for both call depths at 

869 # once. Either way the warning still fires from this module, 

870 # not the manager's true caller, which is enough: only the 

871 # DeprecationWarning's presence/category/message are load- 

872 # bearing (see the specified test), not the reported line. 

873 if self._shared: 

874 warnings.warn(_SHARED_MUTATION_MESSAGE, DeprecationWarning, 

875 stacklevel=3) 

876 object.__setattr__(self, "_generation", self._generation + 1) 

877 

878 def __setattr__(self, name: str, value: object) -> None: 

879 if name == "_shared": 

880 # the flag the whole shared-mutation DeprecationWarning 

881 # mechanism hinges on: only the module-level singleton flip 

882 # (object.__setattr__ below) may set it 

883 raise AttributeError( 

884 "Constants._shared is read-only; it marks the module " 

885 "singleton and is set once at import" 

886 ) 

887 if name == "empty_attribute_default": 

888 raise AttributeError( 

889 "empty_attribute_default was removed in 2.0 (#255): " 

890 "empty attributes are always ''" 

891 ) 

892 if name == "regexes": 

893 raise TypeError( 

894 "replacing CONSTANTS.regexes is not supported in 2.0; " 

895 "parsing behavior is configured through named Policy " 

896 "flags" 

897 ) 

898 if name in _SET_FIELDS: 

899 # v1 allowed wholesale reassignment (c.titles = {...}); same 

900 # bare-str/bytes guard as the constructor kwarg path (#238/#241) 

901 value = SetManager( 

902 value, _on_change=self._bump, _field=name) # type: ignore[arg-type] 

903 elif name == "capitalization_exceptions": 

904 value = TupleManager(value, _on_change=self._bump) # type: ignore[arg-type] 

905 elif name in ("nickname_delimiters", "maiden_delimiters"): 

906 value = _DelimiterManager(value, _on_change=self._bump) # type: ignore[arg-type] 

907 elif name in _SCALAR_DEFAULTS and \ 

908 getattr(self, name, _UNSET) == value: 

909 # no-op scalar assignment: managers already suppress no-op 

910 # mutations, so re-assigning the current scalar value must 

911 # not bump the generation (or warn on the shared singleton) 

912 # either. __init__ writes via object.__setattr__, so this 

913 # only runs on real user assignments -- _UNSET never 

914 # actually matches, it just keeps a not-yet-set attribute 

915 # from raising here. Manager-field reassignment above stays 

916 # an unconditional bump: comparing manager contents isn't 

917 # worth it. 

918 object.__setattr__(self, name, value) 

919 return 

920 object.__setattr__(self, name, value) 

921 if name in _MANAGER_FIELDS or name in _SCALAR_DEFAULTS: 

922 self._bump() 

923 

924 def copy(self) -> Constants: 

925 """Independent copy (#260), subclass-preserving. Divergence from 

926 v1 (which deepcopied __dict__): attributes OUTSIDE the known 

927 field surface -- e.g. ad-hoc names stashed on the instance -- 

928 are not carried by copy() or pickling; only the enumerated 

929 fields survive.""" # #260 

930 # An independent instance with its own generation counter and 

931 # its own manager callbacks -- not a shared-state alias like a 

932 # naive attribute-for-attribute copy would produce. v1's copy() 

933 # was `copy.deepcopy(self)`, which builds the new object via 

934 # `type(self).__new__(type(self))` -- NOT by calling `type(self)()` 

935 # -- so a Constants subclass copies as itself without its __init__ 

936 # running (and without needing to satisfy whatever signature that 

937 # __init__ might require). Mirrored here with an explicit __new__ 

938 # bypass rather than type(self)(). 

939 new = object.__new__(type(self)) 

940 object.__setattr__(new, "_generation", 0) 

941 for name in _SET_FIELDS: 

942 object.__setattr__( 

943 new, name, 

944 SetManager(getattr(self, name), _on_change=new._bump)) 

945 object.__setattr__(new, "capitalization_exceptions", TupleManager( 

946 dict(self.capitalization_exceptions), _on_change=new._bump)) 

947 for bucket in ("nickname_delimiters", "maiden_delimiters"): 

948 object.__setattr__(new, bucket, _DelimiterManager( 

949 dict(getattr(self, bucket)), _on_change=new._bump)) 

950 object.__setattr__(new, "regexes", _RegexesProxy()) 

951 for name in _SCALAR_DEFAULTS: 

952 object.__setattr__(new, name, getattr(self, name)) 

953 return new 

954 

955 def __repr__(self) -> str: # #221 

956 # Collections (some with hundreds of entries, e.g. titles/prefixes) 

957 # are summarized as counts rather than dumped in full, like v1. 

958 # Scalars are only shown when they differ from the library default 

959 # -- _SCALAR_DEFAULTS stands in for v1's `getattr(type(self), name)` 

960 # class-level default, since this shim's scalar defaults are 

961 # instance attributes set in __init__, not class attributes. 

962 lines = [f" {name}: {len(getattr(self, name))}" 

963 for name in _REPR_COLLECTION_ATTRS] 

964 lines += [ 

965 f" {name}: {value!r}" for name in _REPR_SCALAR_ATTRS 

966 if (value := getattr(self, name)) != _SCALAR_DEFAULTS[name] 

967 ] 

968 return "<Constants : [\n" + "\n".join(lines) + "\n]>" 

969 

970 # -- snapshot ----------------------------------------------------------- 

971 

972 def _snapshot(self) -> tuple[Lexicon, Policy, _RenderDefaults]: 

973 # generation-keyed cache: rebuilding the Lexicon re-normalizes 

974 # ~1400 vocabulary entries (~185us) -- the same dirty-tracking 

975 # the facade uses, applied one level up. A pure read either way 

976 # (no bump, no warning). 

977 cached = getattr(self, "_snapshot_cache", None) 

978 if cached is not None and cached[0] == self._generation: 

979 return cached[1] # type: ignore[no-any-return] 

980 snapshot = self._build_snapshot() 

981 object.__setattr__( 

982 self, "_snapshot_cache", (self._generation, snapshot)) 

983 return snapshot 

984 

985 def _build_snapshot(self) -> tuple[Lexicon, Policy, _RenderDefaults]: 

986 """Resolve this v1-shaped, mutable Constants into the frozen 

987 2.0 value objects it corresponds to. A pure read: no 

988 generation bump, no deprecation warning even on the shared 

989 singleton -- only direct attribute mutation is on the 3.0 

990 removal path. 

991 """ 

992 from nameparser.config.conjunctions import CONJUNCTIONS_AMBIGUOUS 

993 from nameparser.config.maiden_markers import MAIDEN_MARKERS 

994 from nameparser.config.suffixes import GLUED_HONORIFICS 

995 from nameparser.config.surnames import KOREAN_SURNAMES 

996 acronyms = frozenset(self.suffix_acronyms) 

997 particles = frozenset(self.prefixes) 

998 conjunctions = frozenset(self.conjunctions) 

999 bound = frozenset(self.bound_first_names) 

1000 ambiguous_acronyms = frozenset(self.suffix_acronyms_ambiguous) & acronyms 

1001 # Drop any ambiguous acronym from the word set rather than the 

1002 # other way round. Lexicon forbids the overlap because the word 

1003 # branch bypasses the period gate, and adding an ambiguous 

1004 # acronym to suffix_not_acronyms is INERT in v1 anyway: 

1005 # is_suffix already accepts it via the acronym branch, and 

1006 # reserve_last keeps it as the surname. So ignoring the 

1007 # addition reproduces v1 ("Jack Ma" keeps last='Ma'), where 

1008 # dropping it from the AMBIGUOUS set instead ungated the word 

1009 # and lost the family name -- a silent misparse worse than the 

1010 # raise it avoided. 

1011 suffix_words = frozenset(self.suffix_not_acronyms) - ambiguous_acronyms 

1012 # keep in sync with _lexicon._default_lexicon() (pinned by 

1013 # tests/v2/test_config_shim.py::test_snapshot_field_translation) 

1014 lexicon = Lexicon( 

1015 titles=frozenset(self.titles), 

1016 # TRANSLATE, do not filter. The two versions build the same 

1017 # lookup key differently: v1 joins the raw title run and 

1018 # then applies lc(), which strips only the whole string's 

1019 # edge periods, so an interior word keeps its own ("lt. 

1020 # col"). v2 normalizes each token and then joins ("lt col"). 

1021 # Re-folding per word converts a v1 entry into the v2 

1022 # spelling; filtering instead dropped every multi-word 

1023 # honorific containing an abbreviation or a conjunction and 

1024 # silently swapped given and family. 

1025 # Only entries v1 could actually match: its key is the 

1026 # joined title run, so always single-spaced and never 

1027 # empty. An entry holding a whitespace run was inert there 

1028 # (translating it would start matching), and one that folds 

1029 # away entirely would trip _normset's empty-entry check on 

1030 # a config v1 simply ignored. 

1031 given_name_titles=frozenset( 

1032 t for t in ( 

1033 _title_key(e.split()) 

1034 for e in self.first_name_titles 

1035 if e == " ".join(e.split()) 

1036 ) if t), 

1037 suffix_acronyms=acronyms, 

1038 suffix_words=suffix_words, 

1039 # Intersect with acronyms: Lexicon enforces ambiguous <= 

1040 # acronyms; v1 behaves the same when an acronym is deleted 

1041 # but its ambiguous entry lingers (the entry stops 

1042 # mattering). 

1043 suffix_acronyms_ambiguous=ambiguous_acronyms, 

1044 particles=particles, 

1045 # complement translation: v1 marks the never-given subset; 

1046 # v2 marks the may-be-given subset. The trailing union keeps 

1047 # a config v1 accepted: particles.py asserts its own data has 

1048 # no word in both NON_GIVEN_NAME_PARTICLES and 

1049 # BOUND_GIVEN_NAMES, but nothing stops a caller adding one at 

1050 # runtime, and v1 then lets the bound rule win (leading "dos 

1051 # Santos Silva" parses first="dos Santos"). Treating such a 

1052 # word as may-be-given reproduces that rather than raising. 

1053 # 

1054 # KNOWN DEVIATION, pinned by 

1055 # test_bound_never_given_prefix_deviates_on_two_pieces: v1's 

1056 # join has a reserve_last guard, so with only two pieces it 

1057 # does NOT fire and the word stays never-given ("dos Santos" 

1058 # -> last="dos Santos"). Promotion here is unconditional, so 

1059 # that case reads given="dos", family="Santos". v1's rule is 

1060 # piece-count dependent and a static vocabulary set cannot 

1061 # express it; the alternative is raising on a config v1 

1062 # accepted, which is worse. Only reachable via a runtime 

1063 # config the shipped data forbids. 

1064 particles_ambiguous=( 

1065 particles - frozenset(self.non_first_name_prefixes)) 

1066 | (bound & particles), 

1067 conjunctions=conjunctions, 

1068 # no v1 manager of its own: the ambiguous-connective 

1069 # subset is 2.4 behavior (#383/#479), so it rides in the 

1070 # snapshot only. Lexicon does NOT check this pair -- unlike 

1071 # honorific_tails against suffix_words below -- so the 

1072 # intersection is a provable no-op, kept only for 

1073 # `_snapshot() == Lexicon.default()` legibility; the v1 

1074 # knob (deleting the conjunction) turns the marking off 

1075 # through the fork's own base-vocabulary test rather than 

1076 # through this intersection. 

1077 conjunctions_ambiguous=CONJUNCTIONS_AMBIGUOUS & conjunctions, 

1078 bound_given_names=bound, 

1079 # v1 Constants has no manager for these (#274 is 2.0 

1080 # behavior); the data module is the only source 

1081 maiden_markers=MAIDEN_MARKERS, 

1082 # likewise no v1 manager: the unspaced-name segmentation 

1083 # vocabulary is 2.0 behavior (#271), so it rides in the 

1084 # snapshot only -- v1's Constants surface stays frozen. 

1085 surnames=KOREAN_SURNAMES, 

1086 # likewise no v1 manager: the glued-honorific tail set is 

1087 # 2.1 behavior (#308), so it rides in the snapshot only. 

1088 # Intersect with the word set: Lexicon enforces tails <= 

1089 # suffix_words, and v1 semantics are that deleting a suffix 

1090 # word turns the behavior off -- a lingering tail simply 

1091 # stops mattering, the same rule ambiguous_acronyms gets 

1092 # against suffix_acronyms above. 

1093 honorific_tails=GLUED_HONORIFICS & suffix_words, 

1094 # TupleManager is dict[str, object] (v1 parity: values were 

1095 # never statically str-typed); every real entry is a str, 

1096 # same assumption _DelimiterManager's sentinel lookup makes 

1097 capitalization_exceptions=tuple( 

1098 sorted(self.capitalization_exceptions.items())), # type: ignore[arg-type] 

1099 ) 

1100 rules = frozenset({PatronymicRule.EAST_SLAVIC, PatronymicRule.TURKIC}) \ 

1101 if self.patronymic_name_order else frozenset() 

1102 policy = Policy( 

1103 patronymic_rules=rules, 

1104 middle_as_family=self.middle_name_as_last, 

1105 nickname_delimiters=frozenset( 

1106 _SENTINEL_PAIRS[k] for k in self.nickname_delimiters), 

1107 # v1 precedence: a pair in BOTH v1 buckets parses as nickname. 

1108 # Policy itself resolves overlap the other way (maiden wins), 

1109 # so pre-subtract here to keep the facade at v1 behavior. 

1110 maiden_delimiters=frozenset( 

1111 _SENTINEL_PAIRS[k] for k in self.maiden_delimiters 

1112 if k not in self.nickname_delimiters), 

1113 # suffix_delimiter is a _RenderDefaults-only field here; the 

1114 # facade layers it onto extra_suffix_delimiters per 

1115 # instance -- _snapshot() itself stays pure translation 

1116 ) 

1117 defaults = _RenderDefaults( 

1118 self.string_format, self.initials_format, self.initials_delimiter, 

1119 self.initials_separator, self.suffix_delimiter, 

1120 self.capitalize_name, self.force_mixed_case_capitalization) 

1121 return lexicon, policy, defaults 

1122 

1123 # -- pickle ----------------------------------------------------------- 

1124 

1125 def __getstate__(self) -> dict[str, object]: 

1126 state: dict[str, object] = {} 

1127 for name in _SET_FIELDS: 

1128 state[name] = set(getattr(self, name)) 

1129 state["capitalization_exceptions"] = dict( 

1130 self.capitalization_exceptions) 

1131 state["nickname_delimiters"] = dict(self.nickname_delimiters) 

1132 state["maiden_delimiters"] = dict(self.maiden_delimiters) 

1133 for name in _SCALAR_DEFAULTS: 

1134 state[name] = getattr(self, name) 

1135 return state 

1136 

1137 def __setstate__(self, state: dict[str, object]) -> None: 

1138 if "suffixes_prefixes_titles" in state: 

1139 # pre-1.3.0 blob: its dir()-sweep __getstate__ captured this 

1140 # computed property. The 1.4 DeprecationWarning promised 

1141 # ValueError in 2.0 (#279). 

1142 raise ValueError( 

1143 "this pickle was written by nameparser <= 1.2.x (#279); " 

1144 "re-pickle under 1.3/1.4 to migrate, or re-create the " 

1145 "configuration. See " 

1146 "https://github.com/derek73/python-nameparser/issues/279" 

1147 ) 

1148 # Accepts BOTH shapes with a single overlay: the shim's own 

1149 # state and v1.3/1.4 state (public field names -> manager/ 

1150 # scalar values) share every key that matters, so no shape 

1151 # marker is needed. empty_attribute_default is accepted and 

1152 # DROPPED (#255: empty is always '' in 2.0). 

1153 state = {k: v for k, v in state.items() 

1154 if k != "empty_attribute_default"} 

1155 self.__init__() # type: ignore[misc] # defaults, then overlay 

1156 # (managers re-wrapped below so _on_change points at THIS 

1157 # instance, not whatever produced the incoming state) 

1158 managers: dict[str, SetManager] = {} 

1159 for name in _SET_FIELDS: 

1160 if name in state: 

1161 managers[name] = SetManager( 

1162 state[name], _on_change=self._bump) # type: ignore[arg-type] 

1163 # SetManager normalized on construction, so the frozen 1.3/1.4 

1164 # vocabulary's dead entries are matchable in their normalized 

1165 # spelling here. Subtract only when ALL EIGHT are present -- 

1166 # the pre-2.0 signature; a 2.0 user who re-added one or two 

1167 # keeps them through a round-trip (see _LEGACY_DEAD_ENTRIES). 

1168 legacy = all( 

1169 name in managers and entry in managers[name] 

1170 for name, entries in _LEGACY_DEAD_ENTRIES.items() 

1171 for entry in entries 

1172 ) 

1173 for name, manager in managers.items(): 

1174 if legacy: 

1175 # Reach past the public discard() deliberately: this is 

1176 # part of restoring the state, not a mutation of it, and 

1177 # must not bump the generation of an instance that is 

1178 # still being built. 

1179 manager._elements -= _LEGACY_DEAD_ENTRIES.get( 

1180 name, frozenset()) 

1181 object.__setattr__(self, name, manager) 

1182 if "capitalization_exceptions" in state: 

1183 object.__setattr__( 

1184 self, "capitalization_exceptions", TupleManager( 

1185 state["capitalization_exceptions"], # type: ignore[arg-type] 

1186 _on_change=self._bump)) 

1187 for bucket in ("nickname_delimiters", "maiden_delimiters"): 

1188 if bucket in state: 

1189 object.__setattr__(self, bucket, _DelimiterManager( 

1190 state[bucket], _on_change=self._bump)) # type: ignore[arg-type] 

1191 for name in _SCALAR_DEFAULTS: 

1192 if name in state: 

1193 object.__setattr__(self, name, state[name]) 

1194 

1195 

1196CONSTANTS = Constants() 

1197object.__setattr__(CONSTANTS, "_shared", True)