-
Notifications
You must be signed in to change notification settings - Fork 111
Expand file tree
/
Copy pathparser.py
More file actions
1654 lines (1448 loc) · 71 KB
/
Copy pathparser.py
File metadata and controls
1654 lines (1448 loc) · 71 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
import re
import warnings
from collections.abc import Callable, Iterable, Iterator
from operator import itemgetter
from itertools import groupby
from typing import overload
from nameparser.util import HumanNameAttributeT, lc
from nameparser.util import log
from nameparser.config import CONSTANTS
from nameparser.config import Constants
from nameparser.config import DEFAULT_ENCODING
def group_contiguous_integers(data: Iterable[int]) -> list[tuple[int, int]]:
"""
return list of tuples containing first and last index
position of contiguous numbers in a series
"""
ranges: list[tuple[int, int]] = []
for key, group_with_indices in groupby(enumerate(data), lambda i: i[0] - i[1]):
group = list(map(itemgetter(1), group_with_indices))
if len(group) > 1:
ranges.append((group[0], group[-1]))
return ranges
class HumanName:
"""
Parse a person's name into individual components.
Instantiation assigns to ``full_name``, and assignment to
:py:attr:`full_name` triggers :py:func:`parse_full_name`. After parsing the
name, these instance attributes are available. Alternatively, you can pass
any of the instance attributes to the constructor method and skip the parsing
process. If any of the the instance attributes are passed to the constructor
as keywords, :py:func:`parse_full_name` will not be performed.
**HumanName Instance Attributes**
* :py:attr:`title`
* :py:attr:`first`
* :py:attr:`middle`
* :py:attr:`last`
* :py:attr:`suffix`
* :py:attr:`nickname`
* :py:attr:`maiden`
* :py:attr:`surnames`
* :py:attr:`given_names`
:param str full_name: The name string to be parsed.
:param constants:
a :py:class:`~nameparser.config.Constants` instance (subclasses are
honored). Defaults to the shared module-level ``CONSTANTS``. For
`per-instance config <customize.html>`_, pass ``Constants()`` for
fresh library defaults, or ``CONSTANTS.copy()`` for a private
snapshot of the current shared config. Passing ``None`` also builds
a fresh ``Constants()``, but is deprecated (warns; raises
``TypeError`` in 2.0, see issue #260) since it silently discards any
customizations the caller may have expected to carry over. Anything
else raises ``TypeError``.
:param str encoding: string representing the encoding of your input
(deprecated with ``bytes`` input, removal in 2.0 — decode before
passing; see issue #245)
:param str string_format: python string formatting
:param str initials_format: python initials string formatting
:param str initials_delimter: string delimiter for initials
:param str initials_separator: string separator between consecutive initials
:param str suffix_delimiter: additional delimiter to split post-comma parts
before suffix detection, e.g. ``" - "`` for ``"RN - CRNA"``
:param str first: first name
:param str middle: middle name
:param str last: last name
:param str title: The title or prenominal
:param str suffix: The suffix or postnominal
:param str nickname: Nicknames
:param str maiden: Maiden name
"""
original: str | bytes = ''
"""
The original string, untouched by the parser.
"""
_members = ['title', 'first', 'middle', 'last', 'suffix', 'nickname', 'maiden']
_full_name = ''
title_list: list[str]
first_list: list[str]
middle_list: list[str]
last_list: list[str]
suffix_list: list[str]
nickname_list: list[str]
maiden_list: list[str]
_had_comma: bool
def __init__(
self,
full_name: str | bytes = "",
constants: Constants | None = CONSTANTS,
encoding: str = DEFAULT_ENCODING,
string_format: str | None = None,
initials_format: str | None = None,
initials_delimiter: str | None = None,
initials_separator: str | None = None,
suffix_delimiter: str | None = None,
first: str | list[str] | None = None,
middle: str | list[str] | None = None,
last: str | list[str] | None = None,
title: str | list[str] | None = None,
suffix: str | list[str] | None = None,
nickname: str | list[str] | None = None,
maiden: str | list[str] | None = None,
) -> None:
# calls _validate_constants directly (not through the C setter) so
# the deprecation warning below attributes to this constructor's
# caller rather than to the setter, mirroring _apply_full_name below
self._C = self._validate_constants(constants, stacklevel=3)
# Lookup entries derived while parsing this instance (period-joined
# titles/suffixes like "Lt.Gov.", conjunction-joined pieces like
# "Mr. and Mrs." or "von und zu"). Kept separate from self.C so that
# parsing never writes into the config — which is usually the shared
# module-level CONSTANTS — keeping results independent of what was
# parsed before and config reads safe across threads. Values are
# lc()-normalized, mirroring how SetManager stores them. Reset at the
# start of each parse_full_name() run.
self._derived_titles: set[str] = set()
self._derived_suffixes: set[str] = set()
self._derived_conjunctions: set[str] = set()
self._derived_prefixes: set[str] = set()
self.encoding = encoding
self.string_format = string_format if string_format is not None else self.C.string_format
self.initials_format = initials_format if initials_format is not None else self.C.initials_format
self.initials_delimiter = initials_delimiter if initials_delimiter is not None else self.C.initials_delimiter
self.initials_separator = initials_separator if initials_separator is not None else self.C.initials_separator
self.suffix_delimiter = suffix_delimiter if suffix_delimiter is not None else self.C.suffix_delimiter
self._had_comma = False
if (first or middle or last or title or suffix or nickname or maiden):
self.first = first
self.middle = middle
self.last = last
self.title = title
self.suffix = suffix
self.nickname = nickname
self.maiden = maiden
else:
# calls _apply_full_name directly (not the setter) so the
# deprecation warning below attributes to this constructor's
# caller rather than to the setter
self._apply_full_name(full_name, stacklevel=3)
@staticmethod
def _validate_constants(constants: 'Constants | None', *, stacklevel: int) -> 'Constants':
# Shared by the constructor and the C setter so both assignment paths
# give the same immediate TypeError instead of one bypassing the
# other and failing far from the cause (#239).
if constants is None:
# deprecated 1.4.0, raises TypeError in 2.0 (#260, removal #261):
# None means "build a fresh private Constants()", the opposite of
# what None conventionally means (the default is the *shared*
# CONSTANTS) -- an easy trap since customizing CONSTANTS then
# passing None elsewhere silently drops those customizations with
# no error. CONSTANTS.copy() is the explicit spelling for the
# other reading: a private snapshot of the current shared config.
warnings.warn(
"Passing constants=None is deprecated and will raise "
"TypeError in 2.0; use constants=Constants() for fresh "
"library defaults, or constants=CONSTANTS.copy() to snapshot "
"the current shared config. See "
"https://github.com/derek73/python-nameparser/issues/260",
DeprecationWarning,
stacklevel=stacklevel,
)
return Constants()
if not isinstance(constants, Constants):
# passing the class itself is the likeliest mistake, and
# reporting it as "got type" would only add confusion
hint = (" (a class was passed; did you mean Constants()?)"
if isinstance(constants, type) else "")
raise TypeError(
"constants must be a Constants instance or None, "
f"got {type(constants).__name__}{hint}"
)
return constants
@property
def C(self) -> 'Constants':
"""
A reference to the configuration for this instance, which may or may not be
a reference to the shared, module-wide instance at
:py:mod:`~nameparser.config.CONSTANTS`. See `Customizing the Parser
<customize.html>`_.
Assigning a non-``Constants`` value (besides ``None``, which builds a
fresh private ``Constants()`` and emits a ``DeprecationWarning`` --
see :py:meth:`~nameparser.parser.HumanName.__init__`) raises the same
``TypeError`` as passing an invalid ``constants`` argument to the
constructor (#239).
"""
return self._C
@C.setter
def C(self, constants: 'Constants | None') -> None:
self._C = self._validate_constants(constants, stacklevel=3)
def __getstate__(self) -> dict:
state = self.__dict__.copy()
c = state.pop('_C')
state['C'] = None if c is CONSTANTS else c # sentinel: restore shared singleton on load
return state
def __setstate__(self, state: dict) -> None:
state = dict(state)
c = state.pop('C', None)
self._C = CONSTANTS if c is None else c
self.__dict__.update(state)
# pickles from before the per-parse derived sets existed lack them;
# backfill so the is_* predicates work without a re-parse
for attr in ('_derived_titles', '_derived_suffixes',
'_derived_conjunctions', '_derived_prefixes'):
self.__dict__.setdefault(attr, set())
def __iter__(self) -> Iterator[str]:
return (value for member in self._members
if (value := getattr(self, member)))
def __len__(self) -> int:
return sum(1 for member in self._members if getattr(self, member))
def __eq__(self, other: object) -> bool:
"""
.. deprecated:: 1.3.0
Removed in 2.0 (see issue #223); use :py:meth:`matches`.
HumanName instances are equal to other objects whose
lower case unicode representation is the same. Note the
differences from :py:meth:`matches`: this compares formatted
output, so it depends on ``string_format`` and cannot see
``maiden``, and it stringifies operands of any type.
"""
warnings.warn(
"HumanName == comparison is deprecated and will be removed in "
"2.0; use matches() instead. See "
"https://github.com/derek73/python-nameparser/issues/223",
DeprecationWarning,
stacklevel=2,
)
return str(self).lower() == str(other).lower()
@overload
def __getitem__(self, key: slice) -> list[str]: ...
@overload
def __getitem__(self, key: str) -> str: ...
def __getitem__(self, key: slice | str) -> str | list[str]:
"""
.. deprecated:: 1.4.0
Slice access (``name[1:-3]``) is removed in 2.0 (see issue
#258); field access by position has no real use case.
String-key access (``name['first']``) is unaffected.
"""
if isinstance(key, slice):
warnings.warn(
"Slicing a HumanName by position is deprecated and will be "
"removed in 2.0; access the named attributes instead. See "
"https://github.com/derek73/python-nameparser/issues/258",
DeprecationWarning,
stacklevel=2,
)
return [getattr(self, x) for x in self._members[key]]
else:
return getattr(self, key)
def __setitem__(self, key: str, value: str | list[str] | None) -> None:
"""
.. deprecated:: 1.4.0
Removed in 2.0 (see issue #258); it duplicates plain attribute
assignment. Use ``name.first = value`` instead.
"""
warnings.warn(
"HumanName item assignment is deprecated and will be removed "
"in 2.0; it duplicates plain attribute assignment, use "
"name.first = value instead. See "
"https://github.com/derek73/python-nameparser/issues/258",
DeprecationWarning,
stacklevel=2,
)
if key in self._members:
self._set_list(key, value)
else:
raise KeyError("Not a valid HumanName attribute", key)
def __str__(self) -> str:
if self.string_format is not None:
# string_format = "{title} {first} {middle} {last} {suffix} ({nickname})"
# Empty attributes must render as '' (not empty_attribute_default,
# which may be None) so str.format does not interpolate the
# literal "None" into the output, which cannot be scrubbed
# afterward without corrupting name text containing the same
# substring (#254).
_s = self.string_format.format(**{k: v or '' for k, v in self.as_dict().items()})
# remove trailing punctuation from missing nicknames
_s = _s.replace(" ()", "").replace(" ''", "").replace(' ""', "")
_s = self.C.regexes.space_before_comma.sub(',', _s)
return self.collapse_whitespace(_s).strip(', ')
return " ".join(self)
def __hash__(self) -> int:
"""
.. deprecated:: 1.3.0
Removed in 2.0 (see issue #223); use :py:meth:`comparison_key`
for sets, dicts, and dedup.
"""
warnings.warn(
"hash(HumanName) is deprecated and will be removed in 2.0; use "
"comparison_key() for sets and dicts. See "
"https://github.com/derek73/python-nameparser/issues/223",
DeprecationWarning,
stacklevel=2,
)
# __eq__ compares lowercased strings, so hash the lowercased string
# to keep equal instances in the same hash bucket.
return hash(str(self).lower())
def __repr__(self) -> str:
attrs = (
f" title: {self.title or ''!r}\n"
f" first: {self.first or ''!r}\n"
f" middle: {self.middle or ''!r}\n"
f" last: {self.last or ''!r}\n"
f" suffix: {self.suffix or ''!r}\n"
f" nickname: {self.nickname or ''!r}\n"
f" maiden: {self.maiden or ''!r}"
)
return f"<{self.__class__.__name__} : [\n{attrs}\n]>"
def as_dict(self, include_empty: bool = True) -> dict[str, str]:
"""
Return the parsed name as a dictionary of its attributes.
:param bool include_empty: Include keys in the dictionary for empty name attributes.
:rtype: dict
.. doctest::
>>> name = HumanName("Bob Dole")
>>> name.as_dict()
{'title': '', 'first': 'Bob', 'middle': '', 'last': 'Dole', 'suffix': '', 'nickname': '', 'maiden': ''}
>>> name.as_dict(False)
{'first': 'Bob', 'last': 'Dole'}
"""
d = {}
for m in self._members:
if include_empty:
d[m] = getattr(self, m)
else:
val = getattr(self, m)
if val:
d[m] = val
return d
def comparison_key(self) -> tuple[str, ...]:
"""
The seven name components (title, first, middle, last, suffix,
nickname, maiden) as a lowercased tuple: a canonical, hashable
identity for the parsed name. Use it for dedup, dict keys, and
sorting or grouping, e.g.
``unique = {n.comparison_key(): n for n in names}.values()``.
Built from the ``*_list`` attributes, so it is unaffected by
display settings like ``string_format`` and
``empty_attribute_default``.
Empty or unparsable input yields the all-empty key, so such names
all compare equal and collide in dedup; screen them out with
``len(name) == 0`` first.
.. doctest::
>>> HumanName("Dr. Juan Q. Xavier de la Vega III").comparison_key()
('dr.', 'juan', 'q. xavier', 'de la vega', 'iii', '', '')
"""
return tuple(
" ".join(getattr(self, member + "_list")).lower()
for member in self._members
)
def matches(self, other: 'str | HumanName') -> bool:
"""
Compare parsed components case-insensitively; the semantic
replacement for the deprecated ``==``. A ``str`` argument is parsed
first, using this instance's configuration, so any written form of
the same name matches; a ``HumanName`` argument is compared as
already parsed — its own configuration determined its components.
Two empty or unparsable names match each other; check
``len(name) == 0`` to screen them.
.. doctest::
>>> name = HumanName("Dr. Juan Q. Xavier de la Vega III")
>>> name.matches("de la vega, dr. juan Q. xavier III")
True
>>> name.matches("Juan de la Vega")
False
Unlike the deprecated ``==``, all seven components participate
(including ``maiden``, which the default ``string_format`` omits)
and display settings have no effect. Raises ``TypeError`` for
anything that is not a ``str`` or ``HumanName``; guard optional
values explicitly, e.g. ``x is not None and name.matches(x)``.
Parses string arguments on every call. When matching one name
against many candidates, parse the candidates once or compare
:py:meth:`comparison_key` values instead.
"""
if isinstance(other, HumanName):
return self.comparison_key() == other.comparison_key()
if isinstance(other, str):
return self.comparison_key() == type(self)(other, self.C).comparison_key()
raise TypeError(
f"matches() requires a str or HumanName, got {type(other).__name__}"
)
def _process_initial(self, name_part: str, firstname: bool = False) -> str:
"""
Name parts may include prefixes or conjunctions. This function filters these from the name unless it is
a first name, since first names cannot be conjunctions or prefixes.
"""
# split() rather than split(" "): *_list attributes assigned directly
# bypass parse_pieces whitespace normalization, and split(" ") yields
# empty strings for repeated spaces (#232)
parts = name_part.split()
initials = []
for part in parts:
if not (self.is_prefix(part) or self.is_conjunction(part)) or firstname:
initials.append(part[0])
if len(initials) > 0:
return self.initials_separator.join(initials)
# Return '' (never empty_attribute_default, which may be None) when a
# part has no initialable words, e.g. a middle name consisting only of
# prefixes ("de la"). Callers drop these parts entirely.
return ''
def _initials_lists(self) -> tuple[list[str], list[str], list[str]]:
"""Initials for the first, middle and last name groups. Parts that
yield no initials (e.g. a prefix-only middle name like "de la") are
dropped rather than kept as empty strings.
"""
def group_initials(names: list[str], firstname: bool = False) -> list[str]:
return [i for i in (self._process_initial(n, firstname) for n in names if n) if i]
return (group_initials(self.first_list, True),
group_initials(self.middle_list),
group_initials(self.last_list))
def initials_list(self) -> list[str]:
"""
Returns the initials as a list
.. doctest::
>>> name = HumanName("Sir Bob Andrew Dole")
>>> name.initials_list()
['B', 'A', 'D']
>>> name = HumanName("J. Doe")
>>> name.initials_list()
['J', 'D']
"""
first_initials_list, middle_initials_list, last_initials_list = self._initials_lists()
return first_initials_list + middle_initials_list + last_initials_list
def initials(self) -> str:
"""
Return formatted initials for the name, controlled by
``initials_format``, ``initials_delimiter``, and ``initials_separator``.
``initials_delimiter`` is appended after each individual initial.
``initials_separator`` is placed between consecutive initials within
a name group (first, middle, or last). Both can be set as
``Constants`` attributes or as ``HumanName`` constructor kwargs.
.. doctest::
>>> name = HumanName("Sir Bob Andrew Dole")
>>> name.initials()
'B. A. D.'
>>> name = HumanName("Sir Bob Andrew Dole", initials_format="{first} {middle}")
>>> name.initials()
'B. A.'
>>> name = HumanName("Doe, John A.", initials_delimiter="", initials_separator="")
>>> name.initials()
'J A D'
"""
first_initials_list, middle_initials_list, last_initials_list = self._initials_lists()
# Empty name groups must render as '' (not empty_attribute_default,
# which may be None) so str.format does not interpolate the literal
# "None" into the output. A fully-empty result falls back to
# empty_attribute_default, matching the other attribute accessors
# (e.g. ``first``).
initials_dict = {
"first": (self.initials_delimiter + self.initials_separator).join(first_initials_list) + self.initials_delimiter
if len(first_initials_list) else "",
"middle": (self.initials_delimiter + self.initials_separator).join(middle_initials_list) + self.initials_delimiter
if len(middle_initials_list) else "",
"last": (self.initials_delimiter + self.initials_separator).join(last_initials_list) + self.initials_delimiter
if len(last_initials_list) else ""
}
_s = self.initials_format.format(**initials_dict) # noqa: UP032
return self.collapse_whitespace(_s) or self.C.empty_attribute_default
@property
def has_own_config(self) -> bool:
"""
True if this instance is not using the shared module-level
configuration.
"""
return self.C is not CONSTANTS
# attributes
@property
def title(self) -> str:
"""
The person's titles. Any string of consecutive pieces in
:py:mod:`~nameparser.config.titles` or
:py:mod:`~nameparser.config.conjunctions`
at the beginning of :py:attr:`full_name`.
"""
return " ".join(self.title_list) or self.C.empty_attribute_default
@title.setter
def title(self, value: str | list[str] | None) -> None:
self._set_list('title', value)
@property
def first(self) -> str:
"""
The person's first name. The first name piece after any known
:py:attr:`title` pieces parsed from :py:attr:`full_name`.
"""
return " ".join(self.first_list) or self.C.empty_attribute_default
@first.setter
def first(self, value: str | list[str] | None) -> None:
self._set_list('first', value)
@property
def middle(self) -> str:
"""
The person's middle names. All name pieces after the first name and
before the last name parsed from :py:attr:`full_name`.
"""
return " ".join(self.middle_list) or self.C.empty_attribute_default
@middle.setter
def middle(self, value: str | list[str] | None) -> None:
self._set_list('middle', value)
@property
def last(self) -> str:
"""
The person's last name. The last name piece parsed from
:py:attr:`full_name`.
"""
return " ".join(self.last_list) or self.C.empty_attribute_default
@last.setter
def last(self, value: str | list[str] | None) -> None:
self._set_list('last', value)
@property
def suffix(self) -> str:
"""
The persons's suffixes. Pieces at the end of the name that are found in
:py:mod:`~nameparser.config.suffixes`, or pieces that are at the end
of comma separated formats, e.g.
"Lastname, Title Firstname Middle[,] Suffix [, Suffix]" parsed
from :py:attr:`full_name`.
"""
return ", ".join(self.suffix_list) or self.C.empty_attribute_default
@suffix.setter
def suffix(self, value: str | list[str] | None) -> None:
self._set_list('suffix', value)
@property
def nickname(self) -> str:
"""
The person's nicknames. Any text found inside of quotes (``""``) or
parenthesis (``()``)
"""
return " ".join(self.nickname_list) or self.C.empty_attribute_default
@nickname.setter
def nickname(self, value: str | list[str] | None) -> None:
self._set_list('nickname', value)
@property
def maiden(self) -> str:
"""
The person's maiden (alternate/prior) last name. Empty unless a
delimiter has been routed to it via
:py:attr:`~nameparser.config.Constants.maiden_delimiters` -- see the
"Routing to Maiden Name" section of the customization docs.
"""
return " ".join(self.maiden_list) or self.C.empty_attribute_default
@maiden.setter
def maiden(self, value: str | list[str] | None) -> None:
self._set_list('maiden', value)
@property
def surnames_list(self) -> list[str]:
"""
List of middle names followed by last name.
"""
return self.middle_list + self.last_list
@property
def surnames(self) -> str:
"""
A string of all middle names followed by the last name.
"""
return " ".join(self.surnames_list) or self.C.empty_attribute_default
@property
def given_names_list(self) -> list[str]:
"""
List of first name followed by middle names.
"""
return self.first_list + self.middle_list
@property
def given_names(self) -> str:
"""
A string of the first name followed by all middle names.
"""
return " ".join(self.given_names_list) or self.C.empty_attribute_default
def _split_last(self) -> tuple[list[str], list[str]]:
"""Return (prefix_particles, base_words) split from the last name.
The base_words list is never empty: if every word in the last name
matches a prefix particle, the guard fires and all words are returned
as the base with an empty prefix list (heuristic: a family name is
assumed not to consist entirely of particles).
>>> HumanName("Vincent van Gogh")._split_last()
(['van'], ['Gogh'])
>>> HumanName("Anh Do")._split_last()
([], ['Do'])
"""
words = " ".join(self.last_list).split()
i = 0
while i < len(words) and self.is_prefix(words[i]):
i += 1
if i == len(words):
# Heuristic: assume a family name isn't entirely composed of
# particles (e.g. surname "Do" which also appears in PREFIXES).
# Don't strip — treat the whole last name as the base.
return [], words
return words[:i], words[i:]
@property
def last_prefixes_list(self) -> list[str]:
"""
List of leading prefix particles in the last name (the *tussenvoegsel*).
Returns ``[]`` when there are none, including the case where every word
in the last name matches a prefix — see :py:meth:`_split_last`.
>>> HumanName("Juan de la Vega").last_prefixes_list
['de', 'la']
"""
return self._split_last()[0]
@property
def last_base_list(self) -> list[str]:
"""
List of last-name words after stripping leading prefix particles.
Never empty: when every word matches a prefix, no stripping occurs and
the full last name is returned — see :py:meth:`_split_last`.
>>> HumanName("Vincent van Gogh").last_base_list
['Gogh']
"""
return self._split_last()[1]
@property
def last_base(self) -> str:
"""
The last name with leading prefix particles removed (the core surname).
For ``"van Gogh"`` this is ``"Gogh"``; for ``"Smith"`` it is ``"Smith"``.
``last`` is always unchanged. When every word in the last name matches a
prefix particle, no stripping occurs and the full last name is returned.
>>> HumanName("Vincent van Gogh").last_base
'Gogh'
>>> HumanName("John Smith").last_base
'Smith'
"""
return " ".join(self.last_base_list) or self.C.empty_attribute_default
@property
def last_prefixes(self) -> str:
"""
The leading prefix particle(s) of the last name (the *tussenvoegsel*).
Returns ``""`` (or ``empty_attribute_default``) when there are none,
including when every word in the last name matches a prefix particle
(the all-particles guard; see :py:meth:`_split_last`).
>>> HumanName("Vincent van Gogh").last_prefixes
'van'
>>> HumanName("Juan de la Vega").last_prefixes
'de la'
"""
return " ".join(self.last_prefixes_list) or self.C.empty_attribute_default
# setter methods
def _set_list(self, attr: str, value: str | list[str] | None) -> None:
if isinstance(value, list):
val = value
elif isinstance(value, (str, bytes)):
val = [value]
elif value is None:
val = []
else:
raise TypeError(
"Can only assign strings, lists or None to name attributes."
f" Got {type(value)}")
setattr(self, attr+"_list", self.parse_pieces(val))
# Parse helpers
def is_title(self, value: str) -> bool:
"""Is in the :py:data:`~nameparser.config.titles.TITLES` set or was
derived as a title earlier in this parse (e.g. ``"Lt.Gov."``,
``"Mr. and Mrs."``)."""
word = lc(value)
return word in self.C.titles or word in self._derived_titles
def is_leading_title(self, piece: str) -> bool:
"""
True if ``piece`` is a known title, or an unrecognized multi-letter
word ending in a single trailing period (e.g. ``"Major."``). The
``{2,}`` in the ``period_abbreviation`` regex, not a separate
``is_an_initial()`` check, is what excludes single-letter initials
like ``"J."``. Only meaningful for pieces in the title position
(before the first name is set) — a period-abbreviation appearing
later in the name is left as a middle name. The match is not
registered in ``C.titles`` or the per-parse derived titles, so
matching ``"Major."`` here never makes ``"Major"`` (or ``"Major."``)
a recognized title elsewhere, even within the same parse.
"""
return self.is_title(piece) or bool(self.C.regexes.period_abbreviation.match(piece))
def is_conjunction(self, piece: str | list[str]) -> bool:
"""Is in the conjunctions set — config or derived earlier in this
parse (e.g. ``"of the"``) — and not :py:func:`is_an_initial()`."""
if isinstance(piece, list):
for item in piece:
if self.is_conjunction(item):
return True
return False
return (piece.lower() in self.C.conjunctions
or piece.lower() in self._derived_conjunctions) \
and not self.is_an_initial(piece)
def is_prefix(self, piece: str | list[str]) -> bool:
"""
Lowercased, leading/trailing-periods-stripped version of piece is in the
:py:data:`~nameparser.config.prefixes.PREFIXES` set, or was derived as
a prefix earlier in this parse (e.g. ``"von und"``).
"""
if isinstance(piece, list):
for item in piece:
if self.is_prefix(item):
return True
return False
word = lc(piece)
return word in self.C.prefixes or word in self._derived_prefixes
def is_bound_first_name(self, piece: str) -> bool:
"""Lowercased, leading/trailing-periods-stripped version of piece is in :py:attr:`~nameparser.config.Constants.bound_first_names`."""
return lc(piece) in self.C.bound_first_names
def is_non_first_name_prefix(self, piece: str) -> bool:
"""Lowercased, leading/trailing-periods-stripped version of piece is in
:py:attr:`~nameparser.config.Constants.non_first_name_prefixes`."""
return lc(piece) in self.C.non_first_name_prefixes
def _join_bound_first_name(self, pieces: list[str], reserve_last: bool) -> list[str]:
"""Join a first-name prefix to its following piece.
Finds the first non-title piece; if it is in ``bound_first_names``,
merges it with the next piece — unless ``reserve_last`` is True and no
further piece would remain for the last name.
"""
fi = next((i for i, p in enumerate(pieces) if not self.is_title(p)), None)
if fi is None:
return pieces
if not self.is_bound_first_name(pieces[fi]):
return pieces
next_i = fi + 1
if next_i >= len(pieces):
return pieces
if reserve_last:
# Count non-suffix pieces from next_i onward; need ≥2 so the join
# target and at least one last-name piece both exist.
non_suffix_remaining = sum(
1 for p in pieces[next_i:] if not self.is_suffix(p)
)
if non_suffix_remaining <= 1:
return pieces
pieces[fi] = pieces[fi] + " " + pieces[next_i]
del pieces[next_i]
return pieces
def is_roman_numeral(self, value: str) -> bool:
"""
Matches the ``roman_numeral`` regular expression in
:py:data:`~nameparser.config.regexes.REGEXES`.
"""
return bool(self.C.regexes.roman_numeral.match(value))
def is_suffix(self, piece: str | list[str]) -> bool:
"""
Is in the suffixes set — or was derived as a period-joined suffix
earlier in this parse (e.g. ``"JD.CPA"``) — and not
:py:func:`is_an_initial()`.
Some suffixes may be acronyms (M.B.A) while some are not (Jr.),
so we remove the periods from `piece` when testing against
`C.suffix_acronyms`.
"""
# suffixes may have periods inside them like "M.D."
if isinstance(piece, list):
for item in piece:
if self.is_suffix(item):
return True
return False
else:
word = lc(piece)
return ((word.replace('.', '') in self.C.suffix_acronyms)
or (word in self.C.suffix_not_acronyms)
or (word in self._derived_suffixes)) \
and not self.is_an_initial(piece)
def are_suffixes(self, pieces: Iterable[str]) -> bool:
"""Return True if all pieces are suffixes.
Vacuously True for an empty iterable — the piece loops in
:py:func:`parse_full_name` rely on this to route the final piece
to the last-name branch.
"""
for piece in pieces:
if not self.is_suffix(piece):
return False
return True
def is_suffix_lenient(self, piece: str) -> bool:
"""Like is_suffix(), but suffix_not_acronyms members are accepted
unconditionally, bypassing is_suffix()'s is_an_initial() veto.
This covers all suffix_not_acronyms members (i, ii, iii, iv, v, jr,
sr, etc.), case-insensitively, including single-letter entries that
is_suffix() would otherwise reject. Only safe for pieces in
unambiguous positions, e.g. after a comma ("John Ingram, V").
"""
return lc(piece) in self.C.suffix_not_acronyms or self.is_suffix(piece)
def expand_suffix_delimiter(self, part: str) -> list[str]:
"""Split a single post-comma part on :py:attr:`suffix_delimiter`,
if configured. Used only at suffix-consumption sites, where a part
has already been identified as a suffix group, so splitting it
further can't misparse an unrelated name segment. Returns ``[part]``
unchanged if no delimiter is configured.
"""
if not self.suffix_delimiter:
return [part]
return [p for p in (p.strip() for p in part.split(self.suffix_delimiter)) if p]
def are_suffixes_after_comma(self, pieces: Iterable[str]) -> bool:
"""Return True if all pieces are suffixes by the lenient
:py:func:`is_suffix_lenient` test. Used when detecting suffix-comma
format (e.g. "John Ingram, V") where the post-comma position is
unambiguous.
"""
return all(self.is_suffix_lenient(piece) for piece in pieces)
def is_rootname(self, piece: str) -> bool:
"""
Is not a known title, suffix or prefix. Just first, middle, last names.
"""
word = lc(piece)
return word not in self.C.suffixes_prefixes_titles \
and word not in self._derived_titles \
and word not in self._derived_suffixes \
and word not in self._derived_prefixes \
and not self.is_an_initial(piece)
def is_an_initial(self, value: str) -> bool:
"""
Words with a single period at the end, or a single uppercase letter.
Matches the ``initial`` regular expression in
:py:data:`~nameparser.config.regexes.REGEXES`.
"""
return bool(self.C.regexes.initial.match(value))
def is_east_slavic_patronymic(self, piece: str) -> bool:
"""
Return True if ``piece`` ends with a recognised East-Slavic patronymic
suffix, checked against both Latin-script and Cyrillic patterns in
``self.C.regexes``. Latin suffixes: ``-ovich``, ``-ovna``, ``-evich``,
``-evna``, ``-ichna``, and the irregular forms ``-ilyich``, ``-kuzmich``,
``-lukich``, ``-fomich``, ``-fokich``. Cyrillic equivalents are matched
by a separate pattern.
"""
return bool(
self.C.regexes.east_slavic_patronymic.search(piece)
or self.C.regexes.east_slavic_patronymic_cyrillic.search(piece)
)
def is_turkic_patronymic_marker(self, piece: str) -> bool:
"""
Return True if ``piece`` is exactly a recognised Turkic patronymic
marker word (e.g. ``oglu``, ``qizi``, ``uly``), checked against both
Latin-script and Cyrillic patterns in ``self.C.regexes``. Unlike
East-Slavic patronymics, these are standalone marker words, not
suffixes, so the match is whole-word rather than a suffix search.
"""
return bool(
self.C.regexes.turkic_patronymic_marker.match(piece)
or self.C.regexes.turkic_patronymic_marker_cyrillic.match(piece)
)
# full_name parser
@property
def full_name(self) -> str:
"""The string output of the HumanName instance."""
return str(self)
@full_name.setter
def full_name(self, value: str | bytes) -> None:
self._apply_full_name(value, stacklevel=3)
def _apply_full_name(self, value: str | bytes, *, stacklevel: int) -> None:
# Shared by the setter and the constructor so each can call it
# directly with a stacklevel that attributes the warning to *its own*
# caller -- the constructor going through the setter would otherwise
# add a frame and misattribute the warning to this module.
self.original = value
if isinstance(value, bytes):
# deprecated 1.3.0, raises TypeError in 2.0 (#245)
warnings.warn(
"Passing bytes to HumanName is deprecated and will raise "
"TypeError in 2.0; decode it first, e.g. "
"value.decode('utf-8'). See "
"https://github.com/derek73/python-nameparser/issues/245",
DeprecationWarning,
stacklevel=stacklevel,
)
self._full_name = value.decode(self.encoding)
else:
self._full_name = value
self.parse_full_name()
def collapse_whitespace(self, string: str) -> str:
# collapse multiple spaces into single space
string = self.C.regexes.spaces.sub(" ", string.strip())
if string and self.C.regexes.commas.fullmatch(string[-1]):
string = string[:-1]
return string
def pre_process(self) -> None:
"""
This method happens at the beginning of the :py:func:`parse_full_name`
before any other processing of the string aside from unicode
normalization, so it's a good place to do any custom handling in a
subclass. Runs :py:func:`squash_bidi`, :py:func:`parse_nicknames` and
:py:func:`squash_emoji`.
"""
self.squash_bidi()
self.fix_phd()
self.parse_nicknames()
self.squash_emoji()
def handle_east_slavic_patronymic_name_order(self) -> None:
"""
When patronymic_name_order is enabled, detect Russian formal order