nameparser 2.0.0rc2__tar.gz → 2.1.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.1.0}/PKG-INFO +48 -8
- {nameparser-2.0.0rc2 → nameparser-2.1.0}/README.rst +45 -7
- {nameparser-2.0.0rc2 → nameparser-2.1.0}/nameparser/__init__.py +7 -3
- {nameparser-2.0.0rc2 → nameparser-2.1.0}/nameparser/_config_shim.py +30 -12
- {nameparser-2.0.0rc2 → nameparser-2.1.0}/nameparser/_facade.py +18 -0
- {nameparser-2.0.0rc2 → nameparser-2.1.0}/nameparser/_lexicon.py +64 -7
- {nameparser-2.0.0rc2 → nameparser-2.1.0}/nameparser/_parser.py +131 -17
- {nameparser-2.0.0rc2 → nameparser-2.1.0}/nameparser/_pipeline/__init__.py +4 -3
- {nameparser-2.0.0rc2 → nameparser-2.1.0}/nameparser/_pipeline/_assemble.py +25 -7
- {nameparser-2.0.0rc2 → nameparser-2.1.0}/nameparser/_pipeline/_assign.py +84 -6
- {nameparser-2.0.0rc2 → nameparser-2.1.0}/nameparser/_pipeline/_classify.py +3 -1
- {nameparser-2.0.0rc2 → nameparser-2.1.0}/nameparser/_pipeline/_extract.py +9 -1
- {nameparser-2.0.0rc2 → nameparser-2.1.0}/nameparser/_pipeline/_group.py +72 -5
- {nameparser-2.0.0rc2 → nameparser-2.1.0}/nameparser/_pipeline/_post_rules.py +2 -1
- nameparser-2.1.0/nameparser/_pipeline/_script_segment.py +767 -0
- {nameparser-2.0.0rc2 → nameparser-2.1.0}/nameparser/_pipeline/_segment.py +22 -51
- {nameparser-2.0.0rc2 → nameparser-2.1.0}/nameparser/_pipeline/_state.py +24 -10
- nameparser-2.1.0/nameparser/_pipeline/_tokenize.py +208 -0
- nameparser-2.1.0/nameparser/_pipeline/_vocab.py +356 -0
- nameparser-2.1.0/nameparser/_policy.py +916 -0
- {nameparser-2.0.0rc2 → nameparser-2.1.0}/nameparser/_render.py +12 -0
- {nameparser-2.0.0rc2 → nameparser-2.1.0}/nameparser/_types.py +109 -2
- {nameparser-2.0.0rc2 → nameparser-2.1.0}/nameparser/_version.py +2 -2
- {nameparser-2.0.0rc2 → nameparser-2.1.0}/nameparser/config/conjunctions.py +5 -1
- nameparser-2.1.0/nameparser/config/maiden_markers.py +70 -0
- {nameparser-2.0.0rc2 → nameparser-2.1.0}/nameparser/config/suffixes.py +118 -0
- nameparser-2.1.0/nameparser/config/surnames.py +49 -0
- {nameparser-2.0.0rc2 → nameparser-2.1.0}/nameparser/locales/__init__.py +22 -0
- nameparser-2.1.0/nameparser/locales/ja.py +228 -0
- nameparser-2.1.0/nameparser/locales/zh.py +113 -0
- {nameparser-2.0.0rc2 → nameparser-2.1.0/nameparser.egg-info}/PKG-INFO +48 -8
- {nameparser-2.0.0rc2 → nameparser-2.1.0}/nameparser.egg-info/SOURCES.txt +7 -0
- nameparser-2.1.0/nameparser.egg-info/requires.txt +3 -0
- {nameparser-2.0.0rc2 → nameparser-2.1.0}/pyproject.toml +19 -0
- {nameparser-2.0.0rc2 → nameparser-2.1.0}/tests/test_nicknames.py +39 -0
- nameparser-2.1.0/tests/v2/cases.py +1807 -0
- {nameparser-2.0.0rc2 → nameparser-2.1.0}/tests/v2/conftest.py +10 -15
- {nameparser-2.0.0rc2 → nameparser-2.1.0}/tests/v2/pipeline/test_assemble.py +7 -4
- {nameparser-2.0.0rc2 → nameparser-2.1.0}/tests/v2/pipeline/test_assign.py +68 -1
- {nameparser-2.0.0rc2 → nameparser-2.1.0}/tests/v2/pipeline/test_classify.py +21 -1
- {nameparser-2.0.0rc2 → nameparser-2.1.0}/tests/v2/pipeline/test_group.py +92 -1
- nameparser-2.1.0/tests/v2/pipeline/test_script_segment.py +815 -0
- {nameparser-2.0.0rc2 → nameparser-2.1.0}/tests/v2/pipeline/test_segment.py +14 -0
- {nameparser-2.0.0rc2 → nameparser-2.1.0}/tests/v2/pipeline/test_state.py +15 -2
- nameparser-2.1.0/tests/v2/pipeline/test_tokenize.py +228 -0
- nameparser-2.1.0/tests/v2/pipeline/test_vocab.py +373 -0
- {nameparser-2.0.0rc2 → nameparser-2.1.0}/tests/v2/test_benchmark.py +94 -12
- {nameparser-2.0.0rc2 → nameparser-2.1.0}/tests/v2/test_cli.py +13 -0
- {nameparser-2.0.0rc2 → nameparser-2.1.0}/tests/v2/test_config_shim.py +20 -0
- {nameparser-2.0.0rc2 → nameparser-2.1.0}/tests/v2/test_contracts.py +28 -8
- nameparser-2.1.0/tests/v2/test_differential.py +696 -0
- {nameparser-2.0.0rc2 → nameparser-2.1.0}/tests/v2/test_facade.py +52 -0
- nameparser-2.1.0/tests/v2/test_facade_cases.py +153 -0
- {nameparser-2.0.0rc2 → nameparser-2.1.0}/tests/v2/test_layering.py +18 -8
- {nameparser-2.0.0rc2 → nameparser-2.1.0}/tests/v2/test_lexicon.py +63 -3
- nameparser-2.1.0/tests/v2/test_locales.py +1202 -0
- {nameparser-2.0.0rc2 → nameparser-2.1.0}/tests/v2/test_parser.py +377 -2
- {nameparser-2.0.0rc2 → nameparser-2.1.0}/tests/v2/test_policy.py +298 -5
- {nameparser-2.0.0rc2 → nameparser-2.1.0}/tests/v2/test_properties.py +204 -12
- nameparser-2.1.0/tests/v2/test_regex_sync.py +366 -0
- {nameparser-2.0.0rc2 → nameparser-2.1.0}/tests/v2/test_reprs.py +29 -1
- {nameparser-2.0.0rc2 → nameparser-2.1.0}/tests/v2/test_types.py +78 -2
- 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/config/maiden_markers.py +0 -42
- nameparser-2.0.0rc2/tests/v2/cases.py +0 -469
- 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_facade_cases.py +0 -75
- nameparser-2.0.0rc2/tests/v2/test_locales.py +0 -550
- nameparser-2.0.0rc2/tests/v2/test_regex_sync.py +0 -151
- {nameparser-2.0.0rc2 → nameparser-2.1.0}/AUTHORS +0 -0
- {nameparser-2.0.0rc2 → nameparser-2.1.0}/LICENSE +0 -0
- {nameparser-2.0.0rc2 → nameparser-2.1.0}/MANIFEST.in +0 -0
- {nameparser-2.0.0rc2 → nameparser-2.1.0}/nameparser/__main__.py +0 -0
- {nameparser-2.0.0rc2 → nameparser-2.1.0}/nameparser/_locale.py +0 -0
- {nameparser-2.0.0rc2 → nameparser-2.1.0}/nameparser/config/__init__.py +0 -0
- {nameparser-2.0.0rc2 → nameparser-2.1.0}/nameparser/config/_invariants.py +0 -0
- {nameparser-2.0.0rc2 → nameparser-2.1.0}/nameparser/config/bound_first_names.py +0 -0
- {nameparser-2.0.0rc2 → nameparser-2.1.0}/nameparser/config/capitalization.py +0 -0
- {nameparser-2.0.0rc2 → nameparser-2.1.0}/nameparser/config/prefixes.py +0 -0
- {nameparser-2.0.0rc2 → nameparser-2.1.0}/nameparser/config/regexes.py +0 -0
- {nameparser-2.0.0rc2 → nameparser-2.1.0}/nameparser/config/titles.py +0 -0
- {nameparser-2.0.0rc2 → nameparser-2.1.0}/nameparser/locales/ru.py +0 -0
- {nameparser-2.0.0rc2 → nameparser-2.1.0}/nameparser/locales/tr_az.py +0 -0
- {nameparser-2.0.0rc2 → nameparser-2.1.0}/nameparser/parser.py +0 -0
- {nameparser-2.0.0rc2 → nameparser-2.1.0}/nameparser/py.typed +0 -0
- {nameparser-2.0.0rc2 → nameparser-2.1.0}/nameparser/util.py +0 -0
- {nameparser-2.0.0rc2 → nameparser-2.1.0}/nameparser.egg-info/dependency_links.txt +0 -0
- {nameparser-2.0.0rc2 → nameparser-2.1.0}/nameparser.egg-info/top_level.txt +0 -0
- {nameparser-2.0.0rc2 → nameparser-2.1.0}/setup.cfg +0 -0
- {nameparser-2.0.0rc2 → nameparser-2.1.0}/tests/__init__.py +0 -0
- {nameparser-2.0.0rc2 → nameparser-2.1.0}/tests/base.py +0 -0
- {nameparser-2.0.0rc2 → nameparser-2.1.0}/tests/conftest.py +0 -0
- {nameparser-2.0.0rc2 → nameparser-2.1.0}/tests/test_bound_first_names.py +0 -0
- {nameparser-2.0.0rc2 → nameparser-2.1.0}/tests/test_brute_force.py +0 -0
- {nameparser-2.0.0rc2 → nameparser-2.1.0}/tests/test_capitalization.py +0 -0
- {nameparser-2.0.0rc2 → nameparser-2.1.0}/tests/test_comma_variants.py +0 -0
- {nameparser-2.0.0rc2 → nameparser-2.1.0}/tests/test_conjunctions.py +0 -0
- {nameparser-2.0.0rc2 → nameparser-2.1.0}/tests/test_constants.py +0 -0
- {nameparser-2.0.0rc2 → nameparser-2.1.0}/tests/test_east_slavic_patronymic_order.py +0 -0
- {nameparser-2.0.0rc2 → nameparser-2.1.0}/tests/test_first_name.py +0 -0
- {nameparser-2.0.0rc2 → nameparser-2.1.0}/tests/test_initials.py +0 -0
- {nameparser-2.0.0rc2 → nameparser-2.1.0}/tests/test_middle_name_as_last.py +0 -0
- {nameparser-2.0.0rc2 → nameparser-2.1.0}/tests/test_output_format.py +0 -0
- {nameparser-2.0.0rc2 → nameparser-2.1.0}/tests/test_prefixes.py +0 -0
- {nameparser-2.0.0rc2 → nameparser-2.1.0}/tests/test_python_api.py +0 -0
- {nameparser-2.0.0rc2 → nameparser-2.1.0}/tests/test_suffixes.py +0 -0
- {nameparser-2.0.0rc2 → nameparser-2.1.0}/tests/test_titles.py +0 -0
- {nameparser-2.0.0rc2 → nameparser-2.1.0}/tests/test_turkic_patronymic_order.py +0 -0
- {nameparser-2.0.0rc2 → nameparser-2.1.0}/tests/test_variations.py +0 -0
- {nameparser-2.0.0rc2 → nameparser-2.1.0}/tests/v2/__init__.py +0 -0
- {nameparser-2.0.0rc2 → nameparser-2.1.0}/tests/v2/pipeline/__init__.py +0 -0
- {nameparser-2.0.0rc2 → nameparser-2.1.0}/tests/v2/pipeline/test_extract.py +0 -0
- {nameparser-2.0.0rc2 → nameparser-2.1.0}/tests/v2/pipeline/test_post_rules.py +0 -0
- {nameparser-2.0.0rc2 → nameparser-2.1.0}/tests/v2/test_cases.py +0 -0
- {nameparser-2.0.0rc2 → nameparser-2.1.0}/tests/v2/test_locale.py +0 -0
- {nameparser-2.0.0rc2 → nameparser-2.1.0}/tests/v2/test_render.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: nameparser
|
|
3
|
-
Version: 2.0
|
|
3
|
+
Version: 2.1.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,41 @@ 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')
|
|
54
78
|
>>> name.render("{family}, {given}")
|
|
55
79
|
'de la Vega, Juan'
|
|
56
80
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
81
|
+
>>> parse("김민준").family # Korean: unspaced, split on the census list
|
|
82
|
+
'김'
|
|
83
|
+
>>> parse("高橋 みなみ").family # Japanese: kanji with kana, family first
|
|
84
|
+
'高橋'
|
|
85
|
+
>>> parse("김민준씨").suffix # an honorific written against the name
|
|
86
|
+
'씨'
|
|
87
|
+
>>> parse("г-н Иван Петров").title # Cyrillic title
|
|
88
|
+
'г-н'
|
|
89
|
+
>>> parse("محمد بن سلمان").family # Arabic: بن chains onto the family name
|
|
90
|
+
'بن سلمان'
|
|
91
|
+
|
|
92
|
+
>>> from nameparser import locales, parser_for
|
|
93
|
+
>>> chinese = parser_for(locales.ZH) # Han text does not say which language
|
|
94
|
+
>>> chinese.parse("毛泽东").family # so splitting it is opt-in
|
|
95
|
+
'毛'
|
|
96
|
+
>>> russian = parser_for(locales.RU)
|
|
97
|
+
>>> russian.parse("Сидоров Иван Петрович").family
|
|
98
|
+
'Сидоров'
|
|
99
|
+
>>> locales.available()
|
|
100
|
+
('ja', 'ru', 'tr_az', 'zh')
|
|
61
101
|
|
|
62
102
|
Learn more
|
|
63
103
|
----------
|
|
@@ -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,41 @@ 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')
|
|
28
50
|
>>> name.render("{family}, {given}")
|
|
29
51
|
'de la Vega, Juan'
|
|
30
52
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
53
|
+
>>> parse("김민준").family # Korean: unspaced, split on the census list
|
|
54
|
+
'김'
|
|
55
|
+
>>> parse("高橋 みなみ").family # Japanese: kanji with kana, family first
|
|
56
|
+
'高橋'
|
|
57
|
+
>>> parse("김민준씨").suffix # an honorific written against the name
|
|
58
|
+
'씨'
|
|
59
|
+
>>> parse("г-н Иван Петров").title # Cyrillic title
|
|
60
|
+
'г-н'
|
|
61
|
+
>>> parse("محمد بن سلمان").family # Arabic: بن chains onto the family name
|
|
62
|
+
'بن سلمان'
|
|
63
|
+
|
|
64
|
+
>>> from nameparser import locales, parser_for
|
|
65
|
+
>>> chinese = parser_for(locales.ZH) # Han text does not say which language
|
|
66
|
+
>>> chinese.parse("毛泽东").family # so splitting it is opt-in
|
|
67
|
+
'毛'
|
|
68
|
+
>>> russian = parser_for(locales.RU)
|
|
69
|
+
>>> russian.parse("Сидоров Иван Петрович").family
|
|
70
|
+
'Сидоров'
|
|
71
|
+
>>> locales.available()
|
|
72
|
+
('ja', 'ru', 'tr_az', 'zh')
|
|
35
73
|
|
|
36
74
|
Learn more
|
|
37
75
|
----------
|
|
@@ -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
|
]
|
|
@@ -986,10 +986,23 @@ class Constants:
|
|
|
986
986
|
removal path.
|
|
987
987
|
"""
|
|
988
988
|
from nameparser.config.maiden_markers import MAIDEN_MARKERS
|
|
989
|
+
from nameparser.config.suffixes import GLUED_HONORIFICS
|
|
990
|
+
from nameparser.config.surnames import KOREAN_SURNAMES
|
|
989
991
|
acronyms = frozenset(self.suffix_acronyms)
|
|
990
992
|
particles = frozenset(self.prefixes)
|
|
991
993
|
bound = frozenset(self.bound_first_names)
|
|
992
994
|
ambiguous_acronyms = frozenset(self.suffix_acronyms_ambiguous) & acronyms
|
|
995
|
+
# Drop any ambiguous acronym from the word set rather than the
|
|
996
|
+
# other way round. Lexicon forbids the overlap because the word
|
|
997
|
+
# branch bypasses the period gate, and adding an ambiguous
|
|
998
|
+
# acronym to suffix_not_acronyms is INERT in v1 anyway:
|
|
999
|
+
# is_suffix already accepts it via the acronym branch, and
|
|
1000
|
+
# reserve_last keeps it as the surname. So ignoring the
|
|
1001
|
+
# addition reproduces v1 ("Jack Ma" keeps last='Ma'), where
|
|
1002
|
+
# dropping it from the AMBIGUOUS set instead ungated the word
|
|
1003
|
+
# and lost the family name -- a silent misparse worse than the
|
|
1004
|
+
# raise it avoided.
|
|
1005
|
+
suffix_words = frozenset(self.suffix_not_acronyms) - ambiguous_acronyms
|
|
993
1006
|
# keep in sync with _lexicon._default_lexicon() (pinned by
|
|
994
1007
|
# tests/v2/test_config_shim.py::test_snapshot_field_translation)
|
|
995
1008
|
lexicon = Lexicon(
|
|
@@ -1016,18 +1029,7 @@ class Constants:
|
|
|
1016
1029
|
if e == " ".join(e.split())
|
|
1017
1030
|
) if t),
|
|
1018
1031
|
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,
|
|
1032
|
+
suffix_words=suffix_words,
|
|
1031
1033
|
# Intersect with acronyms: Lexicon enforces ambiguous <=
|
|
1032
1034
|
# acronyms; v1 behaves the same when an acronym is deleted
|
|
1033
1035
|
# but its ambiguous entry lingers (the entry stops
|
|
@@ -1061,6 +1063,22 @@ class Constants:
|
|
|
1061
1063
|
# v1 Constants has no manager for these (#274 is 2.0
|
|
1062
1064
|
# behavior); the data module is the only source
|
|
1063
1065
|
maiden_markers=frozenset(MAIDEN_MARKERS),
|
|
1066
|
+
# likewise no v1 manager: the unspaced-name segmentation
|
|
1067
|
+
# vocabulary is 2.0 behavior (#271), so it rides in the
|
|
1068
|
+
# snapshot only -- v1's Constants surface stays frozen.
|
|
1069
|
+
# Unwrapped where maiden_markers above is wrapped: this
|
|
1070
|
+
# module is born frozen (#293), so no wrap
|
|
1071
|
+
surnames=KOREAN_SURNAMES,
|
|
1072
|
+
# likewise no v1 manager: the glued-honorific tail set is
|
|
1073
|
+
# 2.1 behavior (#308), so it rides in the snapshot only.
|
|
1074
|
+
# Wrapped, unlike surnames above: suffixes.py is still a
|
|
1075
|
+
# mutable v1 module, not born-frozen like surnames.py
|
|
1076
|
+
# (#293). Intersect with the word set: Lexicon enforces
|
|
1077
|
+
# tails <= suffix_words, and v1 semantics are that deleting
|
|
1078
|
+
# a suffix word turns the behavior off -- a lingering tail
|
|
1079
|
+
# simply stops mattering, the same rule ambiguous_acronyms
|
|
1080
|
+
# gets against suffix_acronyms above.
|
|
1081
|
+
honorific_tails=frozenset(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
|
|
@@ -623,6 +623,24 @@ class HumanName:
|
|
|
623
623
|
# spelling the facade actually exposes as attributes.
|
|
624
624
|
return getattr(self, _V1_SPELLING.get(key, key))
|
|
625
625
|
|
|
626
|
+
def __setattr__(self, name: str, value: object) -> None:
|
|
627
|
+
# "given"/"family" are the 2.0 spellings of first/last; the
|
|
628
|
+
# facade has no such attributes, so plain assignment creates a
|
|
629
|
+
# stray instance attribute while the parse (and .first/.last)
|
|
630
|
+
# keeps the old value -- a silently forked name. Warn but
|
|
631
|
+
# still set: ad-hoc attribute stashing is a legal v1 pattern,
|
|
632
|
+
# so any code that worked keeps working. Only these two names
|
|
633
|
+
# warn -- the other five 2.0 field names are real properties
|
|
634
|
+
# whose setters work, and Role members reach here as their
|
|
635
|
+
# string values (StrEnum).
|
|
636
|
+
if name in _V1_SPELLING:
|
|
637
|
+
warnings.warn(
|
|
638
|
+
f"assigning HumanName.{name} creates an inert attribute; "
|
|
639
|
+
f"the parse is unchanged -- use .{_V1_SPELLING[name]} "
|
|
640
|
+
f"(the v1 spelling) to update the name",
|
|
641
|
+
UserWarning, stacklevel=2)
|
|
642
|
+
super().__setattr__(name, value)
|
|
643
|
+
|
|
626
644
|
def as_dict(self, include_empty: bool = True) -> dict[str, str]:
|
|
627
645
|
"""The seven v1-named components as a dict; include_empty=False
|
|
628
646
|
drops empty fields."""
|
|
@@ -23,20 +23,44 @@ from typing import cast
|
|
|
23
23
|
_VOCAB_FIELDS = (
|
|
24
24
|
"titles", "given_name_titles", "suffix_acronyms", "suffix_words",
|
|
25
25
|
"suffix_acronyms_ambiguous", "particles", "particles_ambiguous",
|
|
26
|
-
"conjunctions", "bound_given_names", "maiden_markers",
|
|
26
|
+
"conjunctions", "bound_given_names", "maiden_markers", "surnames",
|
|
27
|
+
"honorific_tails",
|
|
27
28
|
)
|
|
28
29
|
|
|
29
|
-
#: (marker, base, why) triples. Each marker
|
|
30
|
-
#: base vocabulary are read and carries no vocabulary of its own,
|
|
31
|
-
#: entry outside the base is a configuration mistake -- but the
|
|
32
|
-
#: differs per pair, and the reason is recorded here rather
|
|
33
|
-
#: generalized, because an orphan is NOT simply inert
|
|
30
|
+
#: (marker, base, why) triples. Each marker QUALIFIES how entries of
|
|
31
|
+
#: its base vocabulary are read and carries no vocabulary of its own,
|
|
32
|
+
#: so an entry outside the base is a configuration mistake -- but the
|
|
33
|
+
#: mistake differs per pair, and the reason is recorded here rather
|
|
34
|
+
#: than generalized, because an orphan is NOT simply inert. Nor is the
|
|
35
|
+
#: qualification one-directional: the first two NARROW their base (an
|
|
36
|
+
#: entry is read as vocabulary in fewer places), while honorific_tails
|
|
37
|
+
#: WIDENS it, granting a suffix word the glued position on top of the
|
|
38
|
+
#: whole-token match every suffix word already gets.
|
|
34
39
|
#:
|
|
35
40
|
#: * particles_ambiguous: _assign keys on the tag alone, so an orphan
|
|
36
41
|
#: makes the parse emit a spurious particle-or-given ambiguity.
|
|
37
42
|
#: * suffix_acronyms_ambiguous: _vocab returns True on the ambiguous
|
|
38
43
|
#: set before testing suffix_acronyms, so an orphan silently turns a
|
|
39
44
|
#: word into a period-gated suffix.
|
|
45
|
+
#: * honorific_tails: script_segment peels the tail into its own token
|
|
46
|
+
#: before classify ever runs, so an orphan splits the name and leaves
|
|
47
|
+
#: the fragment stranded inside it -- worse than not peeling at all.
|
|
48
|
+
#: Its base is deliberately NARROWER than what actually claims the
|
|
49
|
+
#: peeled piece: suffix_as_written ORs suffix_words with the
|
|
50
|
+
#: non-ambiguous suffix_acronyms, so a tail listed only as an acronym
|
|
51
|
+
#: would classify fine yet is rejected here. Accepted, and a decision
|
|
52
|
+
#: rather than an oversight -- the three-term predicate is easy to
|
|
53
|
+
#: get wrong in the dangerous direction (an ambiguous acronym admitted
|
|
54
|
+
#: as a tail would peel a period-gated word off a real name), and
|
|
55
|
+
#: nothing needs the acronym half: the shipped tails are CJK
|
|
56
|
+
#: honorifics, which are words.
|
|
57
|
+
#: The same relation is asserted a second time in config/suffixes.py,
|
|
58
|
+
#: over the raw GLUED_HONORIFICS/SUFFIX_NOT_ACRONYMS constants at
|
|
59
|
+
#: import. The two are not redundant in the way they look: that one
|
|
60
|
+
#: is an `assert`, stripped under `python -O`, while the check here
|
|
61
|
+
#: raises unconditionally -- so under -O this is what still holds the
|
|
62
|
+
#: SHIPPED vocabulary to the invariant, as it is the only thing that
|
|
63
|
+
#: ever held a caller's own.
|
|
40
64
|
#:
|
|
41
65
|
#: given_name_titles is deliberately NOT here and has no check of its
|
|
42
66
|
#: own -- see the note in __post_init__ for why every attempt at one
|
|
@@ -46,6 +70,8 @@ _SUBSET_FIELDS = (
|
|
|
46
70
|
"an orphan emits a spurious particle-or-given ambiguity"),
|
|
47
71
|
("suffix_acronyms_ambiguous", "suffix_acronyms",
|
|
48
72
|
"an orphan silently becomes a period-gated suffix"),
|
|
73
|
+
("honorific_tails", "suffix_words",
|
|
74
|
+
"an orphan splits the name and leaves the tail inside it"),
|
|
49
75
|
)
|
|
50
76
|
|
|
51
77
|
|
|
@@ -327,6 +353,31 @@ class Lexicon:
|
|
|
327
353
|
#: field ("née", "geb.", "roz.", ...). Full default list:
|
|
328
354
|
#: :data:`~nameparser.config.maiden_markers.MAIDEN_MARKERS`.
|
|
329
355
|
maiden_markers: frozenset[str] = frozenset()
|
|
356
|
+
#: Family names for the unspaced-name segmentation stage (#271),
|
|
357
|
+
#: matched longest-first against the start of the FIRST token
|
|
358
|
+
#: written wholly in a script :attr:`Policy.segment_scripts
|
|
359
|
+
#: <nameparser.Policy.segment_scripts>` activates. The default
|
|
360
|
+
#: carries the Korean census list
|
|
361
|
+
#: (:data:`~nameparser.config.surnames.KOREAN_SURNAMES`); Chinese
|
|
362
|
+
#: surnames ship in locales.ZH because Han segmentation is opt-in.
|
|
363
|
+
surnames: frozenset[str] = frozenset()
|
|
364
|
+
#: Honorifics that may be peeled off the END of a name token
|
|
365
|
+
#: (#308), matched longest-first: 田中さん splits into 田中 and さん
|
|
366
|
+
#: before the tokens are classified. Every entry must also be a
|
|
367
|
+
#: :attr:`suffix_words` entry -- the peeled tail is claimed by
|
|
368
|
+
#: suffix classification like any other post-nominal. Deliberately
|
|
369
|
+
#: NOT gated on :attr:`Policy.segment_scripts
|
|
370
|
+
#: <nameparser.Policy.segment_scripts>` (unlike :attr:`surnames`
|
|
371
|
+
#: above): 田中さん peels under the default policy, where HAN is in
|
|
372
|
+
#: no activation set, because a tail entry carries its own license
|
|
373
|
+
#: to fire. Entries are matched against the RAW token text, and
|
|
374
|
+
#: only within a name containing a non-ASCII character, so an ASCII
|
|
375
|
+
#: or mixed-case entry is at best conditionally active -- a ``"Jr"``
|
|
376
|
+
#: entry is stored ``"jr"`` and matches only lowercase text. The
|
|
377
|
+
#: field is effectively CJK-scoped in 2.1, which is what the shipped
|
|
378
|
+
#: vocabulary is. Full default list:
|
|
379
|
+
#: :data:`~nameparser.config.suffixes.GLUED_HONORIFICS`.
|
|
380
|
+
honorific_tails: frozenset[str] = frozenset()
|
|
330
381
|
#: Lowercase word -> exact-cased replacement used by capitalized()
|
|
331
382
|
#: ("phd" -> "Ph.D."). Pair-valued: change it with
|
|
332
383
|
#: dataclasses.replace(), not add()/remove(); read it as a mapping
|
|
@@ -569,8 +620,10 @@ def _default_lexicon() -> Lexicon:
|
|
|
569
620
|
from nameparser.config.maiden_markers import MAIDEN_MARKERS
|
|
570
621
|
from nameparser.config.prefixes import NON_FIRST_NAME_PREFIXES, PREFIXES
|
|
571
622
|
from nameparser.config.suffixes import (
|
|
572
|
-
SUFFIX_ACRONYMS, SUFFIX_ACRONYMS_AMBIGUOUS,
|
|
623
|
+
GLUED_HONORIFICS, SUFFIX_ACRONYMS, SUFFIX_ACRONYMS_AMBIGUOUS,
|
|
624
|
+
SUFFIX_NOT_ACRONYMS,
|
|
573
625
|
)
|
|
626
|
+
from nameparser.config.surnames import KOREAN_SURNAMES
|
|
574
627
|
from nameparser.config.titles import FIRST_NAME_TITLES, TITLES
|
|
575
628
|
|
|
576
629
|
# v1 data modules export plain `set[str]`; wrap each at this call site
|
|
@@ -590,6 +643,10 @@ def _default_lexicon() -> Lexicon:
|
|
|
590
643
|
conjunctions=frozenset(CONJUNCTIONS),
|
|
591
644
|
bound_given_names=frozenset(BOUND_FIRST_NAMES),
|
|
592
645
|
maiden_markers=frozenset(MAIDEN_MARKERS),
|
|
646
|
+
# surnames.py is born frozen (#293) -- no call-site wrap needed,
|
|
647
|
+
# unlike the v1 modules above (their wraps drop when #293 lands)
|
|
648
|
+
surnames=KOREAN_SURNAMES,
|
|
649
|
+
honorific_tails=frozenset(GLUED_HONORIFICS),
|
|
593
650
|
# pass canonical pair-tuples so this strictly-typed call site never
|
|
594
651
|
# feeds a Mapping to the tuple-annotated field; __post_init__
|
|
595
652
|
# still tolerates a Mapping at runtime for interactive use
|
|
@@ -11,17 +11,18 @@ from __future__ import annotations
|
|
|
11
11
|
import dataclasses
|
|
12
12
|
import functools
|
|
13
13
|
import warnings
|
|
14
|
-
from dataclasses import dataclass
|
|
14
|
+
from dataclasses import dataclass, field
|
|
15
15
|
|
|
16
16
|
from nameparser._lexicon import Lexicon
|
|
17
17
|
from nameparser._locale import Locale
|
|
18
18
|
from nameparser._pipeline import run
|
|
19
19
|
from nameparser._pipeline._assemble import assemble
|
|
20
20
|
from nameparser._pipeline._state import ParseState
|
|
21
|
-
from nameparser.
|
|
21
|
+
from nameparser._pipeline._vocab import _SCRIPT_MATCHERS
|
|
22
|
+
from nameparser._policy import UNSET, Policy, PolicyPatch, _Unset, apply_patch
|
|
22
23
|
from nameparser._types import (
|
|
23
|
-
FOLDED_TAG, ParsedName, Token, _guarded_getstate,
|
|
24
|
-
_validated_field_strings,
|
|
24
|
+
FOLDED_TAG, ParsedName, Segmenter, Token, _guarded_getstate,
|
|
25
|
+
_guarded_setstate, _validated_field_strings,
|
|
25
26
|
)
|
|
26
27
|
|
|
27
28
|
|
|
@@ -30,18 +31,37 @@ class Parser:
|
|
|
30
31
|
"""A configured name parser: a :class:`Lexicon` (vocabulary) plus
|
|
31
32
|
a :class:`Policy` (behavior), both defaulted when omitted. Build
|
|
32
33
|
one when you need non-default configuration, build it once, and
|
|
33
|
-
call :meth:`parse` many times -- it is immutable
|
|
34
|
-
|
|
34
|
+
call :meth:`parse` many times -- it is immutable and thread-safe.
|
|
35
|
+
|
|
36
|
+
An optional keyword-only ``segmenter`` (a :data:`~nameparser.Segmenter`)
|
|
37
|
+
plugs in outside knowledge of where an unspaced CJK token divides --
|
|
38
|
+
Japanese kanji names, which no bundled list can settle. It is
|
|
39
|
+
consulted only for a token the segmentation stage gates in and the
|
|
40
|
+
vocabulary DECLINES, so a locale pack's surnames always win where
|
|
41
|
+
they match; returning None declines in turn and the token stays
|
|
42
|
+
whole. Two promises narrow when one is supplied (locales spec §4):
|
|
43
|
+
parse-totality gains its one exception -- an exception raised by
|
|
44
|
+
the segmenter propagates, because a user-supplied callable's own
|
|
45
|
+
error is a user-code error, not a content error -- and this Parser
|
|
46
|
+
pickles only if its segmenter does (a module-level function
|
|
47
|
+
pickles; a lambda or closure does not). With no segmenter, both
|
|
48
|
+
promises hold unconditionally: all validity checking happens at
|
|
35
49
|
construction, so a Parser that constructs successfully cannot fail
|
|
36
50
|
at parse time on any str content.
|
|
37
51
|
|
|
38
52
|
(The None field defaults resolve in __post_init__; after
|
|
39
|
-
construction
|
|
40
|
-
state the steady-state truth, hence the assignment
|
|
41
|
-
defaults.)"""
|
|
53
|
+
construction lexicon and policy are always non-None -- the
|
|
54
|
+
annotations state the steady-state truth, hence the assignment
|
|
55
|
+
ignores on the defaults.)"""
|
|
42
56
|
|
|
43
57
|
lexicon: Lexicon = None # type: ignore[assignment] # None -> default()
|
|
44
58
|
policy: Policy = None # type: ignore[assignment] # None -> Policy()
|
|
59
|
+
#: An optional hook supplying outside knowledge of where an unspaced
|
|
60
|
+
#: token divides -- see the class docstring; None leaves such tokens
|
|
61
|
+
#: whole. Keyword-only, so the reserved growth stays additive
|
|
62
|
+
#: (locales spec §4): positional construction keeps its two-argument
|
|
63
|
+
#: shape.
|
|
64
|
+
segmenter: Segmenter | None = field(default=None, kw_only=True)
|
|
45
65
|
|
|
46
66
|
# in the class body so @dataclass(slots=True) keeps them
|
|
47
67
|
__getstate__ = _guarded_getstate
|
|
@@ -58,16 +78,73 @@ class Parser:
|
|
|
58
78
|
elif not isinstance(self.policy, Policy):
|
|
59
79
|
raise TypeError(
|
|
60
80
|
f"policy must be a Policy or None, got {self.policy!r}")
|
|
81
|
+
if self.segmenter is not None and not callable(self.segmenter):
|
|
82
|
+
raise TypeError(
|
|
83
|
+
f"segmenter must be callable or None, got {self.segmenter!r}")
|
|
84
|
+
# A configuration gap that used to be silent (#272's API, made
|
|
85
|
+
# loud before 2.1.0): segment_scripts can activate a script
|
|
86
|
+
# that neither the vocabulary nor a segmenter can ever divide
|
|
87
|
+
# -- the JA pack's whole shape, when its segmenter is
|
|
88
|
+
# forgotten. The parser then behaves identically to a working
|
|
89
|
+
# one minus the feature, which reads as "not working" with no
|
|
90
|
+
# signal why. Statically decidable here, so say it here; a
|
|
91
|
+
# warning rather than an error because the inert pack is a
|
|
92
|
+
# pinned, deliberate property (a JA registration must be safe
|
|
93
|
+
# without the extra), and warnings are filterable by the rare
|
|
94
|
+
# caller who wants exactly that.
|
|
95
|
+
if self.segmenter is None:
|
|
96
|
+
uncovered = sorted(
|
|
97
|
+
script.value
|
|
98
|
+
for script in self.policy.segment_scripts
|
|
99
|
+
if not any(_SCRIPT_MATCHERS[script](entry)
|
|
100
|
+
for entry in self.lexicon.surnames))
|
|
101
|
+
if uncovered:
|
|
102
|
+
names = ", ".join(uncovered)
|
|
103
|
+
one = len(uncovered) == 1
|
|
104
|
+
# the ja hint only where a Japanese script is among the
|
|
105
|
+
# dead ones -- a hangul-only gap (a from-scratch
|
|
106
|
+
# lexicon under the default policy) has different
|
|
107
|
+
# remedies, and pointing it at ja_segmenter would be a
|
|
108
|
+
# non sequitur
|
|
109
|
+
ja_hint = (
|
|
110
|
+
" For Japanese, pass "
|
|
111
|
+
"segmenter=locales.ja_segmenter() (install with: "
|
|
112
|
+
"pip install 'nameparser[ja]')."
|
|
113
|
+
if {"han", "hiragana", "katakana"} & set(uncovered)
|
|
114
|
+
else "")
|
|
115
|
+
warnings.warn(
|
|
116
|
+
f"Policy.segment_scripts activates {names} but the "
|
|
117
|
+
f"vocabulary has no surnames in "
|
|
118
|
+
f"{'that script' if one else 'those scripts'} "
|
|
119
|
+
f"and no segmenter is configured: unspaced names "
|
|
120
|
+
f"written in {'it' if one else 'them'} will never "
|
|
121
|
+
f"divide. Supply covering surnames, pass a "
|
|
122
|
+
f"segmenter, or deactivate with "
|
|
123
|
+
f"Policy(segment_scripts=frozenset()).{ja_hint}",
|
|
124
|
+
UserWarning, stacklevel=3)
|
|
61
125
|
|
|
62
126
|
def __repr__(self) -> str:
|
|
63
|
-
# composes the two bounded component reprs (spec §2 reprs)
|
|
64
|
-
|
|
127
|
+
# composes the two bounded component reprs (spec §2 reprs); the
|
|
128
|
+
# segmenter shows by name, and only when one is set, so the
|
|
129
|
+
# default Parser's repr is unchanged
|
|
130
|
+
seg = ""
|
|
131
|
+
if self.segmenter is not None:
|
|
132
|
+
# never repr() the callable itself: a partial reprs its
|
|
133
|
+
# bound arguments and a callable instance its address, both
|
|
134
|
+
# unbounded -- the class name is the bounded fallback
|
|
135
|
+
name = (getattr(self.segmenter, "__qualname__", None)
|
|
136
|
+
or type(self.segmenter).__name__)
|
|
137
|
+
seg = f", segmenter={name}"
|
|
138
|
+
return f"Parser({self.lexicon!r}, {self.policy!r}{seg})"
|
|
65
139
|
|
|
66
140
|
def parse(self, text: str) -> ParsedName:
|
|
67
141
|
"""Parse one name string into a :class:`ParsedName`. Never
|
|
68
142
|
raises on string content (unparseable input yields empty
|
|
69
143
|
fields plus ambiguities); non-str raises TypeError eagerly,
|
|
70
|
-
with a decode hint for bytes (bytes support ended with 1.x).
|
|
144
|
+
with a decode hint for bytes (bytes support ended with 1.x).
|
|
145
|
+
The one exception to that totality is a configured
|
|
146
|
+
``segmenter``, whose own exceptions propagate (see the class
|
|
147
|
+
docstring)."""
|
|
71
148
|
if isinstance(text, bytes):
|
|
72
149
|
raise TypeError(
|
|
73
150
|
"parse() takes str, not bytes -- decode first, e.g. "
|
|
@@ -75,7 +152,7 @@ class Parser:
|
|
|
75
152
|
if not isinstance(text, str):
|
|
76
153
|
raise TypeError(f"parse() takes str, got {text!r}")
|
|
77
154
|
state = ParseState(original=text, lexicon=self.lexicon,
|
|
78
|
-
policy=self.policy)
|
|
155
|
+
policy=self.policy, segmenter=self.segmenter)
|
|
79
156
|
return assemble(run(state))
|
|
80
157
|
|
|
81
158
|
# -- editing ----------------------------------------------------------
|
|
@@ -92,7 +169,11 @@ class Parser:
|
|
|
92
169
|
role choices and ambiguities are discarded -- every harvested
|
|
93
170
|
token takes the named field's role -- and its structural
|
|
94
171
|
behavior applies: delimiter characters do not become tokens,
|
|
95
|
-
and a
|
|
172
|
+
and a maiden marker is consumed as in parsing -- mid-value
|
|
173
|
+
always, and leading a DELIMITED value under a policy routing
|
|
174
|
+
that pair to maiden, where "(née Jones)" revises to "Jones"
|
|
175
|
+
while the bare "née Jones" keeps its marker, a leading marker
|
|
176
|
+
in an undelimited value being no marker at all (#329).
|
|
96
177
|
Tokens are synthetic (span=None); original is unchanged; a
|
|
97
178
|
value with no name content (empty, whitespace, or punctuation
|
|
98
179
|
only) clears the field; ambiguities referencing replaced
|
|
@@ -151,14 +232,27 @@ def parse(text: str) -> ParsedName:
|
|
|
151
232
|
return _default_parser().parse(text)
|
|
152
233
|
|
|
153
234
|
|
|
154
|
-
def parser_for(*locales: Locale, base: Parser | None = None
|
|
235
|
+
def parser_for(*locales: Locale, base: Parser | None = None,
|
|
236
|
+
segmenter: Segmenter | None | _Unset = UNSET) -> Parser:
|
|
155
237
|
"""Lexicon fragments unioned left-to-right onto base's; policy
|
|
156
238
|
patches applied left-to-right (later wins; set-valued fields union
|
|
157
239
|
per the patch metadata). Validation errors raised while applying a
|
|
158
240
|
pack are wrapped with that pack's identity (spec §4 amendment) --
|
|
159
241
|
PolicyPatch validates lazily, so with stacked packs the raw error
|
|
160
242
|
would otherwise point at nothing. Two packs setting the same SCALAR
|
|
161
|
-
field is a declared conflict: UserWarning, later wins.
|
|
243
|
+
field is a declared conflict: UserWarning, later wins.
|
|
244
|
+
|
|
245
|
+
A ``segmenter`` is passed straight through to the built Parser --
|
|
246
|
+
``parser_for(locales.JA, segmenter=locales.ja_segmenter())`` is how
|
|
247
|
+
a pack and a segmenter combine, since packs are pure data and
|
|
248
|
+
cannot supply one. The argument has THREE states, the same
|
|
249
|
+
:data:`~nameparser.UNSET` spelling a PolicyPatch field uses, because
|
|
250
|
+
None is a meaningful value here and not an absence: omitted (UNSET)
|
|
251
|
+
carries base's segmenter through unchanged; a callable OVERRIDES
|
|
252
|
+
base's (later wins, the rule scalar policy fields follow); and an
|
|
253
|
+
explicit ``None`` CLEARS base's, which is how you derive an
|
|
254
|
+
unsegmented parser from a segmented one without rebuilding its
|
|
255
|
+
lexicon and policy by hand."""
|
|
162
256
|
if base is not None and not isinstance(base, Parser):
|
|
163
257
|
raise TypeError(f"base must be a Parser or None, got {base!r}")
|
|
164
258
|
for loc in locales:
|
|
@@ -166,6 +260,15 @@ def parser_for(*locales: Locale, base: Parser | None = None) -> Parser:
|
|
|
166
260
|
raise TypeError(f"parser_for() takes Locale packs, got {loc!r}")
|
|
167
261
|
lexicon = base.lexicon if base is not None else Lexicon.default()
|
|
168
262
|
policy = base.policy if base is not None else Policy()
|
|
263
|
+
# Resolved here rather than at the return because the return builds
|
|
264
|
+
# a FRESH Parser: any field not listed there silently takes its
|
|
265
|
+
# default, and a dropped segmenter would be invisible. UNSET, not
|
|
266
|
+
# None, is what "not given" means -- None is the CLEAR request, and
|
|
267
|
+
# collapsing the two would make an explicit
|
|
268
|
+
# parser_for(..., segmenter=None) silently inherit the very
|
|
269
|
+
# segmenter it was asked to drop.
|
|
270
|
+
if segmenter is UNSET:
|
|
271
|
+
segmenter = base.segmenter if base is not None else None
|
|
169
272
|
scalar_setters: dict[str, str] = {}
|
|
170
273
|
for loc in locales:
|
|
171
274
|
for f in dataclasses.fields(PolicyPatch):
|
|
@@ -189,4 +292,15 @@ def parser_for(*locales: Locale, base: Parser | None = None) -> Parser:
|
|
|
189
292
|
# a subclass with extra mandatory args would break this rewrap
|
|
190
293
|
raise type(exc)(
|
|
191
294
|
f"while applying locale {loc.code!r}: {exc}") from exc
|
|
192
|
-
|
|
295
|
+
# Construction warnings (the segmenterless-activation check in
|
|
296
|
+
# Parser.__post_init__) re-emit from THIS frame: its stacklevel is
|
|
297
|
+
# sized for direct Parser(...) construction, and through this
|
|
298
|
+
# function's extra frame the default single-line rendering would
|
|
299
|
+
# point into the library instead of at the caller -- the exact
|
|
300
|
+
# call the message tells them to change.
|
|
301
|
+
with warnings.catch_warnings(record=True) as caught:
|
|
302
|
+
warnings.simplefilter("always")
|
|
303
|
+
built = Parser(lexicon=lexicon, policy=policy, segmenter=segmenter)
|
|
304
|
+
for w in caught:
|
|
305
|
+
warnings.warn(w.message, stacklevel=2)
|
|
306
|
+
return built
|
|
@@ -16,14 +16,15 @@ from nameparser._pipeline._classify import classify
|
|
|
16
16
|
from nameparser._pipeline._extract import extract_delimited
|
|
17
17
|
from nameparser._pipeline._group import group
|
|
18
18
|
from nameparser._pipeline._post_rules import post_rules
|
|
19
|
+
from nameparser._pipeline._script_segment import script_segment
|
|
19
20
|
from nameparser._pipeline._segment import segment
|
|
20
21
|
from nameparser._pipeline._state import ParseState
|
|
21
22
|
from nameparser._pipeline._tokenize import tokenize
|
|
22
23
|
|
|
23
|
-
#: The full
|
|
24
|
+
#: The full eight-stage fold.
|
|
24
25
|
STAGES: tuple[Callable[[ParseState], ParseState], ...] = (
|
|
25
|
-
extract_delimited, tokenize, segment,
|
|
26
|
-
post_rules,
|
|
26
|
+
extract_delimited, tokenize, segment, script_segment, classify,
|
|
27
|
+
group, assign, post_rules,
|
|
27
28
|
)
|
|
28
29
|
|
|
29
30
|
|