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()