Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/nameparser/parser.py: 75%
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1import re
2import warnings
3from collections.abc import Callable, Iterable, Iterator
4from operator import itemgetter
5from itertools import groupby
7from typing import overload
9from nameparser.util import HumanNameAttributeT, lc
10from nameparser.util import log
11from nameparser.config import CONSTANTS
12from nameparser.config import Constants
13from nameparser.config import DEFAULT_ENCODING
15def group_contiguous_integers(data: Iterable[int]) -> list[tuple[int, int]]:
16 """
17 return list of tuples containing first and last index
18 position of contiguous numbers in a series
19 """
20 ranges: list[tuple[int, int]] = []
21 for key, group_with_indices in groupby(enumerate(data), lambda i: i[0] - i[1]):
22 group = list(map(itemgetter(1), group_with_indices))
23 if len(group) > 1:
24 ranges.append((group[0], group[-1]))
25 return ranges
28class HumanName:
29 """
30 Parse a person's name into individual components.
32 Instantiation assigns to ``full_name``, and assignment to
33 :py:attr:`full_name` triggers :py:func:`parse_full_name`. After parsing the
34 name, these instance attributes are available. Alternatively, you can pass
35 any of the instance attributes to the constructor method and skip the parsing
36 process. If any of the the instance attributes are passed to the constructor
37 as keywords, :py:func:`parse_full_name` will not be performed.
39 **HumanName Instance Attributes**
41 * :py:attr:`title`
42 * :py:attr:`first`
43 * :py:attr:`middle`
44 * :py:attr:`last`
45 * :py:attr:`suffix`
46 * :py:attr:`nickname`
47 * :py:attr:`maiden`
48 * :py:attr:`surnames`
49 * :py:attr:`given_names`
51 :param str full_name: The name string to be parsed.
52 :param constants:
53 a :py:class:`~nameparser.config.Constants` instance (subclasses are
54 honored). Defaults to the shared module-level ``CONSTANTS``. For
55 `per-instance config <customize.html>`_, pass ``Constants()`` for
56 fresh library defaults, or ``CONSTANTS.copy()`` for a private
57 snapshot of the current shared config. Passing ``None`` also builds
58 a fresh ``Constants()``, but is deprecated (warns; raises
59 ``TypeError`` in 2.0, see issue #260) since it silently discards any
60 customizations the caller may have expected to carry over. Anything
61 else raises ``TypeError``.
62 :param str encoding: string representing the encoding of your input
63 (deprecated with ``bytes`` input, removal in 2.0 — decode before
64 passing; see issue #245)
65 :param str string_format: python string formatting
66 :param str initials_format: python initials string formatting
67 :param str initials_delimter: string delimiter for initials
68 :param str initials_separator: string separator between consecutive initials
69 :param str suffix_delimiter: additional delimiter to split post-comma parts
70 before suffix detection, e.g. ``" - "`` for ``"RN - CRNA"``
71 :param str first: first name
72 :param str middle: middle name
73 :param str last: last name
74 :param str title: The title or prenominal
75 :param str suffix: The suffix or postnominal
76 :param str nickname: Nicknames
77 :param str maiden: Maiden name
78 """
80 original: str | bytes = ''
81 """
82 The original string, untouched by the parser.
83 """
85 _members = ['title', 'first', 'middle', 'last', 'suffix', 'nickname', 'maiden']
86 _full_name = ''
88 title_list: list[str]
89 first_list: list[str]
90 middle_list: list[str]
91 last_list: list[str]
92 suffix_list: list[str]
93 nickname_list: list[str]
94 maiden_list: list[str]
95 _had_comma: bool
97 def __init__(
98 self,
99 full_name: str | bytes = "",
100 constants: Constants | None = CONSTANTS,
101 encoding: str = DEFAULT_ENCODING,
102 string_format: str | None = None,
103 initials_format: str | None = None,
104 initials_delimiter: str | None = None,
105 initials_separator: str | None = None,
106 suffix_delimiter: str | None = None,
107 first: str | list[str] | None = None,
108 middle: str | list[str] | None = None,
109 last: str | list[str] | None = None,
110 title: str | list[str] | None = None,
111 suffix: str | list[str] | None = None,
112 nickname: str | list[str] | None = None,
113 maiden: str | list[str] | None = None,
114 ) -> None:
115 # calls _validate_constants directly (not through the C setter) so
116 # the deprecation warning below attributes to this constructor's
117 # caller rather than to the setter, mirroring _apply_full_name below
118 self._C = self._validate_constants(constants, stacklevel=3)
120 # Lookup entries derived while parsing this instance (period-joined
121 # titles/suffixes like "Lt.Gov.", conjunction-joined pieces like
122 # "Mr. and Mrs." or "von und zu"). Kept separate from self.C so that
123 # parsing never writes into the config — which is usually the shared
124 # module-level CONSTANTS — keeping results independent of what was
125 # parsed before and config reads safe across threads. Values are
126 # lc()-normalized, mirroring how SetManager stores them. Reset at the
127 # start of each parse_full_name() run.
128 self._derived_titles: set[str] = set()
129 self._derived_suffixes: set[str] = set()
130 self._derived_conjunctions: set[str] = set()
131 self._derived_prefixes: set[str] = set()
133 self.encoding = encoding
134 self.string_format = string_format if string_format is not None else self.C.string_format
135 self.initials_format = initials_format if initials_format is not None else self.C.initials_format
136 self.initials_delimiter = initials_delimiter if initials_delimiter is not None else self.C.initials_delimiter
137 self.initials_separator = initials_separator if initials_separator is not None else self.C.initials_separator
138 self.suffix_delimiter = suffix_delimiter if suffix_delimiter is not None else self.C.suffix_delimiter
139 self._had_comma = False
140 if (first or middle or last or title or suffix or nickname or maiden):
141 self.first = first
142 self.middle = middle
143 self.last = last
144 self.title = title
145 self.suffix = suffix
146 self.nickname = nickname
147 self.maiden = maiden
148 else:
149 # calls _apply_full_name directly (not the setter) so the
150 # deprecation warning below attributes to this constructor's
151 # caller rather than to the setter
152 self._apply_full_name(full_name, stacklevel=3)
154 @staticmethod
155 def _validate_constants(constants: 'Constants | None', *, stacklevel: int) -> 'Constants':
156 # Shared by the constructor and the C setter so both assignment paths
157 # give the same immediate TypeError instead of one bypassing the
158 # other and failing far from the cause (#239).
159 if constants is None:
160 # deprecated 1.4.0, raises TypeError in 2.0 (#260, removal #261):
161 # None means "build a fresh private Constants()", the opposite of
162 # what None conventionally means (the default is the *shared*
163 # CONSTANTS) -- an easy trap since customizing CONSTANTS then
164 # passing None elsewhere silently drops those customizations with
165 # no error. CONSTANTS.copy() is the explicit spelling for the
166 # other reading: a private snapshot of the current shared config.
167 warnings.warn(
168 "Passing constants=None is deprecated and will raise "
169 "TypeError in 2.0; use constants=Constants() for fresh "
170 "library defaults, or constants=CONSTANTS.copy() to snapshot "
171 "the current shared config. See "
172 "https://github.com/derek73/python-nameparser/issues/260",
173 DeprecationWarning,
174 stacklevel=stacklevel,
175 )
176 return Constants()
177 if not isinstance(constants, Constants):
178 # passing the class itself is the likeliest mistake, and
179 # reporting it as "got type" would only add confusion
180 hint = (" (a class was passed; did you mean Constants()?)"
181 if isinstance(constants, type) else "")
182 raise TypeError(
183 "constants must be a Constants instance or None, "
184 f"got {type(constants).__name__}{hint}"
185 )
186 return constants
188 @property
189 def C(self) -> 'Constants':
190 """
191 A reference to the configuration for this instance, which may or may not be
192 a reference to the shared, module-wide instance at
193 :py:mod:`~nameparser.config.CONSTANTS`. See `Customizing the Parser
194 <customize.html>`_.
196 Assigning a non-``Constants`` value (besides ``None``, which builds a
197 fresh private ``Constants()`` and emits a ``DeprecationWarning`` --
198 see :py:meth:`~nameparser.parser.HumanName.__init__`) raises the same
199 ``TypeError`` as passing an invalid ``constants`` argument to the
200 constructor (#239).
201 """
202 return self._C
204 @C.setter
205 def C(self, constants: 'Constants | None') -> None:
206 self._C = self._validate_constants(constants, stacklevel=3)
208 def __getstate__(self) -> dict:
209 state = self.__dict__.copy()
210 c = state.pop('_C')
211 state['C'] = None if c is CONSTANTS else c # sentinel: restore shared singleton on load
212 return state
214 def __setstate__(self, state: dict) -> None:
215 state = dict(state)
216 c = state.pop('C', None)
217 self._C = CONSTANTS if c is None else c
218 self.__dict__.update(state)
219 # pickles from before the per-parse derived sets existed lack them;
220 # backfill so the is_* predicates work without a re-parse
221 for attr in ('_derived_titles', '_derived_suffixes',
222 '_derived_conjunctions', '_derived_prefixes'):
223 self.__dict__.setdefault(attr, set())
225 def __iter__(self) -> Iterator[str]:
226 return (value for member in self._members
227 if (value := getattr(self, member)))
229 def __len__(self) -> int:
230 return sum(1 for member in self._members if getattr(self, member))
232 def __eq__(self, other: object) -> bool:
233 """
234 .. deprecated:: 1.3.0
235 Removed in 2.0 (see issue #223); use :py:meth:`matches`.
237 HumanName instances are equal to other objects whose
238 lower case unicode representation is the same. Note the
239 differences from :py:meth:`matches`: this compares formatted
240 output, so it depends on ``string_format`` and cannot see
241 ``maiden``, and it stringifies operands of any type.
242 """
243 warnings.warn(
244 "HumanName == comparison is deprecated and will be removed in "
245 "2.0; use matches() instead. See "
246 "https://github.com/derek73/python-nameparser/issues/223",
247 DeprecationWarning,
248 stacklevel=2,
249 )
250 return str(self).lower() == str(other).lower()
252 @overload
253 def __getitem__(self, key: slice) -> list[str]: ...
254 @overload
255 def __getitem__(self, key: str) -> str: ...
256 def __getitem__(self, key: slice | str) -> str | list[str]:
257 """
258 .. deprecated:: 1.4.0
259 Slice access (``name[1:-3]``) is removed in 2.0 (see issue
260 #258); field access by position has no real use case.
261 String-key access (``name['first']``) is unaffected.
262 """
263 if isinstance(key, slice):
264 warnings.warn(
265 "Slicing a HumanName by position is deprecated and will be "
266 "removed in 2.0; access the named attributes instead. See "
267 "https://github.com/derek73/python-nameparser/issues/258",
268 DeprecationWarning,
269 stacklevel=2,
270 )
271 return [getattr(self, x) for x in self._members[key]]
272 else:
273 return getattr(self, key)
275 def __setitem__(self, key: str, value: str | list[str] | None) -> None:
276 """
277 .. deprecated:: 1.4.0
278 Removed in 2.0 (see issue #258); it duplicates plain attribute
279 assignment. Use ``name.first = value`` instead.
280 """
281 warnings.warn(
282 "HumanName item assignment is deprecated and will be removed "
283 "in 2.0; it duplicates plain attribute assignment, use "
284 "name.first = value instead. See "
285 "https://github.com/derek73/python-nameparser/issues/258",
286 DeprecationWarning,
287 stacklevel=2,
288 )
289 if key in self._members:
290 self._set_list(key, value)
291 else:
292 raise KeyError("Not a valid HumanName attribute", key)
294 def __str__(self) -> str:
295 if self.string_format is not None:
296 # string_format = "{title} {first} {middle} {last} {suffix} ({nickname})"
297 # Empty attributes must render as '' (not empty_attribute_default,
298 # which may be None) so str.format does not interpolate the
299 # literal "None" into the output, which cannot be scrubbed
300 # afterward without corrupting name text containing the same
301 # substring (#254).
302 _s = self.string_format.format(**{k: v or '' for k, v in self.as_dict().items()})
303 # remove trailing punctuation from missing nicknames
304 _s = _s.replace(" ()", "").replace(" ''", "").replace(' ""', "")
305 _s = self.C.regexes.space_before_comma.sub(',', _s)
306 return self.collapse_whitespace(_s).strip(', ')
307 return " ".join(self)
309 def __hash__(self) -> int:
310 """
311 .. deprecated:: 1.3.0
312 Removed in 2.0 (see issue #223); use :py:meth:`comparison_key`
313 for sets, dicts, and dedup.
314 """
315 warnings.warn(
316 "hash(HumanName) is deprecated and will be removed in 2.0; use "
317 "comparison_key() for sets and dicts. See "
318 "https://github.com/derek73/python-nameparser/issues/223",
319 DeprecationWarning,
320 stacklevel=2,
321 )
322 # __eq__ compares lowercased strings, so hash the lowercased string
323 # to keep equal instances in the same hash bucket.
324 return hash(str(self).lower())
326 def __repr__(self) -> str:
327 attrs = (
328 f" title: {self.title or ''!r}\n"
329 f" first: {self.first or ''!r}\n"
330 f" middle: {self.middle or ''!r}\n"
331 f" last: {self.last or ''!r}\n"
332 f" suffix: {self.suffix or ''!r}\n"
333 f" nickname: {self.nickname or ''!r}\n"
334 f" maiden: {self.maiden or ''!r}"
335 )
336 return f"<{self.__class__.__name__} : [\n{attrs}\n]>"
338 def as_dict(self, include_empty: bool = True) -> dict[str, str]:
339 """
340 Return the parsed name as a dictionary of its attributes.
342 :param bool include_empty: Include keys in the dictionary for empty name attributes.
343 :rtype: dict
345 .. doctest::
347 >>> name = HumanName("Bob Dole")
348 >>> name.as_dict()
349 {'title': '', 'first': 'Bob', 'middle': '', 'last': 'Dole', 'suffix': '', 'nickname': '', 'maiden': ''}
350 >>> name.as_dict(False)
351 {'first': 'Bob', 'last': 'Dole'}
353 """
354 d = {}
355 for m in self._members:
356 if include_empty:
357 d[m] = getattr(self, m)
358 else:
359 val = getattr(self, m)
360 if val:
361 d[m] = val
362 return d
364 def comparison_key(self) -> tuple[str, ...]:
365 """
366 The seven name components (title, first, middle, last, suffix,
367 nickname, maiden) as a lowercased tuple: a canonical, hashable
368 identity for the parsed name. Use it for dedup, dict keys, and
369 sorting or grouping, e.g.
370 ``unique = {n.comparison_key(): n for n in names}.values()``.
372 Built from the ``*_list`` attributes, so it is unaffected by
373 display settings like ``string_format`` and
374 ``empty_attribute_default``.
376 Empty or unparsable input yields the all-empty key, so such names
377 all compare equal and collide in dedup; screen them out with
378 ``len(name) == 0`` first.
380 .. doctest::
382 >>> HumanName("Dr. Juan Q. Xavier de la Vega III").comparison_key()
383 ('dr.', 'juan', 'q. xavier', 'de la vega', 'iii', '', '')
385 """
386 return tuple(
387 " ".join(getattr(self, member + "_list")).lower()
388 for member in self._members
389 )
391 def matches(self, other: 'str | HumanName') -> bool:
392 """
393 Compare parsed components case-insensitively; the semantic
394 replacement for the deprecated ``==``. A ``str`` argument is parsed
395 first, using this instance's configuration, so any written form of
396 the same name matches; a ``HumanName`` argument is compared as
397 already parsed — its own configuration determined its components.
398 Two empty or unparsable names match each other; check
399 ``len(name) == 0`` to screen them.
401 .. doctest::
403 >>> name = HumanName("Dr. Juan Q. Xavier de la Vega III")
404 >>> name.matches("de la vega, dr. juan Q. xavier III")
405 True
406 >>> name.matches("Juan de la Vega")
407 False
409 Unlike the deprecated ``==``, all seven components participate
410 (including ``maiden``, which the default ``string_format`` omits)
411 and display settings have no effect. Raises ``TypeError`` for
412 anything that is not a ``str`` or ``HumanName``; guard optional
413 values explicitly, e.g. ``x is not None and name.matches(x)``.
415 Parses string arguments on every call. When matching one name
416 against many candidates, parse the candidates once or compare
417 :py:meth:`comparison_key` values instead.
418 """
419 if isinstance(other, HumanName):
420 return self.comparison_key() == other.comparison_key()
421 if isinstance(other, str):
422 return self.comparison_key() == type(self)(other, self.C).comparison_key()
423 raise TypeError(
424 f"matches() requires a str or HumanName, got {type(other).__name__}"
425 )
427 def _process_initial(self, name_part: str, firstname: bool = False) -> str:
428 """
429 Name parts may include prefixes or conjunctions. This function filters these from the name unless it is
430 a first name, since first names cannot be conjunctions or prefixes.
431 """
432 # split() rather than split(" "): *_list attributes assigned directly
433 # bypass parse_pieces whitespace normalization, and split(" ") yields
434 # empty strings for repeated spaces (#232)
435 parts = name_part.split()
436 initials = []
437 for part in parts:
438 if not (self.is_prefix(part) or self.is_conjunction(part)) or firstname:
439 initials.append(part[0])
440 if len(initials) > 0:
441 return self.initials_separator.join(initials)
442 # Return '' (never empty_attribute_default, which may be None) when a
443 # part has no initialable words, e.g. a middle name consisting only of
444 # prefixes ("de la"). Callers drop these parts entirely.
445 return ''
447 def _initials_lists(self) -> tuple[list[str], list[str], list[str]]:
448 """Initials for the first, middle and last name groups. Parts that
449 yield no initials (e.g. a prefix-only middle name like "de la") are
450 dropped rather than kept as empty strings.
451 """
452 def group_initials(names: list[str], firstname: bool = False) -> list[str]:
453 return [i for i in (self._process_initial(n, firstname) for n in names if n) if i]
454 return (group_initials(self.first_list, True),
455 group_initials(self.middle_list),
456 group_initials(self.last_list))
458 def initials_list(self) -> list[str]:
459 """
460 Returns the initials as a list
462 .. doctest::
464 >>> name = HumanName("Sir Bob Andrew Dole")
465 >>> name.initials_list()
466 ['B', 'A', 'D']
467 >>> name = HumanName("J. Doe")
468 >>> name.initials_list()
469 ['J', 'D']
470 """
471 first_initials_list, middle_initials_list, last_initials_list = self._initials_lists()
472 return first_initials_list + middle_initials_list + last_initials_list
474 def initials(self) -> str:
475 """
476 Return formatted initials for the name, controlled by
477 ``initials_format``, ``initials_delimiter``, and ``initials_separator``.
479 ``initials_delimiter`` is appended after each individual initial.
480 ``initials_separator`` is placed between consecutive initials within
481 a name group (first, middle, or last). Both can be set as
482 ``Constants`` attributes or as ``HumanName`` constructor kwargs.
484 .. doctest::
486 >>> name = HumanName("Sir Bob Andrew Dole")
487 >>> name.initials()
488 'B. A. D.'
489 >>> name = HumanName("Sir Bob Andrew Dole", initials_format="{first} {middle}")
490 >>> name.initials()
491 'B. A.'
492 >>> name = HumanName("Doe, John A.", initials_delimiter="", initials_separator="")
493 >>> name.initials()
494 'J A D'
495 """
497 first_initials_list, middle_initials_list, last_initials_list = self._initials_lists()
499 # Empty name groups must render as '' (not empty_attribute_default,
500 # which may be None) so str.format does not interpolate the literal
501 # "None" into the output. A fully-empty result falls back to
502 # empty_attribute_default, matching the other attribute accessors
503 # (e.g. ``first``).
504 initials_dict = {
505 "first": (self.initials_delimiter + self.initials_separator).join(first_initials_list) + self.initials_delimiter
506 if len(first_initials_list) else "",
507 "middle": (self.initials_delimiter + self.initials_separator).join(middle_initials_list) + self.initials_delimiter
508 if len(middle_initials_list) else "",
509 "last": (self.initials_delimiter + self.initials_separator).join(last_initials_list) + self.initials_delimiter
510 if len(last_initials_list) else ""
511 }
513 _s = self.initials_format.format(**initials_dict) # noqa: UP032
514 return self.collapse_whitespace(_s) or self.C.empty_attribute_default
516 @property
517 def has_own_config(self) -> bool:
518 """
519 True if this instance is not using the shared module-level
520 configuration.
521 """
522 return self.C is not CONSTANTS
524 # attributes
526 @property
527 def title(self) -> str:
528 """
529 The person's titles. Any string of consecutive pieces in
530 :py:mod:`~nameparser.config.titles` or
531 :py:mod:`~nameparser.config.conjunctions`
532 at the beginning of :py:attr:`full_name`.
533 """
534 return " ".join(self.title_list) or self.C.empty_attribute_default
536 @title.setter
537 def title(self, value: str | list[str] | None) -> None:
538 self._set_list('title', value)
540 @property
541 def first(self) -> str:
542 """
543 The person's first name. The first name piece after any known
544 :py:attr:`title` pieces parsed from :py:attr:`full_name`.
545 """
546 return " ".join(self.first_list) or self.C.empty_attribute_default
548 @first.setter
549 def first(self, value: str | list[str] | None) -> None:
550 self._set_list('first', value)
552 @property
553 def middle(self) -> str:
554 """
555 The person's middle names. All name pieces after the first name and
556 before the last name parsed from :py:attr:`full_name`.
557 """
558 return " ".join(self.middle_list) or self.C.empty_attribute_default
560 @middle.setter
561 def middle(self, value: str | list[str] | None) -> None:
562 self._set_list('middle', value)
564 @property
565 def last(self) -> str:
566 """
567 The person's last name. The last name piece parsed from
568 :py:attr:`full_name`.
569 """
570 return " ".join(self.last_list) or self.C.empty_attribute_default
572 @last.setter
573 def last(self, value: str | list[str] | None) -> None:
574 self._set_list('last', value)
576 @property
577 def suffix(self) -> str:
578 """
579 The persons's suffixes. Pieces at the end of the name that are found in
580 :py:mod:`~nameparser.config.suffixes`, or pieces that are at the end
581 of comma separated formats, e.g.
582 "Lastname, Title Firstname Middle[,] Suffix [, Suffix]" parsed
583 from :py:attr:`full_name`.
584 """
585 return ", ".join(self.suffix_list) or self.C.empty_attribute_default
587 @suffix.setter
588 def suffix(self, value: str | list[str] | None) -> None:
589 self._set_list('suffix', value)
591 @property
592 def nickname(self) -> str:
593 """
594 The person's nicknames. Any text found inside of quotes (``""``) or
595 parenthesis (``()``)
596 """
597 return " ".join(self.nickname_list) or self.C.empty_attribute_default
599 @nickname.setter
600 def nickname(self, value: str | list[str] | None) -> None:
601 self._set_list('nickname', value)
603 @property
604 def maiden(self) -> str:
605 """
606 The person's maiden (alternate/prior) last name. Empty unless a
607 delimiter has been routed to it via
608 :py:attr:`~nameparser.config.Constants.maiden_delimiters` -- see the
609 "Routing to Maiden Name" section of the customization docs.
610 """
611 return " ".join(self.maiden_list) or self.C.empty_attribute_default
613 @maiden.setter
614 def maiden(self, value: str | list[str] | None) -> None:
615 self._set_list('maiden', value)
617 @property
618 def surnames_list(self) -> list[str]:
619 """
620 List of middle names followed by last name.
621 """
622 return self.middle_list + self.last_list
624 @property
625 def surnames(self) -> str:
626 """
627 A string of all middle names followed by the last name.
628 """
629 return " ".join(self.surnames_list) or self.C.empty_attribute_default
631 @property
632 def given_names_list(self) -> list[str]:
633 """
634 List of first name followed by middle names.
635 """
636 return self.first_list + self.middle_list
638 @property
639 def given_names(self) -> str:
640 """
641 A string of the first name followed by all middle names.
642 """
643 return " ".join(self.given_names_list) or self.C.empty_attribute_default
645 def _split_last(self) -> tuple[list[str], list[str]]:
646 """Return (prefix_particles, base_words) split from the last name.
648 The base_words list is never empty: if every word in the last name
649 matches a prefix particle, the guard fires and all words are returned
650 as the base with an empty prefix list (heuristic: a family name is
651 assumed not to consist entirely of particles).
653 >>> HumanName("Vincent van Gogh")._split_last()
654 (['van'], ['Gogh'])
655 >>> HumanName("Anh Do")._split_last()
656 ([], ['Do'])
657 """
658 words = " ".join(self.last_list).split()
659 i = 0
660 while i < len(words) and self.is_prefix(words[i]):
661 i += 1
662 if i == len(words):
663 # Heuristic: assume a family name isn't entirely composed of
664 # particles (e.g. surname "Do" which also appears in PREFIXES).
665 # Don't strip — treat the whole last name as the base.
666 return [], words
667 return words[:i], words[i:]
669 @property
670 def last_prefixes_list(self) -> list[str]:
671 """
672 List of leading prefix particles in the last name (the *tussenvoegsel*).
673 Returns ``[]`` when there are none, including the case where every word
674 in the last name matches a prefix — see :py:meth:`_split_last`.
676 >>> HumanName("Juan de la Vega").last_prefixes_list
677 ['de', 'la']
678 """
679 return self._split_last()[0]
681 @property
682 def last_base_list(self) -> list[str]:
683 """
684 List of last-name words after stripping leading prefix particles.
685 Never empty: when every word matches a prefix, no stripping occurs and
686 the full last name is returned — see :py:meth:`_split_last`.
688 >>> HumanName("Vincent van Gogh").last_base_list
689 ['Gogh']
690 """
691 return self._split_last()[1]
693 @property
694 def last_base(self) -> str:
695 """
696 The last name with leading prefix particles removed (the core surname).
697 For ``"van Gogh"`` this is ``"Gogh"``; for ``"Smith"`` it is ``"Smith"``.
698 ``last`` is always unchanged. When every word in the last name matches a
699 prefix particle, no stripping occurs and the full last name is returned.
701 >>> HumanName("Vincent van Gogh").last_base
702 'Gogh'
703 >>> HumanName("John Smith").last_base
704 'Smith'
705 """
706 return " ".join(self.last_base_list) or self.C.empty_attribute_default
708 @property
709 def last_prefixes(self) -> str:
710 """
711 The leading prefix particle(s) of the last name (the *tussenvoegsel*).
712 Returns ``""`` (or ``empty_attribute_default``) when there are none,
713 including when every word in the last name matches a prefix particle
714 (the all-particles guard; see :py:meth:`_split_last`).
716 >>> HumanName("Vincent van Gogh").last_prefixes
717 'van'
718 >>> HumanName("Juan de la Vega").last_prefixes
719 'de la'
720 """
721 return " ".join(self.last_prefixes_list) or self.C.empty_attribute_default
723 # setter methods
725 def _set_list(self, attr: str, value: str | list[str] | None) -> None:
726 if isinstance(value, list):
727 val = value
728 elif isinstance(value, (str, bytes)):
729 val = [value]
730 elif value is None:
731 val = []
732 else:
733 raise TypeError(
734 "Can only assign strings, lists or None to name attributes."
735 f" Got {type(value)}")
736 setattr(self, attr+"_list", self.parse_pieces(val))
738 # Parse helpers
739 def is_title(self, value: str) -> bool:
740 """Is in the :py:data:`~nameparser.config.titles.TITLES` set or was
741 derived as a title earlier in this parse (e.g. ``"Lt.Gov."``,
742 ``"Mr. and Mrs."``)."""
743 word = lc(value)
744 return word in self.C.titles or word in self._derived_titles
746 def is_leading_title(self, piece: str) -> bool:
747 """
748 True if ``piece`` is a known title, or an unrecognized multi-letter
749 word ending in a single trailing period (e.g. ``"Major."``). The
750 ``{2,}`` in the ``period_abbreviation`` regex, not a separate
751 ``is_an_initial()`` check, is what excludes single-letter initials
752 like ``"J."``. Only meaningful for pieces in the title position
753 (before the first name is set) — a period-abbreviation appearing
754 later in the name is left as a middle name. The match is not
755 registered in ``C.titles`` or the per-parse derived titles, so
756 matching ``"Major."`` here never makes ``"Major"`` (or ``"Major."``)
757 a recognized title elsewhere, even within the same parse.
758 """
759 return self.is_title(piece) or bool(self.C.regexes.period_abbreviation.match(piece))
761 def is_conjunction(self, piece: str | list[str]) -> bool:
762 """Is in the conjunctions set — config or derived earlier in this
763 parse (e.g. ``"of the"``) — and not :py:func:`is_an_initial()`."""
764 if isinstance(piece, list):
765 for item in piece:
766 if self.is_conjunction(item):
767 return True
768 return False
769 return (piece.lower() in self.C.conjunctions
770 or piece.lower() in self._derived_conjunctions) \
771 and not self.is_an_initial(piece)
773 def is_prefix(self, piece: str | list[str]) -> bool:
774 """
775 Lowercased, leading/trailing-periods-stripped version of piece is in the
776 :py:data:`~nameparser.config.prefixes.PREFIXES` set, or was derived as
777 a prefix earlier in this parse (e.g. ``"von und"``).
778 """
779 if isinstance(piece, list):
780 for item in piece:
781 if self.is_prefix(item):
782 return True
783 return False
784 word = lc(piece)
785 return word in self.C.prefixes or word in self._derived_prefixes
787 def is_bound_first_name(self, piece: str) -> bool:
788 """Lowercased, leading/trailing-periods-stripped version of piece is in :py:attr:`~nameparser.config.Constants.bound_first_names`."""
789 return lc(piece) in self.C.bound_first_names
791 def is_non_first_name_prefix(self, piece: str) -> bool:
792 """Lowercased, leading/trailing-periods-stripped version of piece is in
793 :py:attr:`~nameparser.config.Constants.non_first_name_prefixes`."""
794 return lc(piece) in self.C.non_first_name_prefixes
796 def _join_bound_first_name(self, pieces: list[str], reserve_last: bool) -> list[str]:
797 """Join a first-name prefix to its following piece.
799 Finds the first non-title piece; if it is in ``bound_first_names``,
800 merges it with the next piece — unless ``reserve_last`` is True and no
801 further piece would remain for the last name.
802 """
803 fi = next((i for i, p in enumerate(pieces) if not self.is_title(p)), None)
804 if fi is None:
805 return pieces
806 if not self.is_bound_first_name(pieces[fi]):
807 return pieces
808 next_i = fi + 1
809 if next_i >= len(pieces):
810 return pieces
811 if reserve_last:
812 # Count non-suffix pieces from next_i onward; need ≥2 so the join
813 # target and at least one last-name piece both exist.
814 non_suffix_remaining = sum(
815 1 for p in pieces[next_i:] if not self.is_suffix(p)
816 )
817 if non_suffix_remaining <= 1:
818 return pieces
819 pieces[fi] = pieces[fi] + " " + pieces[next_i]
820 del pieces[next_i]
821 return pieces
823 def is_roman_numeral(self, value: str) -> bool:
824 """
825 Matches the ``roman_numeral`` regular expression in
826 :py:data:`~nameparser.config.regexes.REGEXES`.
827 """
828 return bool(self.C.regexes.roman_numeral.match(value))
830 def is_suffix(self, piece: str | list[str]) -> bool:
831 """
832 Is in the suffixes set — or was derived as a period-joined suffix
833 earlier in this parse (e.g. ``"JD.CPA"``) — and not
834 :py:func:`is_an_initial()`.
836 Some suffixes may be acronyms (M.B.A) while some are not (Jr.),
837 so we remove the periods from `piece` when testing against
838 `C.suffix_acronyms`.
839 """
840 # suffixes may have periods inside them like "M.D."
841 if isinstance(piece, list):
842 for item in piece:
843 if self.is_suffix(item):
844 return True
845 return False
846 else:
847 word = lc(piece)
848 return ((word.replace('.', '') in self.C.suffix_acronyms)
849 or (word in self.C.suffix_not_acronyms)
850 or (word in self._derived_suffixes)) \
851 and not self.is_an_initial(piece)
853 def are_suffixes(self, pieces: Iterable[str]) -> bool:
854 """Return True if all pieces are suffixes.
856 Vacuously True for an empty iterable — the piece loops in
857 :py:func:`parse_full_name` rely on this to route the final piece
858 to the last-name branch.
859 """
860 for piece in pieces:
861 if not self.is_suffix(piece):
862 return False
863 return True
865 def is_suffix_lenient(self, piece: str) -> bool:
866 """Like is_suffix(), but suffix_not_acronyms members are accepted
867 unconditionally, bypassing is_suffix()'s is_an_initial() veto.
869 This covers all suffix_not_acronyms members (i, ii, iii, iv, v, jr,
870 sr, etc.), case-insensitively, including single-letter entries that
871 is_suffix() would otherwise reject. Only safe for pieces in
872 unambiguous positions, e.g. after a comma ("John Ingram, V").
873 """
874 return lc(piece) in self.C.suffix_not_acronyms or self.is_suffix(piece)
876 def expand_suffix_delimiter(self, part: str) -> list[str]:
877 """Split a single post-comma part on :py:attr:`suffix_delimiter`,
878 if configured. Used only at suffix-consumption sites, where a part
879 has already been identified as a suffix group, so splitting it
880 further can't misparse an unrelated name segment. Returns ``[part]``
881 unchanged if no delimiter is configured.
882 """
883 if not self.suffix_delimiter:
884 return [part]
885 return [p for p in (p.strip() for p in part.split(self.suffix_delimiter)) if p]
887 def are_suffixes_after_comma(self, pieces: Iterable[str]) -> bool:
888 """Return True if all pieces are suffixes by the lenient
889 :py:func:`is_suffix_lenient` test. Used when detecting suffix-comma
890 format (e.g. "John Ingram, V") where the post-comma position is
891 unambiguous.
892 """
893 return all(self.is_suffix_lenient(piece) for piece in pieces)
895 def is_rootname(self, piece: str) -> bool:
896 """
897 Is not a known title, suffix or prefix. Just first, middle, last names.
898 """
899 word = lc(piece)
900 return word not in self.C.suffixes_prefixes_titles \
901 and word not in self._derived_titles \
902 and word not in self._derived_suffixes \
903 and word not in self._derived_prefixes \
904 and not self.is_an_initial(piece)
906 def is_an_initial(self, value: str) -> bool:
907 """
908 Words with a single period at the end, or a single uppercase letter.
910 Matches the ``initial`` regular expression in
911 :py:data:`~nameparser.config.regexes.REGEXES`.
912 """
913 return bool(self.C.regexes.initial.match(value))
915 def is_east_slavic_patronymic(self, piece: str) -> bool:
916 """
917 Return True if ``piece`` ends with a recognised East-Slavic patronymic
918 suffix, checked against both Latin-script and Cyrillic patterns in
919 ``self.C.regexes``. Latin suffixes: ``-ovich``, ``-ovna``, ``-evich``,
920 ``-evna``, ``-ichna``, and the irregular forms ``-ilyich``, ``-kuzmich``,
921 ``-lukich``, ``-fomich``, ``-fokich``. Cyrillic equivalents are matched
922 by a separate pattern.
923 """
924 return bool(
925 self.C.regexes.east_slavic_patronymic.search(piece)
926 or self.C.regexes.east_slavic_patronymic_cyrillic.search(piece)
927 )
929 def is_turkic_patronymic_marker(self, piece: str) -> bool:
930 """
931 Return True if ``piece`` is exactly a recognised Turkic patronymic
932 marker word (e.g. ``oglu``, ``qizi``, ``uly``), checked against both
933 Latin-script and Cyrillic patterns in ``self.C.regexes``. Unlike
934 East-Slavic patronymics, these are standalone marker words, not
935 suffixes, so the match is whole-word rather than a suffix search.
936 """
937 return bool(
938 self.C.regexes.turkic_patronymic_marker.match(piece)
939 or self.C.regexes.turkic_patronymic_marker_cyrillic.match(piece)
940 )
942 # full_name parser
944 @property
945 def full_name(self) -> str:
946 """The string output of the HumanName instance."""
947 return str(self)
949 @full_name.setter
950 def full_name(self, value: str | bytes) -> None:
951 self._apply_full_name(value, stacklevel=3)
953 def _apply_full_name(self, value: str | bytes, *, stacklevel: int) -> None:
954 # Shared by the setter and the constructor so each can call it
955 # directly with a stacklevel that attributes the warning to *its own*
956 # caller -- the constructor going through the setter would otherwise
957 # add a frame and misattribute the warning to this module.
958 self.original = value
960 if isinstance(value, bytes):
961 # deprecated 1.3.0, raises TypeError in 2.0 (#245)
962 warnings.warn(
963 "Passing bytes to HumanName is deprecated and will raise "
964 "TypeError in 2.0; decode it first, e.g. "
965 "value.decode('utf-8'). See "
966 "https://github.com/derek73/python-nameparser/issues/245",
967 DeprecationWarning,
968 stacklevel=stacklevel,
969 )
970 self._full_name = value.decode(self.encoding)
971 else:
972 self._full_name = value
974 self.parse_full_name()
976 def collapse_whitespace(self, string: str) -> str:
977 # collapse multiple spaces into single space
978 string = self.C.regexes.spaces.sub(" ", string.strip())
979 if string and self.C.regexes.commas.fullmatch(string[-1]):
980 string = string[:-1]
981 return string
983 def pre_process(self) -> None:
984 """
986 This method happens at the beginning of the :py:func:`parse_full_name`
987 before any other processing of the string aside from unicode
988 normalization, so it's a good place to do any custom handling in a
989 subclass. Runs :py:func:`squash_bidi`, :py:func:`parse_nicknames` and
990 :py:func:`squash_emoji`.
992 """
993 self.squash_bidi()
994 self.fix_phd()
995 self.parse_nicknames()
996 self.squash_emoji()
998 def handle_east_slavic_patronymic_name_order(self) -> None:
999 """
1000 When patronymic_name_order is enabled, detect Russian formal order
1001 (Surname GivenName Patronymic) and rotate to Western order.
1002 Fires only for no-comma, single-token first/middle/last where the last
1003 token is a patronymic and the middle token is not. Title, suffix, and
1004 nickname parts do not affect this guard — reordering proceeds regardless
1005 of whether they are present.
1006 """
1007 if (
1008 not self._had_comma
1009 and len(self.first_list) == 1
1010 and len(self.middle_list) == 1
1011 and len(self.last_list) == 1
1012 and self.is_east_slavic_patronymic(self.last_list[0])
1013 and not self.is_east_slavic_patronymic(self.middle_list[0])
1014 ):
1015 self.first_list, self.middle_list, self.last_list = (
1016 self.middle_list,
1017 self.last_list,
1018 self.first_list,
1019 )
1021 def handle_turkic_patronymic_name_order(self) -> None:
1022 """
1023 When patronymic_name_order is enabled, detect the reversed Turkic
1024 formal order (Surname GivenName PatronymicRoot Marker) and rotate to
1025 Western order. Fires only for the strict 4-token, no-comma shape:
1026 single-token first/last and exactly two middle tokens, where the last
1027 token is a recognised Turkic patronymic marker.
1028 """
1029 if (
1030 not self._had_comma
1031 and len(self.first_list) == 1
1032 and len(self.middle_list) == 2
1033 and len(self.last_list) == 1
1034 and self.is_turkic_patronymic_marker(self.last_list[0])
1035 ):
1036 self.first_list, self.middle_list, self.last_list = (
1037 [self.middle_list[0]],
1038 [self.middle_list[1], self.last_list[0]],
1039 self.first_list,
1040 )
1042 def handle_non_first_name_prefix(self) -> None:
1043 """
1044 A leading prefix that is never a first name means the whole name is a
1045 surname -- fold first (and any middle) into last. Keys on the parsed
1046 first name, so a non-leading particle ("Jean de Mesnil") is untouched
1047 and title/suffix are preserved. The middle_list/last_list guard leaves a
1048 degenerate bare "de" as first="de" rather than inventing a surname.
1049 """
1050 if (len(self.first_list) == 1
1051 and self.is_non_first_name_prefix(self.first_list[0])
1052 and (self.middle_list or self.last_list)):
1053 self.last_list = self.first_list + self.middle_list + self.last_list
1054 self.first_list = []
1055 self.middle_list = []
1057 def handle_middle_name_as_last(self) -> None:
1058 """
1059 When middle_name_as_last is enabled, fold middle_list into last_list
1060 (prepended, preserving order) and clear middle_list. No-op when
1061 middle_list is already empty.
1062 """
1063 self.last_list = self.middle_list + self.last_list
1064 self.middle_list = []
1066 def post_process(self) -> None:
1067 """
1068 This happens at the end of the :py:func:`parse_full_name` after
1069 all other processing has taken place. Runs :py:func:`handle_firstnames`
1070 and :py:func:`handle_capitalization`.
1071 """
1072 self.handle_firstnames()
1073 self.handle_non_first_name_prefix()
1074 if self.C.patronymic_name_order:
1075 self.handle_east_slavic_patronymic_name_order()
1076 self.handle_turkic_patronymic_name_order()
1077 if self.C.middle_name_as_last:
1078 self.handle_middle_name_as_last()
1079 self.handle_capitalization()
1081 def fix_phd(self) -> None:
1082 _re = self.C.regexes.phd
1084 if match := _re.search(self._full_name):
1085 self.suffix_list.extend(match.groups())
1086 self._full_name = _re.sub("", self._full_name)
1088 def parse_nicknames(self) -> None:
1089 """
1090 Delimited content in the name is routed to either the nickname or
1091 maiden bucket, based on which of
1092 :py:attr:`~nameparser.config.Constants.nickname_delimiters` /
1093 :py:attr:`~nameparser.config.Constants.maiden_delimiters` the matching
1094 delimiter belongs to -- unless that content is suffix-shaped -- an
1095 unambiguous suffix_not_acronyms/suffix_acronyms member, or content
1096 ending in a period -- in which case it's left in place (undelimited)
1097 for normal downstream suffix/title/word parsing instead. This happens
1098 before any other processing of the name.
1100 Single quotes cannot span white space characters and must border
1101 white space to allow for quotes in names like O'Connor and Kawai'ae'a.
1102 Double quotes and parenthesis can span white space.
1104 By default, ``nickname_delimiters`` holds the three built-in
1105 delimiters (``quoted_word``, ``double_quotes`` and ``parenthesis``,
1106 resolved live from :py:attr:`~nameparser.config.Constants.regexes` so
1107 overriding e.g. ``CONSTANTS.regexes.parenthesis`` keeps affecting
1108 nickname parsing) and ``maiden_delimiters`` is empty. Move a key
1109 between the two dicts, e.g.
1110 ``maiden_delimiters['parenthesis'] = nickname_delimiters.pop('parenthesis')``,
1111 to route it to ``maiden`` instead, or add a new compiled pattern under
1112 any key to recognize an additional delimiter -- see the "Adding
1113 Custom Nickname Delimiters" and "Routing to Maiden Name" sections of
1114 the customization docs.
1115 """
1117 def handle_match(target_list: list[str]) -> Callable[['re.Match[str]'], str]:
1118 def _handle(m: 're.Match[str]') -> str:
1119 # Fall back to the whole match when the regex has no capturing
1120 # group (e.g. a custom override regex without one, like
1121 # EMPTY_REGEX) -- mirrors the old code's use of findall(), which
1122 # returns the whole match for group-less patterns.
1123 content = m.group(1) if m.lastindex else m.group(0)
1124 stripped = lc(content)
1125 # Inlined rather than calling self.is_suffix(content): is_suffix()
1126 # also rejects single-letter initials via is_an_initial(), which
1127 # isn't relevant here, and the suffix_acronyms_ambiguous exclusion
1128 # needs to be interleaved into the acronym branch specifically.
1129 # Acronym suffixes may have periods between every letter (e.g.
1130 # "M.D", "Ph.D") that aren't necessarily trailing, so -- exactly
1131 # like is_suffix() -- strip all periods before checking
1132 # suffix_acronyms/suffix_acronyms_ambiguous membership. Bare
1133 # `stripped` (lc() only strips leading/trailing periods) is still
1134 # used for suffix_not_acronyms, matching is_suffix()'s asymmetry.
1135 acronym_stripped = stripped.replace('.', '')
1136 is_unambiguous_suffix = (
1137 stripped in self.C.suffix_not_acronyms
1138 or (acronym_stripped in self.C.suffix_acronyms
1139 and acronym_stripped not in self.C.suffix_acronyms_ambiguous)
1140 )
1141 if is_unambiguous_suffix or content.endswith('.'):
1142 # Leave the bare content -- no delimiters -- so downstream
1143 # word-splitting/suffix-matching sees it exactly as if it had
1144 # never been wrapped in parens/quotes. is_suffix()/lc() only
1145 # strip periods, never parens/quotes, so returning m.group(0)
1146 # here (e.g. literal "(Ret)") would never match
1147 # suffix_not_acronyms ("ret").
1148 return content
1149 target_list.append(content)
1150 return ''
1151 return _handle
1153 # Same handle_match for every delimiter: suffix-shaped content is rare
1154 # in quotes but not impossible, and the logic is delimiter-agnostic,
1155 # so there's no reason to special-case parenthesis here. A string
1156 # delimiter value names a Constants.regexes entry, resolved via
1157 # getattr() so overriding e.g. self.C.regexes.parenthesis keeps
1158 # working; anything else is already a compiled pattern, added
1159 # directly by a caller. Unlike a caller directly querying
1160 # self.C.regexes (where RegexTupleManager's EMPTY_REGEX default for
1161 # an unknown attribute is harmless -- the caller sees the pattern and
1162 # can react to it), a bad string here is an internal cross-reference
1163 # the delimiter dict itself is responsible for keeping valid.
1164 # EMPTY_REGEX matches the empty string at every position, so
1165 # silently falling back to it would not just skip the delimiter --
1166 # handle_match() would fire on every zero-width match and append ''
1167 # into the bucket's list repeatedly, producing a truthy
1168 # whitespace-only nickname/maiden while leaving the real delimited
1169 # content (e.g. literal parentheses) unstripped. Fail loudly instead.
1170 for bucket, delimiters in (
1171 ('nickname', self.C.nickname_delimiters),
1172 ('maiden', self.C.maiden_delimiters),
1173 ):
1174 target_list = getattr(self, bucket + '_list')
1175 _handle_match = handle_match(target_list)
1176 for raw_pattern in delimiters.values():
1177 if isinstance(raw_pattern, re.Pattern):
1178 _re = raw_pattern
1179 elif raw_pattern in self.C.regexes:
1180 _re = getattr(self.C.regexes, raw_pattern)
1181 else:
1182 raise ValueError(
1183 f"{bucket}_delimiters references unknown regexes key {raw_pattern!r}. "
1184 f"Known regexes keys: {sorted(self.C.regexes)}"
1185 )
1186 self._full_name = _re.sub(_handle_match, self._full_name)
1188 def squash_emoji(self) -> None:
1189 """
1190 Remove emoji from the input string.
1191 """
1192 re_emoji = self.C.regexes.emoji
1193 if re_emoji and re_emoji.search(self._full_name):
1194 self._full_name = re_emoji.sub('', self._full_name)
1196 def squash_bidi(self) -> None:
1197 """
1198 Remove invisible bidirectional control characters from the input
1199 string. They carry no name content but stick to the parts they
1200 surround, so parsed attributes stop comparing equal to the clean name.
1201 """
1202 re_bidi = self.C.regexes.bidi
1203 if re_bidi and re_bidi.search(self._full_name):
1204 self._full_name = re_bidi.sub('', self._full_name)
1206 def handle_firstnames(self) -> None:
1207 """
1208 If there are only two parts and one is a title, assume it's a last name
1209 instead of a first name. e.g. Mr. Johnson. Unless it's a special title
1210 like "Sir", then when it's followed by a single name that name is always
1211 a first name.
1212 """
1213 if self.title \
1214 and len(self) == 2 \
1215 and lc(self.title) not in self.C.first_name_titles:
1216 self.last, self.first = self.first, self.last
1218 def parse_full_name(self) -> None:
1219 """
1221 The main parse method for the parser. This method is run upon
1222 assignment to the :py:attr:`full_name` attribute or instantiation.
1224 Basic flow is to hand off to :py:func:`pre_process` to handle
1225 nicknames. It then splits on commas and chooses a code path depending
1226 on the number of commas.
1228 :py:func:`parse_pieces` then splits those parts on spaces and
1229 :py:func:`join_on_conjunctions` joins any pieces next to conjunctions.
1230 """
1232 self.title_list = []
1233 self.first_list = []
1234 self.middle_list = []
1235 self.last_list = []
1236 self.suffix_list = []
1237 self.nickname_list = []
1238 self.maiden_list = []
1240 # each parse derives these from scratch; entries from a previous
1241 # full_name must not influence this one
1242 self._derived_titles = set()
1243 self._derived_suffixes = set()
1244 self._derived_conjunctions = set()
1245 self._derived_prefixes = set()
1247 self.pre_process()
1249 self._full_name = self.collapse_whitespace(self._full_name)
1251 # break up full_name by commas. A missing "commas" key in a custom
1252 # regexes dict falls back to RegexTupleManager's EMPTY_REGEX, whose
1253 # .split() matches between every character rather than not
1254 # splitting at all -- guard against that so a custom regexes dict
1255 # that omits "commas" disables the comma split instead of shattering
1256 # the name into single characters.
1257 commas = self.C.regexes.commas
1258 parts = [x.strip() for x in (commas.split(self._full_name) if commas.pattern else [self._full_name])]
1259 self._had_comma = len(parts) > 1
1261 log.debug("full_name: %s", self._full_name)
1262 log.debug("parts: %s", parts)
1264 if len(parts) == 1:
1266 # no commas, title first middle middle middle last suffix
1267 # part[0]
1269 pieces = self.parse_pieces(parts)
1270 pieces = self._join_bound_first_name(pieces, reserve_last=True)
1271 p_len = len(pieces)
1272 for i, piece in enumerate(pieces):
1273 try:
1274 nxt = pieces[i + 1]
1275 except IndexError:
1276 nxt = None
1278 # title must have a next piece, unless it's just a title
1279 if not self.first \
1280 and (nxt or p_len == 1) \
1281 and self.is_leading_title(piece):
1282 self.title_list.append(piece)
1283 continue
1284 if not self.first:
1285 if p_len == 1 and self.nickname:
1286 self.last_list.append(piece)
1287 continue
1288 self.first_list.append(piece)
1289 continue
1290 if self.are_suffixes(pieces[i+1:]) or \
1291 (
1292 # if the next piece is the last piece and a roman
1293 # numeral but this piece is not an initial
1294 nxt is not None and \
1295 self.is_roman_numeral(nxt) and i == p_len - 2
1296 and not self.is_an_initial(piece)
1297 ):
1298 # any piece reaching this check as the final piece lands
1299 # here: are_suffixes() is vacuously True for the empty
1300 # tail, making this the last-name branch as well as the
1301 # suffix branch
1302 self.last_list.append(piece)
1303 self.suffix_list += pieces[i+1:]
1304 break
1306 self.middle_list.append(piece)
1307 else:
1308 # if all the end parts are suffixes and there is more than one piece
1309 # in the first part. (Suffixes will never appear after last names
1310 # only, and allows potential first names to be in suffixes, e.g.
1311 # "Johnson, Bart"
1313 post_comma_pieces = self.parse_pieces(parts[1].split(' '), 1)
1315 # Detection must see the delimiter-expanded words too, or a
1316 # delimiter-joined suffix group like "RN - CRNA" would never be
1317 # recognized as suffix-comma format in the first place.
1318 suffix_delimiter_pieces = [word for part in self.expand_suffix_delimiter(parts[1])
1319 for word in part.split(' ')]
1321 if self.are_suffixes_after_comma(suffix_delimiter_pieces) \
1322 and len(parts[0].split(' ')) > 1:
1324 # suffix comma:
1325 # title first middle last [suffix], suffix [suffix] [, suffix]
1326 # parts[0], parts[1:...]
1328 for part in parts[1:]:
1329 # skip empty segments from doubled commas, mirroring the
1330 # parts[2:] guard in the lastname-comma path below
1331 if part:
1332 self.suffix_list += self.expand_suffix_delimiter(part)
1333 pieces = self.parse_pieces(parts[0].split(' '))
1334 pieces = self._join_bound_first_name(pieces, reserve_last=True)
1335 log.debug("pieces: %s", str(pieces))
1336 for i, piece in enumerate(pieces):
1337 try:
1338 nxt = pieces[i + 1]
1339 except IndexError:
1340 nxt = None
1342 if not self.first \
1343 and (nxt or len(pieces) == 1) \
1344 and self.is_leading_title(piece):
1345 self.title_list.append(piece)
1346 continue
1347 if not self.first:
1348 self.first_list.append(piece)
1349 continue
1350 if self.are_suffixes(pieces[i+1:]):
1351 # any piece reaching this check as the final piece
1352 # lands here: are_suffixes() is vacuously True for the
1353 # empty tail, making this the last-name branch as well
1354 # as the suffix branch
1355 self.last_list.append(piece)
1356 self.suffix_list = pieces[i+1:] + self.suffix_list
1357 break
1358 self.middle_list.append(piece)
1359 else:
1361 # lastname comma:
1362 # last [suffix], title first middles[,] suffix [,suffix]
1363 # parts[0], parts[1], parts[2:...]
1365 log.debug("post-comma pieces: %s", str(post_comma_pieces))
1366 post_comma_pieces = self._join_bound_first_name(post_comma_pieces, reserve_last=False)
1368 # lastname part may have suffixes in it
1369 lastname_pieces = self.parse_pieces(parts[0].split(' '), 1)
1370 for piece in lastname_pieces:
1371 # the first one is always a last name, even if it looks like
1372 # a suffix
1373 if self.is_suffix(piece) and len(self.last_list) > 0:
1374 self.suffix_list.append(piece)
1375 else:
1376 self.last_list.append(piece)
1378 for i, piece in enumerate(post_comma_pieces):
1379 try:
1380 nxt = post_comma_pieces[i + 1]
1381 except IndexError:
1382 nxt = None
1384 if not self.first \
1385 and (nxt or len(post_comma_pieces) == 1) \
1386 and self.is_leading_title(piece):
1387 self.title_list.append(piece)
1388 continue
1389 if not self.first:
1390 self.first_list.append(piece)
1391 continue
1392 # A trailing token in a two-part lastname-comma name is
1393 # unambiguously positioned, so use the lenient test that
1394 # accepts suffix_not_acronyms members is_suffix() would
1395 # veto as initials. When parts[2] exists the caller
1396 # already declared an explicit suffix via comma (e.g.
1397 # 'Doe, Rev. John V, Jr.'), making the trailing token
1398 # more likely a middle initial.
1399 if self.is_suffix(piece) or \
1400 (nxt is None and len(parts) == 2
1401 and self.is_suffix_lenient(piece)):
1402 self.suffix_list.append(piece)
1403 continue
1404 self.middle_list.append(piece)
1405 for part in parts[2:]:
1406 # skip empty segments from doubled commas ("Doe, John,, Jr.")
1407 # without dropping the segments that follow them
1408 if part:
1409 self.suffix_list += self.expand_suffix_delimiter(part)
1411 self.post_process()
1413 def parse_pieces(self, parts: Iterable[str], additional_parts_count: int = 0) -> list[str]:
1414 """
1415 Split parts on spaces and remove commas, join on conjunctions and
1416 lastname prefixes. Tokens that are empty after stripping spaces and
1417 commas are dropped, so the returned pieces never contain empty
1418 strings. If parts have periods in the middle, try splitting
1419 on periods and check if the parts are titles or suffixes. If they are,
1420 register the periods-joined part as a derived title/suffix for this
1421 parse so it will be recognized; the constants are not modified.
1423 :param list parts: name part strings from the comma split
1424 :param int additional_parts_count:
1426 if the comma format contains other parts, we need to know
1427 how many there are to decide if things should be considered a
1428 conjunction.
1429 :return: pieces split on spaces and joined on conjunctions
1430 :rtype: list
1431 """
1433 output: list[str] = []
1434 for part in parts:
1435 if not isinstance(part, (str, bytes)):
1436 raise TypeError("Name parts must be strings. "
1437 f" Got {type(part)}")
1438 # drop tokens that strip to nothing (e.g. from a bare "," input or
1439 # an empty comma segment) so no empty piece reaches the parse
1440 # loops and the public *_list attributes
1441 output += [s for s in (x.strip(' ,') for x in part.split(' ')) if s]
1443 # If part contains periods, check if it's multiple titles or suffixes
1444 # together without spaces. If so, register the periods-joined part as
1445 # a derived title/suffix for this parse so it gets recognized later
1446 for part in output:
1447 # if this part has a period not at the beginning or end
1448 if self.C.regexes.period_not_at_end and self.C.regexes.period_not_at_end.match(part):
1449 # split on periods, any of the split pieces titles or suffixes?
1450 # ("Lt.Gov.")
1451 period_chunks = part.split(".")
1452 titles = list(filter(self.is_title, period_chunks))
1453 suffixes = list(filter(self.is_suffix, period_chunks))
1455 # register the part so it will be found by the is_* checks
1456 if titles:
1457 self._derived_titles.add(lc(part))
1458 continue
1459 if suffixes:
1460 self._derived_suffixes.add(lc(part))
1461 continue
1463 return self.join_on_conjunctions(output, additional_parts_count)
1465 def join_on_conjunctions(self, pieces: list[str], additional_parts_count: int = 0) -> list[str]:
1466 """
1467 Join conjunctions to surrounding pieces. Title- and prefix-aware. e.g.:
1469 ['Mr.', 'and', 'Mrs.', 'John', 'Doe'] ==>
1470 ['Mr. and Mrs.', 'John', 'Doe']
1472 ['The', 'Secretary', 'of', 'State', 'Hillary', 'Clinton'] ==>
1473 ['The Secretary of State', 'Hillary', 'Clinton']
1475 When joining titles, registers the newly formed piece as a derived
1476 title for the current parse so it will be recognized correctly later
1477 in the same parse. E.g. while parsing the example names above,
1478 'The Secretary of State' and 'Mr. and Mrs.' are treated as titles.
1479 The configuration in ``self.C`` is never modified.
1481 :param list pieces: name pieces strings after split on spaces
1482 :param int additional_parts_count:
1483 :return: new list with piece next to conjunctions merged into one piece
1484 with spaces in it.
1485 :rtype: list
1487 """
1488 length = len(pieces) + additional_parts_count
1489 # don't join on conjunctions if there's only 2 parts
1490 if length < 3:
1491 return pieces
1493 rootname_pieces = [p for p in pieces if self.is_rootname(p)]
1494 total_length = len(rootname_pieces) + additional_parts_count
1496 # find all the conjunctions, join any conjunctions that are next to each
1497 # other, then join those newly joined conjunctions and any single
1498 # conjunctions to the piece before and after it
1499 conj_index = [i for i, piece in enumerate(pieces)
1500 if self.is_conjunction(piece)]
1502 contiguous_conj_i = group_contiguous_integers(conj_index)
1504 # process ranges in reverse so deleting one range doesn't shift the
1505 # indices of ranges still to be processed
1506 for cont_i in reversed(contiguous_conj_i):
1507 new_piece = " ".join(pieces[cont_i[0]: cont_i[1]+1])
1508 pieces[cont_i[0]:cont_i[1]+1] = [new_piece]
1509 # register newly joined conjunctions to be found later this parse
1510 self._derived_conjunctions.add(lc(new_piece))
1512 if len(pieces) == 1:
1513 # if there's only one piece left, nothing left to do
1514 return pieces
1516 # refresh conjunction index locations
1517 conj_index = [i for i, piece in enumerate(pieces) if self.is_conjunction(piece)]
1519 def register_joined_piece(new_piece: str, neighbor: str) -> None:
1520 if self.is_title(neighbor):
1521 # when joining to a title, make new_piece a title too
1522 self._derived_titles.add(lc(new_piece))
1523 if self.is_prefix(neighbor):
1524 # when joining to a prefix, make new_piece a prefix too, so
1525 # e.g. "von" + "und" bridges into "von und" and can still
1526 # chain onto a following prefix/lastname (see "von und zu")
1527 self._derived_prefixes.add(lc(new_piece))
1529 def shift_conj_index(past: int, by: int) -> None:
1530 # after removing pieces at/after `past`, indices of the
1531 # remaining conjunctions need to shift down by `by`
1532 for j, val in enumerate(conj_index):
1533 if val > past:
1534 conj_index[j] = val - by
1536 for i in conj_index:
1537 if len(pieces[i]) == 1 and total_length < 4 and pieces[i].isalpha():
1538 # if there are only 3 total parts (minus known titles, suffixes
1539 # and prefixes) and this conjunction is a single letter, prefer
1540 # treating it as an initial rather than a conjunction.
1541 # http://code.google.com/p/python-nameparser/issues/detail?id=11
1542 continue
1544 start = max(0, i - 1)
1545 end = min(len(pieces), i + 2)
1546 new_piece = " ".join(pieces[start:end])
1547 neighbor = pieces[start] if start < i else pieces[end - 1]
1548 register_joined_piece(new_piece, neighbor)
1549 pieces[start:end] = [new_piece]
1550 shift_conj_index(past=i, by=end - start - 1)
1552 # join prefixes to following lastnames: ['de la Vega'], ['van Buren']
1553 i = 0
1554 while i < len(pieces):
1555 # total_length >= 1 covers essentially all real input, so this
1556 # treats any leading piece as a first name rather than a prefix.
1557 leading_first_name = i == 0 and total_length >= 1
1558 if not self.is_prefix(pieces[i]) or leading_first_name:
1559 i += 1
1560 continue
1562 # absorb any immediately-adjacent prefixes into one contiguous run
1563 # e.g. "von und zu der" ==> chain them all before looking further
1564 j = i + 1
1565 while j < len(pieces) and self.is_prefix(pieces[j]):
1566 j += 1
1568 # then join everything after the run until the next prefix or suffix
1569 while j < len(pieces) and not self.is_prefix(pieces[j]) and not self.is_suffix(pieces[j]):
1570 j += 1
1572 pieces[i:j] = [' '.join(pieces[i:j])]
1573 i += 1
1575 log.debug("pieces: %s", pieces)
1576 return pieces
1578 # Capitalization Support
1580 def cap_word(self, word: str, attribute: HumanNameAttributeT) -> str:
1581 if (self.is_prefix(word) and attribute in ('last', 'middle')) \
1582 or self.is_conjunction(word):
1583 return word.lower()
1584 exceptions = self.C.capitalization_exceptions
1585 key = lc(word)
1586 for k in (key, key.replace('.', '')):
1587 if k in exceptions:
1588 return exceptions[k]
1589 mac_match = self.C.regexes.mac.match(word)
1590 if mac_match:
1591 def cap_after_mac(m: re.Match) -> str:
1592 return m.group(1).capitalize() + m.group(2).capitalize()
1593 return self.C.regexes.mac.sub(cap_after_mac, word)
1594 else:
1595 return word.capitalize()
1597 def cap_piece(self, piece: str, attribute: HumanNameAttributeT) -> str:
1598 if not piece:
1599 return ""
1601 def replacement(m: re.Match) -> str:
1602 return self.cap_word(m.group(0), attribute)
1604 return self.C.regexes.word.sub(replacement, piece)
1606 def capitalize(self, force: bool | None = None) -> None:
1607 """
1608 The HumanName class can try to guess the correct capitalization of name
1609 entered in all upper or lower case. By default, it will not adjust the
1610 case of names entered in mixed case. To run capitalization on all names
1611 pass the parameter `force=True`.
1613 :param bool force: Forces capitalization of mixed case strings. This
1614 parameter overrides rules set within
1615 :py:class:`~nameparser.config.CONSTANTS`.
1617 **Usage**
1619 .. doctest:: capitalize
1621 >>> name = HumanName('bob v. de la macdole-eisenhower phd')
1622 >>> name.capitalize()
1623 >>> str(name)
1624 'Bob V. de la MacDole-Eisenhower Ph.D.'
1625 >>> # Don't touch good names
1626 >>> name = HumanName('Shirley Maclaine')
1627 >>> name.capitalize()
1628 >>> str(name)
1629 'Shirley Maclaine'
1630 >>> name.capitalize(force=True)
1631 >>> str(name)
1632 'Shirley MacLaine'
1634 """
1635 name = str(self)
1636 force = self.C.force_mixed_case_capitalization \
1637 if force is None else force
1639 if not force and not (name == name.upper() or name == name.lower()):
1640 return
1641 self.title_list = self.cap_piece(self.title, 'title').split()
1642 self.first_list = self.cap_piece(self.first, 'first').split()
1643 self.middle_list = self.cap_piece(self.middle, 'middle').split()
1644 self.last_list = self.cap_piece(self.last, 'last').split()
1645 # suffix is stored comma-separated ("Ph.D., J.D."), not space-separated
1646 self.suffix_list = [s for s in self.cap_piece(self.suffix, 'suffix').split(', ') if s]
1648 def handle_capitalization(self) -> None:
1649 """
1650 Handles capitalization configurations set within
1651 :py:class:`~nameparser.config.CONSTANTS`.
1652 """
1653 if self.C.capitalize_name:
1654 self.capitalize()