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.
- {nameparser-2.0.0rc2/nameparser.egg-info → nameparser-2.2.0}/PKG-INFO +50 -8
- {nameparser-2.0.0rc2 → nameparser-2.2.0}/README.rst +47 -7
- {nameparser-2.0.0rc2 → nameparser-2.2.0}/nameparser/__init__.py +7 -3
- {nameparser-2.0.0rc2 → nameparser-2.2.0}/nameparser/__main__.py +1 -1
- {nameparser-2.0.0rc2 → nameparser-2.2.0}/nameparser/_config_shim.py +61 -43
- {nameparser-2.0.0rc2 → nameparser-2.2.0}/nameparser/_facade.py +77 -20
- {nameparser-2.0.0rc2 → nameparser-2.2.0}/nameparser/_lexicon.py +160 -49
- nameparser-2.2.0/nameparser/_parser.py +314 -0
- {nameparser-2.0.0rc2 → nameparser-2.2.0}/nameparser/_pipeline/__init__.py +4 -3
- {nameparser-2.0.0rc2 → nameparser-2.2.0}/nameparser/_pipeline/_assemble.py +29 -7
- nameparser-2.2.0/nameparser/_pipeline/_assign.py +429 -0
- nameparser-2.2.0/nameparser/_pipeline/_classify.py +228 -0
- {nameparser-2.0.0rc2 → nameparser-2.2.0}/nameparser/_pipeline/_extract.py +82 -20
- nameparser-2.2.0/nameparser/_pipeline/_group.py +989 -0
- nameparser-2.2.0/nameparser/_pipeline/_pieces.py +313 -0
- nameparser-2.2.0/nameparser/_pipeline/_post_rules.py +629 -0
- nameparser-2.2.0/nameparser/_pipeline/_script_segment.py +678 -0
- nameparser-2.2.0/nameparser/_pipeline/_segment.py +90 -0
- nameparser-2.2.0/nameparser/_pipeline/_state.py +146 -0
- nameparser-2.2.0/nameparser/_pipeline/_tokenize.py +197 -0
- nameparser-2.2.0/nameparser/_pipeline/_vocab.py +505 -0
- nameparser-2.2.0/nameparser/_policy.py +928 -0
- nameparser-2.2.0/nameparser/_render.py +316 -0
- {nameparser-2.0.0rc2 → nameparser-2.2.0}/nameparser/_types.py +247 -26
- nameparser-2.2.0/nameparser/_version.py +19 -0
- nameparser-2.2.0/nameparser/config/__init__.py +41 -0
- nameparser-2.2.0/nameparser/config/_deprecated.py +141 -0
- nameparser-2.2.0/nameparser/config/_invariants.py +53 -0
- nameparser-2.2.0/nameparser/config/bound_first_names.py +23 -0
- nameparser-2.2.0/nameparser/config/bound_given_names.py +60 -0
- {nameparser-2.0.0rc2 → nameparser-2.2.0}/nameparser/config/conjunctions.py +7 -3
- nameparser-2.2.0/nameparser/config/maiden_markers.py +111 -0
- nameparser-2.2.0/nameparser/config/particles.py +275 -0
- nameparser-2.2.0/nameparser/config/prefixes.py +38 -0
- nameparser-2.2.0/nameparser/config/suffixes.py +943 -0
- nameparser-2.2.0/nameparser/config/surnames.py +48 -0
- {nameparser-2.0.0rc2 → nameparser-2.2.0}/nameparser/config/titles.py +58 -19
- {nameparser-2.0.0rc2 → nameparser-2.2.0}/nameparser/locales/__init__.py +26 -3
- nameparser-2.2.0/nameparser/locales/ja.py +231 -0
- {nameparser-2.0.0rc2 → nameparser-2.2.0}/nameparser/locales/ru.py +5 -3
- {nameparser-2.0.0rc2 → nameparser-2.2.0}/nameparser/locales/tr_az.py +3 -2
- nameparser-2.2.0/nameparser/locales/zh.py +114 -0
- {nameparser-2.0.0rc2 → nameparser-2.2.0}/nameparser/parser.py +2 -1
- {nameparser-2.0.0rc2 → nameparser-2.2.0/nameparser.egg-info}/PKG-INFO +50 -8
- {nameparser-2.0.0rc2 → nameparser-2.2.0}/nameparser.egg-info/SOURCES.txt +22 -2
- nameparser-2.2.0/nameparser.egg-info/requires.txt +3 -0
- {nameparser-2.0.0rc2 → nameparser-2.2.0}/pyproject.toml +19 -0
- nameparser-2.0.0rc2/tests/test_bound_first_names.py → nameparser-2.2.0/tests/test_bound_given_names.py +58 -1
- nameparser-2.2.0/tests/test_capitalization.py +364 -0
- {nameparser-2.0.0rc2 → nameparser-2.2.0}/tests/test_conjunctions.py +1 -1
- {nameparser-2.0.0rc2 → nameparser-2.2.0}/tests/test_first_name.py +24 -4
- {nameparser-2.0.0rc2 → nameparser-2.2.0}/tests/test_initials.py +27 -6
- {nameparser-2.0.0rc2 → nameparser-2.2.0}/tests/test_nicknames.py +39 -1
- nameparser-2.0.0rc2/tests/test_prefixes.py → nameparser-2.2.0/tests/test_particles.py +29 -25
- {nameparser-2.0.0rc2 → nameparser-2.2.0}/tests/test_python_api.py +1 -1
- {nameparser-2.0.0rc2 → nameparser-2.2.0}/tests/test_titles.py +3 -2
- nameparser-2.2.0/tests/v2/_differential_fixtures.py +109 -0
- nameparser-2.2.0/tests/v2/cases.py +3601 -0
- {nameparser-2.0.0rc2 → nameparser-2.2.0}/tests/v2/conftest.py +10 -15
- {nameparser-2.0.0rc2 → nameparser-2.2.0}/tests/v2/pipeline/test_assemble.py +7 -4
- nameparser-2.2.0/tests/v2/pipeline/test_assign.py +423 -0
- {nameparser-2.0.0rc2 → nameparser-2.2.0}/tests/v2/pipeline/test_classify.py +21 -1
- nameparser-2.2.0/tests/v2/pipeline/test_group.py +1130 -0
- nameparser-2.2.0/tests/v2/pipeline/test_pieces.py +103 -0
- nameparser-2.2.0/tests/v2/pipeline/test_post_rules.py +816 -0
- nameparser-2.2.0/tests/v2/pipeline/test_script_segment.py +815 -0
- {nameparser-2.0.0rc2 → nameparser-2.2.0}/tests/v2/pipeline/test_segment.py +14 -0
- {nameparser-2.0.0rc2 → nameparser-2.2.0}/tests/v2/pipeline/test_state.py +23 -4
- nameparser-2.2.0/tests/v2/pipeline/test_tokenize.py +228 -0
- nameparser-2.2.0/tests/v2/pipeline/test_vocab.py +454 -0
- nameparser-2.2.0/tests/v2/rules_doc.py +200 -0
- nameparser-2.2.0/tests/v2/test_benchmark.py +344 -0
- nameparser-2.2.0/tests/v2/test_cases.py +72 -0
- {nameparser-2.0.0rc2 → nameparser-2.2.0}/tests/v2/test_cli.py +13 -0
- nameparser-2.2.0/tests/v2/test_config_aliases.py +404 -0
- {nameparser-2.0.0rc2 → nameparser-2.2.0}/tests/v2/test_config_shim.py +29 -6
- nameparser-2.2.0/tests/v2/test_contracts.py +241 -0
- nameparser-2.2.0/tests/v2/test_differential.py +1361 -0
- nameparser-2.2.0/tests/v2/test_doc_citations.py +176 -0
- nameparser-2.2.0/tests/v2/test_doc_spellings.py +30 -0
- {nameparser-2.0.0rc2 → nameparser-2.2.0}/tests/v2/test_facade.py +97 -2
- nameparser-2.2.0/tests/v2/test_facade_cases.py +166 -0
- {nameparser-2.0.0rc2 → nameparser-2.2.0}/tests/v2/test_layering.py +80 -12
- nameparser-2.2.0/tests/v2/test_ledger_guards.py +2452 -0
- {nameparser-2.0.0rc2 → nameparser-2.2.0}/tests/v2/test_lexicon.py +99 -7
- nameparser-2.2.0/tests/v2/test_locales.py +1215 -0
- nameparser-2.2.0/tests/v2/test_parser.py +1499 -0
- {nameparser-2.0.0rc2 → nameparser-2.2.0}/tests/v2/test_policy.py +298 -5
- {nameparser-2.0.0rc2 → nameparser-2.2.0}/tests/v2/test_properties.py +223 -18
- {nameparser-2.0.0rc2 → nameparser-2.2.0}/tests/v2/test_regex_sync.py +69 -9
- nameparser-2.2.0/tests/v2/test_render.py +641 -0
- {nameparser-2.0.0rc2 → nameparser-2.2.0}/tests/v2/test_reprs.py +44 -2
- nameparser-2.2.0/tests/v2/test_rules_doc.py +145 -0
- nameparser-2.2.0/tests/v2/test_rules_doc_grammar.py +95 -0
- {nameparser-2.0.0rc2 → nameparser-2.2.0}/tests/v2/test_types.py +78 -2
- nameparser-2.0.0rc2/nameparser/_parser.py +0 -192
- nameparser-2.0.0rc2/nameparser/_pipeline/_assign.py +0 -276
- nameparser-2.0.0rc2/nameparser/_pipeline/_classify.py +0 -95
- nameparser-2.0.0rc2/nameparser/_pipeline/_group.py +0 -341
- nameparser-2.0.0rc2/nameparser/_pipeline/_post_rules.py +0 -129
- nameparser-2.0.0rc2/nameparser/_pipeline/_segment.py +0 -122
- nameparser-2.0.0rc2/nameparser/_pipeline/_state.py +0 -94
- nameparser-2.0.0rc2/nameparser/_pipeline/_tokenize.py +0 -125
- nameparser-2.0.0rc2/nameparser/_pipeline/_vocab.py +0 -126
- nameparser-2.0.0rc2/nameparser/_policy.py +0 -504
- nameparser-2.0.0rc2/nameparser/_render.py +0 -165
- nameparser-2.0.0rc2/nameparser/_version.py +0 -9
- nameparser-2.0.0rc2/nameparser/config/__init__.py +0 -20
- nameparser-2.0.0rc2/nameparser/config/_invariants.py +0 -27
- nameparser-2.0.0rc2/nameparser/config/bound_first_names.py +0 -28
- nameparser-2.0.0rc2/nameparser/config/maiden_markers.py +0 -42
- nameparser-2.0.0rc2/nameparser/config/prefixes.py +0 -130
- nameparser-2.0.0rc2/nameparser/config/suffixes.py +0 -733
- nameparser-2.0.0rc2/tests/test_capitalization.py +0 -144
- nameparser-2.0.0rc2/tests/v2/cases.py +0 -469
- nameparser-2.0.0rc2/tests/v2/pipeline/test_assign.py +0 -171
- nameparser-2.0.0rc2/tests/v2/pipeline/test_group.py +0 -161
- nameparser-2.0.0rc2/tests/v2/pipeline/test_post_rules.py +0 -122
- nameparser-2.0.0rc2/tests/v2/pipeline/test_tokenize.py +0 -91
- nameparser-2.0.0rc2/tests/v2/pipeline/test_vocab.py +0 -40
- nameparser-2.0.0rc2/tests/v2/test_benchmark.py +0 -122
- nameparser-2.0.0rc2/tests/v2/test_cases.py +0 -26
- nameparser-2.0.0rc2/tests/v2/test_contracts.py +0 -84
- nameparser-2.0.0rc2/tests/v2/test_facade_cases.py +0 -75
- nameparser-2.0.0rc2/tests/v2/test_locales.py +0 -550
- nameparser-2.0.0rc2/tests/v2/test_parser.py +0 -438
- nameparser-2.0.0rc2/tests/v2/test_render.py +0 -290
- {nameparser-2.0.0rc2 → nameparser-2.2.0}/AUTHORS +0 -0
- {nameparser-2.0.0rc2 → nameparser-2.2.0}/LICENSE +0 -0
- {nameparser-2.0.0rc2 → nameparser-2.2.0}/MANIFEST.in +0 -0
- {nameparser-2.0.0rc2 → nameparser-2.2.0}/nameparser/_locale.py +0 -0
- {nameparser-2.0.0rc2 → nameparser-2.2.0}/nameparser/config/capitalization.py +0 -0
- {nameparser-2.0.0rc2 → nameparser-2.2.0}/nameparser/config/regexes.py +0 -0
- {nameparser-2.0.0rc2 → nameparser-2.2.0}/nameparser/py.typed +0 -0
- {nameparser-2.0.0rc2 → nameparser-2.2.0}/nameparser/util.py +0 -0
- {nameparser-2.0.0rc2 → nameparser-2.2.0}/nameparser.egg-info/dependency_links.txt +0 -0
- {nameparser-2.0.0rc2 → nameparser-2.2.0}/nameparser.egg-info/top_level.txt +0 -0
- {nameparser-2.0.0rc2 → nameparser-2.2.0}/setup.cfg +0 -0
- {nameparser-2.0.0rc2 → nameparser-2.2.0}/tests/__init__.py +0 -0
- {nameparser-2.0.0rc2 → nameparser-2.2.0}/tests/base.py +0 -0
- {nameparser-2.0.0rc2 → nameparser-2.2.0}/tests/conftest.py +0 -0
- {nameparser-2.0.0rc2 → nameparser-2.2.0}/tests/test_brute_force.py +0 -0
- {nameparser-2.0.0rc2 → nameparser-2.2.0}/tests/test_comma_variants.py +0 -0
- {nameparser-2.0.0rc2 → nameparser-2.2.0}/tests/test_constants.py +0 -0
- {nameparser-2.0.0rc2 → nameparser-2.2.0}/tests/test_east_slavic_patronymic_order.py +0 -0
- {nameparser-2.0.0rc2 → nameparser-2.2.0}/tests/test_middle_name_as_last.py +0 -0
- {nameparser-2.0.0rc2 → nameparser-2.2.0}/tests/test_output_format.py +0 -0
- {nameparser-2.0.0rc2 → nameparser-2.2.0}/tests/test_suffixes.py +0 -0
- {nameparser-2.0.0rc2 → nameparser-2.2.0}/tests/test_turkic_patronymic_order.py +0 -0
- {nameparser-2.0.0rc2 → nameparser-2.2.0}/tests/test_variations.py +0 -0
- {nameparser-2.0.0rc2 → nameparser-2.2.0}/tests/v2/__init__.py +0 -0
- {nameparser-2.0.0rc2 → nameparser-2.2.0}/tests/v2/pipeline/__init__.py +0 -0
- {nameparser-2.0.0rc2 → nameparser-2.2.0}/tests/v2/pipeline/test_extract.py +0 -0
- {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
|
|
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
|
|
53
|
-
|
|
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
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
|
27
|
-
|
|
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
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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,5 +1,6 @@
|
|
|
1
|
-
"""v1 ``Constants`` compatibility shim over Lexicon/Policy
|
|
2
|
-
|
|
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
|
|
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
|
|
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
|
|
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 (
|
|
476
|
-
|
|
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
|
-
(
|
|
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,
|
|
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.
|
|
671
|
+
from nameparser.config.bound_given_names import BOUND_GIVEN_NAMES
|
|
671
672
|
from nameparser.config.conjunctions import CONJUNCTIONS
|
|
672
|
-
from nameparser.config.
|
|
673
|
-
|
|
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,
|
|
677
|
+
SUFFIX_ACRONYMS, SUFFIX_ACRONYMS_AMBIGUOUS, SUFFIX_WORDS,
|
|
677
678
|
)
|
|
678
|
-
from nameparser.config.titles import
|
|
679
|
+
from nameparser.config.titles import GIVEN_NAME_TITLES, TITLES
|
|
679
680
|
return {
|
|
680
|
-
"prefixes":
|
|
681
|
+
"prefixes": PARTICLES,
|
|
681
682
|
"suffix_acronyms": SUFFIX_ACRONYMS,
|
|
682
|
-
"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":
|
|
686
|
+
"first_name_titles": GIVEN_NAME_TITLES,
|
|
686
687
|
"conjunctions": CONJUNCTIONS,
|
|
687
|
-
"bound_first_names":
|
|
688
|
-
"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
|
-
(
|
|
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 (
|
|
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
|
|
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
|
|
720
|
-
|
|
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
|
|
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
|
-
|
|
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:
|
|
1040
|
-
# no word in both
|
|
1041
|
-
#
|
|
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=
|
|
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
|
|
1085
|
-
#
|
|
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 (
|
|
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,
|
|
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
|
|
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 (
|
|
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
|
|
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
|
-
#
|
|
476
|
-
#
|
|
477
|
-
#
|
|
478
|
-
#
|
|
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
|
|
521
|
-
#
|
|
522
|
-
#
|
|
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
|
|
528
|
-
|
|
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
|
-
|
|
533
|
-
|
|
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
|
|
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
|
-
|
|
703
|
-
|
|
704
|
-
|
|
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))
|