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