nameparser 2.2.0__tar.gz → 2.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 (151) hide show
  1. {nameparser-2.2.0/nameparser.egg-info → nameparser-2.4.0}/PKG-INFO +28 -13
  2. {nameparser-2.2.0 → nameparser-2.4.0}/README.rst +26 -12
  3. {nameparser-2.2.0 → nameparser-2.4.0}/nameparser/__init__.py +2 -0
  4. {nameparser-2.2.0 → nameparser-2.4.0}/nameparser/_config_shim.py +226 -85
  5. {nameparser-2.2.0 → nameparser-2.4.0}/nameparser/_facade.py +233 -45
  6. {nameparser-2.2.0 → nameparser-2.4.0}/nameparser/_lexicon.py +324 -52
  7. {nameparser-2.2.0 → nameparser-2.4.0}/nameparser/_parser.py +68 -15
  8. {nameparser-2.2.0 → nameparser-2.4.0}/nameparser/_pipeline/_assemble.py +47 -5
  9. nameparser-2.4.0/nameparser/_pipeline/_assign.py +1465 -0
  10. nameparser-2.4.0/nameparser/_pipeline/_classify.py +349 -0
  11. nameparser-2.4.0/nameparser/_pipeline/_comma.py +432 -0
  12. {nameparser-2.2.0 → nameparser-2.4.0}/nameparser/_pipeline/_extract.py +2 -3
  13. nameparser-2.4.0/nameparser/_pipeline/_group.py +1532 -0
  14. nameparser-2.4.0/nameparser/_pipeline/_pieces.py +1957 -0
  15. {nameparser-2.2.0 → nameparser-2.4.0}/nameparser/_pipeline/_post_rules.py +314 -221
  16. {nameparser-2.2.0 → nameparser-2.4.0}/nameparser/_pipeline/_script_segment.py +247 -90
  17. nameparser-2.4.0/nameparser/_pipeline/_segment.py +210 -0
  18. nameparser-2.4.0/nameparser/_pipeline/_state.py +359 -0
  19. {nameparser-2.2.0 → nameparser-2.4.0}/nameparser/_pipeline/_tokenize.py +6 -7
  20. nameparser-2.4.0/nameparser/_pipeline/_vocab.py +1436 -0
  21. {nameparser-2.2.0 → nameparser-2.4.0}/nameparser/_policy.py +174 -23
  22. nameparser-2.4.0/nameparser/_render.py +694 -0
  23. {nameparser-2.2.0 → nameparser-2.4.0}/nameparser/_types.py +288 -27
  24. {nameparser-2.2.0 → nameparser-2.4.0}/nameparser/_version.py +4 -4
  25. {nameparser-2.2.0 → nameparser-2.4.0}/nameparser/config/_invariants.py +6 -5
  26. nameparser-2.4.0/nameparser/config/capitalization.py +76 -0
  27. nameparser-2.4.0/nameparser/config/conjunctions.py +92 -0
  28. {nameparser-2.2.0 → nameparser-2.4.0}/nameparser/config/particles.py +29 -1
  29. {nameparser-2.2.0 → nameparser-2.4.0}/nameparser/config/suffixes.py +147 -67
  30. {nameparser-2.2.0 → nameparser-2.4.0}/nameparser/config/titles.py +207 -7
  31. {nameparser-2.2.0 → nameparser-2.4.0}/nameparser/locales/ja.py +3 -1
  32. {nameparser-2.2.0 → nameparser-2.4.0}/nameparser/util.py +3 -2
  33. {nameparser-2.2.0 → nameparser-2.4.0/nameparser.egg-info}/PKG-INFO +28 -13
  34. {nameparser-2.2.0 → nameparser-2.4.0}/nameparser.egg-info/SOURCES.txt +3 -0
  35. {nameparser-2.2.0 → nameparser-2.4.0}/pyproject.toml +14 -0
  36. {nameparser-2.2.0 → nameparser-2.4.0}/tests/test_capitalization.py +244 -60
  37. {nameparser-2.2.0 → nameparser-2.4.0}/tests/test_conjunctions.py +1 -2
  38. {nameparser-2.2.0 → nameparser-2.4.0}/tests/test_constants.py +3 -3
  39. {nameparser-2.2.0 → nameparser-2.4.0}/tests/test_initials.py +81 -0
  40. {nameparser-2.2.0 → nameparser-2.4.0}/tests/test_output_format.py +1 -1
  41. {nameparser-2.2.0 → nameparser-2.4.0}/tests/test_suffixes.py +78 -22
  42. {nameparser-2.2.0 → nameparser-2.4.0}/tests/test_titles.py +26 -7
  43. {nameparser-2.2.0 → nameparser-2.4.0}/tests/v2/_differential_fixtures.py +12 -1
  44. nameparser-2.4.0/tests/v2/cases.py +11997 -0
  45. {nameparser-2.2.0 → nameparser-2.4.0}/tests/v2/conftest.py +3 -1
  46. nameparser-2.4.0/tests/v2/pipeline/test_assemble.py +252 -0
  47. nameparser-2.4.0/tests/v2/pipeline/test_assign.py +918 -0
  48. nameparser-2.4.0/tests/v2/pipeline/test_classify.py +815 -0
  49. nameparser-2.4.0/tests/v2/pipeline/test_comma.py +396 -0
  50. {nameparser-2.2.0 → nameparser-2.4.0}/tests/v2/pipeline/test_group.py +976 -101
  51. nameparser-2.4.0/tests/v2/pipeline/test_pieces.py +1298 -0
  52. {nameparser-2.2.0 → nameparser-2.4.0}/tests/v2/pipeline/test_post_rules.py +454 -24
  53. {nameparser-2.2.0 → nameparser-2.4.0}/tests/v2/pipeline/test_script_segment.py +251 -21
  54. {nameparser-2.2.0 → nameparser-2.4.0}/tests/v2/pipeline/test_segment.py +35 -44
  55. nameparser-2.4.0/tests/v2/pipeline/test_state.py +365 -0
  56. nameparser-2.4.0/tests/v2/pipeline/test_vocab.py +876 -0
  57. {nameparser-2.2.0 → nameparser-2.4.0}/tests/v2/rules_doc.py +70 -1
  58. nameparser-2.4.0/tests/v2/test_benchmark.py +1139 -0
  59. nameparser-2.4.0/tests/v2/test_cases.py +345 -0
  60. {nameparser-2.2.0 → nameparser-2.4.0}/tests/v2/test_config_shim.py +429 -2
  61. {nameparser-2.2.0 → nameparser-2.4.0}/tests/v2/test_contracts.py +5 -0
  62. nameparser-2.4.0/tests/v2/test_differential.py +4792 -0
  63. nameparser-2.4.0/tests/v2/test_doc_citations.py +397 -0
  64. nameparser-2.4.0/tests/v2/test_facade.py +1095 -0
  65. {nameparser-2.2.0 → nameparser-2.4.0}/tests/v2/test_facade_cases.py +118 -0
  66. {nameparser-2.2.0 → nameparser-2.4.0}/tests/v2/test_layering.py +18 -6
  67. nameparser-2.4.0/tests/v2/test_ledger_guards.py +8902 -0
  68. {nameparser-2.2.0 → nameparser-2.4.0}/tests/v2/test_lexicon.py +334 -9
  69. {nameparser-2.2.0 → nameparser-2.4.0}/tests/v2/test_locales.py +172 -2
  70. nameparser-2.4.0/tests/v2/test_order_correspondence.py +206 -0
  71. {nameparser-2.2.0 → nameparser-2.4.0}/tests/v2/test_parser.py +521 -92
  72. {nameparser-2.2.0 → nameparser-2.4.0}/tests/v2/test_policy.py +159 -3
  73. nameparser-2.4.0/tests/v2/test_properties.py +3334 -0
  74. {nameparser-2.2.0 → nameparser-2.4.0}/tests/v2/test_regex_sync.py +37 -2
  75. nameparser-2.4.0/tests/v2/test_render.py +1696 -0
  76. nameparser-2.4.0/tests/v2/test_rules_doc_grammar.py +172 -0
  77. nameparser-2.2.0/nameparser/_pipeline/_assign.py +0 -429
  78. nameparser-2.2.0/nameparser/_pipeline/_classify.py +0 -228
  79. nameparser-2.2.0/nameparser/_pipeline/_group.py +0 -989
  80. nameparser-2.2.0/nameparser/_pipeline/_pieces.py +0 -313
  81. nameparser-2.2.0/nameparser/_pipeline/_segment.py +0 -90
  82. nameparser-2.2.0/nameparser/_pipeline/_state.py +0 -146
  83. nameparser-2.2.0/nameparser/_pipeline/_vocab.py +0 -505
  84. nameparser-2.2.0/nameparser/_render.py +0 -316
  85. nameparser-2.2.0/nameparser/config/capitalization.py +0 -16
  86. nameparser-2.2.0/nameparser/config/conjunctions.py +0 -37
  87. nameparser-2.2.0/tests/v2/cases.py +0 -3601
  88. nameparser-2.2.0/tests/v2/pipeline/test_assemble.py +0 -133
  89. nameparser-2.2.0/tests/v2/pipeline/test_assign.py +0 -423
  90. nameparser-2.2.0/tests/v2/pipeline/test_classify.py +0 -98
  91. nameparser-2.2.0/tests/v2/pipeline/test_pieces.py +0 -103
  92. nameparser-2.2.0/tests/v2/pipeline/test_state.py +0 -129
  93. nameparser-2.2.0/tests/v2/pipeline/test_vocab.py +0 -454
  94. nameparser-2.2.0/tests/v2/test_benchmark.py +0 -344
  95. nameparser-2.2.0/tests/v2/test_cases.py +0 -72
  96. nameparser-2.2.0/tests/v2/test_differential.py +0 -1361
  97. nameparser-2.2.0/tests/v2/test_doc_citations.py +0 -176
  98. nameparser-2.2.0/tests/v2/test_facade.py +0 -628
  99. nameparser-2.2.0/tests/v2/test_ledger_guards.py +0 -2452
  100. nameparser-2.2.0/tests/v2/test_properties.py +0 -557
  101. nameparser-2.2.0/tests/v2/test_render.py +0 -641
  102. nameparser-2.2.0/tests/v2/test_rules_doc_grammar.py +0 -95
  103. {nameparser-2.2.0 → nameparser-2.4.0}/AUTHORS +0 -0
  104. {nameparser-2.2.0 → nameparser-2.4.0}/LICENSE +0 -0
  105. {nameparser-2.2.0 → nameparser-2.4.0}/MANIFEST.in +0 -0
  106. {nameparser-2.2.0 → nameparser-2.4.0}/nameparser/__main__.py +0 -0
  107. {nameparser-2.2.0 → nameparser-2.4.0}/nameparser/_locale.py +0 -0
  108. {nameparser-2.2.0 → nameparser-2.4.0}/nameparser/_pipeline/__init__.py +0 -0
  109. {nameparser-2.2.0 → nameparser-2.4.0}/nameparser/config/__init__.py +0 -0
  110. {nameparser-2.2.0 → nameparser-2.4.0}/nameparser/config/_deprecated.py +0 -0
  111. {nameparser-2.2.0 → nameparser-2.4.0}/nameparser/config/bound_first_names.py +0 -0
  112. {nameparser-2.2.0 → nameparser-2.4.0}/nameparser/config/bound_given_names.py +0 -0
  113. {nameparser-2.2.0 → nameparser-2.4.0}/nameparser/config/maiden_markers.py +0 -0
  114. {nameparser-2.2.0 → nameparser-2.4.0}/nameparser/config/prefixes.py +0 -0
  115. {nameparser-2.2.0 → nameparser-2.4.0}/nameparser/config/regexes.py +0 -0
  116. {nameparser-2.2.0 → nameparser-2.4.0}/nameparser/config/surnames.py +0 -0
  117. {nameparser-2.2.0 → nameparser-2.4.0}/nameparser/locales/__init__.py +0 -0
  118. {nameparser-2.2.0 → nameparser-2.4.0}/nameparser/locales/ru.py +0 -0
  119. {nameparser-2.2.0 → nameparser-2.4.0}/nameparser/locales/tr_az.py +0 -0
  120. {nameparser-2.2.0 → nameparser-2.4.0}/nameparser/locales/zh.py +0 -0
  121. {nameparser-2.2.0 → nameparser-2.4.0}/nameparser/parser.py +0 -0
  122. {nameparser-2.2.0 → nameparser-2.4.0}/nameparser/py.typed +0 -0
  123. {nameparser-2.2.0 → nameparser-2.4.0}/nameparser.egg-info/dependency_links.txt +0 -0
  124. {nameparser-2.2.0 → nameparser-2.4.0}/nameparser.egg-info/requires.txt +0 -0
  125. {nameparser-2.2.0 → nameparser-2.4.0}/nameparser.egg-info/top_level.txt +0 -0
  126. {nameparser-2.2.0 → nameparser-2.4.0}/setup.cfg +0 -0
  127. {nameparser-2.2.0 → nameparser-2.4.0}/tests/__init__.py +0 -0
  128. {nameparser-2.2.0 → nameparser-2.4.0}/tests/base.py +0 -0
  129. {nameparser-2.2.0 → nameparser-2.4.0}/tests/conftest.py +0 -0
  130. {nameparser-2.2.0 → nameparser-2.4.0}/tests/test_bound_given_names.py +0 -0
  131. {nameparser-2.2.0 → nameparser-2.4.0}/tests/test_brute_force.py +0 -0
  132. {nameparser-2.2.0 → nameparser-2.4.0}/tests/test_comma_variants.py +0 -0
  133. {nameparser-2.2.0 → nameparser-2.4.0}/tests/test_east_slavic_patronymic_order.py +0 -0
  134. {nameparser-2.2.0 → nameparser-2.4.0}/tests/test_first_name.py +0 -0
  135. {nameparser-2.2.0 → nameparser-2.4.0}/tests/test_middle_name_as_last.py +0 -0
  136. {nameparser-2.2.0 → nameparser-2.4.0}/tests/test_nicknames.py +0 -0
  137. {nameparser-2.2.0 → nameparser-2.4.0}/tests/test_particles.py +0 -0
  138. {nameparser-2.2.0 → nameparser-2.4.0}/tests/test_python_api.py +0 -0
  139. {nameparser-2.2.0 → nameparser-2.4.0}/tests/test_turkic_patronymic_order.py +0 -0
  140. {nameparser-2.2.0 → nameparser-2.4.0}/tests/test_variations.py +0 -0
  141. {nameparser-2.2.0 → nameparser-2.4.0}/tests/v2/__init__.py +0 -0
  142. {nameparser-2.2.0 → nameparser-2.4.0}/tests/v2/pipeline/__init__.py +0 -0
  143. {nameparser-2.2.0 → nameparser-2.4.0}/tests/v2/pipeline/test_extract.py +0 -0
  144. {nameparser-2.2.0 → nameparser-2.4.0}/tests/v2/pipeline/test_tokenize.py +0 -0
  145. {nameparser-2.2.0 → nameparser-2.4.0}/tests/v2/test_cli.py +0 -0
  146. {nameparser-2.2.0 → nameparser-2.4.0}/tests/v2/test_config_aliases.py +0 -0
  147. {nameparser-2.2.0 → nameparser-2.4.0}/tests/v2/test_doc_spellings.py +0 -0
  148. {nameparser-2.2.0 → nameparser-2.4.0}/tests/v2/test_locale.py +0 -0
  149. {nameparser-2.2.0 → nameparser-2.4.0}/tests/v2/test_reprs.py +0 -0
  150. {nameparser-2.2.0 → nameparser-2.4.0}/tests/v2/test_rules_doc.py +0 -0
  151. {nameparser-2.2.0 → nameparser-2.4.0}/tests/v2/test_types.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nameparser
3
- Version: 2.2.0
3
+ Version: 2.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
@@ -14,6 +14,7 @@ Classifier: Programming Language :: Python :: 3.11
14
14
  Classifier: Programming Language :: Python :: 3.12
15
15
  Classifier: Programming Language :: Python :: 3.13
16
16
  Classifier: Programming Language :: Python :: 3.14
17
+ Classifier: Programming Language :: Python :: 3.15
17
18
  Classifier: Development Status :: 5 - Production/Stable
18
19
  Classifier: Natural Language :: English
19
20
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
@@ -35,19 +36,25 @@ nameparser parses human names into seven fields — title, given, middle,
35
36
  family, suffix, nickname, maiden. Results are immutable, configuration is
36
37
  composable, and locale packs are opt-in.
37
38
 
38
- 📣 **nameparser 2.0 is out.** Existing ``HumanName`` code keeps working
39
- through 2.x, and most 1.x code needs no changes. The `migration guide
40
- <https://nameparser.readthedocs.io/en/latest/migrate.html>`__ has the
41
- field-by-field map. Please `open an issue
42
- <https://github.com/derek73/python-nameparser/issues>`__ for anything that
43
- parses wrong.
44
-
45
- **2.1 adds East Asian name support.** Chinese, Japanese and Korean names
46
- written in their own scripts are read family-first, unspaced Korean names
47
- are split against the census surname list, and CJK honorifics are
48
- recognized. See `East Asian names
39
+ 📣 **nameparser 2.4 is out.** It uses letter case to read post-nominal
40
+ credentials much more reliably — ``Jack MA`` is Jack with an MA and
41
+ ``Jack Ma`` is the surname Ma, and ``Jane Smith, PhD MA`` keeps both
42
+ credentials together — recognizes more naming conventions (Catalan,
43
+ Irish, Finnish, Malay and others), and writes credentials the usual way
44
+ when it repairs case. The `release log
45
+ <https://nameparser.readthedocs.io/en/latest/release_log.html>`__ has the
46
+ details.
47
+
48
+ Chinese, Japanese and Korean names written in their own scripts are read
49
+ family-first, unspaced Korean names are split against the census surname
50
+ list, and CJK honorifics are recognized. See `East Asian names
49
51
  <https://nameparser.readthedocs.io/en/latest/usage.html#east-asian-names>`__.
50
52
 
53
+ Existing ``HumanName`` code from 1.x keeps working through 2.x, and most
54
+ of it needs no changes; see `Coming from 1.x`_ below. Please `open an
55
+ issue <https://github.com/derek73/python-nameparser/issues>`__ for anything
56
+ that parses wrong.
57
+
51
58
  Installation
52
59
  ------------
53
60
 
@@ -80,6 +87,13 @@ Quick Start Example
80
87
  >>> name.render("{family}, {given}")
81
88
  'de la Vega, Juan'
82
89
 
90
+ >>> parse("Jack MA").suffix # capitals mark a credential
91
+ 'MA'
92
+ >>> parse("Jack Ma").family # and a surname
93
+ 'Ma'
94
+ >>> parse("Jane Smith, PhD MA").suffix
95
+ 'PhD MA'
96
+
83
97
  >>> parse("김민준").family # Korean: unspaced, split on the census list
84
98
  '김'
85
99
  >>> parse("高橋 みなみ").family # Japanese: kanji with kana, family first
@@ -119,8 +133,9 @@ deprecations 1.3 and 1.4 announced, so if your test suite runs clean on
119
133
  Two things that check will not catch: four removals 1.4 never warned
120
134
  about (three raise on contact, the fourth only warns), and one that
121
135
  changes results silently — ``name == "John Smith"`` is now ``False``.
136
+ Later 2.x releases add a few breaks of their own.
122
137
  `Migrating from HumanName <https://nameparser.readthedocs.io/en/latest/migrate.html>`__
123
- covers both, and translates a v1 customization into the new API whenever
138
+ covers all of them, and translates a v1 customization into the new API whenever
124
139
  that's convenient for you.
125
140
 
126
141
  See the `release log <https://nameparser.readthedocs.io/en/latest/release_log.html>`__
@@ -7,19 +7,25 @@ nameparser parses human names into seven fields — title, given, middle,
7
7
  family, suffix, nickname, maiden. Results are immutable, configuration is
8
8
  composable, and locale packs are opt-in.
9
9
 
10
- 📣 **nameparser 2.0 is out.** Existing ``HumanName`` code keeps working
11
- through 2.x, and most 1.x code needs no changes. The `migration guide
12
- <https://nameparser.readthedocs.io/en/latest/migrate.html>`__ has the
13
- field-by-field map. Please `open an issue
14
- <https://github.com/derek73/python-nameparser/issues>`__ for anything that
15
- parses wrong.
16
-
17
- **2.1 adds East Asian name support.** Chinese, Japanese and Korean names
18
- written in their own scripts are read family-first, unspaced Korean names
19
- are split against the census surname list, and CJK honorifics are
20
- recognized. See `East Asian names
10
+ 📣 **nameparser 2.4 is out.** It uses letter case to read post-nominal
11
+ credentials much more reliably — ``Jack MA`` is Jack with an MA and
12
+ ``Jack Ma`` is the surname Ma, and ``Jane Smith, PhD MA`` keeps both
13
+ credentials together — recognizes more naming conventions (Catalan,
14
+ Irish, Finnish, Malay and others), and writes credentials the usual way
15
+ when it repairs case. The `release log
16
+ <https://nameparser.readthedocs.io/en/latest/release_log.html>`__ has the
17
+ details.
18
+
19
+ Chinese, Japanese and Korean names written in their own scripts are read
20
+ family-first, unspaced Korean names are split against the census surname
21
+ list, and CJK honorifics are recognized. See `East Asian names
21
22
  <https://nameparser.readthedocs.io/en/latest/usage.html#east-asian-names>`__.
22
23
 
24
+ Existing ``HumanName`` code from 1.x keeps working through 2.x, and most
25
+ of it needs no changes; see `Coming from 1.x`_ below. Please `open an
26
+ issue <https://github.com/derek73/python-nameparser/issues>`__ for anything
27
+ that parses wrong.
28
+
23
29
  Installation
24
30
  ------------
25
31
 
@@ -52,6 +58,13 @@ Quick Start Example
52
58
  >>> name.render("{family}, {given}")
53
59
  'de la Vega, Juan'
54
60
 
61
+ >>> parse("Jack MA").suffix # capitals mark a credential
62
+ 'MA'
63
+ >>> parse("Jack Ma").family # and a surname
64
+ 'Ma'
65
+ >>> parse("Jane Smith, PhD MA").suffix
66
+ 'PhD MA'
67
+
55
68
  >>> parse("김민준").family # Korean: unspaced, split on the census list
56
69
  '김'
57
70
  >>> parse("高橋 みなみ").family # Japanese: kanji with kana, family first
@@ -91,8 +104,9 @@ deprecations 1.3 and 1.4 announced, so if your test suite runs clean on
91
104
  Two things that check will not catch: four removals 1.4 never warned
92
105
  about (three raise on contact, the fourth only warns), and one that
93
106
  changes results silently — ``name == "John Smith"`` is now ``False``.
107
+ Later 2.x releases add a few breaks of their own.
94
108
  `Migrating from HumanName <https://nameparser.readthedocs.io/en/latest/migrate.html>`__
95
- covers both, and translates a v1 customization into the new API whenever
109
+ covers all of them, and translates a v1 customization into the new API whenever
96
110
  that's convenient for you.
97
111
 
98
112
  See the `release log <https://nameparser.readthedocs.io/en/latest/release_log.html>`__
@@ -16,6 +16,7 @@ from nameparser._policy import (
16
16
  FAMILY_FIRST_GIVEN_LAST,
17
17
  GIVEN_FIRST,
18
18
  UNSET,
19
+ CapsSuffixes,
19
20
  PatronymicRule,
20
21
  Policy,
21
22
  PolicyPatch,
@@ -40,6 +41,7 @@ __all__ = [
40
41
  "Span", "Role", "Token", "Ambiguity", "AmbiguityKind", "ParsedName",
41
42
  "STABLE_TAGS", "Segmentation", "Segmenter",
42
43
  "Lexicon", "Policy", "PolicyPatch", "PatronymicRule", "Script", "UNSET",
44
+ "CapsSuffixes",
43
45
  "GIVEN_FIRST", "FAMILY_FIRST", "FAMILY_FIRST_GIVEN_LAST",
44
46
  "DEFAULT_NICKNAME_DELIMITERS", "DEFAULT_SCRIPT_ORDERS", "Locale",
45
47
  "Parser", "parse", "parser_for",
@@ -28,7 +28,9 @@ from collections.abc import (
28
28
  )
29
29
  from typing import NamedTuple, Self
30
30
 
31
- from nameparser._lexicon import Lexicon, _title_key
31
+ from nameparser._lexicon import (
32
+ Lexicon, _MaskValueError, _normalize, _title_key, _warn_dead_entry,
33
+ )
32
34
  from nameparser._parser import Parser
33
35
  from nameparser._policy import PatronymicRule, Policy
34
36
  from nameparser.util import lc
@@ -433,8 +435,9 @@ class TupleManager(dict[str, object]):
433
435
 
434
436
  #: The named delimiter buckets, translated to the ``Policy``
435
437
  #: (open, close) pairs they stand for. The first three are
436
- #: v1's; the rest are the #273 typographic conventions, named so the
437
- #: v1 keyed idioms (pop/move/del) work on them like the originals.
438
+ #: v1's; the rest are the #273 typographic conventions plus #597's
439
+ #: halfwidth corner brackets, named so the v1 keyed idioms
440
+ #: (pop/move/del) work on them like the originals.
438
441
  #: Keep in sync with DEFAULT_NICKNAME_DELIMITERS in _policy.py (pinned
439
442
  #: by the default-Constants equality test).
440
443
  _SENTINEL_PAIRS = {
@@ -448,6 +451,7 @@ _SENTINEL_PAIRS = {
448
451
  "reversed_guillemets": ("»", "«"),
449
452
  "corner_brackets": ("「", "」"),
450
453
  "white_corner_brackets": ("『", "』"),
454
+ "halfwidth_corner_brackets": ("「", "」"),
451
455
  "fullwidth_parenthesis": ("(", ")"),
452
456
  }
453
457
 
@@ -474,7 +478,7 @@ class RegexTupleManager(TupleManager): # pickle-compat: do NOT delete
474
478
  class _DelimiterManager(TupleManager):
475
479
  """v1 ``nickname_delimiters``/``maiden_delimiters`` bucket. In 2.0
476
480
  only the named sentinels in ``_DELIMITER_SENTINELS`` exist (the v1
477
- trio plus the #273 typographic pairs) -- assigning any
481
+ trio plus the typographic pairs, #273 and #597) -- assigning any
478
482
  other key raises so a caller reaches for a custom-delimiter Policy
479
483
  kwarg instead of a dict entry that silently does nothing. ``pop()``/
480
484
  ``__setitem__``/``__delitem__`` stay open (inherited) for the
@@ -619,6 +623,57 @@ _MANAGER_FIELDS = _SET_FIELDS + (
619
623
  "capitalization_exceptions", "nickname_delimiters", "maiden_delimiters",
620
624
  )
621
625
 
626
+
627
+ def _v1_matchable(entry: str) -> bool:
628
+ """Should the shim translate this config entry at all? v1 compared
629
+ ``lc()`` of a parsed piece, and a piece comes from a whitespace
630
+ split, re-joined only with single spaces, so it is never empty and
631
+ never holds edge whitespace, a whitespace run or a non-space
632
+ whitespace character. An entry failing the whitespace test matched
633
+ nothing in 1.x; translating it either starts matching (Lexicon strips the
634
+ whitespace) or trips a Lexicon invariant the set algebra in
635
+ ``_build_snapshot`` could not see (#541). ``lc`` first, for a
636
+ ``capitalization_exceptions`` key, which TupleManager stores as
637
+ written; set entries are already ``lc()``-folded. The one exception,
638
+ and the one departure from 1.4.0: an entry Lexicon folds to empty,
639
+ which since 2.3.0 (FULL_STOPS, #322) includes a lone CJK full stop.
640
+ 1.4.0 through 2.2.0 could match a token made of that full stop, but
641
+ since 2.3.0 Lexicon's fold empties both the entry and that token's
642
+ lookup key, so translating it only raises (#582)."""
643
+ folded = lc(entry)
644
+ return (bool(folded) and folded == " ".join(folded.split())
645
+ and bool(_normalize(folded)))
646
+
647
+
648
+ # Two entries of nameparser 1.4.0's own shipped TITLES carry a trailing
649
+ # space, so 1.4.0 never matched them ("Actor John Smith" -> first
650
+ # "Actor"). A restored 1.4 pickle carries them and the user never wrote
651
+ # them: they are dropped like any unmatchable entry but not named in the
652
+ # warning. The 2.x data module spells them without the space. Not the
653
+ # same roster as _LEGACY_DEAD_ENTRIES, on purpose: that one is subtracted
654
+ # in __setstate__, only when the whole pre-2.0 set is present, so a user's
655
+ # own re-added entry survives; this one is skipped at snapshot time from
656
+ # any source, and nothing else about it depends on how the entry got here.
657
+ _V14_SHIPPED_UNMATCHABLE = frozenset({
658
+ ("titles", "actor "), ("titles", "television "),
659
+ })
660
+
661
+
662
+ def _warn_unmatchable(dropped: list[tuple[str, str]]) -> None:
663
+ listed = ", ".join(f"{field}: {entry!r}" for field, entry in dropped)
664
+ remedy = "; ".join(
665
+ f"del constants.capitalization_exceptions[{entry!r}]"
666
+ if field == "capitalization_exceptions"
667
+ else f"constants.{field}.remove({entry!r})"
668
+ for field, entry in dropped)
669
+ _warn_dead_entry(
670
+ f"ignoring Constants entries that match no name word (each is "
671
+ f"empty, only full stops and spaces, or holds whitespace no name word "
672
+ f"carries): {listed}. "
673
+ f"Add the stripped word if one was meant, and remove these: "
674
+ f"{remedy}")
675
+
676
+
622
677
  #: v1's Constants.__repr__ field order (#221) -- kept as its own tuple
623
678
  #: rather than reusing _SET_FIELDS, whose order differs (v1 lists
624
679
  #: suffix_acronyms_ambiguous last, not fourth).
@@ -989,13 +1044,35 @@ class Constants:
989
1044
  singleton -- only direct attribute mutation is on the 3.0
990
1045
  removal path.
991
1046
  """
1047
+ from nameparser.config.conjunctions import CONJUNCTIONS_AMBIGUOUS
992
1048
  from nameparser.config.maiden_markers import MAIDEN_MARKERS
993
1049
  from nameparser.config.suffixes import GLUED_HONORIFICS
994
1050
  from nameparser.config.surnames import KOREAN_SURNAMES
995
- acronyms = frozenset(self.suffix_acronyms)
996
- particles = frozenset(self.prefixes)
997
- bound = frozenset(self.bound_first_names)
998
- ambiguous_acronyms = frozenset(self.suffix_acronyms_ambiguous) & acronyms
1051
+ # Every set field and every capitalization_exceptions key passes
1052
+ # _v1_matchable BEFORE any set algebra below: an entry v1 could
1053
+ # never match, or one Lexicon folds to empty (#582), is dropped
1054
+ # and named in one warning (#541). The survivors are passed RAW,
1055
+ # as Lexicon folds each field on its own; only the two cross-set
1056
+ # computations below compare folded spellings.
1057
+ dropped: list[tuple[str, str]] = []
1058
+
1059
+ def matchable(field: str) -> frozenset[str]:
1060
+ kept = []
1061
+ for entry in sorted(getattr(self, field)):
1062
+ if _v1_matchable(entry):
1063
+ kept.append(entry)
1064
+ elif (field, entry) not in _V14_SHIPPED_UNMATCHABLE:
1065
+ dropped.append((field, entry))
1066
+ return frozenset(kept)
1067
+
1068
+ acronyms = matchable("suffix_acronyms")
1069
+ particles = matchable("prefixes")
1070
+ conjunctions = matchable("conjunctions")
1071
+ bound = matchable("bound_first_names")
1072
+ titles = matchable("titles")
1073
+ first_name_titles = matchable("first_name_titles")
1074
+ non_given = matchable("non_first_name_prefixes")
1075
+ ambiguous_acronyms = matchable("suffix_acronyms_ambiguous") & acronyms
999
1076
  # Drop any ambiguous acronym from the word set rather than the
1000
1077
  # other way round. Lexicon forbids the overlap because the word
1001
1078
  # branch bypasses the period gate, and adding an ambiguous
@@ -1006,85 +1083,149 @@ class Constants:
1006
1083
  # dropping it from the AMBIGUOUS set instead ungated the word
1007
1084
  # and lost the family name -- a silent misparse worse than the
1008
1085
  # raise it avoided.
1009
- suffix_words = frozenset(self.suffix_not_acronyms) - ambiguous_acronyms
1086
+ # Folded here, and only here (#582): Lexicon folds each field on
1087
+ # its own but checks this disjointness AFTER its fold, so the
1088
+ # shim must compare folded spellings exactly where Lexicon
1089
+ # re-checks. Folding anything else would change readings on
1090
+ # configs that never raised.
1091
+ ambiguous_folded = {_normalize(a) for a in ambiguous_acronyms}
1092
+ suffix_words = frozenset(
1093
+ w for w in matchable("suffix_not_acronyms")
1094
+ if _normalize(w) not in ambiguous_folded)
1095
+ # Likewise folded only here (#582): Lexicon checks bound_given_names
1096
+ # & particles against particles_ambiguous after its own fold.
1097
+ bound_folded = {_normalize(b) for b in bound}
1098
+ bound_particles = frozenset(
1099
+ p for p in particles if _normalize(p) in bound_folded)
1100
+ capitalization_exceptions: list[tuple[str, object]] = []
1101
+ for key, value in sorted(self.capitalization_exceptions.items()):
1102
+ if _v1_matchable(key):
1103
+ capitalization_exceptions.append((key, value))
1104
+ else:
1105
+ dropped.append(("capitalization_exceptions", key))
1106
+ if dropped:
1107
+ _warn_unmatchable(dropped)
1010
1108
  # keep in sync with _lexicon._default_lexicon() (pinned by
1011
1109
  # tests/v2/test_config_shim.py::test_snapshot_field_translation)
1012
- lexicon = Lexicon(
1013
- titles=frozenset(self.titles),
1014
- # TRANSLATE, do not filter. The two versions build the same
1015
- # lookup key differently: v1 joins the raw title run and
1016
- # then applies lc(), which strips only the whole string's
1017
- # edge periods, so an interior word keeps its own ("lt.
1018
- # col"). v2 normalizes each token and then joins ("lt col").
1019
- # Re-folding per word converts a v1 entry into the v2
1020
- # spelling; filtering instead dropped every multi-word
1021
- # honorific containing an abbreviation or a conjunction and
1022
- # silently swapped given and family.
1023
- # Only entries v1 could actually match: its key is the
1024
- # joined title run, so always single-spaced and never
1025
- # empty. An entry holding a whitespace run was inert there
1026
- # (translating it would start matching), and one that folds
1027
- # away entirely would trip _normset's empty-entry check on
1028
- # a config v1 simply ignored.
1029
- given_name_titles=frozenset(
1030
- t for t in (
1031
- _title_key(e.split())
1032
- for e in self.first_name_titles
1033
- if e == " ".join(e.split())
1034
- ) if t),
1035
- suffix_acronyms=acronyms,
1036
- suffix_words=suffix_words,
1037
- # Intersect with acronyms: Lexicon enforces ambiguous <=
1038
- # acronyms; v1 behaves the same when an acronym is deleted
1039
- # but its ambiguous entry lingers (the entry stops
1040
- # mattering).
1041
- suffix_acronyms_ambiguous=ambiguous_acronyms,
1042
- particles=particles,
1043
- # complement translation: v1 marks the never-given subset;
1044
- # v2 marks the may-be-given subset. The trailing union keeps
1045
- # a config v1 accepted: particles.py asserts its own data has
1046
- # no word in both NON_GIVEN_NAME_PARTICLES and
1047
- # BOUND_GIVEN_NAMES, but nothing stops a caller adding one at
1048
- # runtime, and v1 then lets the bound rule win (leading "dos
1049
- # Santos Silva" parses first="dos Santos"). Treating such a
1050
- # word as may-be-given reproduces that rather than raising.
1051
- #
1052
- # KNOWN DEVIATION, pinned by
1053
- # test_bound_never_given_prefix_deviates_on_two_pieces: v1's
1054
- # join has a reserve_last guard, so with only two pieces it
1055
- # does NOT fire and the word stays never-given ("dos Santos"
1056
- # -> last="dos Santos"). Promotion here is unconditional, so
1057
- # that case reads given="dos", family="Santos". v1's rule is
1058
- # piece-count dependent and a static vocabulary set cannot
1059
- # express it; the alternative is raising on a config v1
1060
- # accepted, which is worse. Only reachable via a runtime
1061
- # config the shipped data forbids.
1062
- particles_ambiguous=(
1063
- particles - frozenset(self.non_first_name_prefixes))
1064
- | (bound & particles),
1065
- conjunctions=frozenset(self.conjunctions),
1066
- bound_given_names=bound,
1067
- # v1 Constants has no manager for these (#274 is 2.0
1068
- # behavior); the data module is the only source
1069
- maiden_markers=MAIDEN_MARKERS,
1070
- # likewise no v1 manager: the unspaced-name segmentation
1071
- # vocabulary is 2.0 behavior (#271), so it rides in the
1072
- # snapshot only -- v1's Constants surface stays frozen.
1073
- surnames=KOREAN_SURNAMES,
1074
- # likewise no v1 manager: the glued-honorific tail set is
1075
- # 2.1 behavior (#308), so it rides in the snapshot only.
1076
- # Intersect with the word set: Lexicon enforces tails <=
1077
- # suffix_words, and v1 semantics are that deleting a suffix
1078
- # word turns the behavior off -- a lingering tail simply
1079
- # stops mattering, the same rule ambiguous_acronyms gets
1080
- # against suffix_acronyms above.
1081
- honorific_tails=GLUED_HONORIFICS & suffix_words,
1082
- # TupleManager is dict[str, object] (v1 parity: values were
1083
- # never statically str-typed); every real entry is a str,
1084
- # same assumption _DelimiterManager's sentinel lookup makes
1085
- capitalization_exceptions=tuple(
1086
- sorted(self.capitalization_exceptions.items())), # type: ignore[arg-type]
1087
- )
1110
+ # A capitalization_exceptions value that does not spell its key
1111
+ # is the one DECIDED exception to this method's never-raise
1112
+ # rule (#459, decisions.md#R4 and #3-0-reevaluations).
1113
+ # _normpairs raises _MaskValueError for exactly that shape, and
1114
+ # the except clause re-spells its offered fix for a v1 caller.
1115
+ # Caught by TYPE, so every other Lexicon error passes through
1116
+ # unchanged. Only the three SUBSET checks (particles_ambiguous
1117
+ # inside particles, suffix_acronyms_ambiguous inside
1118
+ # suffix_acronyms, honorific_tails inside suffix_words) are
1119
+ # satisfied BY CONSTRUCTION: each is built below as an
1120
+ # intersection or union with its OWN base -- particles_ambiguous
1121
+ # from `particles` itself, honorific_tails from `suffix_words`
1122
+ # itself -- so the image of a subset relation under any
1123
+ # element-wise normalization is still a subset, whatever the
1124
+ # raw spelling. The CONTRADICTION check (bound_given_names &
1125
+ # particles inside particles_ambiguous) and the GATE-BYPASS check
1126
+ # (suffix_acronyms_ambiguous disjoint from suffix_words) do NOT
1127
+ # have that property -- each compares two sets built by SEPARATE
1128
+ # set arithmetic on the shim's own strings. They hold here
1129
+ # because the entries are filtered by _v1_matchable and the two
1130
+ # cross-set computations (suffix_words, bound_particles) compare
1131
+ # _normalize'd spellings, which are the ones Lexicon compares;
1132
+ # every other field is passed raw, as Lexicon folds it itself
1133
+ # (#541, #582).
1134
+ # What else reaches it is a non-str value's TypeError -- a raise
1135
+ # v1 also had, later, at capitalize().
1136
+ try:
1137
+ lexicon = Lexicon(
1138
+ titles=titles,
1139
+ # TRANSLATE, do not filter. The two versions build the same
1140
+ # lookup key differently: v1 joins the raw title run and
1141
+ # then applies lc(), which strips only the whole string's
1142
+ # edge periods, so an interior word keeps its own ("lt.
1143
+ # col"). v2 normalizes each token and then joins ("lt col").
1144
+ # Re-folding per word converts a v1 entry into the v2
1145
+ # spelling; filtering instead dropped every multi-word
1146
+ # honorific containing an abbreviation or a conjunction and
1147
+ # silently swapped given and family.
1148
+ # Entries that match no name word were dropped above
1149
+ # (_v1_matchable); since that includes every entry
1150
+ # Lexicon folds to empty, no key built here is empty.
1151
+ given_name_titles=frozenset(
1152
+ _title_key(e.split()) for e in first_name_titles),
1153
+ suffix_acronyms=acronyms,
1154
+ suffix_words=suffix_words,
1155
+ # Intersect with acronyms: Lexicon enforces ambiguous <=
1156
+ # acronyms; v1 behaves the same when an acronym is deleted
1157
+ # but its ambiguous entry lingers (the entry stops
1158
+ # mattering).
1159
+ suffix_acronyms_ambiguous=ambiguous_acronyms,
1160
+ particles=particles,
1161
+ # complement translation: v1 marks the never-given subset;
1162
+ # v2 marks the may-be-given subset. The trailing union keeps
1163
+ # a config v1 accepted: particles.py asserts its own data has
1164
+ # no word in both NON_GIVEN_NAME_PARTICLES and
1165
+ # BOUND_GIVEN_NAMES, but nothing stops a caller adding one at
1166
+ # runtime, and v1 then lets the bound rule win (leading "dos
1167
+ # Santos Silva" parses first="dos Santos"). Treating such a
1168
+ # word as may-be-given reproduces that rather than raising.
1169
+ #
1170
+ # KNOWN DEVIATION, pinned by
1171
+ # test_bound_never_given_prefix_deviates_on_two_pieces: v1's
1172
+ # join has a reserve_last guard, so with only two pieces it
1173
+ # does NOT fire and the word stays never-given ("dos Santos"
1174
+ # -> last="dos Santos"). Promotion here is unconditional, so
1175
+ # that case reads given="dos", family="Santos". v1's rule is
1176
+ # piece-count dependent and a static vocabulary set cannot
1177
+ # express it; the alternative is raising on a config v1
1178
+ # accepted, which is worse. Only reachable via a runtime
1179
+ # config the shipped data forbids.
1180
+ particles_ambiguous=(
1181
+ particles - non_given)
1182
+ | bound_particles,
1183
+ conjunctions=conjunctions,
1184
+ # no v1 manager of its own: the ambiguous-connective
1185
+ # subset is 2.4 behavior (#383/#479), so it rides in the
1186
+ # snapshot only. Lexicon does NOT check this pair -- unlike
1187
+ # honorific_tails against suffix_words below -- so the
1188
+ # intersection is a provable no-op, kept only for
1189
+ # `_snapshot() == Lexicon.default()` legibility; the v1
1190
+ # knob (deleting the conjunction) turns the marking off
1191
+ # through the fork's own base-vocabulary test rather than
1192
+ # through this intersection.
1193
+ conjunctions_ambiguous=CONJUNCTIONS_AMBIGUOUS & conjunctions,
1194
+ bound_given_names=bound,
1195
+ # v1 Constants has no manager for these (#274 is 2.0
1196
+ # behavior); the data module is the only source
1197
+ maiden_markers=MAIDEN_MARKERS,
1198
+ # likewise no v1 manager: the unspaced-name segmentation
1199
+ # vocabulary is 2.0 behavior (#271), so it rides in the
1200
+ # snapshot only -- v1's Constants surface stays frozen.
1201
+ surnames=KOREAN_SURNAMES,
1202
+ # likewise no v1 manager: the glued-honorific tail set is
1203
+ # 2.1 behavior (#308), so it rides in the snapshot only.
1204
+ # Intersect with the word set: Lexicon enforces tails <=
1205
+ # suffix_words, and v1 semantics are that deleting a suffix
1206
+ # word turns the behavior off -- a lingering tail simply
1207
+ # stops mattering, the same rule ambiguous_acronyms gets
1208
+ # against suffix_acronyms above.
1209
+ honorific_tails=GLUED_HONORIFICS & suffix_words,
1210
+ # TupleManager is dict[str, object] (v1 parity: values were
1211
+ # never statically str-typed); every real entry is a str,
1212
+ # same assumption _DelimiterManager's sentinel lookup makes.
1213
+ # NOT translated: see the comment above the try: block.
1214
+ capitalization_exceptions=tuple(capitalization_exceptions), # type: ignore[arg-type]
1215
+ )
1216
+ except _MaskValueError as e:
1217
+ # A v1-ONLY message built from the fields, not `{e}` --
1218
+ # the 2.0 exception's own message offers the 2.0
1219
+ # `capitalization_exceptions=((...),)` constructor spelling,
1220
+ # which no v1 Constants caller can paste.
1221
+ raise ValueError(
1222
+ f"capitalization_exceptions value {e.value!r} for key "
1223
+ f"{e.key!r} does not spell the key's letters and "
1224
+ f"digits: a value is a case mask, the key's own "
1225
+ f"letters and digits recased -- write "
1226
+ f"constants.capitalization_exceptions[{e.key!r}] = "
1227
+ f"{e.offered!r}"
1228
+ ) from e
1088
1229
  rules = frozenset({PatronymicRule.EAST_SLAVIC, PatronymicRule.TURKIC}) \
1089
1230
  if self.patronymic_name_order else frozenset()
1090
1231
  policy = Policy(