Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/nameparser/config/__init__.py: 66%

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

308 statements  

1""" 

2The :py:mod:`nameparser.config` module manages the configuration of the 

3nameparser. 

4 

5:py:class:`~nameparser.config.Constants` is for application-level 

6configuration, set once at startup. ``CONSTANTS``, the module-level instance 

7used by every ``HumanName`` created without its own config, is the only 

8channel that reaches parses happening in code you don't own (helpers, 

9pipelines, a third-party library using nameparser internally) -- the same 

10role ``logging`` and ``locale`` play elsewhere. Import it and change it 

11directly: 

12 

13:: 

14 

15 >>> from nameparser.config import CONSTANTS 

16 >>> CONSTANTS.titles.remove('hon').add('chemistry','dean') # doctest: +SKIP 

17 

18For anything scoped -- one dataset, one library, one test -- pass your own 

19:py:class:`Constants` instance as the second argument upon instantiation 

20instead: ``Constants()`` for fresh library defaults, or ``CONSTANTS.copy()`` 

21for a private snapshot of the current module config. 

22 

23:: 

24 

25 >>> from nameparser import HumanName 

26 >>> from nameparser.config import Constants 

27 >>> hn = HumanName("Dean Robert Johns", Constants()) 

28 >>> hn.C.titles.add('dean') # doctest: +SKIP 

29 >>> hn.parse_full_name() # need to run this again after config changes 

30 

31Mixing the two up is where the surprises come from, not the API itself: if 

32you do not pass your own :py:class:`Constants` instance as the second 

33argument, ``hn.C`` will be a reference to the module config, and a change 

34there reaches every other instance sharing it. See `Customizing the Parser 

35<customize.html>`_. 

36 

37.. deprecated:: 1.4.0 

38 Passing ``None`` as the second argument also builds a fresh 

39 ``Constants()``, but is deprecated in favor of the explicit spellings 

40 above; it will raise ``TypeError`` in 2.0 (issue #260). 

41""" 

42import copy 

43import inspect 

44import re 

45import sys 

46import warnings 

47from collections.abc import Callable, Iterable, Iterator, Mapping, Set 

48from typing import Any, TypeVar, overload 

49 

50if sys.version_info >= (3, 11): 

51 from typing import Self 

52else: 

53 from typing_extensions import Self 

54 

55from nameparser.util import lc 

56from nameparser.config.prefixes import PREFIXES, NON_FIRST_NAME_PREFIXES 

57from nameparser.config.bound_first_names import BOUND_FIRST_NAMES 

58from nameparser.config.capitalization import CAPITALIZATION_EXCEPTIONS 

59from nameparser.config.conjunctions import CONJUNCTIONS 

60from nameparser.config.suffixes import SUFFIX_ACRONYMS 

61from nameparser.config.suffixes import SUFFIX_NOT_ACRONYMS 

62from nameparser.config.suffixes import SUFFIX_ACRONYMS_AMBIGUOUS 

63from nameparser.config.titles import TITLES 

64from nameparser.config.titles import FIRST_NAME_TITLES 

65from nameparser.config.regexes import EMPTY_REGEX, REGEXES 

66 

67DEFAULT_ENCODING = 'UTF-8' 

68 

69 

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

71 # A bare string is an iterable of its characters, so e.g. set('dr') or 

72 # dict('ab') would silently shred it, and bytes iterates to ints, which 

73 # can never match parsed str tokens -- shared by SetManager's constructor/ 

74 # operands (#238) and TupleManager's constructor (#242). 

75 if isinstance(value, bytes): 

76 raise TypeError( 

77 f"expected {expected}, got a single bytes; " 

78 f"decode it first: [{value!r}.decode()]" 

79 ) 

80 if isinstance(value, str): 

81 raise TypeError( 

82 f"expected {expected}, got a single str; wrap it in a list: [{value!r}]" 

83 ) 

84 

85 

86class SetManager(Set): 

87 ''' 

88 Easily add and remove config variables per module or instance. Subclass of 

89 ``collections.abc.Set``. 

90 

91 Special functionality beyond that provided by set() is to normalize 

92 constants for comparison (lowercase, leading/trailing periods stripped) 

93 when they are add()ed and remove()d, and to allow passing multiple 

94 string arguments to the :py:func:`add()` and :py:func:`remove()` 

95 methods. The constructor and the set operators apply the same 

96 normalization to their elements and operands, so every entry is stored 

97 in the form the parser's lookups expect, and they reject a bare string 

98 with ``TypeError``, since e.g. ``set('dr')`` would silently build a set 

99 of single characters. 

100 

101 ''' 

102 

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

104 

105 @classmethod 

106 def _normalized_elements(cls, elements: Iterable[str]) -> set[str]: 

107 # a SetManager's elements were validated and normalized when it was 

108 # built, so copy them instead of re-validating — this is what keeps 

109 # chained unions (suffixes_prefixes_titles) and default Constants() 

110 # construction from re-checking ~1,400 entries per step 

111 if isinstance(elements, SetManager): 

112 return set(elements.elements) 

113 _reject_bare_str_or_bytes(elements, "an iterable of strings") 

114 # apply the same lc() normalization (lowercase, strip leading/ 

115 # trailing periods) that add() applies, and reject junk elements: 

116 # lc() on bytes or int crashes without naming the culprit, and 

117 # lc(None) silently transmutes to ''. Divergence from add() is 

118 # deliberate: add_with_encoding() decodes bytes for back-compat, 

119 # bulk boundaries stay strict. 

120 normalized = set() 

121 for s in elements: 

122 if isinstance(s, bytes): 

123 raise TypeError( 

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

125 ) 

126 if not isinstance(s, str): 

127 raise TypeError( 

128 f"expected str elements, got {type(s).__name__}: {s!r}" 

129 ) 

130 normalized.add(lc(s)) 

131 return normalized 

132 

133 @classmethod 

134 def _from_normalized(cls, elements: set[str]) -> 'SetManager': 

135 # Private fast constructor: bypasses __init__ so results aren't 

136 # re-validated element by element. This performs NO validation or 

137 # normalization of `elements` -- the caller is fully responsible 

138 # for guaranteeing every element is already a str that has passed 

139 # through lc(). Only call this with a set built from other 

140 # SetManagers' already-normalized .elements (operator results, 

141 # prebuilt default copies); passing anything else silently defeats 

142 # the constructor's #238 guarantees with no error raised here. 

143 obj = cls.__new__(cls) 

144 obj.elements = elements 

145 obj._on_change = None 

146 return obj 

147 

148 def __init__(self, elements: Iterable[str]) -> None: 

149 self.elements = self._normalized_elements(elements) 

150 # Optional invalidation hook, wired by an owning Constants so that 

151 # in-place add()/remove() can clear its cached suffixes_prefixes_titles 

152 # union. None when the manager is used standalone. 

153 self._on_change = None 

154 

155 def __call__(self) -> Set[str]: 

156 """ 

157 .. deprecated:: 1.3.0 

158 Removed in 2.0 (see issue #243). Returns the raw underlying set, 

159 so mutating it bypasses normalization and cache invalidation; 

160 iterate the manager or copy with ``set(manager)`` instead. 

161 """ 

162 warnings.warn( 

163 "Calling a SetManager to get the raw underlying set is " 

164 "deprecated and will be removed in 2.0; iterate the manager or " 

165 "copy it with set(manager) instead. See " 

166 "https://github.com/derek73/python-nameparser/issues/243", 

167 DeprecationWarning, 

168 stacklevel=2, 

169 ) 

170 return self.elements 

171 

172 def __repr__(self) -> str: 

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

174 # depends on string hash randomization, which varies per process. 

175 elements = "{" + ", ".join(repr(e) for e in sorted(self.elements)) + "}" if self.elements else "set()" 

176 return f"SetManager({elements})" # used for docs 

177 

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

179 return iter(self.elements) 

180 

181 def __contains__(self, value: object) -> bool: 

182 # add()/remove()/the constructor/the operators all normalize (lowercase, 

183 # strip leading/trailing periods) before comparing; without the same 

184 # normalization here, `'Dr.' in c.titles` returns False even though 

185 # every other operation on the same value succeeds (#244). The parser's 

186 # own lookups (e.g. `piece.lower() in self.C.conjunctions`) already pass 

187 # an lc()-normalized value, which is the hot path during parsing, so 

188 # try the raw value first and only pay for lc() on a miss. 

189 if value in self.elements: 

190 return True 

191 return isinstance(value, str) and lc(value) in self.elements 

192 

193 def __len__(self) -> int: 

194 return len(self.elements) 

195 

196 # The ABC mixins compare raw operand elements against stored (normalized) 

197 # ones, and their __or__/__and__ accept a bare str as Iterable, so every 

198 # operand is validated and normalized here. Results are built with plain 

199 # set ops on already-normalized elements instead of delegating to the 

200 # mixins, whose _from_iterable would re-validate the whole result 

201 # through __init__. 

202 # 

203 # the runtime ABC accepts any Iterable operand, so annotate honestly and 

204 # ignore typeshed's narrower AbstractSet declarations 

205 def __or__(self, other: Iterable[str]) -> 'SetManager': # type: ignore[override] 

206 return self._from_normalized(self.elements | self._normalized_elements(other)) 

207 

208 __ror__ = __or__ 

209 

210 def __and__(self, other: Iterable[str]) -> 'SetManager': # type: ignore[override] 

211 return self._from_normalized(self.elements & self._normalized_elements(other)) 

212 

213 __rand__ = __and__ 

214 

215 def __sub__(self, other: Iterable[str]) -> 'SetManager': # type: ignore[override] 

216 return self._from_normalized(self.elements - self._normalized_elements(other)) 

217 

218 def __rsub__(self, other: Iterable[str]) -> 'SetManager': 

219 return self._from_normalized(self._normalized_elements(other) - self.elements) 

220 

221 def __xor__(self, other: Iterable[str]) -> 'SetManager': # type: ignore[override] 

222 return self._from_normalized(self.elements ^ self._normalized_elements(other)) 

223 

224 __rxor__ = __xor__ 

225 

226 def _add_normalized(self, s: str | bytes, encoding: str | None, *, stacklevel: int) -> None: 

227 # Shared by add() and add_with_encoding() so each can call it 

228 # directly with a stacklevel that attributes the warning to *its own* 

229 # caller -- add() delegating to add_with_encoding() would otherwise 

230 # add a frame and misattribute the warning to this module. 

231 stdin_encoding = None 

232 if sys.stdin: 

233 stdin_encoding = sys.stdin.encoding 

234 encoding = encoding or stdin_encoding or DEFAULT_ENCODING 

235 if isinstance(s, bytes): 

236 warnings.warn( 

237 "Passing bytes to SetManager.add()/add_with_encoding() is " 

238 "deprecated and will raise TypeError in 2.0; decode it " 

239 "first, e.g. value.decode('utf-8'). See " 

240 "https://github.com/derek73/python-nameparser/issues/245", 

241 DeprecationWarning, 

242 stacklevel=stacklevel, 

243 ) 

244 s = s.decode(encoding) 

245 normalized = lc(s) 

246 if normalized not in self.elements: 

247 self.elements.add(normalized) 

248 if self._on_change: 

249 self._on_change() 

250 

251 def add_with_encoding(self, s: str | bytes, encoding: str | None = None) -> None: 

252 """ 

253 Add the lowercased, leading/trailing-periods-stripped version of the string to the set. Pass an 

254 explicit `encoding` parameter to specify the encoding of binary strings that 

255 are not DEFAULT_ENCODING (UTF-8). 

256 

257 .. deprecated:: 1.3.0 

258 ``bytes`` arguments will raise ``TypeError`` in 2.0 (see issue 

259 #245); decode before adding. 

260 

261 .. deprecated:: 1.4.0 

262 The method itself is removed in 2.0 (see issue #245); use 

263 :py:func:`add` instead, decoding bytes first. 

264 """ 

265 warnings.warn( 

266 "SetManager.add_with_encoding() is deprecated and will be " 

267 "removed in 2.0; use add() instead (decode bytes first). See " 

268 "https://github.com/derek73/python-nameparser/issues/245", 

269 DeprecationWarning, 

270 stacklevel=2, 

271 ) 

272 self._add_normalized(s, encoding, stacklevel=3) 

273 

274 def add(self, *strings: str) -> Self: 

275 """ 

276 Add the lowercased, leading/trailing-periods-stripped version of the string arguments to the set. 

277 Returns ``self`` for chaining. 

278 

279 .. deprecated:: 1.3.0 

280 ``bytes`` arguments will raise ``TypeError`` in 2.0 (see issue 

281 #245); decode before adding. 

282 """ 

283 for s in strings: 

284 self._add_normalized(s, None, stacklevel=3) 

285 

286 return self 

287 

288 def remove(self, *strings: str) -> Self: 

289 """ 

290 Remove the lower case and no-period version of the string arguments from the set. 

291 Returns ``self`` for chaining. 

292 

293 .. deprecated:: 1.3.0 

294 Removing a *missing* member currently does nothing but will 

295 raise ``KeyError`` in 2.0, matching ``set.remove`` (see issue 

296 #243); use :py:func:`discard` to ignore missing members. 

297 """ 

298 changed = False 

299 for s in strings: 

300 if (lower := lc(s)) in self.elements: 

301 self.elements.remove(lower) 

302 changed = True 

303 else: 

304 warnings.warn( 

305 "SetManager.remove() of a missing member currently does " 

306 "nothing, but will raise KeyError in 2.0; use discard() " 

307 "to ignore missing members. See " 

308 "https://github.com/derek73/python-nameparser/issues/243", 

309 DeprecationWarning, 

310 stacklevel=2, 

311 ) 

312 if changed and self._on_change: 

313 self._on_change() 

314 return self 

315 

316 def discard(self, *strings: str) -> Self: 

317 """ 

318 Remove the lower case and no-period version of the string arguments 

319 from the set if present; missing members are ignored, like 

320 ``set.discard``. Returns ``self`` for chaining. 

321 """ 

322 changed = False 

323 for s in strings: 

324 if (lower := lc(s)) in self.elements: 

325 self.elements.remove(lower) 

326 changed = True 

327 if changed and self._on_change: 

328 self._on_change() 

329 return self 

330 

331 def clear(self) -> Self: 

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

333 if self.elements: 

334 self.elements.clear() 

335 if self._on_change: 

336 self._on_change() 

337 return self 

338 

339 

340T = TypeVar('T') 

341 

342 

343def _is_dunder(attr: str) -> bool: 

344 # Dunder names are Python's protocol probes (copy looks up __deepcopy__, 

345 # inspect.unwrap looks up __wrapped__, typing's GenericAlias.__call__ sets 

346 # __orig_class__, ...), never config keys. The TupleManager attribute hooks 

347 # all route dunders to normal object-attribute behavior so those probes 

348 # work instead of being mistaken for dict entries. 

349 return attr.startswith("__") and attr.endswith("__") 

350 

351 

352# The default config sets are module constants that never change, so 

353# validate and normalize each one exactly once at import. Constants() 

354# copies these via _normalized_elements' SetManager fast path instead of 

355# re-checking ~1,400 elements per construction — a cost that otherwise 

356# repeats on the per-instance-config path, HumanName(constants=Constants()). 

357# 

358# This snapshot is taken once, at import time: mutating a raw constant 

359# (e.g. `TITLES.add('x')`) after import is *not* picked up by Constants() 

360# built afterward, since the identity check in Constants.__init__ reuses 

361# this frozen SetManager rather than re-wrapping the (now-changed) raw 

362# set. That's a behavior change from re-wrapping every time, but the 

363# documented customization path mutates the SetManager wrapper on a 

364# Constants instance (``CONSTANTS.titles.add(...)``), not the raw 

365# constant, so this only affects an unsupported/undocumented pattern. 

366_DEFAULT_PREFIXES = SetManager(PREFIXES) 

367_DEFAULT_SUFFIX_ACRONYMS = SetManager(SUFFIX_ACRONYMS) 

368_DEFAULT_SUFFIX_NOT_ACRONYMS = SetManager(SUFFIX_NOT_ACRONYMS) 

369_DEFAULT_SUFFIX_ACRONYMS_AMBIGUOUS = SetManager(SUFFIX_ACRONYMS_AMBIGUOUS) 

370_DEFAULT_TITLES = SetManager(TITLES) 

371_DEFAULT_FIRST_NAME_TITLES = SetManager(FIRST_NAME_TITLES) 

372_DEFAULT_CONJUNCTIONS = SetManager(CONJUNCTIONS) 

373_DEFAULT_BOUND_FIRST_NAMES = SetManager(BOUND_FIRST_NAMES) 

374_DEFAULT_NON_FIRST_NAME_PREFIXES = SetManager(NON_FIRST_NAME_PREFIXES) 

375 

376 

377class TupleManager(dict[str, T]): 

378 ''' 

379 A dictionary with dot.notation access. Subclass of ``dict``. Wraps the 

380 mapping config constants (``capitalization_exceptions``, ``regexes``, and 

381 the nickname/maiden delimiter buckets). The name is historical: before 

382 1.3.0 these constants were tuples of pairs. 

383 ''' 

384 

385 def __init__( 

386 self, 

387 arg: Mapping[str, T] | Iterable[tuple[str, T]] = (), 

388 **kwargs: T, 

389 ) -> None: 

390 # dict.__init__ accepts a bare str/bytes as an iterable-of-pairs 

391 # argument (each character iterates further, and dict() only 

392 # complains once it hits a "pair" of the wrong length) and accepts an 

393 # iterable of 2-character strings as if each one were a (key, value) 

394 # pair, silently shredding it -- mirrors SetManager's guard against 

395 # the same class of mistake (#238), applied to the mapping 

396 # constructor's own failure modes (#242). 

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

398 if not isinstance(arg, Mapping): 

399 checked = [] 

400 for item in arg: 

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

402 raise TypeError( 

403 "expected (key, value) pairs, got a " 

404 f"{'bytes' if isinstance(item, bytes) else 'str'} " 

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

406 "splits into a key and a value" 

407 ) 

408 checked.append(item) 

409 arg = checked 

410 super().__init__(arg, **kwargs) 

411 

412 def _warn_unknown_key(self, attr: str) -> None: 

413 # Deprecated 1.4.0, raises AttributeError in 2.0 (#256): a misspelled 

414 # key otherwise degrades silently with no traceback pointing at the 

415 # typo. 

416 warnings.warn( 

417 f"{attr!r} is not a known key ({', '.join(sorted(self))}); " 

418 "unknown-key attribute access is deprecated and will raise " 

419 "AttributeError in 2.0. Use .get() for intentional soft access. " 

420 "See https://github.com/derek73/python-nameparser/issues/256", 

421 DeprecationWarning, 

422 stacklevel=3, 

423 ) 

424 

425 def __getattr__(self, attr: str) -> T | None: 

426 # Otherwise the dict default (None) is mistaken for a real protocol hook. 

427 if _is_dunder(attr): 

428 raise AttributeError(attr) 

429 # Single-underscore introspection probes (IPython/Jupyter's 

430 # _repr_html_, _ipython_canary_method_should_not_exist_, etc.) are 

431 # never config keys either -- no real config key starts with '_'. 

432 if attr not in self and not attr.startswith('_'): 

433 self._warn_unknown_key(attr) 

434 return self.get(attr) 

435 

436 def __setattr__(self, attr: str, value: T) -> None: 

437 # Fall back to normal object attribute storage for dunders; everything 

438 # else keeps the dict-backed dot-notation behavior this class exists 

439 # for. Concretely: constructing a subscripted generic, e.g. 

440 # TupleManager[re.Pattern[str] | str](...), makes typing's 

441 # GenericAlias.__call__ set `__orig_class__` on the new instance right 

442 # after __init__ returns. Without this guard that assignment falls 

443 # through to dict.__setitem__ and silently inserts a bogus 

444 # '__orig_class__' entry into the dict itself, corrupting 

445 # .values()/iteration. 

446 if _is_dunder(attr): 

447 object.__setattr__(self, attr, value) 

448 else: 

449 self[attr] = value 

450 

451 def __delattr__(self, attr: str) -> None: 

452 if _is_dunder(attr): 

453 object.__delattr__(self, attr) 

454 else: 

455 del self[attr] 

456 

457 def __getstate__(self) -> Mapping[str, T]: 

458 return dict(self) 

459 

460 def __setstate__(self, state: Mapping[str, T]) -> None: 

461 self.update(state) 

462 

463 def __reduce__(self) -> tuple[type, tuple[()], Mapping[str, T]]: 

464 # Use type(self), not TupleManager, so subclasses such as 

465 # RegexTupleManager survive a pickle round-trip instead of being 

466 # downgraded to a plain TupleManager (which loses the EMPTY_REGEX 

467 # default for unknown keys). 

468 return (type(self), (), self.__getstate__()) 

469 

470 

471class RegexTupleManager(TupleManager[re.Pattern[str]]): 

472 def __getattr__(self, attr: str) -> re.Pattern[str]: 

473 # Otherwise EMPTY_REGEX is returned for a dunder probe; copy.deepcopy 

474 # then tries to call the returned re.Pattern and raises TypeError. 

475 if _is_dunder(attr): 

476 raise AttributeError(attr) 

477 if attr not in self and not attr.startswith('_'): 

478 self._warn_unknown_key(attr) 

479 return self.get(attr, EMPTY_REGEX) 

480 

481 

482class _SetManagerAttribute: 

483 """Descriptor enforcing ``isinstance(value, SetManager)`` on assignment. 

484 

485 Backs the five plain SetManager attributes (``first_name_titles``, 

486 ``conjunctions``, ``bound_first_names``, ``non_first_name_prefixes``, 

487 ``suffix_acronyms_ambiguous``). Without this guard, e.g. 

488 ``c.conjunctions = 'and'`` is accepted silently, and every later 

489 ``piece.lower() in self.C.conjunctions`` becomes a substring test against 

490 the plain str instead of a set membership test (#241). 

491 

492 ``_CachedUnionMember`` subclasses this to add ``_pst`` cache invalidation 

493 for the four attributes whose union ``Constants`` caches. 

494 """ 

495 

496 _attr: str 

497 

498 def __set_name__(self, owner: type, name: str) -> None: 

499 self._attr = '_' + name 

500 

501 @overload 

502 def __get__(self, obj: None, objtype: type | None = None) -> '_SetManagerAttribute': ... 

503 @overload 

504 def __get__(self, obj: 'Constants', objtype: type | None = None) -> SetManager: ... 

505 

506 def __get__(self, obj: 'Constants | None', objtype: type | None = None) -> 'SetManager | _SetManagerAttribute': 

507 if obj is None: 

508 return self 

509 return getattr(obj, self._attr) 

510 

511 def _validate(self, value: SetManager) -> None: 

512 if not isinstance(value, SetManager): 

513 raise TypeError( 

514 f"Expected a SetManager instance, got {type(value).__name__!r}. " 

515 "Wrap your iterable: SetManager(['mr', 'ms'])" 

516 ) 

517 

518 def __set__(self, obj: 'Constants', value: SetManager) -> None: 

519 self._validate(value) 

520 setattr(obj, self._attr, value) 

521 

522 

523class _CachedUnionMember(_SetManagerAttribute): 

524 """Descriptor for the four ``SetManager`` attributes whose union ``Constants`` 

525 caches in ``_pst`` (``prefixes``, ``suffix_acronyms``, ``suffix_not_acronyms``, 

526 ``titles``). 

527 

528 Assigning a new manager — or mutating one in place via ``add()`` / ``remove()`` 

529 — invalidates that cache. Keeping the behavior on a descriptor scopes it to 

530 exactly these attributes, beside their declarations, rather than spreading it 

531 across a catch-all ``__setattr__`` and a separate attribute-name list. 

532 """ 

533 

534 def __set__(self, obj: 'Constants', value: SetManager) -> None: 

535 self._validate(value) 

536 previous = getattr(obj, self._attr, None) 

537 if isinstance(previous, SetManager): 

538 previous._on_change = None # detach the replaced manager so it no longer invalidates 

539 value._on_change = obj._invalidate_pst 

540 obj._invalidate_pst() 

541 setattr(obj, self._attr, value) 

542 

543 

544class _EmptyAttributeDefaultAttribute: 

545 """Descriptor backing ``Constants.empty_attribute_default``. 

546 

547 .. deprecated:: 1.4.0 

548 Assignment is deprecated (see issue #255): the only legal value 

549 left once ``None`` support goes in 2.0 is the default ``''``, so a 

550 dial with one position isn't configuration. 

551 """ 

552 

553 _attr = '_empty_attribute_default' 

554 

555 def __get__(self, obj: 'Constants | None', objtype: type | None = None) -> str: 

556 # Annotated `str`, not `str | None`, to match the pre-descriptor 

557 # plain-attribute inference: None is documented/supported (see the 

558 # class docstring), but typing it honestly cascades `| None` 

559 # through every public str-typed name accessor (title, first, ...). 

560 # Returning '' rather than `self` on class access (unlike 

561 # _SetManagerAttribute, which returns `self`) is also load-bearing 

562 # for Constants.__repr__'s `getattr(type(self), name)` default 

563 # comparison in _repr_scalar_attrs -- returning `self` there would 

564 # make every Constants() show this attribute as "customized". 

565 if obj is None: 

566 return '' 

567 return getattr(obj, self._attr, '') 

568 

569 def __set__(self, obj: 'Constants', value: str | None) -> None: 

570 if value is not None and not isinstance(value, str): 

571 raise TypeError( 

572 f"empty_attribute_default must be a str or None, got " 

573 f"{type(value).__name__!r}" 

574 ) 

575 warnings.warn( 

576 "Assigning Constants.empty_attribute_default is deprecated and " 

577 "will raise TypeError in 2.0; empty attributes will always " 

578 "return ''. See " 

579 "https://github.com/derek73/python-nameparser/issues/255", 

580 DeprecationWarning, 

581 stacklevel=2, 

582 ) 

583 setattr(obj, self._attr, value) 

584 

585 

586class Constants: 

587 """ 

588 An instance of this class hold all of the configuration constants for the parser. 

589 

590 :param set prefixes:  

591 :py:attr:`prefixes` wrapped with :py:class:`SetManager`. 

592 :param set titles:  

593 :py:attr:`titles` wrapped with :py:class:`SetManager`. 

594 :param set first_name_titles:  

595 :py:attr:`~titles.FIRST_NAME_TITLES` wrapped with :py:class:`SetManager`. 

596 :param set suffix_acronyms:  

597 :py:attr:`~suffixes.SUFFIX_ACRONYMS` wrapped with :py:class:`SetManager`. 

598 :param set suffix_not_acronyms: 

599 :py:attr:`~suffixes.SUFFIX_NOT_ACRONYMS` wrapped with :py:class:`SetManager`. 

600 :param set suffix_acronyms_ambiguous: 

601 :py:attr:`~suffixes.SUFFIX_ACRONYMS_AMBIGUOUS` wrapped with :py:class:`SetManager`. 

602 :param set conjunctions: 

603 :py:attr:`conjunctions` wrapped with :py:class:`SetManager`. 

604 :param set bound_first_names: 

605 :py:attr:`~bound_first_names.BOUND_FIRST_NAMES` wrapped with :py:class:`SetManager`. 

606 :param set non_first_name_prefixes: 

607 :py:attr:`~prefixes.NON_FIRST_NAME_PREFIXES` wrapped with :py:class:`SetManager`. 

608 The subset of prefixes that are never a first name, so a *leading* one 

609 marks the whole name as a surname. Must stay disjoint from 

610 ``bound_first_names``. 

611 :type capitalization_exceptions: dict or iterable of (key, value) tuples 

612 :param capitalization_exceptions: 

613 :py:attr:`~capitalization.CAPITALIZATION_EXCEPTIONS` wrapped with :py:class:`TupleManager`. 

614 :type regexes: dict or iterable of (name, compiled pattern) tuples 

615 :param regexes: 

616 :py:attr:`~regexes.REGEXES` wrapped with :py:class:`RegexTupleManager`. 

617 

618 :py:attr:`nickname_delimiters` and :py:attr:`maiden_delimiters` are not 

619 constructor arguments -- they're always set in ``__init__`` (see the 

620 comment there for the string-sentinel-vs-compiled-pattern mechanism) -- 

621 but are documented here since they're the two `Constants` attributes a 

622 caller is most likely to want to look up: per-bucket 

623 :py:class:`TupleManager` collections that :py:meth:`~nameparser.parser.HumanName.parse_nicknames` 

624 consults to route delimited content into ``nickname``/``maiden``. See 

625 the "Adding Custom Nickname Delimiters" and "Routing to Maiden Name" 

626 sections of the customization docs. 

627 """ 

628 

629 prefixes = _CachedUnionMember() 

630 suffix_acronyms = _CachedUnionMember() 

631 suffix_not_acronyms = _CachedUnionMember() 

632 titles = _CachedUnionMember() 

633 first_name_titles = _SetManagerAttribute() 

634 conjunctions = _SetManagerAttribute() 

635 bound_first_names = _SetManagerAttribute() 

636 non_first_name_prefixes = _SetManagerAttribute() 

637 suffix_acronyms_ambiguous = _SetManagerAttribute() 

638 capitalization_exceptions: TupleManager[str] 

639 regexes: RegexTupleManager 

640 nickname_delimiters: TupleManager[re.Pattern[str] | str] 

641 maiden_delimiters: TupleManager[re.Pattern[str] | str] 

642 _pst: Set[str] | None 

643 

644 string_format: str | None = "{title} {first} {middle} {last} {suffix} ({nickname})" 

645 """ 

646 The default string format use for all new `HumanName` instances. 

647 """ 

648 

649 initials_format = "{first} {middle} {last}" 

650 """ 

651 The default initials format used for all new `HumanName` instances. 

652 """ 

653 

654 initials_delimiter = "." 

655 """ 

656 The default initials delimiter used for all new `HumanName` instances. 

657 Will be used to add a delimiter between each initial. 

658 """ 

659 

660 initials_separator = " " 

661 """ 

662 The default separator placed between consecutive initials within a name 

663 group (first, middle, or last). Distinct from ``initials_delimiter``, 

664 which is the trailing character after each individual initial. 

665 

666 With defaults ``initials_delimiter="."`` and ``initials_separator=" "``, 

667 ``initials()`` produces ``"J. A. D."``. Setting ``initials_separator=""`` 

668 with ``initials_delimiter="."`` and ``initials_format="{first}{middle}{last}"`` 

669 produces ``"J.A.D."``. With the default ``initials_format``, group-level 

670 spacing from the template is still applied. 

671 """ 

672 

673 suffix_delimiter: str | None = None 

674 """ 

675 If set, an additional delimiter used to split suffix groups after 

676 comma-splitting. For example, setting ``suffix_delimiter=" - "`` allows 

677 ``"RN - CRNA"`` to be parsed as two separate suffixes. Default is 

678 ``None`` (no additional splitting beyond the standard comma split). 

679 

680 Note: setting this to ``","`` or ``", "`` has no additional effect — 

681 the full name is already split on comma characters first (including the 

682 Arabic ``،`` and fullwidth ``,`` variants), and each resulting part is 

683 stripped of surrounding whitespace before this step runs. 

684 

685 The delimiter is only applied to parts once they've been identified as 

686 a suffix group, so it never leaks into a first- or middle-name part. For 

687 example, in inverted format (``"Last, First, suffix"``) a hyphenated 

688 given name like ``"Doe, Mary - Kate, RN"`` with ``suffix_delimiter=" - "`` 

689 does not get mistaken for a suffix split. 

690 """ 

691 

692 empty_attribute_default = _EmptyAttributeDefaultAttribute() 

693 """ 

694 Default return value for empty attributes. 

695 

696 .. deprecated:: 1.4.0 

697 Assignment emits ``DeprecationWarning``; the option is removed in 

698 2.0 (see issue #255) and empty attributes will always return ``''``. 

699 

700 .. doctest:: 

701 

702 >>> import warnings 

703 >>> from nameparser.config import CONSTANTS 

704 >>> with warnings.catch_warnings(): 

705 ... warnings.simplefilter('ignore', DeprecationWarning) 

706 ... CONSTANTS.empty_attribute_default = None 

707 >>> name = HumanName("John Doe") 

708 >>> print(name.title) 

709 None 

710 >>> name.first 

711 'John' 

712 >>> with warnings.catch_warnings(): 

713 ... warnings.simplefilter('ignore', DeprecationWarning) 

714 ... CONSTANTS.empty_attribute_default = '' 

715 

716 """ 

717 

718 capitalize_name = False 

719 """ 

720 If set, applies :py:meth:`~nameparser.parser.HumanName.capitalize` to 

721 :py:class:`~nameparser.parser.HumanName` instance. 

722 

723 .. doctest:: 

724 

725 >>> from nameparser.config import CONSTANTS 

726 >>> CONSTANTS.capitalize_name = True 

727 >>> name = HumanName("bob v. de la macdole-eisenhower phd") 

728 >>> str(name) 

729 'Bob V. de la MacDole-Eisenhower Ph.D.' 

730 >>> CONSTANTS.capitalize_name = False 

731 

732 """ 

733 

734 force_mixed_case_capitalization = False 

735 """ 

736 If set, forces the capitalization of mixed case strings when 

737 :py:meth:`~nameparser.parser.HumanName.capitalize` is called. 

738 

739 .. doctest:: 

740 

741 >>> from nameparser.config import CONSTANTS 

742 >>> CONSTANTS.force_mixed_case_capitalization = True 

743 >>> name = HumanName('Shirley Maclaine') 

744 >>> name.capitalize() 

745 >>> str(name) 

746 'Shirley MacLaine' 

747 >>> CONSTANTS.force_mixed_case_capitalization = False 

748 

749 """ 

750 

751 patronymic_name_order = False 

752 """ 

753 If set, detects names in Russian formal order (``Surname GivenName Patronymic``) 

754 by recognizing a trailing East-Slavic patronymic suffix on the last token, and 

755 rotates the three name parts so that ``first``/``middle``/``last`` map to 

756 given name / patronymic / surname respectively. Detection requires exactly one 

757 token in each of first, middle, and last; names with multi-part given names or 

758 multiple middle names are left unchanged. 

759 

760 Also detects reversed-order Azerbaijani/Central-Asian Turkic patronymics 

761 (``Surname GivenName PatronymicRoot Marker``, e.g. ``oglu``/``qizi``), a 

762 structurally different, standalone-marker-word patronymic family. Detection 

763 requires exactly one token in each of first and last, exactly two tokens in 

764 middle, and the last token a recognised Turkic marker. 

765 

766 Opt-in because a Western person whose surname happens to end in a patronymic 

767 suffix (e.g. ``"David Michael Abramovich"``) will be reordered incorrectly 

768 when the flag is on. Enable only when your data is predominantly Russian 

769 formal-order names. 

770 

771 For per-instance control without a shared ``Constants``, pass a dedicated 

772 instance: ``HumanName("...", constants=Constants(patronymic_name_order=True))``. 

773 

774 .. doctest:: 

775 

776 >>> from nameparser import HumanName 

777 >>> from nameparser.config import Constants 

778 >>> C = Constants(patronymic_name_order=True) 

779 >>> hn = HumanName("Ivanov Ivan Ivanovich", constants=C) 

780 >>> hn.first, hn.middle, hn.last 

781 ('Ivan', 'Ivanovich', 'Ivanov') 

782 >>> hn2 = HumanName("Aliyev Vusal Said oglu", constants=C) 

783 >>> hn2.first, hn2.middle, hn2.last 

784 ('Vusal', 'Said oglu', 'Aliyev') 

785 

786 """ 

787 

788 middle_name_as_last = False 

789 """ 

790 If set, folds middle names into the last name: ``middle_list`` is prepended 

791 to ``last_list`` and ``middle_list`` is cleared, so ``.last`` becomes what 

792 ``.surnames`` already was and ``.middle`` becomes empty. Useful for naming 

793 systems with no middle-name concept, where everything after the given name 

794 is lineage/family (e.g. Arabic patronymic chaining: given + father + 

795 grandfather + family). 

796 

797 The fold is uniform across both no-comma and comma ("Last, First Middle") 

798 input, so the two written forms of a name converge on the same result. 

799 

800 For per-instance control without a shared ``Constants``, pass a dedicated 

801 instance: ``HumanName("...", constants=Constants(middle_name_as_last=True))``. 

802 

803 .. doctest:: 

804 

805 >>> from nameparser import HumanName 

806 >>> from nameparser.config import Constants 

807 >>> C = Constants(middle_name_as_last=True) 

808 >>> hn = HumanName("Mohamad Ahmad Ali Hassan", constants=C) 

809 >>> hn.first, hn.middle, hn.last 

810 ('Mohamad', '', 'Ahmad Ali Hassan') 

811 

812 """ 

813 

814 def __init__(self, 

815 prefixes: Iterable[str] = PREFIXES, 

816 suffix_acronyms: Iterable[str] = SUFFIX_ACRONYMS, 

817 suffix_not_acronyms: Iterable[str] = SUFFIX_NOT_ACRONYMS, 

818 suffix_acronyms_ambiguous: Iterable[str] = SUFFIX_ACRONYMS_AMBIGUOUS, 

819 titles: Iterable[str] = TITLES, 

820 first_name_titles: Iterable[str] = FIRST_NAME_TITLES, 

821 conjunctions: Iterable[str] = CONJUNCTIONS, 

822 bound_first_names: Iterable[str] = BOUND_FIRST_NAMES, 

823 non_first_name_prefixes: Iterable[str] = NON_FIRST_NAME_PREFIXES, 

824 capitalization_exceptions: Mapping[str, str] | Iterable[tuple[str, str]] = CAPITALIZATION_EXCEPTIONS, 

825 regexes: Mapping[str, re.Pattern[str]] | Iterable[tuple[str, re.Pattern[str]]] = REGEXES, 

826 patronymic_name_order: bool = False, 

827 middle_name_as_last: bool = False, 

828 ) -> None: 

829 # These four descriptor assignments call _CachedUnionMember.__set__, which 

830 # calls _invalidate_pst() and establishes self._pst. They must come before 

831 # any read of suffixes_prefixes_titles. 

832 # untouched defaults (identity check) copy the prebuilt module-level 

833 # managers instead of re-validating the raw constants element by 

834 # element; user-supplied iterables still get the full check 

835 self.prefixes = SetManager(_DEFAULT_PREFIXES if prefixes is PREFIXES else prefixes) 

836 self.suffix_acronyms = SetManager(_DEFAULT_SUFFIX_ACRONYMS if suffix_acronyms is SUFFIX_ACRONYMS else suffix_acronyms) 

837 self.suffix_not_acronyms = SetManager(_DEFAULT_SUFFIX_NOT_ACRONYMS if suffix_not_acronyms is SUFFIX_NOT_ACRONYMS else suffix_not_acronyms) 

838 self.titles = SetManager(_DEFAULT_TITLES if titles is TITLES else titles) 

839 self.first_name_titles = SetManager(_DEFAULT_FIRST_NAME_TITLES if first_name_titles is FIRST_NAME_TITLES else first_name_titles) 

840 self.conjunctions = SetManager(_DEFAULT_CONJUNCTIONS if conjunctions is CONJUNCTIONS else conjunctions) 

841 self.bound_first_names = SetManager(_DEFAULT_BOUND_FIRST_NAMES if bound_first_names is BOUND_FIRST_NAMES else bound_first_names) 

842 self.non_first_name_prefixes = SetManager(_DEFAULT_NON_FIRST_NAME_PREFIXES if non_first_name_prefixes is NON_FIRST_NAME_PREFIXES else non_first_name_prefixes) 

843 self.suffix_acronyms_ambiguous = SetManager(_DEFAULT_SUFFIX_ACRONYMS_AMBIGUOUS if suffix_acronyms_ambiguous is SUFFIX_ACRONYMS_AMBIGUOUS else suffix_acronyms_ambiguous) 

844 self.capitalization_exceptions = TupleManager(capitalization_exceptions) 

845 self.regexes = RegexTupleManager(regexes) 

846 # Per-bucket delimiter collections that parse_nicknames() consults to 

847 # route delimited content into nickname_list / maiden_list. Each value 

848 # is either a compiled re.Pattern (a custom delimiter a caller adds -- 

849 # the old extra_nickname_delimiters use case, see issue #112) or the 

850 # string name of a self.regexes entry to resolve live at parse time. 

851 # The latter is how the three built-ins (quoted_word, double_quotes, 

852 # parenthesis) stay linked to self.regexes, so overriding e.g. 

853 # self.regexes.parenthesis keeps affecting nickname/maiden parsing 

854 # exactly as before. Move a key between the two dicts 

855 # (`maiden_delimiters['parenthesis'] = 

856 # nickname_delimiters.pop('parenthesis')`) to change which bucket it 

857 # routes to without losing that live link. maiden_delimiters starts 

858 # empty -- maiden is off until a caller routes a delimiter to it. 

859 # See issue #22. 

860 # Only seed a built-in name if it's actually present in self.regexes -- 

861 # a caller who overrides regexes with a minimal custom set (dropping 

862 # e.g. "parenthesis" entirely) shouldn't end up with a dangling 

863 # string sentinel that parse_nicknames() would treat as a mistake. 

864 # See parse_nicknames()'s fail-loud check on an unresolvable sentinel. 

865 self.nickname_delimiters = TupleManager[re.Pattern[str] | str]({ 

866 name: name for name in ('quoted_word', 'double_quotes', 'parenthesis') 

867 if name in self.regexes 

868 }) 

869 self.maiden_delimiters = TupleManager[re.Pattern[str] | str]() 

870 self.patronymic_name_order = patronymic_name_order 

871 self.middle_name_as_last = middle_name_as_last 

872 

873 def _invalidate_pst(self) -> None: 

874 self._pst = None 

875 

876 @property 

877 def suffixes_prefixes_titles(self) -> Set[str]: 

878 if self._pst is None: 

879 self._pst = self.prefixes | self.suffix_acronyms | self.suffix_not_acronyms | self.titles 

880 return self._pst 

881 

882 _repr_collection_attrs = ( 

883 'prefixes', 'suffix_acronyms', 'suffix_not_acronyms', 'titles', 

884 'first_name_titles', 'conjunctions', 'bound_first_names', 

885 'non_first_name_prefixes', 'suffix_acronyms_ambiguous', 

886 ) 

887 _repr_scalar_attrs = ( 

888 'string_format', 'initials_format', 'initials_delimiter', 

889 'initials_separator', 'suffix_delimiter', 'empty_attribute_default', 

890 'capitalize_name', 'force_mixed_case_capitalization', 

891 'patronymic_name_order', 'middle_name_as_last', 

892 ) 

893 

894 def __repr__(self) -> str: 

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

896 # are summarized as counts rather than dumped in full. Scalars are 

897 # only shown when they differ from the class default, so a plain 

898 # Constants() reads as just the collection sizes. 

899 lines = [f" {name}: {len(getattr(self, name))}" for name in self._repr_collection_attrs] 

900 lines += [ 

901 f" {name}: {value!r}" for name in self._repr_scalar_attrs 

902 if (value := getattr(self, name)) != getattr(type(self), name) 

903 ] 

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

905 

906 def copy(self) -> 'Constants': 

907 """ 

908 Return a detached deep copy of this ``Constants`` instance, preserving 

909 its current customizations -- unlike :py:class:`Constants`'s own 

910 constructor, which always starts from library defaults. Useful for 

911 snapshotting the shared module-level ``CONSTANTS`` (including 

912 whatever it's been customized with) into a private instance, e.g. 

913 ``CONSTANTS.copy()``. Relies on the same ``__getstate__``/``__setstate__`` 

914 pair pickling uses, so it's as cheap and correct as pickle round-tripping. 

915 """ 

916 return copy.deepcopy(self) 

917 

918 def __setstate__(self, state: Mapping[str, Any]) -> None: 

919 # Restore each saved attribute directly. The previous implementation 

920 # passed the whole state dict to __init__ as the ``prefixes`` argument, 

921 # which silently reverted every collection to its module default on 

922 # unpickling. 

923 self._pst = None 

924 legacy_format = False 

925 for name, value in state.items(): 

926 # inspect.getattr_static, not getattr, so descriptors are 

927 # inspected directly rather than triggering their __get__. 

928 descriptor = inspect.getattr_static(type(self), name, None) 

929 # Migration shim: pickles written before this fix (1.3.0 and earlier, 

930 # including 1.2.1) used a dir() sweep for __getstate__, so their state 

931 # carries the read-only ``suffixes_prefixes_titles`` property. Skip any 

932 # such computed property rather than raising AttributeError on its 

933 # missing setter; the real config is restored from the other keys. We 

934 # don't promise to read pre-fix blobs forever — this only smooths 

935 # migration for anyone persisting them, and can be dropped a release 

936 # or two after 1.3.0 once they've re-pickled. 

937 if isinstance(descriptor, property): 

938 legacy_format = True 

939 continue 

940 if isinstance(descriptor, _EmptyAttributeDefaultAttribute): 

941 # Bypass the descriptor's setter: restoring saved state isn't 

942 # a user assignment, so it shouldn't emit #255's deprecation 

943 # warning on every unpickle/copy() of a customized instance. 

944 setattr(self, descriptor._attr, value) 

945 continue 

946 setattr(self, name, value) 

947 if legacy_format: 

948 # Once per __setstate__ call, not once per skipped key (see 

949 # issue #279): the 2.0 removal turns this into a ValueError 

950 # naming the same fix. 

951 warnings.warn( 

952 "Loading a legacy-format Constants pickle (written by " 

953 "nameparser <= 1.2.x, before the 1.3.0 pickle fix) is " 

954 "deprecated and will raise ValueError in 2.0; re-pickle " 

955 "under 1.3/1.4 to migrate. See " 

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

957 DeprecationWarning, 

958 stacklevel=2, 

959 ) 

960 # Verify each descriptor-backed attr was restored. Without this, a missing 

961 # key surfaces later as AttributeError: 'Constants' object has no attribute 

962 # '_prefixes' — the private mangled name, not the public one, making it 

963 # very hard to diagnose. 

964 for attr in (n for n, v in vars(type(self)).items() if isinstance(v, _SetManagerAttribute)): 

965 if not hasattr(self, '_' + attr): 

966 raise ValueError( 

967 f"Pickle state is missing required field {attr!r}. " 

968 "The state blob may be truncated or from an incompatible version." 

969 ) 

970 

971 def __getstate__(self) -> Mapping[str, Any]: 

972 # Pickle the instance's own configuration: the collections built in 

973 # __init__ plus any instance-level scalar overrides. 

974 # _CachedUnionMember descriptors store their values with a leading 

975 # underscore (e.g. `_prefixes` for `prefixes`) so that the descriptor's 

976 # __set__ owns assignment. We map those back to the public names so 

977 # __setstate__ can restore them through the descriptor, re-wiring the 

978 # invalidation callbacks. All other underscore-prefixed names (_pst, etc.) 

979 # are private/cache and are intentionally excluded. 

980 state: dict[str, Any] = {} 

981 for name, val in self.__dict__.items(): 

982 if name.startswith('_'): 

983 public = name[1:] 

984 descriptor = inspect.getattr_static(type(self), public, None) 

985 if isinstance(descriptor, (_SetManagerAttribute, _EmptyAttributeDefaultAttribute)): 

986 state[public] = val 

987 else: 

988 state[name] = val 

989 return state 

990 

991 

992#: A module-level instance of the :py:class:`Constants()` class. 

993#: Provides a common instance for the module to share 

994#: to easily adjust configuration for the entire module. 

995#: See `Customizing the Parser with Your Own Configuration <customize.html>`_. 

996CONSTANTS = Constants()