nameparser 1.3.0__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.0/nameparser.egg-info → nameparser-1.4.0}/PKG-INFO +44 -24
- {nameparser-1.3.0 → nameparser-1.4.0}/README.rst +43 -23
- {nameparser-1.3.0 → nameparser-1.4.0}/nameparser/_version.py +1 -1
- {nameparser-1.3.0 → nameparser-1.4.0}/nameparser/config/__init__.py +154 -19
- {nameparser-1.3.0 → nameparser-1.4.0}/nameparser/config/regexes.py +11 -0
- {nameparser-1.3.0 → nameparser-1.4.0}/nameparser/parser.py +89 -13
- {nameparser-1.3.0 → nameparser-1.4.0/nameparser.egg-info}/PKG-INFO +44 -24
- {nameparser-1.3.0 → nameparser-1.4.0}/nameparser.egg-info/SOURCES.txt +1 -0
- {nameparser-1.3.0 → nameparser-1.4.0}/pyproject.toml +1 -0
- {nameparser-1.3.0 → nameparser-1.4.0}/tests/conftest.py +16 -2
- nameparser-1.4.0/tests/test_comma_variants.py +53 -0
- {nameparser-1.3.0 → nameparser-1.4.0}/tests/test_constants.py +310 -32
- {nameparser-1.3.0 → nameparser-1.4.0}/tests/test_initials.py +12 -6
- {nameparser-1.3.0 → nameparser-1.4.0}/tests/test_nicknames.py +7 -6
- {nameparser-1.3.0 → nameparser-1.4.0}/tests/test_output_format.py +44 -0
- {nameparser-1.3.0 → nameparser-1.4.0}/tests/test_python_api.py +41 -12
- {nameparser-1.3.0 → nameparser-1.4.0}/tests/test_variations.py +7 -1
- {nameparser-1.3.0 → nameparser-1.4.0}/AUTHORS +0 -0
- {nameparser-1.3.0 → nameparser-1.4.0}/LICENSE +0 -0
- {nameparser-1.3.0 → nameparser-1.4.0}/MANIFEST.in +0 -0
- {nameparser-1.3.0 → nameparser-1.4.0}/nameparser/__init__.py +0 -0
- {nameparser-1.3.0 → nameparser-1.4.0}/nameparser/__main__.py +0 -0
- {nameparser-1.3.0 → nameparser-1.4.0}/nameparser/config/bound_first_names.py +0 -0
- {nameparser-1.3.0 → nameparser-1.4.0}/nameparser/config/capitalization.py +0 -0
- {nameparser-1.3.0 → nameparser-1.4.0}/nameparser/config/conjunctions.py +0 -0
- {nameparser-1.3.0 → nameparser-1.4.0}/nameparser/config/prefixes.py +0 -0
- {nameparser-1.3.0 → nameparser-1.4.0}/nameparser/config/suffixes.py +0 -0
- {nameparser-1.3.0 → nameparser-1.4.0}/nameparser/config/titles.py +0 -0
- {nameparser-1.3.0 → nameparser-1.4.0}/nameparser/py.typed +0 -0
- {nameparser-1.3.0 → nameparser-1.4.0}/nameparser/util.py +0 -0
- {nameparser-1.3.0 → nameparser-1.4.0}/nameparser.egg-info/dependency_links.txt +0 -0
- {nameparser-1.3.0 → nameparser-1.4.0}/nameparser.egg-info/requires.txt +0 -0
- {nameparser-1.3.0 → nameparser-1.4.0}/nameparser.egg-info/top_level.txt +0 -0
- {nameparser-1.3.0 → nameparser-1.4.0}/setup.cfg +0 -0
- {nameparser-1.3.0 → nameparser-1.4.0}/tests/__init__.py +0 -0
- {nameparser-1.3.0 → nameparser-1.4.0}/tests/base.py +0 -0
- {nameparser-1.3.0 → nameparser-1.4.0}/tests/test_bound_first_names.py +0 -0
- {nameparser-1.3.0 → nameparser-1.4.0}/tests/test_brute_force.py +0 -0
- {nameparser-1.3.0 → nameparser-1.4.0}/tests/test_capitalization.py +0 -0
- {nameparser-1.3.0 → nameparser-1.4.0}/tests/test_config_attribute_docstrings.py +0 -0
- {nameparser-1.3.0 → nameparser-1.4.0}/tests/test_conjunctions.py +0 -0
- {nameparser-1.3.0 → nameparser-1.4.0}/tests/test_east_slavic_patronymic_order.py +0 -0
- {nameparser-1.3.0 → nameparser-1.4.0}/tests/test_first_name.py +0 -0
- {nameparser-1.3.0 → nameparser-1.4.0}/tests/test_middle_name_as_last.py +0 -0
- {nameparser-1.3.0 → nameparser-1.4.0}/tests/test_parser_util.py +0 -0
- {nameparser-1.3.0 → nameparser-1.4.0}/tests/test_prefixes.py +0 -0
- {nameparser-1.3.0 → nameparser-1.4.0}/tests/test_suffixes.py +0 -0
- {nameparser-1.3.0 → nameparser-1.4.0}/tests/test_titles.py +0 -0
- {nameparser-1.3.0 → 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
|
|
@@ -57,19 +63,35 @@ are optional. Comma-separated format like "Last, First" is also supported.
|
|
|
57
63
|
2. Lastname [Suffix], Title Firstname (Nickname) Middle Middle[,] Suffix [, Suffix]
|
|
58
64
|
3. Title Firstname M Lastname [Suffix], Suffix [Suffix] [, Suffix]
|
|
59
65
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
66
|
+
How It Works
|
|
67
|
+
~~~~~~~~~~~~
|
|
68
|
+
|
|
69
|
+
The parser works in two layers.
|
|
70
|
+
|
|
71
|
+
A **vocabulary layer** recognizes name pieces by what they are, using
|
|
72
|
+
configurable sets of known words: titles ("Dr."), suffixes ("III", "PhD"),
|
|
73
|
+
last-name prefixes ("de la"), conjunctions ("y", "&"), and delimited
|
|
74
|
+
nicknames ("Doc"). Titles and conjunctions chain together to handle complex
|
|
75
|
+
titles like "Asst Secretary of State"; prefixes join forward so "de la Vega"
|
|
76
|
+
stays one last name. This layer doesn't care where in the string a word
|
|
77
|
+
appears — and it's the layer you customize, by adding or removing entries
|
|
78
|
+
in the sets to fit your dataset.
|
|
79
|
+
|
|
80
|
+
A **positional layer** then assigns everything the vocabulary layer didn't
|
|
81
|
+
claim, based purely on where it sits: the first unclaimed word is the first
|
|
82
|
+
name, the last one is the last name, and anything between them is a middle
|
|
83
|
+
name. There is no semantic understanding — "Dr" is a title when it comes
|
|
84
|
+
before a name and a suffix when it comes after ("pre-nominal" and
|
|
85
|
+
"post-nominal" would probably be better names) — and no attempt to correct
|
|
86
|
+
mistakes in the input.
|
|
87
|
+
|
|
88
|
+
It attempts the best guess that can be made with a simple, deterministic,
|
|
89
|
+
rule-based approach — no statistical models or machine learning; the same
|
|
90
|
+
input always parses the same way. The positional layer assumes Western name
|
|
91
|
+
order (given name first), so the main use case is English and other
|
|
92
|
+
languages that share that structure. It can also try to correct the
|
|
93
|
+
capitalization of names that are all upper- or lowercase. It's not perfect,
|
|
94
|
+
but it gets you pretty far.
|
|
73
95
|
|
|
74
96
|
Installation
|
|
75
97
|
------------
|
|
@@ -116,11 +138,8 @@ Quick Start Example
|
|
|
116
138
|
'Juan de la Vega'
|
|
117
139
|
|
|
118
140
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
the difference between 'title' and 'suffix' is positional, not semantic. "Dr" is a title
|
|
122
|
-
when it comes before the name and a suffix when it comes after. ("Pre-nominal"
|
|
123
|
-
and "post-nominal" would probably be better names.)
|
|
141
|
+
Because the positional layer has no semantic understanding, position is
|
|
142
|
+
everything:
|
|
124
143
|
|
|
125
144
|
::
|
|
126
145
|
|
|
@@ -139,10 +158,11 @@ and "post-nominal" would probably be better names.)
|
|
|
139
158
|
Customization
|
|
140
159
|
-------------
|
|
141
160
|
|
|
142
|
-
Your project may need some adjustment for your dataset.
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
161
|
+
Your project may need some adjustment for your dataset. Most customization
|
|
162
|
+
is vocabulary — `customizing the configured pre-defined sets`_ of titles,
|
|
163
|
+
prefixes, etc. that the vocabulary layer matches against. You can also do
|
|
164
|
+
your own pre- or post-processing, or subclass the `HumanName` class for
|
|
165
|
+
deeper changes. See the `full documentation`_ for more information.
|
|
146
166
|
|
|
147
167
|
|
|
148
168
|
`Full documentation`_
|
|
@@ -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
|
|
@@ -29,19 +35,35 @@ are optional. Comma-separated format like "Last, First" is also supported.
|
|
|
29
35
|
2. Lastname [Suffix], Title Firstname (Nickname) Middle Middle[,] Suffix [, Suffix]
|
|
30
36
|
3. Title Firstname M Lastname [Suffix], Suffix [Suffix] [, Suffix]
|
|
31
37
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
38
|
+
How It Works
|
|
39
|
+
~~~~~~~~~~~~
|
|
40
|
+
|
|
41
|
+
The parser works in two layers.
|
|
42
|
+
|
|
43
|
+
A **vocabulary layer** recognizes name pieces by what they are, using
|
|
44
|
+
configurable sets of known words: titles ("Dr."), suffixes ("III", "PhD"),
|
|
45
|
+
last-name prefixes ("de la"), conjunctions ("y", "&"), and delimited
|
|
46
|
+
nicknames ("Doc"). Titles and conjunctions chain together to handle complex
|
|
47
|
+
titles like "Asst Secretary of State"; prefixes join forward so "de la Vega"
|
|
48
|
+
stays one last name. This layer doesn't care where in the string a word
|
|
49
|
+
appears — and it's the layer you customize, by adding or removing entries
|
|
50
|
+
in the sets to fit your dataset.
|
|
51
|
+
|
|
52
|
+
A **positional layer** then assigns everything the vocabulary layer didn't
|
|
53
|
+
claim, based purely on where it sits: the first unclaimed word is the first
|
|
54
|
+
name, the last one is the last name, and anything between them is a middle
|
|
55
|
+
name. There is no semantic understanding — "Dr" is a title when it comes
|
|
56
|
+
before a name and a suffix when it comes after ("pre-nominal" and
|
|
57
|
+
"post-nominal" would probably be better names) — and no attempt to correct
|
|
58
|
+
mistakes in the input.
|
|
59
|
+
|
|
60
|
+
It attempts the best guess that can be made with a simple, deterministic,
|
|
61
|
+
rule-based approach — no statistical models or machine learning; the same
|
|
62
|
+
input always parses the same way. The positional layer assumes Western name
|
|
63
|
+
order (given name first), so the main use case is English and other
|
|
64
|
+
languages that share that structure. It can also try to correct the
|
|
65
|
+
capitalization of names that are all upper- or lowercase. It's not perfect,
|
|
66
|
+
but it gets you pretty far.
|
|
45
67
|
|
|
46
68
|
Installation
|
|
47
69
|
------------
|
|
@@ -88,11 +110,8 @@ Quick Start Example
|
|
|
88
110
|
'Juan de la Vega'
|
|
89
111
|
|
|
90
112
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
the difference between 'title' and 'suffix' is positional, not semantic. "Dr" is a title
|
|
94
|
-
when it comes before the name and a suffix when it comes after. ("Pre-nominal"
|
|
95
|
-
and "post-nominal" would probably be better names.)
|
|
113
|
+
Because the positional layer has no semantic understanding, position is
|
|
114
|
+
everything:
|
|
96
115
|
|
|
97
116
|
::
|
|
98
117
|
|
|
@@ -111,10 +130,11 @@ and "post-nominal" would probably be better names.)
|
|
|
111
130
|
Customization
|
|
112
131
|
-------------
|
|
113
132
|
|
|
114
|
-
Your project may need some adjustment for your dataset.
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
133
|
+
Your project may need some adjustment for your dataset. Most customization
|
|
134
|
+
is vocabulary — `customizing the configured pre-defined sets`_ of titles,
|
|
135
|
+
prefixes, etc. that the vocabulary layer matches against. You can also do
|
|
136
|
+
your own pre- or post-processing, or subclass the `HumanName` class for
|
|
137
|
+
deeper changes. See the `full documentation`_ for more information.
|
|
118
138
|
|
|
119
139
|
|
|
120
140
|
`Full documentation`_
|
|
@@ -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
|
|
@@ -6,6 +6,12 @@ re_emoji = re.compile('[' # lgtm[py/overly-large-range]
|
|
|
6
6
|
'\U0001F680-\U0001F6FF'
|
|
7
7
|
'\u2600-\u26FF\u2700-\u27BF]+')
|
|
8
8
|
|
|
9
|
+
# Invisible bidirectional formatting characters: ALM, LRM, RLM, the
|
|
10
|
+
# embedding/override marks (LRE/RLE/PDF/LRO/RLO) and the isolates
|
|
11
|
+
# (LRI/RLI/FSI/PDI). Copy-pasted RTL names often carry these; they render
|
|
12
|
+
# as nothing but stick to name parts and break equality/lookups.
|
|
13
|
+
re_bidi = re.compile('[\u061C\u200E\u200F\u202A-\u202E\u2066-\u2069]+')
|
|
14
|
+
|
|
9
15
|
EMPTY_REGEX = re.compile('')
|
|
10
16
|
|
|
11
17
|
REGEXES = {
|
|
@@ -20,8 +26,13 @@ REGEXES = {
|
|
|
20
26
|
"no_vowels": re.compile(r'^[^aeyiuo]+$', re.I),
|
|
21
27
|
"period_not_at_end": re.compile(r'.*\..+$', re.I),
|
|
22
28
|
"emoji": re_emoji,
|
|
29
|
+
"bidi": re_bidi,
|
|
23
30
|
"phd": re.compile(r'\s(ph\.?\s+d\.?)', re.I),
|
|
24
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'[,،,]'),
|
|
25
36
|
"east_slavic_patronymic": re.compile(
|
|
26
37
|
r'(ovich|ovna|evich|evna|ichna|ilyich|kuzmich|lukich|fomich|fokich)$',
|
|
27
38
|
re.I,
|