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.
Files changed (49) hide show
  1. {nameparser-1.3.0/nameparser.egg-info → nameparser-1.4.0}/PKG-INFO +44 -24
  2. {nameparser-1.3.0 → nameparser-1.4.0}/README.rst +43 -23
  3. {nameparser-1.3.0 → nameparser-1.4.0}/nameparser/_version.py +1 -1
  4. {nameparser-1.3.0 → nameparser-1.4.0}/nameparser/config/__init__.py +154 -19
  5. {nameparser-1.3.0 → nameparser-1.4.0}/nameparser/config/regexes.py +11 -0
  6. {nameparser-1.3.0 → nameparser-1.4.0}/nameparser/parser.py +89 -13
  7. {nameparser-1.3.0 → nameparser-1.4.0/nameparser.egg-info}/PKG-INFO +44 -24
  8. {nameparser-1.3.0 → nameparser-1.4.0}/nameparser.egg-info/SOURCES.txt +1 -0
  9. {nameparser-1.3.0 → nameparser-1.4.0}/pyproject.toml +1 -0
  10. {nameparser-1.3.0 → nameparser-1.4.0}/tests/conftest.py +16 -2
  11. nameparser-1.4.0/tests/test_comma_variants.py +53 -0
  12. {nameparser-1.3.0 → nameparser-1.4.0}/tests/test_constants.py +310 -32
  13. {nameparser-1.3.0 → nameparser-1.4.0}/tests/test_initials.py +12 -6
  14. {nameparser-1.3.0 → nameparser-1.4.0}/tests/test_nicknames.py +7 -6
  15. {nameparser-1.3.0 → nameparser-1.4.0}/tests/test_output_format.py +44 -0
  16. {nameparser-1.3.0 → nameparser-1.4.0}/tests/test_python_api.py +41 -12
  17. {nameparser-1.3.0 → nameparser-1.4.0}/tests/test_variations.py +7 -1
  18. {nameparser-1.3.0 → nameparser-1.4.0}/AUTHORS +0 -0
  19. {nameparser-1.3.0 → nameparser-1.4.0}/LICENSE +0 -0
  20. {nameparser-1.3.0 → nameparser-1.4.0}/MANIFEST.in +0 -0
  21. {nameparser-1.3.0 → nameparser-1.4.0}/nameparser/__init__.py +0 -0
  22. {nameparser-1.3.0 → nameparser-1.4.0}/nameparser/__main__.py +0 -0
  23. {nameparser-1.3.0 → nameparser-1.4.0}/nameparser/config/bound_first_names.py +0 -0
  24. {nameparser-1.3.0 → nameparser-1.4.0}/nameparser/config/capitalization.py +0 -0
  25. {nameparser-1.3.0 → nameparser-1.4.0}/nameparser/config/conjunctions.py +0 -0
  26. {nameparser-1.3.0 → nameparser-1.4.0}/nameparser/config/prefixes.py +0 -0
  27. {nameparser-1.3.0 → nameparser-1.4.0}/nameparser/config/suffixes.py +0 -0
  28. {nameparser-1.3.0 → nameparser-1.4.0}/nameparser/config/titles.py +0 -0
  29. {nameparser-1.3.0 → nameparser-1.4.0}/nameparser/py.typed +0 -0
  30. {nameparser-1.3.0 → nameparser-1.4.0}/nameparser/util.py +0 -0
  31. {nameparser-1.3.0 → nameparser-1.4.0}/nameparser.egg-info/dependency_links.txt +0 -0
  32. {nameparser-1.3.0 → nameparser-1.4.0}/nameparser.egg-info/requires.txt +0 -0
  33. {nameparser-1.3.0 → nameparser-1.4.0}/nameparser.egg-info/top_level.txt +0 -0
  34. {nameparser-1.3.0 → nameparser-1.4.0}/setup.cfg +0 -0
  35. {nameparser-1.3.0 → nameparser-1.4.0}/tests/__init__.py +0 -0
  36. {nameparser-1.3.0 → nameparser-1.4.0}/tests/base.py +0 -0
  37. {nameparser-1.3.0 → nameparser-1.4.0}/tests/test_bound_first_names.py +0 -0
  38. {nameparser-1.3.0 → nameparser-1.4.0}/tests/test_brute_force.py +0 -0
  39. {nameparser-1.3.0 → nameparser-1.4.0}/tests/test_capitalization.py +0 -0
  40. {nameparser-1.3.0 → nameparser-1.4.0}/tests/test_config_attribute_docstrings.py +0 -0
  41. {nameparser-1.3.0 → nameparser-1.4.0}/tests/test_conjunctions.py +0 -0
  42. {nameparser-1.3.0 → nameparser-1.4.0}/tests/test_east_slavic_patronymic_order.py +0 -0
  43. {nameparser-1.3.0 → nameparser-1.4.0}/tests/test_first_name.py +0 -0
  44. {nameparser-1.3.0 → nameparser-1.4.0}/tests/test_middle_name_as_last.py +0 -0
  45. {nameparser-1.3.0 → nameparser-1.4.0}/tests/test_parser_util.py +0 -0
  46. {nameparser-1.3.0 → nameparser-1.4.0}/tests/test_prefixes.py +0 -0
  47. {nameparser-1.3.0 → nameparser-1.4.0}/tests/test_suffixes.py +0 -0
  48. {nameparser-1.3.0 → nameparser-1.4.0}/tests/test_titles.py +0 -0
  49. {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.0
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
- Instantiating the `HumanName` class with a string splits on commas and then spaces,
61
- classifying name parts based on placement in the string and matches against known name
62
- pieces like titles and suffixes.
63
-
64
- It correctly handles some common conjunctions and special prefixes to last names
65
- like "del". Titles and conjunctions can be chained together to handle complex
66
- titles like "Asst Secretary of State". It can also try to correct capitalization
67
- of names that are all upper- or lowercase names.
68
-
69
- It attempts the best guess that can be made with a simple, rule-based approach.
70
- Its main use case is English and it is not likely to be useful for languages
71
- that do not conform to the supported name structure. It's not perfect, but it
72
- gets you pretty far.
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
- The parser does not attempt to correct mistakes in the input. It mostly just splits on white
120
- space and puts things in buckets based on their position in the string. This also means
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. You can
143
- do this in your own pre- or post-processing, by `customizing the configured pre-defined
144
- sets`_ of titles, prefixes, etc., or by subclassing the `HumanName` class. See the
145
- `full documentation`_ for more information.
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
- Instantiating the `HumanName` class with a string splits on commas and then spaces,
33
- classifying name parts based on placement in the string and matches against known name
34
- pieces like titles and suffixes.
35
-
36
- It correctly handles some common conjunctions and special prefixes to last names
37
- like "del". Titles and conjunctions can be chained together to handle complex
38
- titles like "Asst Secretary of State". It can also try to correct capitalization
39
- of names that are all upper- or lowercase names.
40
-
41
- It attempts the best guess that can be made with a simple, rule-based approach.
42
- Its main use case is English and it is not likely to be useful for languages
43
- that do not conform to the supported name structure. It's not perfect, but it
44
- gets you pretty far.
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
- The parser does not attempt to correct mistakes in the input. It mostly just splits on white
92
- space and puts things in buckets based on their position in the string. This also means
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. You can
115
- do this in your own pre- or post-processing, by `customizing the configured pre-defined
116
- sets`_ of titles, prefixes, etc., or by subclassing the `HumanName` class. See the
117
- `full documentation`_ for more information.
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, 3, 0)
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
- A module-level instance of :py:class:`~nameparser.config.Constants` is created
6
- and used by default for all HumanName instances. You can adjust the entire module's
7
- configuration by importing this instance and changing it.
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
- You can also adjust the configuration of individual instances by passing
15
- ``None`` as the second argument upon instantiation.
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
- >>> hn = HumanName("Dean Robert Johns", None)
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
- **Potential Gotcha**: If you do not pass ``None`` (or your own
25
- :py:class:`Constants` instance) as the second argument, ``hn.C`` will be a
26
- reference to the module config, possibly yielding unexpected results. See
27
- `Customizing the Parser <customize.html>`_.
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=None).
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 bare commas first, and each resulting
594
- part is stripped of surrounding whitespace before this step runs.
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
- >>> CONSTANTS.empty_attribute_default = None
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
- >>> CONSTANTS.empty_attribute_default = ''
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(getattr(type(self), name, None), property):
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
- if isinstance(getattr(type(self), public, None), _SetManagerAttribute):
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,