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

587 statements  

1import re 

2import warnings 

3from collections.abc import Callable, Iterable, Iterator 

4from operator import itemgetter 

5from itertools import groupby 

6 

7from typing import overload 

8 

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 

14 

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 

26 

27 

28class HumanName: 

29 """ 

30 Parse a person's name into individual components. 

31 

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. 

38 

39 **HumanName Instance Attributes** 

40 

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` 

50 

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 """ 

79 

80 original: str | bytes = '' 

81 """ 

82 The original string, untouched by the parser. 

83 """ 

84 

85 _members = ['title', 'first', 'middle', 'last', 'suffix', 'nickname', 'maiden'] 

86 _full_name = '' 

87 

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 

96 

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) 

119 

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

132 

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) 

153 

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 

187 

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>`_. 

195 

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 

203 

204 @C.setter 

205 def C(self, constants: 'Constants | None') -> None: 

206 self._C = self._validate_constants(constants, stacklevel=3) 

207 

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 

213 

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

224 

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

226 return (value for member in self._members 

227 if (value := getattr(self, member))) 

228 

229 def __len__(self) -> int: 

230 return sum(1 for member in self._members if getattr(self, member)) 

231 

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`. 

236 

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

251 

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) 

274 

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) 

293 

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) 

308 

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

325 

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]>" 

337 

338 def as_dict(self, include_empty: bool = True) -> dict[str, str]: 

339 """ 

340 Return the parsed name as a dictionary of its attributes. 

341 

342 :param bool include_empty: Include keys in the dictionary for empty name attributes. 

343 :rtype: dict 

344 

345 .. doctest:: 

346 

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'} 

352 

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 

363 

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

371 

372 Built from the ``*_list`` attributes, so it is unaffected by 

373 display settings like ``string_format`` and 

374 ``empty_attribute_default``. 

375 

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. 

379 

380 .. doctest:: 

381 

382 >>> HumanName("Dr. Juan Q. Xavier de la Vega III").comparison_key() 

383 ('dr.', 'juan', 'q. xavier', 'de la vega', 'iii', '', '') 

384 

385 """ 

386 return tuple( 

387 " ".join(getattr(self, member + "_list")).lower() 

388 for member in self._members 

389 ) 

390 

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. 

400 

401 .. doctest:: 

402 

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 

408 

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)``. 

414 

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 ) 

426 

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 '' 

446 

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

457 

458 def initials_list(self) -> list[str]: 

459 """ 

460 Returns the initials as a list 

461 

462 .. doctest:: 

463 

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 

473 

474 def initials(self) -> str: 

475 """ 

476 Return formatted initials for the name, controlled by 

477 ``initials_format``, ``initials_delimiter``, and ``initials_separator``. 

478 

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. 

483 

484 .. doctest:: 

485 

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 """ 

496 

497 first_initials_list, middle_initials_list, last_initials_list = self._initials_lists() 

498 

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 } 

512 

513 _s = self.initials_format.format(**initials_dict) # noqa: UP032 

514 return self.collapse_whitespace(_s) or self.C.empty_attribute_default 

515 

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 

523 

524 # attributes 

525 

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 

535 

536 @title.setter 

537 def title(self, value: str | list[str] | None) -> None: 

538 self._set_list('title', value) 

539 

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 

547 

548 @first.setter 

549 def first(self, value: str | list[str] | None) -> None: 

550 self._set_list('first', value) 

551 

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 

559 

560 @middle.setter 

561 def middle(self, value: str | list[str] | None) -> None: 

562 self._set_list('middle', value) 

563 

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 

571 

572 @last.setter 

573 def last(self, value: str | list[str] | None) -> None: 

574 self._set_list('last', value) 

575 

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 

586 

587 @suffix.setter 

588 def suffix(self, value: str | list[str] | None) -> None: 

589 self._set_list('suffix', value) 

590 

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 

598 

599 @nickname.setter 

600 def nickname(self, value: str | list[str] | None) -> None: 

601 self._set_list('nickname', value) 

602 

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 

612 

613 @maiden.setter 

614 def maiden(self, value: str | list[str] | None) -> None: 

615 self._set_list('maiden', value) 

616 

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 

623 

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 

630 

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 

637 

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 

644 

645 def _split_last(self) -> tuple[list[str], list[str]]: 

646 """Return (prefix_particles, base_words) split from the last name. 

647 

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

652 

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:] 

668 

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`. 

675 

676 >>> HumanName("Juan de la Vega").last_prefixes_list 

677 ['de', 'la'] 

678 """ 

679 return self._split_last()[0] 

680 

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`. 

687 

688 >>> HumanName("Vincent van Gogh").last_base_list 

689 ['Gogh'] 

690 """ 

691 return self._split_last()[1] 

692 

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. 

700 

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 

707 

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`). 

715 

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 

722 

723 # setter methods 

724 

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

737 

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 

745 

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

760 

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) 

772 

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 

786 

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 

790 

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 

795 

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. 

798 

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 

822 

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

829 

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

835 

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) 

852 

853 def are_suffixes(self, pieces: Iterable[str]) -> bool: 

854 """Return True if all pieces are suffixes. 

855 

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 

864 

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. 

868 

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) 

875 

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] 

886 

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) 

894 

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) 

905 

906 def is_an_initial(self, value: str) -> bool: 

907 """ 

908 Words with a single period at the end, or a single uppercase letter. 

909 

910 Matches the ``initial`` regular expression in 

911 :py:data:`~nameparser.config.regexes.REGEXES`. 

912 """ 

913 return bool(self.C.regexes.initial.match(value)) 

914 

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 ) 

928 

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 ) 

941 

942 # full_name parser 

943 

944 @property 

945 def full_name(self) -> str: 

946 """The string output of the HumanName instance.""" 

947 return str(self) 

948 

949 @full_name.setter 

950 def full_name(self, value: str | bytes) -> None: 

951 self._apply_full_name(value, stacklevel=3) 

952 

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 

959 

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 

973 

974 self.parse_full_name() 

975 

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 

982 

983 def pre_process(self) -> None: 

984 """ 

985 

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`. 

991 

992 """ 

993 self.squash_bidi() 

994 self.fix_phd() 

995 self.parse_nicknames() 

996 self.squash_emoji() 

997 

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 ) 

1020 

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 ) 

1041 

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 = [] 

1056 

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 = [] 

1065 

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

1080 

1081 def fix_phd(self) -> None: 

1082 _re = self.C.regexes.phd 

1083 

1084 if match := _re.search(self._full_name): 

1085 self.suffix_list.extend(match.groups()) 

1086 self._full_name = _re.sub("", self._full_name) 

1087 

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. 

1099 

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. 

1103 

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 """ 

1116 

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 

1152 

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) 

1187 

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) 

1195 

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) 

1205 

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 

1217 

1218 def parse_full_name(self) -> None: 

1219 """ 

1220 

1221 The main parse method for the parser. This method is run upon 

1222 assignment to the :py:attr:`full_name` attribute or instantiation. 

1223 

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. 

1227 

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 """ 

1231 

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 = [] 

1239 

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

1246 

1247 self.pre_process() 

1248 

1249 self._full_name = self.collapse_whitespace(self._full_name) 

1250 

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 

1260 

1261 log.debug("full_name: %s", self._full_name) 

1262 log.debug("parts: %s", parts) 

1263 

1264 if len(parts) == 1: 

1265 

1266 # no commas, title first middle middle middle last suffix 

1267 # part[0] 

1268 

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 

1277 

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 

1305 

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" 

1312 

1313 post_comma_pieces = self.parse_pieces(parts[1].split(' '), 1) 

1314 

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

1320 

1321 if self.are_suffixes_after_comma(suffix_delimiter_pieces) \ 

1322 and len(parts[0].split(' ')) > 1: 

1323 

1324 # suffix comma: 

1325 # title first middle last [suffix], suffix [suffix] [, suffix] 

1326 # parts[0], parts[1:...] 

1327 

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 

1341 

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: 

1360 

1361 # lastname comma: 

1362 # last [suffix], title first middles[,] suffix [,suffix] 

1363 # parts[0], parts[1], parts[2:...] 

1364 

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) 

1367 

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) 

1377 

1378 for i, piece in enumerate(post_comma_pieces): 

1379 try: 

1380 nxt = post_comma_pieces[i + 1] 

1381 except IndexError: 

1382 nxt = None 

1383 

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) 

1410 

1411 self.post_process() 

1412 

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. 

1422 

1423 :param list parts: name part strings from the comma split 

1424 :param int additional_parts_count: 

1425 

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 """ 

1432 

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] 

1442 

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

1454 

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 

1462 

1463 return self.join_on_conjunctions(output, additional_parts_count) 

1464 

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.: 

1468 

1469 ['Mr.', 'and', 'Mrs.', 'John', 'Doe'] ==> 

1470 ['Mr. and Mrs.', 'John', 'Doe'] 

1471 

1472 ['The', 'Secretary', 'of', 'State', 'Hillary', 'Clinton'] ==> 

1473 ['The Secretary of State', 'Hillary', 'Clinton'] 

1474 

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. 

1480 

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 

1486 

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 

1492 

1493 rootname_pieces = [p for p in pieces if self.is_rootname(p)] 

1494 total_length = len(rootname_pieces) + additional_parts_count 

1495 

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

1501 

1502 contiguous_conj_i = group_contiguous_integers(conj_index) 

1503 

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

1511 

1512 if len(pieces) == 1: 

1513 # if there's only one piece left, nothing left to do 

1514 return pieces 

1515 

1516 # refresh conjunction index locations 

1517 conj_index = [i for i, piece in enumerate(pieces) if self.is_conjunction(piece)] 

1518 

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

1528 

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 

1535 

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 

1543 

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) 

1551 

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 

1561 

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 

1567 

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 

1571 

1572 pieces[i:j] = [' '.join(pieces[i:j])] 

1573 i += 1 

1574 

1575 log.debug("pieces: %s", pieces) 

1576 return pieces 

1577 

1578 # Capitalization Support 

1579 

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

1596 

1597 def cap_piece(self, piece: str, attribute: HumanNameAttributeT) -> str: 

1598 if not piece: 

1599 return "" 

1600 

1601 def replacement(m: re.Match) -> str: 

1602 return self.cap_word(m.group(0), attribute) 

1603 

1604 return self.C.regexes.word.sub(replacement, piece) 

1605 

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`. 

1612 

1613 :param bool force: Forces capitalization of mixed case strings. This 

1614 parameter overrides rules set within 

1615 :py:class:`~nameparser.config.CONSTANTS`. 

1616 

1617 **Usage** 

1618 

1619 .. doctest:: capitalize 

1620 

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' 

1633 

1634 """ 

1635 name = str(self) 

1636 force = self.C.force_mixed_case_capitalization \ 

1637 if force is None else force 

1638 

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] 

1647 

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