"""
The :py:mod:`nameparser.config` module manages the configuration of the
nameparser.
:py:class:`~nameparser.config.Constants` is for application-level
configuration, set once at startup. ``CONSTANTS``, the module-level instance
used by every ``HumanName`` created without its own config, is the only
channel that reaches parses happening in code you don't own (helpers,
pipelines, a third-party library using nameparser internally) -- the same
role ``logging`` and ``locale`` play elsewhere. Import it and change it
directly:
::
>>> from nameparser.config import CONSTANTS
>>> CONSTANTS.titles.remove('hon').add('chemistry','dean') # doctest: +SKIP
For anything scoped -- one dataset, one library, one test -- pass your own
:py:class:`Constants` instance as the second argument upon instantiation
instead: ``Constants()`` for fresh library defaults, or ``CONSTANTS.copy()``
for a private snapshot of the current module config.
::
>>> from nameparser import HumanName
>>> from nameparser.config import Constants
>>> hn = HumanName("Dean Robert Johns", Constants())
>>> hn.C.titles.add('dean') # doctest: +SKIP
>>> hn.parse_full_name() # need to run this again after config changes
Mixing the two up is where the surprises come from, not the API itself: if
you do not pass your own :py:class:`Constants` instance as the second
argument, ``hn.C`` will be a reference to the module config, and a change
there reaches every other instance sharing it. See `Customizing the Parser
<customize.html>`_.
.. deprecated:: 1.4.0
Passing ``None`` as the second argument also builds a fresh
``Constants()``, but is deprecated in favor of the explicit spellings
above; it will raise ``TypeError`` in 2.0 (issue #260).
"""
import copy
import inspect
import re
import sys
import warnings
from collections.abc import Callable, Iterable, Iterator, Mapping, Set
from typing import Any, TypeVar, overload
if sys.version_info >= (3, 11):
from typing import Self
else:
from typing_extensions import Self
from nameparser.util import lc
from nameparser.config.prefixes import PREFIXES, NON_FIRST_NAME_PREFIXES
from nameparser.config.bound_first_names import BOUND_FIRST_NAMES
from nameparser.config.capitalization import CAPITALIZATION_EXCEPTIONS
from nameparser.config.conjunctions import CONJUNCTIONS
from nameparser.config.suffixes import SUFFIX_ACRONYMS
from nameparser.config.suffixes import SUFFIX_NOT_ACRONYMS
from nameparser.config.suffixes import SUFFIX_ACRONYMS_AMBIGUOUS
from nameparser.config.titles import TITLES
from nameparser.config.titles import FIRST_NAME_TITLES
from nameparser.config.regexes import EMPTY_REGEX, REGEXES
DEFAULT_ENCODING = 'UTF-8'
def _reject_bare_str_or_bytes(value: object, expected: str) -> None:
# A bare string is an iterable of its characters, so e.g. set('dr') or
# dict('ab') would silently shred it, and bytes iterates to ints, which
# can never match parsed str tokens -- shared by SetManager's constructor/
# operands (#238) and TupleManager's constructor (#242).
if isinstance(value, bytes):
raise TypeError(
f"expected {expected}, got a single bytes; "
f"decode it first: [{value!r}.decode()]"
)
if isinstance(value, str):
raise TypeError(
f"expected {expected}, got a single str; wrap it in a list: [{value!r}]"
)
[docs]
class SetManager(Set):
'''
Easily add and remove config variables per module or instance. Subclass of
``collections.abc.Set``.
Special functionality beyond that provided by set() is to normalize
constants for comparison (lowercase, leading/trailing periods stripped)
when they are add()ed and remove()d, and to allow passing multiple
string arguments to the :py:func:`add()` and :py:func:`remove()`
methods. The constructor and the set operators apply the same
normalization to their elements and operands, so every entry is stored
in the form the parser's lookups expect, and they reject a bare string
with ``TypeError``, since e.g. ``set('dr')`` would silently build a set
of single characters.
'''
_on_change: Callable[[], None] | None
@classmethod
def _normalized_elements(cls, elements: Iterable[str]) -> set[str]:
# a SetManager's elements were validated and normalized when it was
# built, so copy them instead of re-validating — this is what keeps
# chained unions (suffixes_prefixes_titles) and default Constants()
# construction from re-checking ~1,400 entries per step
if isinstance(elements, SetManager):
return set(elements.elements)
_reject_bare_str_or_bytes(elements, "an iterable of strings")
# apply the same lc() normalization (lowercase, strip leading/
# trailing periods) that add() applies, and reject junk elements:
# lc() on bytes or int crashes without naming the culprit, and
# lc(None) silently transmutes to ''. Divergence from add() is
# deliberate: add_with_encoding() decodes bytes for back-compat,
# bulk boundaries stay strict.
normalized = set()
for s in elements:
if isinstance(s, bytes):
raise TypeError(
f"expected str elements, got bytes; decode it first: {s!r}.decode()"
)
if not isinstance(s, str):
raise TypeError(
f"expected str elements, got {type(s).__name__}: {s!r}"
)
normalized.add(lc(s))
return normalized
@classmethod
def _from_normalized(cls, elements: set[str]) -> 'SetManager':
# Private fast constructor: bypasses __init__ so results aren't
# re-validated element by element. This performs NO validation or
# normalization of `elements` -- the caller is fully responsible
# for guaranteeing every element is already a str that has passed
# through lc(). Only call this with a set built from other
# SetManagers' already-normalized .elements (operator results,
# prebuilt default copies); passing anything else silently defeats
# the constructor's #238 guarantees with no error raised here.
obj = cls.__new__(cls)
obj.elements = elements
obj._on_change = None
return obj
def __init__(self, elements: Iterable[str]) -> None:
self.elements = self._normalized_elements(elements)
# Optional invalidation hook, wired by an owning Constants so that
# in-place add()/remove() can clear its cached suffixes_prefixes_titles
# union. None when the manager is used standalone.
self._on_change = None
def __call__(self) -> Set[str]:
"""
.. deprecated:: 1.3.0
Removed in 2.0 (see issue #243). Returns the raw underlying set,
so mutating it bypasses normalization and cache invalidation;
iterate the manager or copy with ``set(manager)`` instead.
"""
warnings.warn(
"Calling a SetManager to get the raw underlying set is "
"deprecated and will be removed in 2.0; iterate the manager or "
"copy it with set(manager) instead. See "
"https://github.com/derek73/python-nameparser/issues/243",
DeprecationWarning,
stacklevel=2,
)
return self.elements
def __repr__(self) -> str:
# Sorted so repr is stable across runs -- set() iteration order
# depends on string hash randomization, which varies per process.
elements = "{" + ", ".join(repr(e) for e in sorted(self.elements)) + "}" if self.elements else "set()"
return f"SetManager({elements})" # used for docs
def __iter__(self) -> Iterator[str]:
return iter(self.elements)
def __contains__(self, value: object) -> bool:
# add()/remove()/the constructor/the operators all normalize (lowercase,
# strip leading/trailing periods) before comparing; without the same
# normalization here, `'Dr.' in c.titles` returns False even though
# every other operation on the same value succeeds (#244). The parser's
# own lookups (e.g. `piece.lower() in self.C.conjunctions`) already pass
# an lc()-normalized value, which is the hot path during parsing, so
# try the raw value first and only pay for lc() on a miss.
if value in self.elements:
return True
return isinstance(value, str) and lc(value) in self.elements
def __len__(self) -> int:
return len(self.elements)
# The ABC mixins compare raw operand elements against stored (normalized)
# ones, and their __or__/__and__ accept a bare str as Iterable, so every
# operand is validated and normalized here. Results are built with plain
# set ops on already-normalized elements instead of delegating to the
# mixins, whose _from_iterable would re-validate the whole result
# through __init__.
#
# the runtime ABC accepts any Iterable operand, so annotate honestly and
# ignore typeshed's narrower AbstractSet declarations
def __or__(self, other: Iterable[str]) -> 'SetManager': # type: ignore[override]
return self._from_normalized(self.elements | self._normalized_elements(other))
__ror__ = __or__
def __and__(self, other: Iterable[str]) -> 'SetManager': # type: ignore[override]
return self._from_normalized(self.elements & self._normalized_elements(other))
__rand__ = __and__
def __sub__(self, other: Iterable[str]) -> 'SetManager': # type: ignore[override]
return self._from_normalized(self.elements - self._normalized_elements(other))
def __rsub__(self, other: Iterable[str]) -> 'SetManager':
return self._from_normalized(self._normalized_elements(other) - self.elements)
def __xor__(self, other: Iterable[str]) -> 'SetManager': # type: ignore[override]
return self._from_normalized(self.elements ^ self._normalized_elements(other))
__rxor__ = __xor__
def _add_normalized(self, s: str | bytes, encoding: str | None, *, stacklevel: int) -> None:
# Shared by add() and add_with_encoding() so each can call it
# directly with a stacklevel that attributes the warning to *its own*
# caller -- add() delegating to add_with_encoding() would otherwise
# add a frame and misattribute the warning to this module.
stdin_encoding = None
if sys.stdin:
stdin_encoding = sys.stdin.encoding
encoding = encoding or stdin_encoding or DEFAULT_ENCODING
if isinstance(s, bytes):
warnings.warn(
"Passing bytes to SetManager.add()/add_with_encoding() is "
"deprecated and will raise TypeError in 2.0; decode it "
"first, e.g. value.decode('utf-8'). See "
"https://github.com/derek73/python-nameparser/issues/245",
DeprecationWarning,
stacklevel=stacklevel,
)
s = s.decode(encoding)
normalized = lc(s)
if normalized not in self.elements:
self.elements.add(normalized)
if self._on_change:
self._on_change()
[docs]
def add_with_encoding(self, s: str | bytes, encoding: str | None = None) -> None:
"""
Add the lowercased, leading/trailing-periods-stripped version of the string to the set. Pass an
explicit `encoding` parameter to specify the encoding of binary strings that
are not DEFAULT_ENCODING (UTF-8).
.. deprecated:: 1.3.0
``bytes`` arguments will raise ``TypeError`` in 2.0 (see issue
#245); decode before adding.
.. deprecated:: 1.4.0
The method itself is removed in 2.0 (see issue #245); use
:py:func:`add` instead, decoding bytes first.
"""
warnings.warn(
"SetManager.add_with_encoding() is deprecated and will be "
"removed in 2.0; use add() instead (decode bytes first). See "
"https://github.com/derek73/python-nameparser/issues/245",
DeprecationWarning,
stacklevel=2,
)
self._add_normalized(s, encoding, stacklevel=3)
[docs]
def add(self, *strings: str) -> Self:
"""
Add the lowercased, leading/trailing-periods-stripped version of the string arguments to the set.
Returns ``self`` for chaining.
.. deprecated:: 1.3.0
``bytes`` arguments will raise ``TypeError`` in 2.0 (see issue
#245); decode before adding.
"""
for s in strings:
self._add_normalized(s, None, stacklevel=3)
return self
[docs]
def remove(self, *strings: str) -> Self:
"""
Remove the lower case and no-period version of the string arguments from the set.
Returns ``self`` for chaining.
.. deprecated:: 1.3.0
Removing a *missing* member currently does nothing but will
raise ``KeyError`` in 2.0, matching ``set.remove`` (see issue
#243); use :py:func:`discard` to ignore missing members.
"""
changed = False
for s in strings:
if (lower := lc(s)) in self.elements:
self.elements.remove(lower)
changed = True
else:
warnings.warn(
"SetManager.remove() of a missing member currently does "
"nothing, but will raise KeyError in 2.0; use discard() "
"to ignore missing members. See "
"https://github.com/derek73/python-nameparser/issues/243",
DeprecationWarning,
stacklevel=2,
)
if changed and self._on_change:
self._on_change()
return self
[docs]
def discard(self, *strings: str) -> Self:
"""
Remove the lower case and no-period version of the string arguments
from the set if present; missing members are ignored, like
``set.discard``. Returns ``self`` for chaining.
"""
changed = False
for s in strings:
if (lower := lc(s)) in self.elements:
self.elements.remove(lower)
changed = True
if changed and self._on_change:
self._on_change()
return self
[docs]
def clear(self) -> Self:
"""Remove all entries from the set. Returns ``self`` for chaining."""
if self.elements:
self.elements.clear()
if self._on_change:
self._on_change()
return self
T = TypeVar('T')
def _is_dunder(attr: str) -> bool:
# Dunder names are Python's protocol probes (copy looks up __deepcopy__,
# inspect.unwrap looks up __wrapped__, typing's GenericAlias.__call__ sets
# __orig_class__, ...), never config keys. The TupleManager attribute hooks
# all route dunders to normal object-attribute behavior so those probes
# work instead of being mistaken for dict entries.
return attr.startswith("__") and attr.endswith("__")
# The default config sets are module constants that never change, so
# validate and normalize each one exactly once at import. Constants()
# copies these via _normalized_elements' SetManager fast path instead of
# re-checking ~1,400 elements per construction — a cost that otherwise
# repeats on the per-instance-config path, HumanName(constants=Constants()).
#
# This snapshot is taken once, at import time: mutating a raw constant
# (e.g. `TITLES.add('x')`) after import is *not* picked up by Constants()
# built afterward, since the identity check in Constants.__init__ reuses
# this frozen SetManager rather than re-wrapping the (now-changed) raw
# set. That's a behavior change from re-wrapping every time, but the
# documented customization path mutates the SetManager wrapper on a
# Constants instance (``CONSTANTS.titles.add(...)``), not the raw
# constant, so this only affects an unsupported/undocumented pattern.
_DEFAULT_PREFIXES = SetManager(PREFIXES)
_DEFAULT_SUFFIX_ACRONYMS = SetManager(SUFFIX_ACRONYMS)
_DEFAULT_SUFFIX_NOT_ACRONYMS = SetManager(SUFFIX_NOT_ACRONYMS)
_DEFAULT_SUFFIX_ACRONYMS_AMBIGUOUS = SetManager(SUFFIX_ACRONYMS_AMBIGUOUS)
_DEFAULT_TITLES = SetManager(TITLES)
_DEFAULT_FIRST_NAME_TITLES = SetManager(FIRST_NAME_TITLES)
_DEFAULT_CONJUNCTIONS = SetManager(CONJUNCTIONS)
_DEFAULT_BOUND_FIRST_NAMES = SetManager(BOUND_FIRST_NAMES)
_DEFAULT_NON_FIRST_NAME_PREFIXES = SetManager(NON_FIRST_NAME_PREFIXES)
[docs]
class TupleManager(dict[str, T]):
'''
A dictionary with dot.notation access. Subclass of ``dict``. Wraps the
mapping config constants (``capitalization_exceptions``, ``regexes``, and
the nickname/maiden delimiter buckets). The name is historical: before
1.3.0 these constants were tuples of pairs.
'''
def __init__(
self,
arg: Mapping[str, T] | Iterable[tuple[str, T]] = (),
**kwargs: T,
) -> None:
# dict.__init__ accepts a bare str/bytes as an iterable-of-pairs
# argument (each character iterates further, and dict() only
# complains once it hits a "pair" of the wrong length) and accepts an
# iterable of 2-character strings as if each one were a (key, value)
# pair, silently shredding it -- mirrors SetManager's guard against
# the same class of mistake (#238), applied to the mapping
# constructor's own failure modes (#242).
_reject_bare_str_or_bytes(arg, "a mapping or iterable of (key, value) pairs")
if not isinstance(arg, Mapping):
checked = []
for item in arg:
if isinstance(item, (str, bytes)):
raise TypeError(
"expected (key, value) pairs, got a "
f"{'bytes' if isinstance(item, bytes) else 'str'} "
f"element {item!r}; a 2-character string silently "
"splits into a key and a value"
)
checked.append(item)
arg = checked
super().__init__(arg, **kwargs)
def _warn_unknown_key(self, attr: str) -> None:
# Deprecated 1.4.0, raises AttributeError in 2.0 (#256): a misspelled
# key otherwise degrades silently with no traceback pointing at the
# typo.
warnings.warn(
f"{attr!r} is not a known key ({', '.join(sorted(self))}); "
"unknown-key attribute access is deprecated and will raise "
"AttributeError in 2.0. Use .get() for intentional soft access. "
"See https://github.com/derek73/python-nameparser/issues/256",
DeprecationWarning,
stacklevel=3,
)
def __getattr__(self, attr: str) -> T | None:
# Otherwise the dict default (None) is mistaken for a real protocol hook.
if _is_dunder(attr):
raise AttributeError(attr)
# Single-underscore introspection probes (IPython/Jupyter's
# _repr_html_, _ipython_canary_method_should_not_exist_, etc.) are
# never config keys either -- no real config key starts with '_'.
if attr not in self and not attr.startswith('_'):
self._warn_unknown_key(attr)
return self.get(attr)
def __setattr__(self, attr: str, value: T) -> None:
# Fall back to normal object attribute storage for dunders; everything
# else keeps the dict-backed dot-notation behavior this class exists
# for. Concretely: constructing a subscripted generic, e.g.
# TupleManager[re.Pattern[str] | str](...), makes typing's
# GenericAlias.__call__ set `__orig_class__` on the new instance right
# after __init__ returns. Without this guard that assignment falls
# through to dict.__setitem__ and silently inserts a bogus
# '__orig_class__' entry into the dict itself, corrupting
# .values()/iteration.
if _is_dunder(attr):
object.__setattr__(self, attr, value)
else:
self[attr] = value
def __delattr__(self, attr: str) -> None:
if _is_dunder(attr):
object.__delattr__(self, attr)
else:
del self[attr]
def __getstate__(self) -> Mapping[str, T]:
return dict(self)
def __setstate__(self, state: Mapping[str, T]) -> None:
self.update(state)
def __reduce__(self) -> tuple[type, tuple[()], Mapping[str, T]]:
# Use type(self), not TupleManager, so subclasses such as
# RegexTupleManager survive a pickle round-trip instead of being
# downgraded to a plain TupleManager (which loses the EMPTY_REGEX
# default for unknown keys).
return (type(self), (), self.__getstate__())
[docs]
class RegexTupleManager(TupleManager[re.Pattern[str]]):
def __getattr__(self, attr: str) -> re.Pattern[str]:
# Otherwise EMPTY_REGEX is returned for a dunder probe; copy.deepcopy
# then tries to call the returned re.Pattern and raises TypeError.
if _is_dunder(attr):
raise AttributeError(attr)
if attr not in self and not attr.startswith('_'):
self._warn_unknown_key(attr)
return self.get(attr, EMPTY_REGEX)
class _SetManagerAttribute:
"""Descriptor enforcing ``isinstance(value, SetManager)`` on assignment.
Backs the five plain SetManager attributes (``first_name_titles``,
``conjunctions``, ``bound_first_names``, ``non_first_name_prefixes``,
``suffix_acronyms_ambiguous``). Without this guard, e.g.
``c.conjunctions = 'and'`` is accepted silently, and every later
``piece.lower() in self.C.conjunctions`` becomes a substring test against
the plain str instead of a set membership test (#241).
``_CachedUnionMember`` subclasses this to add ``_pst`` cache invalidation
for the four attributes whose union ``Constants`` caches.
"""
_attr: str
def __set_name__(self, owner: type, name: str) -> None:
self._attr = '_' + name
@overload
def __get__(self, obj: None, objtype: type | None = None) -> '_SetManagerAttribute': ...
@overload
def __get__(self, obj: 'Constants', objtype: type | None = None) -> SetManager: ...
def __get__(self, obj: 'Constants | None', objtype: type | None = None) -> 'SetManager | _SetManagerAttribute':
if obj is None:
return self
return getattr(obj, self._attr)
def _validate(self, value: SetManager) -> None:
if not isinstance(value, SetManager):
raise TypeError(
f"Expected a SetManager instance, got {type(value).__name__!r}. "
"Wrap your iterable: SetManager(['mr', 'ms'])"
)
def __set__(self, obj: 'Constants', value: SetManager) -> None:
self._validate(value)
setattr(obj, self._attr, value)
class _CachedUnionMember(_SetManagerAttribute):
"""Descriptor for the four ``SetManager`` attributes whose union ``Constants``
caches in ``_pst`` (``prefixes``, ``suffix_acronyms``, ``suffix_not_acronyms``,
``titles``).
Assigning a new manager — or mutating one in place via ``add()`` / ``remove()``
— invalidates that cache. Keeping the behavior on a descriptor scopes it to
exactly these attributes, beside their declarations, rather than spreading it
across a catch-all ``__setattr__`` and a separate attribute-name list.
"""
def __set__(self, obj: 'Constants', value: SetManager) -> None:
self._validate(value)
previous = getattr(obj, self._attr, None)
if isinstance(previous, SetManager):
previous._on_change = None # detach the replaced manager so it no longer invalidates
value._on_change = obj._invalidate_pst
obj._invalidate_pst()
setattr(obj, self._attr, value)
class _EmptyAttributeDefaultAttribute:
"""Descriptor backing ``Constants.empty_attribute_default``.
.. deprecated:: 1.4.0
Assignment is deprecated (see issue #255): the only legal value
left once ``None`` support goes in 2.0 is the default ``''``, so a
dial with one position isn't configuration.
"""
_attr = '_empty_attribute_default'
def __get__(self, obj: 'Constants | None', objtype: type | None = None) -> str:
# Annotated `str`, not `str | None`, to match the pre-descriptor
# plain-attribute inference: None is documented/supported (see the
# class docstring), but typing it honestly cascades `| None`
# through every public str-typed name accessor (title, first, ...).
# Returning '' rather than `self` on class access (unlike
# _SetManagerAttribute, which returns `self`) is also load-bearing
# for Constants.__repr__'s `getattr(type(self), name)` default
# comparison in _repr_scalar_attrs -- returning `self` there would
# make every Constants() show this attribute as "customized".
if obj is None:
return ''
return getattr(obj, self._attr, '')
def __set__(self, obj: 'Constants', value: str | None) -> None:
if value is not None and not isinstance(value, str):
raise TypeError(
f"empty_attribute_default must be a str or None, got "
f"{type(value).__name__!r}"
)
warnings.warn(
"Assigning Constants.empty_attribute_default is deprecated and "
"will raise TypeError in 2.0; empty attributes will always "
"return ''. See "
"https://github.com/derek73/python-nameparser/issues/255",
DeprecationWarning,
stacklevel=2,
)
setattr(obj, self._attr, value)
[docs]
class Constants:
"""
An instance of this class hold all of the configuration constants for the parser.
:param set prefixes:
:py:attr:`prefixes` wrapped with :py:class:`SetManager`.
:param set titles:
:py:attr:`titles` wrapped with :py:class:`SetManager`.
:param set first_name_titles:
:py:attr:`~titles.FIRST_NAME_TITLES` wrapped with :py:class:`SetManager`.
:param set suffix_acronyms:
:py:attr:`~suffixes.SUFFIX_ACRONYMS` wrapped with :py:class:`SetManager`.
:param set suffix_not_acronyms:
:py:attr:`~suffixes.SUFFIX_NOT_ACRONYMS` wrapped with :py:class:`SetManager`.
:param set suffix_acronyms_ambiguous:
:py:attr:`~suffixes.SUFFIX_ACRONYMS_AMBIGUOUS` wrapped with :py:class:`SetManager`.
:param set conjunctions:
:py:attr:`conjunctions` wrapped with :py:class:`SetManager`.
:param set bound_first_names:
:py:attr:`~bound_first_names.BOUND_FIRST_NAMES` wrapped with :py:class:`SetManager`.
:param set non_first_name_prefixes:
:py:attr:`~prefixes.NON_FIRST_NAME_PREFIXES` wrapped with :py:class:`SetManager`.
The subset of prefixes that are never a first name, so a *leading* one
marks the whole name as a surname. Must stay disjoint from
``bound_first_names``.
:type capitalization_exceptions: dict or iterable of (key, value) tuples
:param capitalization_exceptions:
:py:attr:`~capitalization.CAPITALIZATION_EXCEPTIONS` wrapped with :py:class:`TupleManager`.
:type regexes: dict or iterable of (name, compiled pattern) tuples
:param regexes:
:py:attr:`~regexes.REGEXES` wrapped with :py:class:`RegexTupleManager`.
:py:attr:`nickname_delimiters` and :py:attr:`maiden_delimiters` are not
constructor arguments -- they're always set in ``__init__`` (see the
comment there for the string-sentinel-vs-compiled-pattern mechanism) --
but are documented here since they're the two `Constants` attributes a
caller is most likely to want to look up: per-bucket
:py:class:`TupleManager` collections that :py:meth:`~nameparser.parser.HumanName.parse_nicknames`
consults to route delimited content into ``nickname``/``maiden``. See
the "Adding Custom Nickname Delimiters" and "Routing to Maiden Name"
sections of the customization docs.
"""
prefixes = _CachedUnionMember()
suffix_acronyms = _CachedUnionMember()
suffix_not_acronyms = _CachedUnionMember()
titles = _CachedUnionMember()
first_name_titles = _SetManagerAttribute()
conjunctions = _SetManagerAttribute()
bound_first_names = _SetManagerAttribute()
non_first_name_prefixes = _SetManagerAttribute()
suffix_acronyms_ambiguous = _SetManagerAttribute()
capitalization_exceptions: TupleManager[str]
regexes: RegexTupleManager
nickname_delimiters: TupleManager[re.Pattern[str] | str]
maiden_delimiters: TupleManager[re.Pattern[str] | str]
_pst: Set[str] | None
string_format: str | None = "{title} {first} {middle} {last} {suffix} ({nickname})"
"""
The default string format use for all new `HumanName` instances.
"""
initials_format = "{first} {middle} {last}"
"""
The default initials format used for all new `HumanName` instances.
"""
initials_delimiter = "."
"""
The default initials delimiter used for all new `HumanName` instances.
Will be used to add a delimiter between each initial.
"""
initials_separator = " "
"""
The default separator placed between consecutive initials within a name
group (first, middle, or last). Distinct from ``initials_delimiter``,
which is the trailing character after each individual initial.
With defaults ``initials_delimiter="."`` and ``initials_separator=" "``,
``initials()`` produces ``"J. A. D."``. Setting ``initials_separator=""``
with ``initials_delimiter="."`` and ``initials_format="{first}{middle}{last}"``
produces ``"J.A.D."``. With the default ``initials_format``, group-level
spacing from the template is still applied.
"""
suffix_delimiter: str | None = None
"""
If set, an additional delimiter used to split suffix groups after
comma-splitting. For example, setting ``suffix_delimiter=" - "`` allows
``"RN - CRNA"`` to be parsed as two separate suffixes. Default is
``None`` (no additional splitting beyond the standard comma split).
Note: setting this to ``","`` or ``", "`` has no additional effect —
the full name is already split on comma characters first (including the
Arabic ``،`` and fullwidth ``,`` variants), and each resulting part is
stripped of surrounding whitespace before this step runs.
The delimiter is only applied to parts once they've been identified as
a suffix group, so it never leaks into a first- or middle-name part. For
example, in inverted format (``"Last, First, suffix"``) a hyphenated
given name like ``"Doe, Mary - Kate, RN"`` with ``suffix_delimiter=" - "``
does not get mistaken for a suffix split.
"""
empty_attribute_default = _EmptyAttributeDefaultAttribute()
"""
Default return value for empty attributes.
.. deprecated:: 1.4.0
Assignment emits ``DeprecationWarning``; the option is removed in
2.0 (see issue #255) and empty attributes will always return ``''``.
.. doctest::
>>> import warnings
>>> from nameparser.config import CONSTANTS
>>> with warnings.catch_warnings():
... warnings.simplefilter('ignore', DeprecationWarning)
... CONSTANTS.empty_attribute_default = None
>>> name = HumanName("John Doe")
>>> print(name.title)
None
>>> name.first
'John'
>>> with warnings.catch_warnings():
... warnings.simplefilter('ignore', DeprecationWarning)
... CONSTANTS.empty_attribute_default = ''
"""
capitalize_name = False
"""
If set, applies :py:meth:`~nameparser.parser.HumanName.capitalize` to
:py:class:`~nameparser.parser.HumanName` instance.
.. doctest::
>>> from nameparser.config import CONSTANTS
>>> CONSTANTS.capitalize_name = True
>>> name = HumanName("bob v. de la macdole-eisenhower phd")
>>> str(name)
'Bob V. de la MacDole-Eisenhower Ph.D.'
>>> CONSTANTS.capitalize_name = False
"""
force_mixed_case_capitalization = False
"""
If set, forces the capitalization of mixed case strings when
:py:meth:`~nameparser.parser.HumanName.capitalize` is called.
.. doctest::
>>> from nameparser.config import CONSTANTS
>>> CONSTANTS.force_mixed_case_capitalization = True
>>> name = HumanName('Shirley Maclaine')
>>> name.capitalize()
>>> str(name)
'Shirley MacLaine'
>>> CONSTANTS.force_mixed_case_capitalization = False
"""
patronymic_name_order = False
"""
If set, detects names in Russian formal order (``Surname GivenName Patronymic``)
by recognizing a trailing East-Slavic patronymic suffix on the last token, and
rotates the three name parts so that ``first``/``middle``/``last`` map to
given name / patronymic / surname respectively. Detection requires exactly one
token in each of first, middle, and last; names with multi-part given names or
multiple middle names are left unchanged.
Also detects reversed-order Azerbaijani/Central-Asian Turkic patronymics
(``Surname GivenName PatronymicRoot Marker``, e.g. ``oglu``/``qizi``), a
structurally different, standalone-marker-word patronymic family. Detection
requires exactly one token in each of first and last, exactly two tokens in
middle, and the last token a recognised Turkic marker.
Opt-in because a Western person whose surname happens to end in a patronymic
suffix (e.g. ``"David Michael Abramovich"``) will be reordered incorrectly
when the flag is on. Enable only when your data is predominantly Russian
formal-order names.
For per-instance control without a shared ``Constants``, pass a dedicated
instance: ``HumanName("...", constants=Constants(patronymic_name_order=True))``.
.. doctest::
>>> from nameparser import HumanName
>>> from nameparser.config import Constants
>>> C = Constants(patronymic_name_order=True)
>>> hn = HumanName("Ivanov Ivan Ivanovich", constants=C)
>>> hn.first, hn.middle, hn.last
('Ivan', 'Ivanovich', 'Ivanov')
>>> hn2 = HumanName("Aliyev Vusal Said oglu", constants=C)
>>> hn2.first, hn2.middle, hn2.last
('Vusal', 'Said oglu', 'Aliyev')
"""
middle_name_as_last = False
"""
If set, folds middle names into the last name: ``middle_list`` is prepended
to ``last_list`` and ``middle_list`` is cleared, so ``.last`` becomes what
``.surnames`` already was and ``.middle`` becomes empty. Useful for naming
systems with no middle-name concept, where everything after the given name
is lineage/family (e.g. Arabic patronymic chaining: given + father +
grandfather + family).
The fold is uniform across both no-comma and comma ("Last, First Middle")
input, so the two written forms of a name converge on the same result.
For per-instance control without a shared ``Constants``, pass a dedicated
instance: ``HumanName("...", constants=Constants(middle_name_as_last=True))``.
.. doctest::
>>> from nameparser import HumanName
>>> from nameparser.config import Constants
>>> C = Constants(middle_name_as_last=True)
>>> hn = HumanName("Mohamad Ahmad Ali Hassan", constants=C)
>>> hn.first, hn.middle, hn.last
('Mohamad', '', 'Ahmad Ali Hassan')
"""
def __init__(self,
prefixes: Iterable[str] = PREFIXES,
suffix_acronyms: Iterable[str] = SUFFIX_ACRONYMS,
suffix_not_acronyms: Iterable[str] = SUFFIX_NOT_ACRONYMS,
suffix_acronyms_ambiguous: Iterable[str] = SUFFIX_ACRONYMS_AMBIGUOUS,
titles: Iterable[str] = TITLES,
first_name_titles: Iterable[str] = FIRST_NAME_TITLES,
conjunctions: Iterable[str] = CONJUNCTIONS,
bound_first_names: Iterable[str] = BOUND_FIRST_NAMES,
non_first_name_prefixes: Iterable[str] = NON_FIRST_NAME_PREFIXES,
capitalization_exceptions: Mapping[str, str] | Iterable[tuple[str, str]] = CAPITALIZATION_EXCEPTIONS,
regexes: Mapping[str, re.Pattern[str]] | Iterable[tuple[str, re.Pattern[str]]] = REGEXES,
patronymic_name_order: bool = False,
middle_name_as_last: bool = False,
) -> None:
# These four descriptor assignments call _CachedUnionMember.__set__, which
# calls _invalidate_pst() and establishes self._pst. They must come before
# any read of suffixes_prefixes_titles.
# untouched defaults (identity check) copy the prebuilt module-level
# managers instead of re-validating the raw constants element by
# element; user-supplied iterables still get the full check
self.prefixes = SetManager(_DEFAULT_PREFIXES if prefixes is PREFIXES else prefixes)
self.suffix_acronyms = SetManager(_DEFAULT_SUFFIX_ACRONYMS if suffix_acronyms is SUFFIX_ACRONYMS else suffix_acronyms)
self.suffix_not_acronyms = SetManager(_DEFAULT_SUFFIX_NOT_ACRONYMS if suffix_not_acronyms is SUFFIX_NOT_ACRONYMS else suffix_not_acronyms)
self.titles = SetManager(_DEFAULT_TITLES if titles is TITLES else titles)
self.first_name_titles = SetManager(_DEFAULT_FIRST_NAME_TITLES if first_name_titles is FIRST_NAME_TITLES else first_name_titles)
self.conjunctions = SetManager(_DEFAULT_CONJUNCTIONS if conjunctions is CONJUNCTIONS else conjunctions)
self.bound_first_names = SetManager(_DEFAULT_BOUND_FIRST_NAMES if bound_first_names is BOUND_FIRST_NAMES else bound_first_names)
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)
self.suffix_acronyms_ambiguous = SetManager(_DEFAULT_SUFFIX_ACRONYMS_AMBIGUOUS if suffix_acronyms_ambiguous is SUFFIX_ACRONYMS_AMBIGUOUS else suffix_acronyms_ambiguous)
self.capitalization_exceptions = TupleManager(capitalization_exceptions)
self.regexes = RegexTupleManager(regexes)
# Per-bucket delimiter collections that parse_nicknames() consults to
# route delimited content into nickname_list / maiden_list. Each value
# is either a compiled re.Pattern (a custom delimiter a caller adds --
# the old extra_nickname_delimiters use case, see issue #112) or the
# string name of a self.regexes entry to resolve live at parse time.
# The latter is how the three built-ins (quoted_word, double_quotes,
# parenthesis) stay linked to self.regexes, so overriding e.g.
# self.regexes.parenthesis keeps affecting nickname/maiden parsing
# exactly as before. Move a key between the two dicts
# (`maiden_delimiters['parenthesis'] =
# nickname_delimiters.pop('parenthesis')`) to change which bucket it
# routes to without losing that live link. maiden_delimiters starts
# empty -- maiden is off until a caller routes a delimiter to it.
# See issue #22.
# Only seed a built-in name if it's actually present in self.regexes --
# a caller who overrides regexes with a minimal custom set (dropping
# e.g. "parenthesis" entirely) shouldn't end up with a dangling
# string sentinel that parse_nicknames() would treat as a mistake.
# See parse_nicknames()'s fail-loud check on an unresolvable sentinel.
self.nickname_delimiters = TupleManager[re.Pattern[str] | str]({
name: name for name in ('quoted_word', 'double_quotes', 'parenthesis')
if name in self.regexes
})
self.maiden_delimiters = TupleManager[re.Pattern[str] | str]()
self.patronymic_name_order = patronymic_name_order
self.middle_name_as_last = middle_name_as_last
def _invalidate_pst(self) -> None:
self._pst = None
@property
def suffixes_prefixes_titles(self) -> Set[str]:
if self._pst is None:
self._pst = self.prefixes | self.suffix_acronyms | self.suffix_not_acronyms | self.titles
return self._pst
_repr_collection_attrs = (
'prefixes', 'suffix_acronyms', 'suffix_not_acronyms', 'titles',
'first_name_titles', 'conjunctions', 'bound_first_names',
'non_first_name_prefixes', 'suffix_acronyms_ambiguous',
)
_repr_scalar_attrs = (
'string_format', 'initials_format', 'initials_delimiter',
'initials_separator', 'suffix_delimiter', 'empty_attribute_default',
'capitalize_name', 'force_mixed_case_capitalization',
'patronymic_name_order', 'middle_name_as_last',
)
def __repr__(self) -> str:
# Collections (some with hundreds of entries, e.g. titles/prefixes)
# are summarized as counts rather than dumped in full. Scalars are
# only shown when they differ from the class default, so a plain
# Constants() reads as just the collection sizes.
lines = [f" {name}: {len(getattr(self, name))}" for name in self._repr_collection_attrs]
lines += [
f" {name}: {value!r}" for name in self._repr_scalar_attrs
if (value := getattr(self, name)) != getattr(type(self), name)
]
return "<Constants : [\n" + "\n".join(lines) + "\n]>"
[docs]
def copy(self) -> 'Constants':
"""
Return a detached deep copy of this ``Constants`` instance, preserving
its current customizations -- unlike :py:class:`Constants`'s own
constructor, which always starts from library defaults. Useful for
snapshotting the shared module-level ``CONSTANTS`` (including
whatever it's been customized with) into a private instance, e.g.
``CONSTANTS.copy()``. Relies on the same ``__getstate__``/``__setstate__``
pair pickling uses, so it's as cheap and correct as pickle round-tripping.
"""
return copy.deepcopy(self)
def __setstate__(self, state: Mapping[str, Any]) -> None:
# Restore each saved attribute directly. The previous implementation
# passed the whole state dict to __init__ as the ``prefixes`` argument,
# which silently reverted every collection to its module default on
# unpickling.
self._pst = None
legacy_format = False
for name, value in state.items():
# inspect.getattr_static, not getattr, so descriptors are
# inspected directly rather than triggering their __get__.
descriptor = inspect.getattr_static(type(self), name, None)
# Migration shim: pickles written before this fix (1.3.0 and earlier,
# including 1.2.1) used a dir() sweep for __getstate__, so their state
# carries the read-only ``suffixes_prefixes_titles`` property. Skip any
# such computed property rather than raising AttributeError on its
# missing setter; the real config is restored from the other keys. We
# don't promise to read pre-fix blobs forever — this only smooths
# migration for anyone persisting them, and can be dropped a release
# or two after 1.3.0 once they've re-pickled.
if isinstance(descriptor, property):
legacy_format = True
continue
if isinstance(descriptor, _EmptyAttributeDefaultAttribute):
# Bypass the descriptor's setter: restoring saved state isn't
# a user assignment, so it shouldn't emit #255's deprecation
# warning on every unpickle/copy() of a customized instance.
setattr(self, descriptor._attr, value)
continue
setattr(self, name, value)
if legacy_format:
# Once per __setstate__ call, not once per skipped key (see
# issue #279): the 2.0 removal turns this into a ValueError
# naming the same fix.
warnings.warn(
"Loading a legacy-format Constants pickle (written by "
"nameparser <= 1.2.x, before the 1.3.0 pickle fix) is "
"deprecated and will raise ValueError in 2.0; re-pickle "
"under 1.3/1.4 to migrate. See "
"https://github.com/derek73/python-nameparser/issues/279",
DeprecationWarning,
stacklevel=2,
)
# Verify each descriptor-backed attr was restored. Without this, a missing
# key surfaces later as AttributeError: 'Constants' object has no attribute
# '_prefixes' — the private mangled name, not the public one, making it
# very hard to diagnose.
for attr in (n for n, v in vars(type(self)).items() if isinstance(v, _SetManagerAttribute)):
if not hasattr(self, '_' + attr):
raise ValueError(
f"Pickle state is missing required field {attr!r}. "
"The state blob may be truncated or from an incompatible version."
)
def __getstate__(self) -> Mapping[str, Any]:
# Pickle the instance's own configuration: the collections built in
# __init__ plus any instance-level scalar overrides.
# _CachedUnionMember descriptors store their values with a leading
# underscore (e.g. `_prefixes` for `prefixes`) so that the descriptor's
# __set__ owns assignment. We map those back to the public names so
# __setstate__ can restore them through the descriptor, re-wiring the
# invalidation callbacks. All other underscore-prefixed names (_pst, etc.)
# are private/cache and are intentionally excluded.
state: dict[str, Any] = {}
for name, val in self.__dict__.items():
if name.startswith('_'):
public = name[1:]
descriptor = inspect.getattr_static(type(self), public, None)
if isinstance(descriptor, (_SetManagerAttribute, _EmptyAttributeDefaultAttribute)):
state[public] = val
else:
state[name] = val
return state
#: A module-level instance of the :py:class:`Constants()` class.
#: Provides a common instance for the module to share
#: to easily adjust configuration for the entire module.
#: See `Customizing the Parser with Your Own Configuration <customize.html>`_.
CONSTANTS = Constants()