nameparser 1.3.1__tar.gz → 1.4.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {nameparser-1.3.1/nameparser.egg-info → nameparser-1.4.0}/PKG-INFO +8 -2
- {nameparser-1.3.1 → nameparser-1.4.0}/README.rst +7 -1
- {nameparser-1.3.1 → nameparser-1.4.0}/nameparser/_version.py +1 -1
- {nameparser-1.3.1 → nameparser-1.4.0}/nameparser/config/__init__.py +154 -19
- {nameparser-1.3.1 → nameparser-1.4.0}/nameparser/config/regexes.py +4 -0
- {nameparser-1.3.1 → nameparser-1.4.0}/nameparser/parser.py +69 -10
- {nameparser-1.3.1 → nameparser-1.4.0/nameparser.egg-info}/PKG-INFO +8 -2
- {nameparser-1.3.1 → nameparser-1.4.0}/nameparser.egg-info/SOURCES.txt +1 -0
- {nameparser-1.3.1 → nameparser-1.4.0}/tests/conftest.py +16 -2
- nameparser-1.4.0/tests/test_comma_variants.py +53 -0
- {nameparser-1.3.1 → nameparser-1.4.0}/tests/test_constants.py +310 -32
- {nameparser-1.3.1 → nameparser-1.4.0}/tests/test_initials.py +12 -6
- {nameparser-1.3.1 → nameparser-1.4.0}/tests/test_nicknames.py +7 -6
- {nameparser-1.3.1 → nameparser-1.4.0}/tests/test_output_format.py +9 -4
- {nameparser-1.3.1 → nameparser-1.4.0}/tests/test_python_api.py +41 -12
- {nameparser-1.3.1 → nameparser-1.4.0}/tests/test_variations.py +7 -1
- {nameparser-1.3.1 → nameparser-1.4.0}/AUTHORS +0 -0
- {nameparser-1.3.1 → nameparser-1.4.0}/LICENSE +0 -0
- {nameparser-1.3.1 → nameparser-1.4.0}/MANIFEST.in +0 -0
- {nameparser-1.3.1 → nameparser-1.4.0}/nameparser/__init__.py +0 -0
- {nameparser-1.3.1 → nameparser-1.4.0}/nameparser/__main__.py +0 -0
- {nameparser-1.3.1 → nameparser-1.4.0}/nameparser/config/bound_first_names.py +0 -0
- {nameparser-1.3.1 → nameparser-1.4.0}/nameparser/config/capitalization.py +0 -0
- {nameparser-1.3.1 → nameparser-1.4.0}/nameparser/config/conjunctions.py +0 -0
- {nameparser-1.3.1 → nameparser-1.4.0}/nameparser/config/prefixes.py +0 -0
- {nameparser-1.3.1 → nameparser-1.4.0}/nameparser/config/suffixes.py +0 -0
- {nameparser-1.3.1 → nameparser-1.4.0}/nameparser/config/titles.py +0 -0
- {nameparser-1.3.1 → nameparser-1.4.0}/nameparser/py.typed +0 -0
- {nameparser-1.3.1 → nameparser-1.4.0}/nameparser/util.py +0 -0
- {nameparser-1.3.1 → nameparser-1.4.0}/nameparser.egg-info/dependency_links.txt +0 -0
- {nameparser-1.3.1 → nameparser-1.4.0}/nameparser.egg-info/requires.txt +0 -0
- {nameparser-1.3.1 → nameparser-1.4.0}/nameparser.egg-info/top_level.txt +0 -0
- {nameparser-1.3.1 → nameparser-1.4.0}/pyproject.toml +0 -0
- {nameparser-1.3.1 → nameparser-1.4.0}/setup.cfg +0 -0
- {nameparser-1.3.1 → nameparser-1.4.0}/tests/__init__.py +0 -0
- {nameparser-1.3.1 → nameparser-1.4.0}/tests/base.py +0 -0
- {nameparser-1.3.1 → nameparser-1.4.0}/tests/test_bound_first_names.py +0 -0
- {nameparser-1.3.1 → nameparser-1.4.0}/tests/test_brute_force.py +0 -0
- {nameparser-1.3.1 → nameparser-1.4.0}/tests/test_capitalization.py +0 -0
- {nameparser-1.3.1 → nameparser-1.4.0}/tests/test_config_attribute_docstrings.py +0 -0
- {nameparser-1.3.1 → nameparser-1.4.0}/tests/test_conjunctions.py +0 -0
- {nameparser-1.3.1 → nameparser-1.4.0}/tests/test_east_slavic_patronymic_order.py +0 -0
- {nameparser-1.3.1 → nameparser-1.4.0}/tests/test_first_name.py +0 -0
- {nameparser-1.3.1 → nameparser-1.4.0}/tests/test_middle_name_as_last.py +0 -0
- {nameparser-1.3.1 → nameparser-1.4.0}/tests/test_parser_util.py +0 -0
- {nameparser-1.3.1 → nameparser-1.4.0}/tests/test_prefixes.py +0 -0
- {nameparser-1.3.1 → nameparser-1.4.0}/tests/test_suffixes.py +0 -0
- {nameparser-1.3.1 → nameparser-1.4.0}/tests/test_titles.py +0 -0
- {nameparser-1.3.1 → nameparser-1.4.0}/tests/test_turkic_patronymic_order.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: nameparser
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.4.0
|
|
4
4
|
Summary: A simple Python module for parsing human names into their individual components.
|
|
5
5
|
Author-email: Derek Gulbranson <derek73@gmail.com>
|
|
6
6
|
License: LGPL
|
|
@@ -31,8 +31,14 @@ Name Parser
|
|
|
31
31
|
|
|
32
32
|
|Build Status| |PyPI| |PyPI version| |Documentation| |License| |Downloads| |Codecov|
|
|
33
33
|
|
|
34
|
+
📣 **nameparser 2.0 is in design** — a new immutable core API, with full
|
|
35
|
+
compatibility for existing code through 2.x. Read the `design RFC
|
|
36
|
+
<https://github.com/derek73/python-nameparser/pull/285>`__ and share
|
|
37
|
+
feedback on `the discussion issue
|
|
38
|
+
<https://github.com/derek73/python-nameparser/issues/284>`__.
|
|
39
|
+
|
|
34
40
|
A simple Python (3.10+) module for parsing human names into their
|
|
35
|
-
individual components.
|
|
41
|
+
individual components.
|
|
36
42
|
|
|
37
43
|
* hn.title
|
|
38
44
|
* hn.first
|
|
@@ -3,8 +3,14 @@ Name Parser
|
|
|
3
3
|
|
|
4
4
|
|Build Status| |PyPI| |PyPI version| |Documentation| |License| |Downloads| |Codecov|
|
|
5
5
|
|
|
6
|
+
📣 **nameparser 2.0 is in design** — a new immutable core API, with full
|
|
7
|
+
compatibility for existing code through 2.x. Read the `design RFC
|
|
8
|
+
<https://github.com/derek73/python-nameparser/pull/285>`__ and share
|
|
9
|
+
feedback on `the discussion issue
|
|
10
|
+
<https://github.com/derek73/python-nameparser/issues/284>`__.
|
|
11
|
+
|
|
6
12
|
A simple Python (3.10+) module for parsing human names into their
|
|
7
|
-
individual components.
|
|
13
|
+
individual components.
|
|
8
14
|
|
|
9
15
|
* hn.title
|
|
10
16
|
* hn.first
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
VERSION = (1,
|
|
1
|
+
VERSION = (1, 4, 0)
|
|
2
2
|
__version__ = '.'.join(map(str, VERSION))
|
|
@@ -1,31 +1,46 @@
|
|
|
1
1
|
"""
|
|
2
2
|
The :py:mod:`nameparser.config` module manages the configuration of the
|
|
3
|
-
nameparser.
|
|
3
|
+
nameparser.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
:py:class:`~nameparser.config.Constants` is for application-level
|
|
6
|
+
configuration, set once at startup. ``CONSTANTS``, the module-level instance
|
|
7
|
+
used by every ``HumanName`` created without its own config, is the only
|
|
8
|
+
channel that reaches parses happening in code you don't own (helpers,
|
|
9
|
+
pipelines, a third-party library using nameparser internally) -- the same
|
|
10
|
+
role ``logging`` and ``locale`` play elsewhere. Import it and change it
|
|
11
|
+
directly:
|
|
8
12
|
|
|
9
13
|
::
|
|
10
14
|
|
|
11
15
|
>>> from nameparser.config import CONSTANTS
|
|
12
16
|
>>> CONSTANTS.titles.remove('hon').add('chemistry','dean') # doctest: +SKIP
|
|
13
17
|
|
|
14
|
-
|
|
15
|
-
|
|
18
|
+
For anything scoped -- one dataset, one library, one test -- pass your own
|
|
19
|
+
:py:class:`Constants` instance as the second argument upon instantiation
|
|
20
|
+
instead: ``Constants()`` for fresh library defaults, or ``CONSTANTS.copy()``
|
|
21
|
+
for a private snapshot of the current module config.
|
|
16
22
|
|
|
17
23
|
::
|
|
18
24
|
|
|
19
25
|
>>> from nameparser import HumanName
|
|
20
|
-
>>>
|
|
26
|
+
>>> from nameparser.config import Constants
|
|
27
|
+
>>> hn = HumanName("Dean Robert Johns", Constants())
|
|
21
28
|
>>> hn.C.titles.add('dean') # doctest: +SKIP
|
|
22
29
|
>>> hn.parse_full_name() # need to run this again after config changes
|
|
23
30
|
|
|
24
|
-
|
|
25
|
-
:py:class:`Constants` instance
|
|
26
|
-
reference to the module config,
|
|
27
|
-
`Customizing the Parser
|
|
31
|
+
Mixing the two up is where the surprises come from, not the API itself: if
|
|
32
|
+
you do not pass your own :py:class:`Constants` instance as the second
|
|
33
|
+
argument, ``hn.C`` will be a reference to the module config, and a change
|
|
34
|
+
there reaches every other instance sharing it. See `Customizing the Parser
|
|
35
|
+
<customize.html>`_.
|
|
36
|
+
|
|
37
|
+
.. deprecated:: 1.4.0
|
|
38
|
+
Passing ``None`` as the second argument also builds a fresh
|
|
39
|
+
``Constants()``, but is deprecated in favor of the explicit spellings
|
|
40
|
+
above; it will raise ``TypeError`` in 2.0 (issue #260).
|
|
28
41
|
"""
|
|
42
|
+
import copy
|
|
43
|
+
import inspect
|
|
29
44
|
import re
|
|
30
45
|
import sys
|
|
31
46
|
import warnings
|
|
@@ -242,7 +257,18 @@ class SetManager(Set):
|
|
|
242
257
|
.. deprecated:: 1.3.0
|
|
243
258
|
``bytes`` arguments will raise ``TypeError`` in 2.0 (see issue
|
|
244
259
|
#245); decode before adding.
|
|
260
|
+
|
|
261
|
+
.. deprecated:: 1.4.0
|
|
262
|
+
The method itself is removed in 2.0 (see issue #245); use
|
|
263
|
+
:py:func:`add` instead, decoding bytes first.
|
|
245
264
|
"""
|
|
265
|
+
warnings.warn(
|
|
266
|
+
"SetManager.add_with_encoding() is deprecated and will be "
|
|
267
|
+
"removed in 2.0; use add() instead (decode bytes first). See "
|
|
268
|
+
"https://github.com/derek73/python-nameparser/issues/245",
|
|
269
|
+
DeprecationWarning,
|
|
270
|
+
stacklevel=2,
|
|
271
|
+
)
|
|
246
272
|
self._add_normalized(s, encoding, stacklevel=3)
|
|
247
273
|
|
|
248
274
|
def add(self, *strings: str) -> Self:
|
|
@@ -327,7 +353,7 @@ def _is_dunder(attr: str) -> bool:
|
|
|
327
353
|
# validate and normalize each one exactly once at import. Constants()
|
|
328
354
|
# copies these via _normalized_elements' SetManager fast path instead of
|
|
329
355
|
# re-checking ~1,400 elements per construction — a cost that otherwise
|
|
330
|
-
# repeats on the per-instance-config path, HumanName(constants=
|
|
356
|
+
# repeats on the per-instance-config path, HumanName(constants=Constants()).
|
|
331
357
|
#
|
|
332
358
|
# This snapshot is taken once, at import time: mutating a raw constant
|
|
333
359
|
# (e.g. `TITLES.add('x')`) after import is *not* picked up by Constants()
|
|
@@ -383,10 +409,28 @@ class TupleManager(dict[str, T]):
|
|
|
383
409
|
arg = checked
|
|
384
410
|
super().__init__(arg, **kwargs)
|
|
385
411
|
|
|
412
|
+
def _warn_unknown_key(self, attr: str) -> None:
|
|
413
|
+
# Deprecated 1.4.0, raises AttributeError in 2.0 (#256): a misspelled
|
|
414
|
+
# key otherwise degrades silently with no traceback pointing at the
|
|
415
|
+
# typo.
|
|
416
|
+
warnings.warn(
|
|
417
|
+
f"{attr!r} is not a known key ({', '.join(sorted(self))}); "
|
|
418
|
+
"unknown-key attribute access is deprecated and will raise "
|
|
419
|
+
"AttributeError in 2.0. Use .get() for intentional soft access. "
|
|
420
|
+
"See https://github.com/derek73/python-nameparser/issues/256",
|
|
421
|
+
DeprecationWarning,
|
|
422
|
+
stacklevel=3,
|
|
423
|
+
)
|
|
424
|
+
|
|
386
425
|
def __getattr__(self, attr: str) -> T | None:
|
|
387
426
|
# Otherwise the dict default (None) is mistaken for a real protocol hook.
|
|
388
427
|
if _is_dunder(attr):
|
|
389
428
|
raise AttributeError(attr)
|
|
429
|
+
# Single-underscore introspection probes (IPython/Jupyter's
|
|
430
|
+
# _repr_html_, _ipython_canary_method_should_not_exist_, etc.) are
|
|
431
|
+
# never config keys either -- no real config key starts with '_'.
|
|
432
|
+
if attr not in self and not attr.startswith('_'):
|
|
433
|
+
self._warn_unknown_key(attr)
|
|
390
434
|
return self.get(attr)
|
|
391
435
|
|
|
392
436
|
def __setattr__(self, attr: str, value: T) -> None:
|
|
@@ -430,6 +474,8 @@ class RegexTupleManager(TupleManager[re.Pattern[str]]):
|
|
|
430
474
|
# then tries to call the returned re.Pattern and raises TypeError.
|
|
431
475
|
if _is_dunder(attr):
|
|
432
476
|
raise AttributeError(attr)
|
|
477
|
+
if attr not in self and not attr.startswith('_'):
|
|
478
|
+
self._warn_unknown_key(attr)
|
|
433
479
|
return self.get(attr, EMPTY_REGEX)
|
|
434
480
|
|
|
435
481
|
|
|
@@ -495,6 +541,48 @@ class _CachedUnionMember(_SetManagerAttribute):
|
|
|
495
541
|
setattr(obj, self._attr, value)
|
|
496
542
|
|
|
497
543
|
|
|
544
|
+
class _EmptyAttributeDefaultAttribute:
|
|
545
|
+
"""Descriptor backing ``Constants.empty_attribute_default``.
|
|
546
|
+
|
|
547
|
+
.. deprecated:: 1.4.0
|
|
548
|
+
Assignment is deprecated (see issue #255): the only legal value
|
|
549
|
+
left once ``None`` support goes in 2.0 is the default ``''``, so a
|
|
550
|
+
dial with one position isn't configuration.
|
|
551
|
+
"""
|
|
552
|
+
|
|
553
|
+
_attr = '_empty_attribute_default'
|
|
554
|
+
|
|
555
|
+
def __get__(self, obj: 'Constants | None', objtype: type | None = None) -> str:
|
|
556
|
+
# Annotated `str`, not `str | None`, to match the pre-descriptor
|
|
557
|
+
# plain-attribute inference: None is documented/supported (see the
|
|
558
|
+
# class docstring), but typing it honestly cascades `| None`
|
|
559
|
+
# through every public str-typed name accessor (title, first, ...).
|
|
560
|
+
# Returning '' rather than `self` on class access (unlike
|
|
561
|
+
# _SetManagerAttribute, which returns `self`) is also load-bearing
|
|
562
|
+
# for Constants.__repr__'s `getattr(type(self), name)` default
|
|
563
|
+
# comparison in _repr_scalar_attrs -- returning `self` there would
|
|
564
|
+
# make every Constants() show this attribute as "customized".
|
|
565
|
+
if obj is None:
|
|
566
|
+
return ''
|
|
567
|
+
return getattr(obj, self._attr, '')
|
|
568
|
+
|
|
569
|
+
def __set__(self, obj: 'Constants', value: str | None) -> None:
|
|
570
|
+
if value is not None and not isinstance(value, str):
|
|
571
|
+
raise TypeError(
|
|
572
|
+
f"empty_attribute_default must be a str or None, got "
|
|
573
|
+
f"{type(value).__name__!r}"
|
|
574
|
+
)
|
|
575
|
+
warnings.warn(
|
|
576
|
+
"Assigning Constants.empty_attribute_default is deprecated and "
|
|
577
|
+
"will raise TypeError in 2.0; empty attributes will always "
|
|
578
|
+
"return ''. See "
|
|
579
|
+
"https://github.com/derek73/python-nameparser/issues/255",
|
|
580
|
+
DeprecationWarning,
|
|
581
|
+
stacklevel=2,
|
|
582
|
+
)
|
|
583
|
+
setattr(obj, self._attr, value)
|
|
584
|
+
|
|
585
|
+
|
|
498
586
|
class Constants:
|
|
499
587
|
"""
|
|
500
588
|
An instance of this class hold all of the configuration constants for the parser.
|
|
@@ -590,8 +678,9 @@ class Constants:
|
|
|
590
678
|
``None`` (no additional splitting beyond the standard comma split).
|
|
591
679
|
|
|
592
680
|
Note: setting this to ``","`` or ``", "`` has no additional effect —
|
|
593
|
-
the full name is already split on
|
|
594
|
-
|
|
681
|
+
the full name is already split on comma characters first (including the
|
|
682
|
+
Arabic ``،`` and fullwidth ``,`` variants), and each resulting part is
|
|
683
|
+
stripped of surrounding whitespace before this step runs.
|
|
595
684
|
|
|
596
685
|
The delimiter is only applied to parts once they've been identified as
|
|
597
686
|
a suffix group, so it never leaks into a first- or middle-name part. For
|
|
@@ -600,20 +689,29 @@ class Constants:
|
|
|
600
689
|
does not get mistaken for a suffix split.
|
|
601
690
|
"""
|
|
602
691
|
|
|
603
|
-
empty_attribute_default =
|
|
692
|
+
empty_attribute_default = _EmptyAttributeDefaultAttribute()
|
|
604
693
|
"""
|
|
605
694
|
Default return value for empty attributes.
|
|
606
695
|
|
|
696
|
+
.. deprecated:: 1.4.0
|
|
697
|
+
Assignment emits ``DeprecationWarning``; the option is removed in
|
|
698
|
+
2.0 (see issue #255) and empty attributes will always return ``''``.
|
|
699
|
+
|
|
607
700
|
.. doctest::
|
|
608
701
|
|
|
702
|
+
>>> import warnings
|
|
609
703
|
>>> from nameparser.config import CONSTANTS
|
|
610
|
-
>>>
|
|
704
|
+
>>> with warnings.catch_warnings():
|
|
705
|
+
... warnings.simplefilter('ignore', DeprecationWarning)
|
|
706
|
+
... CONSTANTS.empty_attribute_default = None
|
|
611
707
|
>>> name = HumanName("John Doe")
|
|
612
708
|
>>> print(name.title)
|
|
613
709
|
None
|
|
614
710
|
>>> name.first
|
|
615
711
|
'John'
|
|
616
|
-
>>>
|
|
712
|
+
>>> with warnings.catch_warnings():
|
|
713
|
+
... warnings.simplefilter('ignore', DeprecationWarning)
|
|
714
|
+
... CONSTANTS.empty_attribute_default = ''
|
|
617
715
|
|
|
618
716
|
"""
|
|
619
717
|
|
|
@@ -805,13 +903,29 @@ class Constants:
|
|
|
805
903
|
]
|
|
806
904
|
return "<Constants : [\n" + "\n".join(lines) + "\n]>"
|
|
807
905
|
|
|
906
|
+
def copy(self) -> 'Constants':
|
|
907
|
+
"""
|
|
908
|
+
Return a detached deep copy of this ``Constants`` instance, preserving
|
|
909
|
+
its current customizations -- unlike :py:class:`Constants`'s own
|
|
910
|
+
constructor, which always starts from library defaults. Useful for
|
|
911
|
+
snapshotting the shared module-level ``CONSTANTS`` (including
|
|
912
|
+
whatever it's been customized with) into a private instance, e.g.
|
|
913
|
+
``CONSTANTS.copy()``. Relies on the same ``__getstate__``/``__setstate__``
|
|
914
|
+
pair pickling uses, so it's as cheap and correct as pickle round-tripping.
|
|
915
|
+
"""
|
|
916
|
+
return copy.deepcopy(self)
|
|
917
|
+
|
|
808
918
|
def __setstate__(self, state: Mapping[str, Any]) -> None:
|
|
809
919
|
# Restore each saved attribute directly. The previous implementation
|
|
810
920
|
# passed the whole state dict to __init__ as the ``prefixes`` argument,
|
|
811
921
|
# which silently reverted every collection to its module default on
|
|
812
922
|
# unpickling.
|
|
813
923
|
self._pst = None
|
|
924
|
+
legacy_format = False
|
|
814
925
|
for name, value in state.items():
|
|
926
|
+
# inspect.getattr_static, not getattr, so descriptors are
|
|
927
|
+
# inspected directly rather than triggering their __get__.
|
|
928
|
+
descriptor = inspect.getattr_static(type(self), name, None)
|
|
815
929
|
# Migration shim: pickles written before this fix (1.3.0 and earlier,
|
|
816
930
|
# including 1.2.1) used a dir() sweep for __getstate__, so their state
|
|
817
931
|
# carries the read-only ``suffixes_prefixes_titles`` property. Skip any
|
|
@@ -820,9 +934,29 @@ class Constants:
|
|
|
820
934
|
# don't promise to read pre-fix blobs forever — this only smooths
|
|
821
935
|
# migration for anyone persisting them, and can be dropped a release
|
|
822
936
|
# or two after 1.3.0 once they've re-pickled.
|
|
823
|
-
if isinstance(
|
|
937
|
+
if isinstance(descriptor, property):
|
|
938
|
+
legacy_format = True
|
|
939
|
+
continue
|
|
940
|
+
if isinstance(descriptor, _EmptyAttributeDefaultAttribute):
|
|
941
|
+
# Bypass the descriptor's setter: restoring saved state isn't
|
|
942
|
+
# a user assignment, so it shouldn't emit #255's deprecation
|
|
943
|
+
# warning on every unpickle/copy() of a customized instance.
|
|
944
|
+
setattr(self, descriptor._attr, value)
|
|
824
945
|
continue
|
|
825
946
|
setattr(self, name, value)
|
|
947
|
+
if legacy_format:
|
|
948
|
+
# Once per __setstate__ call, not once per skipped key (see
|
|
949
|
+
# issue #279): the 2.0 removal turns this into a ValueError
|
|
950
|
+
# naming the same fix.
|
|
951
|
+
warnings.warn(
|
|
952
|
+
"Loading a legacy-format Constants pickle (written by "
|
|
953
|
+
"nameparser <= 1.2.x, before the 1.3.0 pickle fix) is "
|
|
954
|
+
"deprecated and will raise ValueError in 2.0; re-pickle "
|
|
955
|
+
"under 1.3/1.4 to migrate. See "
|
|
956
|
+
"https://github.com/derek73/python-nameparser/issues/279",
|
|
957
|
+
DeprecationWarning,
|
|
958
|
+
stacklevel=2,
|
|
959
|
+
)
|
|
826
960
|
# Verify each descriptor-backed attr was restored. Without this, a missing
|
|
827
961
|
# key surfaces later as AttributeError: 'Constants' object has no attribute
|
|
828
962
|
# '_prefixes' — the private mangled name, not the public one, making it
|
|
@@ -847,7 +981,8 @@ class Constants:
|
|
|
847
981
|
for name, val in self.__dict__.items():
|
|
848
982
|
if name.startswith('_'):
|
|
849
983
|
public = name[1:]
|
|
850
|
-
|
|
984
|
+
descriptor = inspect.getattr_static(type(self), public, None)
|
|
985
|
+
if isinstance(descriptor, (_SetManagerAttribute, _EmptyAttributeDefaultAttribute)):
|
|
851
986
|
state[public] = val
|
|
852
987
|
else:
|
|
853
988
|
state[name] = val
|
|
@@ -29,6 +29,10 @@ REGEXES = {
|
|
|
29
29
|
"bidi": re_bidi,
|
|
30
30
|
"phd": re.compile(r'\s(ph\.?\s+d\.?)', re.I),
|
|
31
31
|
"space_before_comma": re.compile(r'\s+,'),
|
|
32
|
+
# ASCII comma plus its Arabic (U+060C) and fullwidth CJK (U+FF0C)
|
|
33
|
+
# counterparts, used to split "Last, First" format and to strip a
|
|
34
|
+
# trailing comma before parsing (#265).
|
|
35
|
+
"commas": re.compile(r'[,،,]'),
|
|
32
36
|
"east_slavic_patronymic": re.compile(
|
|
33
37
|
r'(ovich|ovna|evich|evna|ichna|ilyich|kuzmich|lukich|fomich|fokich)$',
|
|
34
38
|
re.I,
|
|
@@ -51,8 +51,14 @@ class HumanName:
|
|
|
51
51
|
:param str full_name: The name string to be parsed.
|
|
52
52
|
:param constants:
|
|
53
53
|
a :py:class:`~nameparser.config.Constants` instance (subclasses are
|
|
54
|
-
honored).
|
|
55
|
-
|
|
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``.
|
|
56
62
|
:param str encoding: string representing the encoding of your input
|
|
57
63
|
(deprecated with ``bytes`` input, removal in 2.0 — decode before
|
|
58
64
|
passing; see issue #245)
|
|
@@ -106,7 +112,10 @@ class HumanName:
|
|
|
106
112
|
nickname: str | list[str] | None = None,
|
|
107
113
|
maiden: str | list[str] | None = None,
|
|
108
114
|
) -> None:
|
|
109
|
-
|
|
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)
|
|
110
119
|
|
|
111
120
|
# Lookup entries derived while parsing this instance (period-joined
|
|
112
121
|
# titles/suffixes like "Lt.Gov.", conjunction-joined pieces like
|
|
@@ -143,11 +152,27 @@ class HumanName:
|
|
|
143
152
|
self._apply_full_name(full_name, stacklevel=3)
|
|
144
153
|
|
|
145
154
|
@staticmethod
|
|
146
|
-
def _validate_constants(constants: 'Constants | None') -> 'Constants':
|
|
155
|
+
def _validate_constants(constants: 'Constants | None', *, stacklevel: int) -> 'Constants':
|
|
147
156
|
# Shared by the constructor and the C setter so both assignment paths
|
|
148
157
|
# give the same immediate TypeError instead of one bypassing the
|
|
149
158
|
# other and failing far from the cause (#239).
|
|
150
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
|
+
)
|
|
151
176
|
return Constants()
|
|
152
177
|
if not isinstance(constants, Constants):
|
|
153
178
|
# passing the class itself is the likeliest mistake, and
|
|
@@ -169,14 +194,16 @@ class HumanName:
|
|
|
169
194
|
<customize.html>`_.
|
|
170
195
|
|
|
171
196
|
Assigning a non-``Constants`` value (besides ``None``, which builds a
|
|
172
|
-
fresh private ``Constants()``
|
|
173
|
-
|
|
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).
|
|
174
201
|
"""
|
|
175
202
|
return self._C
|
|
176
203
|
|
|
177
204
|
@C.setter
|
|
178
205
|
def C(self, constants: 'Constants | None') -> None:
|
|
179
|
-
self._C = self._validate_constants(constants)
|
|
206
|
+
self._C = self._validate_constants(constants, stacklevel=3)
|
|
180
207
|
|
|
181
208
|
def __getstate__(self) -> dict:
|
|
182
209
|
state = self.__dict__.copy()
|
|
@@ -227,12 +254,38 @@ class HumanName:
|
|
|
227
254
|
@overload
|
|
228
255
|
def __getitem__(self, key: str) -> str: ...
|
|
229
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
|
+
"""
|
|
230
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
|
+
)
|
|
231
271
|
return [getattr(self, x) for x in self._members[key]]
|
|
232
272
|
else:
|
|
233
273
|
return getattr(self, key)
|
|
234
274
|
|
|
235
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
|
+
)
|
|
236
289
|
if key in self._members:
|
|
237
290
|
self._set_list(key, value)
|
|
238
291
|
else:
|
|
@@ -923,7 +976,7 @@ class HumanName:
|
|
|
923
976
|
def collapse_whitespace(self, string: str) -> str:
|
|
924
977
|
# collapse multiple spaces into single space
|
|
925
978
|
string = self.C.regexes.spaces.sub(" ", string.strip())
|
|
926
|
-
if string.
|
|
979
|
+
if string and self.C.regexes.commas.fullmatch(string[-1]):
|
|
927
980
|
string = string[:-1]
|
|
928
981
|
return string
|
|
929
982
|
|
|
@@ -1195,8 +1248,14 @@ class HumanName:
|
|
|
1195
1248
|
|
|
1196
1249
|
self._full_name = self.collapse_whitespace(self._full_name)
|
|
1197
1250
|
|
|
1198
|
-
# break up full_name by commas
|
|
1199
|
-
|
|
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])]
|
|
1200
1259
|
self._had_comma = len(parts) > 1
|
|
1201
1260
|
|
|
1202
1261
|
log.debug("full_name: %s", self._full_name)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: nameparser
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.4.0
|
|
4
4
|
Summary: A simple Python module for parsing human names into their individual components.
|
|
5
5
|
Author-email: Derek Gulbranson <derek73@gmail.com>
|
|
6
6
|
License: LGPL
|
|
@@ -31,8 +31,14 @@ Name Parser
|
|
|
31
31
|
|
|
32
32
|
|Build Status| |PyPI| |PyPI version| |Documentation| |License| |Downloads| |Codecov|
|
|
33
33
|
|
|
34
|
+
📣 **nameparser 2.0 is in design** — a new immutable core API, with full
|
|
35
|
+
compatibility for existing code through 2.x. Read the `design RFC
|
|
36
|
+
<https://github.com/derek73/python-nameparser/pull/285>`__ and share
|
|
37
|
+
feedback on `the discussion issue
|
|
38
|
+
<https://github.com/derek73/python-nameparser/issues/284>`__.
|
|
39
|
+
|
|
34
40
|
A simple Python (3.10+) module for parsing human names into their
|
|
35
|
-
individual components.
|
|
41
|
+
individual components.
|
|
36
42
|
|
|
37
43
|
* hn.title
|
|
38
44
|
* hn.first
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import copy
|
|
2
|
+
import warnings
|
|
2
3
|
from collections.abc import Iterator
|
|
3
4
|
|
|
4
5
|
import pytest
|
|
@@ -60,9 +61,22 @@ def empty_attribute_default(request: pytest.FixtureRequest) -> Iterator[str | No
|
|
|
60
61
|
attr: copy.deepcopy(getattr(CONSTANTS, attr))
|
|
61
62
|
for attr in _COLLECTION_CONFIG_ATTRS
|
|
62
63
|
}
|
|
63
|
-
|
|
64
|
+
# empty_attribute_default assignment is deprecated (#255); this fixture's
|
|
65
|
+
# own systematic exercise of both settings across the whole suite is
|
|
66
|
+
# infrastructure, not a test of the deprecation itself, so it's silenced
|
|
67
|
+
# here (narrowly, around just the assignment) rather than at every one of
|
|
68
|
+
# its ~1500 call sites. Must not wrap `yield` -- that would suppress
|
|
69
|
+
# DeprecationWarning for the test body itself, hiding real ones.
|
|
70
|
+
with warnings.catch_warnings():
|
|
71
|
+
warnings.simplefilter("ignore", DeprecationWarning)
|
|
72
|
+
CONSTANTS.empty_attribute_default = request.param
|
|
64
73
|
yield request.param
|
|
65
74
|
for attr, value in scalar_snapshot.items():
|
|
66
|
-
|
|
75
|
+
if attr == "empty_attribute_default":
|
|
76
|
+
with warnings.catch_warnings():
|
|
77
|
+
warnings.simplefilter("ignore", DeprecationWarning)
|
|
78
|
+
setattr(CONSTANTS, attr, value)
|
|
79
|
+
else:
|
|
80
|
+
setattr(CONSTANTS, attr, value)
|
|
67
81
|
for attr, value in collection_snapshot.items():
|
|
68
82
|
setattr(CONSTANTS, attr, value)
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
|
|
3
|
+
from nameparser import HumanName
|
|
4
|
+
from nameparser.config import Constants
|
|
5
|
+
from nameparser.config.regexes import REGEXES
|
|
6
|
+
|
|
7
|
+
from tests.base import HumanNameTestBase
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class HumanNameCommaVariantsTests(HumanNameTestBase):
|
|
11
|
+
"""Non-ASCII comma characters should split "Last, First" the same as ',' (#265)."""
|
|
12
|
+
|
|
13
|
+
def test_arabic_comma_splits_lastname_format(self) -> None:
|
|
14
|
+
hn = HumanName("سلمان، محمد")
|
|
15
|
+
self.m(hn.first, "محمد", hn)
|
|
16
|
+
self.m(hn.last, "سلمان", hn)
|
|
17
|
+
|
|
18
|
+
def test_fullwidth_comma_splits_lastname_format(self) -> None:
|
|
19
|
+
hn = HumanName("Smith,John")
|
|
20
|
+
self.m(hn.first, "John", hn)
|
|
21
|
+
self.m(hn.last, "Smith", hn)
|
|
22
|
+
|
|
23
|
+
def test_arabic_comma_does_not_pollute_output(self) -> None:
|
|
24
|
+
hn = HumanName("سلمان، محمد")
|
|
25
|
+
self.assertNotIn("،", hn.last)
|
|
26
|
+
self.assertNotIn("،", str(hn))
|
|
27
|
+
|
|
28
|
+
def test_trailing_arabic_comma_stripped(self) -> None:
|
|
29
|
+
# matches ASCII behavior: a single word with a trailing comma has
|
|
30
|
+
# nothing after the comma, so it's a bare name, not "Last,"
|
|
31
|
+
hn = HumanName("سلمان،")
|
|
32
|
+
self.m(hn.first, "سلمان", hn)
|
|
33
|
+
|
|
34
|
+
def test_custom_regexes_without_commas_key_does_not_shatter_name(self) -> None:
|
|
35
|
+
# A custom regexes dict that omits "commas" entirely must not fall
|
|
36
|
+
# back to RegexTupleManager's EMPTY_REGEX default for splitting --
|
|
37
|
+
# re.compile('').split(...) matches between every character, which
|
|
38
|
+
# explodes any name into single-char pieces instead of leaving it
|
|
39
|
+
# unsplit (the EMPTY_REGEX convention elsewhere in this codebase
|
|
40
|
+
# means "feature disabled", not "split on every character").
|
|
41
|
+
# With comma splitting disabled, "Smith, John" is tokenized like any
|
|
42
|
+
# other no-comma input (word tokenizing drops the punctuation),
|
|
43
|
+
# yielding a plain first/last pair -- not the inverted "Last, First"
|
|
44
|
+
# reading, and definitely not single-character pieces.
|
|
45
|
+
# Unknown-key access (here, the deliberately-omitted 'commas') is
|
|
46
|
+
# deprecated for removal in 2.0 (#256); this fallback pattern will
|
|
47
|
+
# AttributeError-crash the parser instead of degrading silently.
|
|
48
|
+
custom = {k: v for k, v in REGEXES.items() if k != 'commas'}
|
|
49
|
+
c = Constants(regexes=custom)
|
|
50
|
+
with pytest.deprecated_call():
|
|
51
|
+
hn = HumanName("Smith, John", constants=c)
|
|
52
|
+
self.m(hn.first, "Smith", hn)
|
|
53
|
+
self.m(hn.last, "John", hn)
|