nameparser 2.0.0rc2__tar.gz → 2.2.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 (154) hide show
  1. {nameparser-2.0.0rc2/nameparser.egg-info → nameparser-2.2.0}/PKG-INFO +50 -8
  2. {nameparser-2.0.0rc2 → nameparser-2.2.0}/README.rst +47 -7
  3. {nameparser-2.0.0rc2 → nameparser-2.2.0}/nameparser/__init__.py +7 -3
  4. {nameparser-2.0.0rc2 → nameparser-2.2.0}/nameparser/__main__.py +1 -1
  5. {nameparser-2.0.0rc2 → nameparser-2.2.0}/nameparser/_config_shim.py +61 -43
  6. {nameparser-2.0.0rc2 → nameparser-2.2.0}/nameparser/_facade.py +77 -20
  7. {nameparser-2.0.0rc2 → nameparser-2.2.0}/nameparser/_lexicon.py +160 -49
  8. nameparser-2.2.0/nameparser/_parser.py +314 -0
  9. {nameparser-2.0.0rc2 → nameparser-2.2.0}/nameparser/_pipeline/__init__.py +4 -3
  10. {nameparser-2.0.0rc2 → nameparser-2.2.0}/nameparser/_pipeline/_assemble.py +29 -7
  11. nameparser-2.2.0/nameparser/_pipeline/_assign.py +429 -0
  12. nameparser-2.2.0/nameparser/_pipeline/_classify.py +228 -0
  13. {nameparser-2.0.0rc2 → nameparser-2.2.0}/nameparser/_pipeline/_extract.py +82 -20
  14. nameparser-2.2.0/nameparser/_pipeline/_group.py +989 -0
  15. nameparser-2.2.0/nameparser/_pipeline/_pieces.py +313 -0
  16. nameparser-2.2.0/nameparser/_pipeline/_post_rules.py +629 -0
  17. nameparser-2.2.0/nameparser/_pipeline/_script_segment.py +678 -0
  18. nameparser-2.2.0/nameparser/_pipeline/_segment.py +90 -0
  19. nameparser-2.2.0/nameparser/_pipeline/_state.py +146 -0
  20. nameparser-2.2.0/nameparser/_pipeline/_tokenize.py +197 -0
  21. nameparser-2.2.0/nameparser/_pipeline/_vocab.py +505 -0
  22. nameparser-2.2.0/nameparser/_policy.py +928 -0
  23. nameparser-2.2.0/nameparser/_render.py +316 -0
  24. {nameparser-2.0.0rc2 → nameparser-2.2.0}/nameparser/_types.py +247 -26
  25. nameparser-2.2.0/nameparser/_version.py +19 -0
  26. nameparser-2.2.0/nameparser/config/__init__.py +41 -0
  27. nameparser-2.2.0/nameparser/config/_deprecated.py +141 -0
  28. nameparser-2.2.0/nameparser/config/_invariants.py +53 -0
  29. nameparser-2.2.0/nameparser/config/bound_first_names.py +23 -0
  30. nameparser-2.2.0/nameparser/config/bound_given_names.py +60 -0
  31. {nameparser-2.0.0rc2 → nameparser-2.2.0}/nameparser/config/conjunctions.py +7 -3
  32. nameparser-2.2.0/nameparser/config/maiden_markers.py +111 -0
  33. nameparser-2.2.0/nameparser/config/particles.py +275 -0
  34. nameparser-2.2.0/nameparser/config/prefixes.py +38 -0
  35. nameparser-2.2.0/nameparser/config/suffixes.py +943 -0
  36. nameparser-2.2.0/nameparser/config/surnames.py +48 -0
  37. {nameparser-2.0.0rc2 → nameparser-2.2.0}/nameparser/config/titles.py +58 -19
  38. {nameparser-2.0.0rc2 → nameparser-2.2.0}/nameparser/locales/__init__.py +26 -3
  39. nameparser-2.2.0/nameparser/locales/ja.py +231 -0
  40. {nameparser-2.0.0rc2 → nameparser-2.2.0}/nameparser/locales/ru.py +5 -3
  41. {nameparser-2.0.0rc2 → nameparser-2.2.0}/nameparser/locales/tr_az.py +3 -2
  42. nameparser-2.2.0/nameparser/locales/zh.py +114 -0
  43. {nameparser-2.0.0rc2 → nameparser-2.2.0}/nameparser/parser.py +2 -1
  44. {nameparser-2.0.0rc2 → nameparser-2.2.0/nameparser.egg-info}/PKG-INFO +50 -8
  45. {nameparser-2.0.0rc2 → nameparser-2.2.0}/nameparser.egg-info/SOURCES.txt +22 -2
  46. nameparser-2.2.0/nameparser.egg-info/requires.txt +3 -0
  47. {nameparser-2.0.0rc2 → nameparser-2.2.0}/pyproject.toml +19 -0
  48. nameparser-2.0.0rc2/tests/test_bound_first_names.py → nameparser-2.2.0/tests/test_bound_given_names.py +58 -1
  49. nameparser-2.2.0/tests/test_capitalization.py +364 -0
  50. {nameparser-2.0.0rc2 → nameparser-2.2.0}/tests/test_conjunctions.py +1 -1
  51. {nameparser-2.0.0rc2 → nameparser-2.2.0}/tests/test_first_name.py +24 -4
  52. {nameparser-2.0.0rc2 → nameparser-2.2.0}/tests/test_initials.py +27 -6
  53. {nameparser-2.0.0rc2 → nameparser-2.2.0}/tests/test_nicknames.py +39 -1
  54. nameparser-2.0.0rc2/tests/test_prefixes.py → nameparser-2.2.0/tests/test_particles.py +29 -25
  55. {nameparser-2.0.0rc2 → nameparser-2.2.0}/tests/test_python_api.py +1 -1
  56. {nameparser-2.0.0rc2 → nameparser-2.2.0}/tests/test_titles.py +3 -2
  57. nameparser-2.2.0/tests/v2/_differential_fixtures.py +109 -0
  58. nameparser-2.2.0/tests/v2/cases.py +3601 -0
  59. {nameparser-2.0.0rc2 → nameparser-2.2.0}/tests/v2/conftest.py +10 -15
  60. {nameparser-2.0.0rc2 → nameparser-2.2.0}/tests/v2/pipeline/test_assemble.py +7 -4
  61. nameparser-2.2.0/tests/v2/pipeline/test_assign.py +423 -0
  62. {nameparser-2.0.0rc2 → nameparser-2.2.0}/tests/v2/pipeline/test_classify.py +21 -1
  63. nameparser-2.2.0/tests/v2/pipeline/test_group.py +1130 -0
  64. nameparser-2.2.0/tests/v2/pipeline/test_pieces.py +103 -0
  65. nameparser-2.2.0/tests/v2/pipeline/test_post_rules.py +816 -0
  66. nameparser-2.2.0/tests/v2/pipeline/test_script_segment.py +815 -0
  67. {nameparser-2.0.0rc2 → nameparser-2.2.0}/tests/v2/pipeline/test_segment.py +14 -0
  68. {nameparser-2.0.0rc2 → nameparser-2.2.0}/tests/v2/pipeline/test_state.py +23 -4
  69. nameparser-2.2.0/tests/v2/pipeline/test_tokenize.py +228 -0
  70. nameparser-2.2.0/tests/v2/pipeline/test_vocab.py +454 -0
  71. nameparser-2.2.0/tests/v2/rules_doc.py +200 -0
  72. nameparser-2.2.0/tests/v2/test_benchmark.py +344 -0
  73. nameparser-2.2.0/tests/v2/test_cases.py +72 -0
  74. {nameparser-2.0.0rc2 → nameparser-2.2.0}/tests/v2/test_cli.py +13 -0
  75. nameparser-2.2.0/tests/v2/test_config_aliases.py +404 -0
  76. {nameparser-2.0.0rc2 → nameparser-2.2.0}/tests/v2/test_config_shim.py +29 -6
  77. nameparser-2.2.0/tests/v2/test_contracts.py +241 -0
  78. nameparser-2.2.0/tests/v2/test_differential.py +1361 -0
  79. nameparser-2.2.0/tests/v2/test_doc_citations.py +176 -0
  80. nameparser-2.2.0/tests/v2/test_doc_spellings.py +30 -0
  81. {nameparser-2.0.0rc2 → nameparser-2.2.0}/tests/v2/test_facade.py +97 -2
  82. nameparser-2.2.0/tests/v2/test_facade_cases.py +166 -0
  83. {nameparser-2.0.0rc2 → nameparser-2.2.0}/tests/v2/test_layering.py +80 -12
  84. nameparser-2.2.0/tests/v2/test_ledger_guards.py +2452 -0
  85. {nameparser-2.0.0rc2 → nameparser-2.2.0}/tests/v2/test_lexicon.py +99 -7
  86. nameparser-2.2.0/tests/v2/test_locales.py +1215 -0
  87. nameparser-2.2.0/tests/v2/test_parser.py +1499 -0
  88. {nameparser-2.0.0rc2 → nameparser-2.2.0}/tests/v2/test_policy.py +298 -5
  89. {nameparser-2.0.0rc2 → nameparser-2.2.0}/tests/v2/test_properties.py +223 -18
  90. {nameparser-2.0.0rc2 → nameparser-2.2.0}/tests/v2/test_regex_sync.py +69 -9
  91. nameparser-2.2.0/tests/v2/test_render.py +641 -0
  92. {nameparser-2.0.0rc2 → nameparser-2.2.0}/tests/v2/test_reprs.py +44 -2
  93. nameparser-2.2.0/tests/v2/test_rules_doc.py +145 -0
  94. nameparser-2.2.0/tests/v2/test_rules_doc_grammar.py +95 -0
  95. {nameparser-2.0.0rc2 → nameparser-2.2.0}/tests/v2/test_types.py +78 -2
  96. nameparser-2.0.0rc2/nameparser/_parser.py +0 -192
  97. nameparser-2.0.0rc2/nameparser/_pipeline/_assign.py +0 -276
  98. nameparser-2.0.0rc2/nameparser/_pipeline/_classify.py +0 -95
  99. nameparser-2.0.0rc2/nameparser/_pipeline/_group.py +0 -341
  100. nameparser-2.0.0rc2/nameparser/_pipeline/_post_rules.py +0 -129
  101. nameparser-2.0.0rc2/nameparser/_pipeline/_segment.py +0 -122
  102. nameparser-2.0.0rc2/nameparser/_pipeline/_state.py +0 -94
  103. nameparser-2.0.0rc2/nameparser/_pipeline/_tokenize.py +0 -125
  104. nameparser-2.0.0rc2/nameparser/_pipeline/_vocab.py +0 -126
  105. nameparser-2.0.0rc2/nameparser/_policy.py +0 -504
  106. nameparser-2.0.0rc2/nameparser/_render.py +0 -165
  107. nameparser-2.0.0rc2/nameparser/_version.py +0 -9
  108. nameparser-2.0.0rc2/nameparser/config/__init__.py +0 -20
  109. nameparser-2.0.0rc2/nameparser/config/_invariants.py +0 -27
  110. nameparser-2.0.0rc2/nameparser/config/bound_first_names.py +0 -28
  111. nameparser-2.0.0rc2/nameparser/config/maiden_markers.py +0 -42
  112. nameparser-2.0.0rc2/nameparser/config/prefixes.py +0 -130
  113. nameparser-2.0.0rc2/nameparser/config/suffixes.py +0 -733
  114. nameparser-2.0.0rc2/tests/test_capitalization.py +0 -144
  115. nameparser-2.0.0rc2/tests/v2/cases.py +0 -469
  116. nameparser-2.0.0rc2/tests/v2/pipeline/test_assign.py +0 -171
  117. nameparser-2.0.0rc2/tests/v2/pipeline/test_group.py +0 -161
  118. nameparser-2.0.0rc2/tests/v2/pipeline/test_post_rules.py +0 -122
  119. nameparser-2.0.0rc2/tests/v2/pipeline/test_tokenize.py +0 -91
  120. nameparser-2.0.0rc2/tests/v2/pipeline/test_vocab.py +0 -40
  121. nameparser-2.0.0rc2/tests/v2/test_benchmark.py +0 -122
  122. nameparser-2.0.0rc2/tests/v2/test_cases.py +0 -26
  123. nameparser-2.0.0rc2/tests/v2/test_contracts.py +0 -84
  124. nameparser-2.0.0rc2/tests/v2/test_facade_cases.py +0 -75
  125. nameparser-2.0.0rc2/tests/v2/test_locales.py +0 -550
  126. nameparser-2.0.0rc2/tests/v2/test_parser.py +0 -438
  127. nameparser-2.0.0rc2/tests/v2/test_render.py +0 -290
  128. {nameparser-2.0.0rc2 → nameparser-2.2.0}/AUTHORS +0 -0
  129. {nameparser-2.0.0rc2 → nameparser-2.2.0}/LICENSE +0 -0
  130. {nameparser-2.0.0rc2 → nameparser-2.2.0}/MANIFEST.in +0 -0
  131. {nameparser-2.0.0rc2 → nameparser-2.2.0}/nameparser/_locale.py +0 -0
  132. {nameparser-2.0.0rc2 → nameparser-2.2.0}/nameparser/config/capitalization.py +0 -0
  133. {nameparser-2.0.0rc2 → nameparser-2.2.0}/nameparser/config/regexes.py +0 -0
  134. {nameparser-2.0.0rc2 → nameparser-2.2.0}/nameparser/py.typed +0 -0
  135. {nameparser-2.0.0rc2 → nameparser-2.2.0}/nameparser/util.py +0 -0
  136. {nameparser-2.0.0rc2 → nameparser-2.2.0}/nameparser.egg-info/dependency_links.txt +0 -0
  137. {nameparser-2.0.0rc2 → nameparser-2.2.0}/nameparser.egg-info/top_level.txt +0 -0
  138. {nameparser-2.0.0rc2 → nameparser-2.2.0}/setup.cfg +0 -0
  139. {nameparser-2.0.0rc2 → nameparser-2.2.0}/tests/__init__.py +0 -0
  140. {nameparser-2.0.0rc2 → nameparser-2.2.0}/tests/base.py +0 -0
  141. {nameparser-2.0.0rc2 → nameparser-2.2.0}/tests/conftest.py +0 -0
  142. {nameparser-2.0.0rc2 → nameparser-2.2.0}/tests/test_brute_force.py +0 -0
  143. {nameparser-2.0.0rc2 → nameparser-2.2.0}/tests/test_comma_variants.py +0 -0
  144. {nameparser-2.0.0rc2 → nameparser-2.2.0}/tests/test_constants.py +0 -0
  145. {nameparser-2.0.0rc2 → nameparser-2.2.0}/tests/test_east_slavic_patronymic_order.py +0 -0
  146. {nameparser-2.0.0rc2 → nameparser-2.2.0}/tests/test_middle_name_as_last.py +0 -0
  147. {nameparser-2.0.0rc2 → nameparser-2.2.0}/tests/test_output_format.py +0 -0
  148. {nameparser-2.0.0rc2 → nameparser-2.2.0}/tests/test_suffixes.py +0 -0
  149. {nameparser-2.0.0rc2 → nameparser-2.2.0}/tests/test_turkic_patronymic_order.py +0 -0
  150. {nameparser-2.0.0rc2 → nameparser-2.2.0}/tests/test_variations.py +0 -0
  151. {nameparser-2.0.0rc2 → nameparser-2.2.0}/tests/v2/__init__.py +0 -0
  152. {nameparser-2.0.0rc2 → nameparser-2.2.0}/tests/v2/pipeline/__init__.py +0 -0
  153. {nameparser-2.0.0rc2 → nameparser-2.2.0}/tests/v2/pipeline/test_extract.py +0 -0
  154. {nameparser-2.0.0rc2 → nameparser-2.2.0}/tests/v2/test_locale.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nameparser
3
- Version: 2.0.0rc2
3
+ Version: 2.2.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
@@ -22,6 +22,8 @@ Requires-Python: >=3.11
22
22
  Description-Content-Type: text/x-rst
23
23
  License-File: LICENSE
24
24
  License-File: AUTHORS
25
+ Provides-Extra: ja
26
+ Requires-Dist: namedivider-python>=0.4; extra == "ja"
25
27
  Dynamic: license-file
26
28
 
27
29
  Name Parser
@@ -33,6 +35,19 @@ nameparser parses human names into seven fields — title, given, middle,
33
35
  family, suffix, nickname, maiden. Results are immutable, configuration is
34
36
  composable, and locale packs are opt-in.
35
37
 
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
49
+ <https://nameparser.readthedocs.io/en/latest/usage.html#east-asian-names>`__.
50
+
36
51
  Installation
37
52
  ------------
38
53
 
@@ -48,16 +63,43 @@ Quick Start Example
48
63
  .. code-block:: python
49
64
 
50
65
  >>> from nameparser import parse
51
- >>> name = parse("Dr. Juan Q. Xavier de la Vega III")
52
- >>> name.given, name.family
53
- ('Juan', 'de la Vega')
66
+ >>> name = parse("Dr. Juan Q. Xavier de la Vega III (Doc Vega)")
67
+ >>> name
68
+ <ParsedName: [
69
+ title: 'Dr.'
70
+ given: 'Juan'
71
+ middle: 'Q. Xavier'
72
+ family: 'de la Vega'
73
+ suffix: 'III'
74
+ nickname: 'Doc Vega'
75
+ ]>
76
+ >>> name.family_base, name.family_particles
77
+ ('Vega', 'de la')
78
+ >>> name.given_names, name.surnames
79
+ ('Juan Q. Xavier', 'Q. Xavier de la Vega')
54
80
  >>> name.render("{family}, {given}")
55
81
  'de la Vega, Juan'
56
82
 
57
- Those seven fields are ``title``, ``given``, ``middle``, ``family``,
58
- ``suffix``, ``nickname``, and ``maiden`` — plus aggregate views like
59
- ``given_names``, ``surnames``, ``family_base``, and ``family_particles``
60
- for combining or splitting them further.
83
+ >>> parse("김민준").family # Korean: unspaced, split on the census list
84
+ '김'
85
+ >>> parse("高橋 みなみ").family # Japanese: kanji with kana, family first
86
+ '高橋'
87
+ >>> parse("김민준씨").suffix # an honorific written against the name
88
+ '씨'
89
+ >>> parse("г-н Иван Петров").title # Cyrillic title
90
+ 'г-н'
91
+ >>> parse("محمد بن سلمان").family # Arabic: بن chains onto the family name
92
+ 'بن سلمان'
93
+
94
+ >>> from nameparser import locales, parser_for
95
+ >>> chinese = parser_for(locales.ZH) # Han text does not say which language
96
+ >>> chinese.parse("毛泽东").family # so splitting it is opt-in
97
+ '毛'
98
+ >>> russian = parser_for(locales.RU)
99
+ >>> russian.parse("Сидоров Иван Петрович").family
100
+ 'Сидоров'
101
+ >>> locales.available()
102
+ ('ja', 'ru', 'tr_az', 'zh')
61
103
 
62
104
  Learn more
63
105
  ----------
@@ -7,6 +7,19 @@ 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
21
+ <https://nameparser.readthedocs.io/en/latest/usage.html#east-asian-names>`__.
22
+
10
23
  Installation
11
24
  ------------
12
25
 
@@ -22,16 +35,43 @@ Quick Start Example
22
35
  .. code-block:: python
23
36
 
24
37
  >>> from nameparser import parse
25
- >>> name = parse("Dr. Juan Q. Xavier de la Vega III")
26
- >>> name.given, name.family
27
- ('Juan', 'de la Vega')
38
+ >>> name = parse("Dr. Juan Q. Xavier de la Vega III (Doc Vega)")
39
+ >>> name
40
+ <ParsedName: [
41
+ title: 'Dr.'
42
+ given: 'Juan'
43
+ middle: 'Q. Xavier'
44
+ family: 'de la Vega'
45
+ suffix: 'III'
46
+ nickname: 'Doc Vega'
47
+ ]>
48
+ >>> name.family_base, name.family_particles
49
+ ('Vega', 'de la')
50
+ >>> name.given_names, name.surnames
51
+ ('Juan Q. Xavier', 'Q. Xavier de la Vega')
28
52
  >>> name.render("{family}, {given}")
29
53
  'de la Vega, Juan'
30
54
 
31
- Those seven fields are ``title``, ``given``, ``middle``, ``family``,
32
- ``suffix``, ``nickname``, and ``maiden`` — plus aggregate views like
33
- ``given_names``, ``surnames``, ``family_base``, and ``family_particles``
34
- for combining or splitting them further.
55
+ >>> parse("김민준").family # Korean: unspaced, split on the census list
56
+ '김'
57
+ >>> parse("高橋 みなみ").family # Japanese: kanji with kana, family first
58
+ '高橋'
59
+ >>> parse("김민준씨").suffix # an honorific written against the name
60
+ '씨'
61
+ >>> parse("г-н Иван Петров").title # Cyrillic title
62
+ 'г-н'
63
+ >>> parse("محمد بن سلمان").family # Arabic: بن chains onto the family name
64
+ 'بن سلمان'
65
+
66
+ >>> from nameparser import locales, parser_for
67
+ >>> chinese = parser_for(locales.ZH) # Han text does not say which language
68
+ >>> chinese.parse("毛泽东").family # so splitting it is opt-in
69
+ '毛'
70
+ >>> russian = parser_for(locales.RU)
71
+ >>> russian.parse("Сидоров Иван Петрович").family
72
+ 'Сидоров'
73
+ >>> locales.available()
74
+ ('ja', 'ru', 'tr_az', 'zh')
35
75
 
36
76
  Learn more
37
77
  ----------
@@ -11,6 +11,7 @@ from nameparser._locale import Locale
11
11
  from nameparser._parser import Parser, parse, parser_for
12
12
  from nameparser._policy import (
13
13
  DEFAULT_NICKNAME_DELIMITERS,
14
+ DEFAULT_SCRIPT_ORDERS,
14
15
  FAMILY_FIRST,
15
16
  FAMILY_FIRST_GIVEN_LAST,
16
17
  GIVEN_FIRST,
@@ -18,6 +19,7 @@ from nameparser._policy import (
18
19
  PatronymicRule,
19
20
  Policy,
20
21
  PolicyPatch,
22
+ Script,
21
23
  )
22
24
  from nameparser._types import (
23
25
  STABLE_TAGS,
@@ -25,6 +27,8 @@ from nameparser._types import (
25
27
  AmbiguityKind,
26
28
  ParsedName,
27
29
  Role,
30
+ Segmentation,
31
+ Segmenter,
28
32
  Span,
29
33
  Token,
30
34
  )
@@ -34,9 +38,9 @@ __all__ = [
34
38
  "HumanName",
35
39
  # v2 core
36
40
  "Span", "Role", "Token", "Ambiguity", "AmbiguityKind", "ParsedName",
37
- "STABLE_TAGS",
38
- "Lexicon", "Policy", "PolicyPatch", "PatronymicRule", "UNSET",
41
+ "STABLE_TAGS", "Segmentation", "Segmenter",
42
+ "Lexicon", "Policy", "PolicyPatch", "PatronymicRule", "Script", "UNSET",
39
43
  "GIVEN_FIRST", "FAMILY_FIRST", "FAMILY_FIRST_GIVEN_LAST",
40
- "DEFAULT_NICKNAME_DELIMITERS", "Locale",
44
+ "DEFAULT_NICKNAME_DELIMITERS", "DEFAULT_SCRIPT_ORDERS", "Locale",
41
45
  "Parser", "parse", "parser_for",
42
46
  ]
@@ -1,4 +1,4 @@
1
- """Command-line debug helper over the 2.0 API (migration spec §6).
1
+ """Command-line debug helper over the 2.0 API.
2
2
 
3
3
  python -m nameparser "Dr. Juan Q. Xavier de la Vega III"
4
4
  python -m nameparser --json "Doe, John"
@@ -1,5 +1,6 @@
1
- """v1 ``Constants`` compatibility shim over Lexicon/Policy (migration
2
- spec §3). ``nameparser.config`` re-exports these names from the swap
1
+ """v1 ``Constants`` compatibility shim over Lexicon/Policy
2
+ (mechanisms.md#CONFIG-SHIM-SNAPSHOT). ``nameparser.config``
3
+ re-exports these names from the swap
3
4
  commit onward; the whole module is deleted in 3.0 with the facade.
4
5
 
5
6
  Layering: facade layer -- may import anything public; here that's
@@ -99,7 +100,7 @@ def _normalize_iterable_of_strings(
99
100
  class SetManager:
100
101
  """v1 ``SetManager`` surface over a plain set of ``lc()``-normalized
101
102
  strings. Mutations call ``_on_change`` (the owning Constants'
102
- generation bump, wired by a later task). ``__call__`` and the
103
+ generation bump, wired by the facade). ``__call__`` and the
103
104
  missing-member-tolerant ``remove()`` are gone per the #243 schedule
104
105
  (warned 1.3.0, removed 2.0): ``remove()`` of a missing member raises
105
106
  ``KeyError``, matching ``set.remove``.
@@ -316,7 +317,7 @@ class TupleManager(dict[str, object]):
316
317
  ``AttributeError`` naming the key (#256, warned 1.4, enforced 2.0 --
317
318
  the v1 ``DeprecationWarning`` is gone, this shim only speaks 2.0).
318
319
  Mutations call ``_on_change`` (the owning Constants' generation
319
- bump, wired by a later task).
320
+ bump, wired by the facade).
320
321
  """
321
322
 
322
323
  _on_change: Callable[[], None] | None
@@ -431,7 +432,7 @@ class TupleManager(dict[str, object]):
431
432
 
432
433
 
433
434
  #: The named delimiter buckets, translated to the ``Policy``
434
- #: (open, close) pairs they stand for (spec §3). The first three are
435
+ #: (open, close) pairs they stand for. The first three are
435
436
  #: v1's; the rest are the #273 typographic conventions, named so the
436
437
  #: v1 keyed idioms (pop/move/del) work on them like the originals.
437
438
  #: Keep in sync with DEFAULT_NICKNAME_DELIMITERS in _policy.py (pinned
@@ -472,8 +473,8 @@ class RegexTupleManager(TupleManager): # pickle-compat: do NOT delete
472
473
 
473
474
  class _DelimiterManager(TupleManager):
474
475
  """v1 ``nickname_delimiters``/``maiden_delimiters`` bucket. In 2.0
475
- only the named sentinels in ``_DELIMITER_SENTINELS`` exist (spec
476
- §3; the v1 trio plus the #273 typographic pairs) -- assigning any
476
+ only the named sentinels in ``_DELIMITER_SENTINELS`` exist (the v1
477
+ trio plus the #273 typographic pairs) -- assigning any
477
478
  other key raises so a caller reaches for a custom-delimiter Policy
478
479
  kwarg instead of a dict entry that silently does nothing. ``pop()``/
479
480
  ``__setitem__``/``__delitem__`` stay open (inherited) for the
@@ -517,7 +518,7 @@ class _RegexesProxy:
517
518
  ``CONSTANTS.regexes.word`` stays informational -- but 2.0 configures
518
519
  parsing behavior through named ``Policy`` flags, not by mutating a
519
520
  regex, so any attribute *or* item assignment raises ``TypeError``
520
- (spec §3's uniform read-only rule).
521
+ (the shim's uniform read-only rule).
521
522
  """
522
523
 
523
524
  @staticmethod
@@ -664,36 +665,38 @@ _SHARED_MUTATION_MESSAGE = (
664
665
  )
665
666
 
666
667
 
667
- def _default_vocab() -> dict[str, set[str]]:
668
+ def _default_vocab() -> dict[str, frozenset[str]]:
668
669
  # v1 data modules stay the single vocabulary source through 2.x
669
670
  # (same rule as Lexicon.default()).
670
- from nameparser.config.bound_first_names import BOUND_FIRST_NAMES
671
+ from nameparser.config.bound_given_names import BOUND_GIVEN_NAMES
671
672
  from nameparser.config.conjunctions import CONJUNCTIONS
672
- from nameparser.config.prefixes import (
673
- NON_FIRST_NAME_PREFIXES, PREFIXES,
673
+ from nameparser.config.particles import (
674
+ NON_GIVEN_NAME_PARTICLES, PARTICLES,
674
675
  )
675
676
  from nameparser.config.suffixes import (
676
- SUFFIX_ACRONYMS, SUFFIX_ACRONYMS_AMBIGUOUS, SUFFIX_NOT_ACRONYMS,
677
+ SUFFIX_ACRONYMS, SUFFIX_ACRONYMS_AMBIGUOUS, SUFFIX_WORDS,
677
678
  )
678
- from nameparser.config.titles import FIRST_NAME_TITLES, TITLES
679
+ from nameparser.config.titles import GIVEN_NAME_TITLES, TITLES
679
680
  return {
680
- "prefixes": PREFIXES,
681
+ "prefixes": PARTICLES,
681
682
  "suffix_acronyms": SUFFIX_ACRONYMS,
682
- "suffix_not_acronyms": SUFFIX_NOT_ACRONYMS,
683
+ "suffix_not_acronyms": SUFFIX_WORDS,
683
684
  "suffix_acronyms_ambiguous": SUFFIX_ACRONYMS_AMBIGUOUS,
684
685
  "titles": TITLES,
685
- "first_name_titles": FIRST_NAME_TITLES,
686
+ "first_name_titles": GIVEN_NAME_TITLES,
686
687
  "conjunctions": CONJUNCTIONS,
687
- "bound_first_names": BOUND_FIRST_NAMES,
688
- "non_first_name_prefixes": NON_FIRST_NAME_PREFIXES,
688
+ "bound_first_names": BOUND_GIVEN_NAMES,
689
+ "non_first_name_prefixes": NON_GIVEN_NAME_PARTICLES,
689
690
  }
690
691
 
691
692
 
692
693
  class _RenderDefaults(NamedTuple):
693
694
  """v1 scalar rendering knobs that have no home on ``Policy``
694
- (spec §3): ``__str__``/initials formatting and capitalization stay
695
+ (mechanisms.md#CONFIG-SHIM-SNAPSHOT): ``__str__``/initials
696
+ formatting and capitalization stay
695
697
  per-Constants defaults, layered onto a shared ``Parser`` by the
696
- facade (a later task) rather than folded into the cache key."""
698
+ facade (nameparser/_facade.py) rather than folded into the cache
699
+ key."""
697
700
 
698
701
  string_format: str | None
699
702
  initials_format: str
@@ -707,7 +710,8 @@ class _RenderDefaults(NamedTuple):
707
710
  @functools.lru_cache(maxsize=64)
708
711
  def _cached_parser(lexicon: Lexicon, policy: Policy) -> Parser:
709
712
  # keyed on hashable value objects: shared across every facade whose
710
- # Constants resolve to the same snapshot (spec §3)
713
+ # Constants resolve to the same snapshot
714
+ # (mechanisms.md#CONFIG-SHIM-SNAPSHOT)
711
715
  return Parser(lexicon=lexicon, policy=policy)
712
716
 
713
717
 
@@ -716,8 +720,8 @@ class Constants:
716
720
  a frozen ``(Lexicon, Policy, _RenderDefaults)`` snapshot via
717
721
  ``_snapshot()``. ``_generation`` increments on every mutation;
718
722
  facades compare it against a cached value to decide whether their
719
- snapshot is stale (dirty-tracking, spec §3 -- the facade itself is
720
- a later task).
723
+ snapshot is stale (dirty-tracking -- the facade side lives in
724
+ nameparser/_facade.py).
721
725
 
722
726
  The module-level ``CONSTANTS`` singleton (below) has ``_shared``
723
727
  flipped to ``True``: any mutation reached through it emits
@@ -980,16 +984,29 @@ class Constants:
980
984
 
981
985
  def _build_snapshot(self) -> tuple[Lexicon, Policy, _RenderDefaults]:
982
986
  """Resolve this v1-shaped, mutable Constants into the frozen
983
- 2.0 value objects it corresponds to (spec §3). A pure read: no
987
+ 2.0 value objects it corresponds to. A pure read: no
984
988
  generation bump, no deprecation warning even on the shared
985
989
  singleton -- only direct attribute mutation is on the 3.0
986
990
  removal path.
987
991
  """
988
992
  from nameparser.config.maiden_markers import MAIDEN_MARKERS
993
+ from nameparser.config.suffixes import GLUED_HONORIFICS
994
+ from nameparser.config.surnames import KOREAN_SURNAMES
989
995
  acronyms = frozenset(self.suffix_acronyms)
990
996
  particles = frozenset(self.prefixes)
991
997
  bound = frozenset(self.bound_first_names)
992
998
  ambiguous_acronyms = frozenset(self.suffix_acronyms_ambiguous) & acronyms
999
+ # Drop any ambiguous acronym from the word set rather than the
1000
+ # other way round. Lexicon forbids the overlap because the word
1001
+ # branch bypasses the period gate, and adding an ambiguous
1002
+ # acronym to suffix_not_acronyms is INERT in v1 anyway:
1003
+ # is_suffix already accepts it via the acronym branch, and
1004
+ # reserve_last keeps it as the surname. So ignoring the
1005
+ # addition reproduces v1 ("Jack Ma" keeps last='Ma'), where
1006
+ # dropping it from the AMBIGUOUS set instead ungated the word
1007
+ # and lost the family name -- a silent misparse worse than the
1008
+ # raise it avoided.
1009
+ suffix_words = frozenset(self.suffix_not_acronyms) - ambiguous_acronyms
993
1010
  # keep in sync with _lexicon._default_lexicon() (pinned by
994
1011
  # tests/v2/test_config_shim.py::test_snapshot_field_translation)
995
1012
  lexicon = Lexicon(
@@ -1016,18 +1033,7 @@ class Constants:
1016
1033
  if e == " ".join(e.split())
1017
1034
  ) if t),
1018
1035
  suffix_acronyms=acronyms,
1019
- # Drop any ambiguous acronym from the word set rather than
1020
- # the other way round. Lexicon forbids the overlap because
1021
- # the word branch bypasses the period gate, and adding an
1022
- # ambiguous acronym to suffix_not_acronyms is INERT in v1
1023
- # anyway: is_suffix already accepts it via the acronym
1024
- # branch, and reserve_last keeps it as the surname. So
1025
- # ignoring the addition reproduces v1 ("Jack Ma" keeps
1026
- # last='Ma'), where dropping it from the AMBIGUOUS set
1027
- # instead ungated the word and lost the family name --
1028
- # a silent misparse worse than the raise it avoided.
1029
- suffix_words=frozenset(
1030
- self.suffix_not_acronyms) - ambiguous_acronyms,
1036
+ suffix_words=suffix_words,
1031
1037
  # Intersect with acronyms: Lexicon enforces ambiguous <=
1032
1038
  # acronyms; v1 behaves the same when an acronym is deleted
1033
1039
  # but its ambiguous entry lingers (the entry stops
@@ -1036,9 +1042,9 @@ class Constants:
1036
1042
  particles=particles,
1037
1043
  # complement translation: v1 marks the never-given subset;
1038
1044
  # v2 marks the may-be-given subset. The trailing union keeps
1039
- # a config v1 accepted: prefixes.py asserts its own data has
1040
- # no word in both non_first_name_prefixes and
1041
- # bound_first_names, but nothing stops a caller adding one at
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
1042
1048
  # runtime, and v1 then lets the bound rule win (leading "dos
1043
1049
  # Santos Silva" parses first="dos Santos"). Treating such a
1044
1050
  # word as may-be-given reproduces that rather than raising.
@@ -1060,7 +1066,19 @@ class Constants:
1060
1066
  bound_given_names=bound,
1061
1067
  # v1 Constants has no manager for these (#274 is 2.0
1062
1068
  # behavior); the data module is the only source
1063
- maiden_markers=frozenset(MAIDEN_MARKERS),
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,
1064
1082
  # TupleManager is dict[str, object] (v1 parity: values were
1065
1083
  # never statically str-typed); every real entry is a str,
1066
1084
  # same assumption _DelimiterManager's sentinel lookup makes
@@ -1081,8 +1099,8 @@ class Constants:
1081
1099
  _SENTINEL_PAIRS[k] for k in self.maiden_delimiters
1082
1100
  if k not in self.nickname_delimiters),
1083
1101
  # suffix_delimiter is a _RenderDefaults-only field here; the
1084
- # facade layers it onto extra_suffix_delimiters per instance
1085
- # (a later task) -- _snapshot() itself stays pure translation
1102
+ # facade layers it onto extra_suffix_delimiters per
1103
+ # instance -- _snapshot() itself stays pure translation
1086
1104
  )
1087
1105
  defaults = _RenderDefaults(
1088
1106
  self.string_format, self.initials_format, self.initials_delimiter,
@@ -1,4 +1,5 @@
1
- """The 2.0 ``HumanName`` facade (migration spec §2): a mutable wrapper
1
+ """The 2.0 ``HumanName`` facade (mechanisms.md#FACADE-CONTRACT): a
2
+ mutable wrapper
2
3
  over a frozen ParsedName, delegating parsing to the core Parser resolved
3
4
  from the bound Constants shim. Keeps every v1 spelling. Deleted in 3.0.
4
5
 
@@ -31,7 +32,8 @@ import nameparser._render as _render
31
32
  from nameparser._config_shim import CONSTANTS, Constants, _cached_parser
32
33
  from nameparser._lexicon import _normalize
33
34
  from nameparser._parser import Parser
34
- from nameparser._types import FOLDED_TAG, ParsedName, Role, Token
35
+ from nameparser._types import (FOLDED_TAG, UNCLASSIFIED_TAG, ParsedName,
36
+ Role, Token)
35
37
 
36
38
  _V2_FIELD = {"first": "given", "last": "family"} # v1 name -> v2 name
37
39
  _V1_SPELLING = {v2: v1 for v1, v2 in _V2_FIELD.items()}
@@ -41,7 +43,8 @@ _MEMBERS = tuple(_V1_SPELLING.get(r.value, r.value) for r in Role)
41
43
 
42
44
 
43
45
 
44
- #: v1 parsing hooks the facade never calls (spec §2 exception 2 / #280).
46
+ #: v1 parsing hooks the facade never calls
47
+ #: (mechanisms.md#FACADE-CONTRACT / #280).
45
48
  _V1_HOOKS = (
46
49
  "pre_process", "post_process", "parse_full_name", "parse_pieces",
47
50
  "parse_nicknames", "join_on_conjunctions", "squash_emoji",
@@ -152,7 +155,8 @@ class HumanName:
152
155
  DeprecationWarning, stacklevel=3)
153
156
 
154
157
  # -- render defaults -----------------------------------------------------
155
- # One-line validating setters (spec §2): assigning a non-str (or, for
158
+ # One-line validating setters (mechanisms.md#FACADE-CONTRACT):
159
+ # assigning a non-str (or, for
156
160
  # the two fields that allow it, non-str-non-None) raises TypeError at
157
161
  # assignment time instead of failing later inside .format().
158
162
 
@@ -218,7 +222,8 @@ class HumanName:
218
222
  # -- config / parsing ---------------------------------------------------
219
223
 
220
224
  def _resolve(self) -> Parser:
221
- """Dirty-tracked parser resolution (spec §3): rebuild the
225
+ """Dirty-tracked parser resolution
226
+ (mechanisms.md#CONFIG-SHIM-SNAPSHOT): rebuild the
222
227
  snapshot only when the bound Constants' generation moved."""
223
228
  gen = self._C._generation
224
229
  if self._snapshot_gen != gen:
@@ -472,10 +477,14 @@ class HumanName:
472
477
  return _normalize(text) in self._lexicon.conjunctions
473
478
 
474
479
  def _split_last(self) -> tuple[list[str], list[str]]:
475
- # v1 parser.py _split_last, verbatim: vocabulary lookup at ACCESS
476
- # time (so assigned last names split too), with the all-particle
477
- # guard (a family name is assumed not to consist entirely of
478
- # particles, e.g. surname "Do" which also appears in PREFIXES)
480
+ # rules.md#R2: "a name part whose every word is particle
481
+ # vocabulary is a part where none of them is doing a
482
+ # particle's work" -- the all-particle guard
483
+ # below is this rule, and predates its statement: v1 assumed a
484
+ # family name does not consist entirely of particles, e.g. the
485
+ # surname "Do" which also appears in PARTICLES. v1
486
+ # parser.py _split_last otherwise verbatim, vocabulary lookup
487
+ # at ACCESS time so assigned last names split too.
479
488
  words = " ".join(self.last_list).split()
480
489
  i = 0
481
490
  while i < len(words) and self._is_particle(words[i]):
@@ -517,20 +526,40 @@ class HumanName:
517
526
  if len(initials) > 0:
518
527
  return self.initials_separator.join(initials)
519
528
  # Return '' (never empty_attribute_default, which may be None)
520
- # when a part has no initialable words, e.g. a middle name
521
- # consisting only of prefixes ("de la"). Callers drop these
522
- # parts entirely.
529
+ # when a part has no initialable words. group_initials below
530
+ # decides what that means: one such element among others is
531
+ # dropped; a group that yields nothing AND is wholly particles
532
+ # initials its words; and a group that yields nothing for any
533
+ # other reason -- a conjunction, or particles mixed with one --
534
+ # is still dropped ("Vega, Santa de y" drops its middle).
523
535
  return ""
524
536
 
525
537
  def _initials_lists(self) -> tuple[list[str], list[str], list[str]]:
526
538
  """Initials for the first, middle and last name groups. Parts
527
- that yield no initials (e.g. a prefix-only middle name like
528
- "de la") are dropped rather than kept as empty strings.
539
+ that yield no initials are dropped rather than kept as empty
540
+ strings -- except a part that is wholly PARTICLES, whose words
541
+ initial as ordinary name words since #404, so the prefix-only
542
+ middle name "de la" is no longer an example of the dropping.
529
543
  """
530
544
  def group_initials(names: list[str],
531
545
  firstname: bool = False) -> list[str]:
532
- return [i for i in (self._process_initial(n, firstname)
533
- for n in names if n) if i]
546
+ got = [i for i in (self._process_initial(n, firstname)
547
+ for n in names if n) if i]
548
+ words = [w for n in names if n for w in n.split()]
549
+ if got or not words or not all(self._is_particle(w)
550
+ for w in words):
551
+ return got
552
+ # rules.md#R3: "except the particles of a part whose every
553
+ # word is one, which are not acting as particles there"
554
+ # -- nothing survived
555
+ # the filter, so the whole group is particles. The
556
+ # facade's twin of the core's
557
+ # UNJOINED_TAG. NOT pinned against it: both case runners
558
+ # compare the seven role fields only, and Case carries no
559
+ # initials column, so the one covering test is
560
+ # tests/test_initials.py::test_initials_middle_name_all_prefixes. _split_last already applies the same guard to
561
+ # the base, which is why last_base was never empty here.
562
+ return [w[0] for w in words]
534
563
  return (group_initials(self.first_list, True),
535
564
  group_initials(self.middle_list),
536
565
  group_initials(self.last_list))
@@ -623,6 +652,24 @@ class HumanName:
623
652
  # spelling the facade actually exposes as attributes.
624
653
  return getattr(self, _V1_SPELLING.get(key, key))
625
654
 
655
+ def __setattr__(self, name: str, value: object) -> None:
656
+ # "given"/"family" are the 2.0 spellings of first/last; the
657
+ # facade has no such attributes, so plain assignment creates a
658
+ # stray instance attribute while the parse (and .first/.last)
659
+ # keeps the old value -- a silently forked name. Warn but
660
+ # still set: ad-hoc attribute stashing is a legal v1 pattern,
661
+ # so any code that worked keeps working. Only these two names
662
+ # warn -- the other five 2.0 field names are real properties
663
+ # whose setters work, and Role members reach here as their
664
+ # string values (StrEnum).
665
+ if name in _V1_SPELLING:
666
+ warnings.warn(
667
+ f"assigning HumanName.{name} creates an inert attribute; "
668
+ f"the parse is unchanged -- use .{_V1_SPELLING[name]} "
669
+ f"(the v1 spelling) to update the name",
670
+ UserWarning, stacklevel=2)
671
+ super().__setattr__(name, value)
672
+
626
673
  def as_dict(self, include_empty: bool = True) -> dict[str, str]:
627
674
  """The seven v1-named components as a dict; include_empty=False
628
675
  drops empty fields."""
@@ -667,7 +714,8 @@ class HumanName:
667
714
  self._suffix_delimiter = state.get("suffix_delimiter",
668
715
  defaults.suffix_delimiter)
669
716
  self._full_name = state.get("_full_name", "")
670
- # Components come back exactly as pickled (spec §2): synthetic
717
+ # Components come back exactly as pickled
718
+ # (mechanisms.md#FACADE-CONTRACT): synthetic
671
719
  # tokens, never a re-parse. Build them per *_list ENTRY rather
672
720
  # than from one joined string -- an entry may hold several words
673
721
  # ("Ph. D.", "Q.C. M.P."), and re-splitting the joined string on
@@ -699,8 +747,17 @@ class HumanName:
699
747
  f"nameparser"
700
748
  )
701
749
  for position, word in enumerate(entry.split()):
702
- tokens.append(Token(
703
- word, None, role,
704
- frozenset({"joined"}) if position else frozenset()))
750
+ # UNCLASSIFIED_TAG for the same reason replace()
751
+ # stamps it: a pickle carries the *_list STRINGS
752
+ # and no tags, so nothing here was read by a parse
753
+ # and case repair must ask the vocabulary rather
754
+ # than read an absent conjunction tag. Without it a
755
+ # restored "juan ortega y gasset" repairs to
756
+ # "Ortega Y Gasset", which is neither v1's answer
757
+ # nor the same name's unpickled one.
758
+ tags = {UNCLASSIFIED_TAG}
759
+ if position:
760
+ tags.add("joined")
761
+ tokens.append(Token(word, None, role, frozenset(tags)))
705
762
  self._parsed = ParsedName(
706
763
  original=str(state.get("original", "")), tokens=tuple(tokens))