nameparser 2.4.1__tar.gz → 2.5.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.4.1/nameparser.egg-info → nameparser-2.5.0}/PKG-INFO +8 -9
- {nameparser-2.4.1 → nameparser-2.5.0}/README.rst +7 -8
- {nameparser-2.4.1 → nameparser-2.5.0}/nameparser/_config_shim.py +1 -1
- {nameparser-2.4.1 → nameparser-2.5.0}/nameparser/_facade.py +5 -5
- {nameparser-2.4.1 → nameparser-2.5.0}/nameparser/_lexicon.py +16 -14
- {nameparser-2.4.1 → nameparser-2.5.0}/nameparser/_parser.py +7 -7
- {nameparser-2.4.1 → nameparser-2.5.0}/nameparser/_pipeline/_assemble.py +24 -5
- {nameparser-2.4.1 → nameparser-2.5.0}/nameparser/_pipeline/_assign.py +144 -62
- {nameparser-2.4.1 → nameparser-2.5.0}/nameparser/_pipeline/_classify.py +81 -32
- {nameparser-2.4.1 → nameparser-2.5.0}/nameparser/_pipeline/_comma.py +187 -51
- {nameparser-2.4.1 → nameparser-2.5.0}/nameparser/_pipeline/_extract.py +8 -8
- {nameparser-2.4.1 → nameparser-2.5.0}/nameparser/_pipeline/_group.py +87 -53
- {nameparser-2.4.1 → nameparser-2.5.0}/nameparser/_pipeline/_pieces.py +126 -77
- {nameparser-2.4.1 → nameparser-2.5.0}/nameparser/_pipeline/_post_rules.py +83 -39
- {nameparser-2.4.1 → nameparser-2.5.0}/nameparser/_pipeline/_script_segment.py +28 -19
- {nameparser-2.4.1 → nameparser-2.5.0}/nameparser/_pipeline/_segment.py +15 -9
- {nameparser-2.4.1 → nameparser-2.5.0}/nameparser/_pipeline/_state.py +6 -6
- {nameparser-2.4.1 → nameparser-2.5.0}/nameparser/_pipeline/_tokenize.py +8 -8
- {nameparser-2.4.1 → nameparser-2.5.0}/nameparser/_pipeline/_vocab.py +143 -53
- {nameparser-2.4.1 → nameparser-2.5.0}/nameparser/_policy.py +8 -6
- {nameparser-2.4.1 → nameparser-2.5.0}/nameparser/_render.py +53 -29
- {nameparser-2.4.1 → nameparser-2.5.0}/nameparser/_types.py +24 -13
- {nameparser-2.4.1 → nameparser-2.5.0}/nameparser/_version.py +1 -1
- {nameparser-2.4.1 → nameparser-2.5.0}/nameparser/config/__init__.py +1 -1
- {nameparser-2.4.1 → nameparser-2.5.0}/nameparser/config/bound_given_names.py +1 -1
- {nameparser-2.4.1 → nameparser-2.5.0}/nameparser/config/capitalization.py +5 -5
- {nameparser-2.4.1 → nameparser-2.5.0}/nameparser/config/conjunctions.py +5 -3
- {nameparser-2.4.1 → nameparser-2.5.0}/nameparser/config/maiden_markers.py +4 -4
- {nameparser-2.4.1 → nameparser-2.5.0}/nameparser/config/particles.py +37 -7
- {nameparser-2.4.1 → nameparser-2.5.0}/nameparser/config/suffixes.py +129 -18
- {nameparser-2.4.1 → nameparser-2.5.0}/nameparser/config/titles.py +18 -12
- {nameparser-2.4.1 → nameparser-2.5.0}/nameparser/locales/ja.py +2 -2
- {nameparser-2.4.1 → nameparser-2.5.0}/nameparser/locales/zh.py +70 -6
- {nameparser-2.4.1 → nameparser-2.5.0/nameparser.egg-info}/PKG-INFO +8 -9
- {nameparser-2.4.1 → nameparser-2.5.0}/nameparser.egg-info/SOURCES.txt +7 -0
- {nameparser-2.4.1 → nameparser-2.5.0}/tests/test_capitalization.py +16 -16
- {nameparser-2.4.1 → nameparser-2.5.0}/tests/test_initials.py +4 -4
- {nameparser-2.4.1 → nameparser-2.5.0}/tests/test_suffixes.py +11 -11
- {nameparser-2.4.1 → nameparser-2.5.0}/tests/test_titles.py +3 -3
- {nameparser-2.4.1 → nameparser-2.5.0}/tests/v2/_differential_fixtures.py +43 -2
- {nameparser-2.4.1 → nameparser-2.5.0}/tests/v2/cases.py +1097 -345
- nameparser-2.5.0/tests/v2/design_docs.py +107 -0
- {nameparser-2.4.1 → nameparser-2.5.0}/tests/v2/pipeline/test_assemble.py +5 -2
- {nameparser-2.4.1 → nameparser-2.5.0}/tests/v2/pipeline/test_assign.py +2 -2
- {nameparser-2.4.1 → nameparser-2.5.0}/tests/v2/pipeline/test_classify.py +41 -18
- {nameparser-2.4.1 → nameparser-2.5.0}/tests/v2/pipeline/test_comma.py +10 -10
- {nameparser-2.4.1 → nameparser-2.5.0}/tests/v2/pipeline/test_group.py +35 -29
- {nameparser-2.4.1 → nameparser-2.5.0}/tests/v2/pipeline/test_pieces.py +9 -9
- {nameparser-2.4.1 → nameparser-2.5.0}/tests/v2/pipeline/test_post_rules.py +21 -19
- {nameparser-2.4.1 → nameparser-2.5.0}/tests/v2/pipeline/test_script_segment.py +1 -1
- {nameparser-2.4.1 → nameparser-2.5.0}/tests/v2/pipeline/test_segment.py +2 -2
- {nameparser-2.4.1 → nameparser-2.5.0}/tests/v2/pipeline/test_vocab.py +5 -5
- {nameparser-2.4.1 → nameparser-2.5.0}/tests/v2/rules_doc.py +3 -5
- {nameparser-2.4.1 → nameparser-2.5.0}/tests/v2/test_benchmark.py +15 -15
- {nameparser-2.4.1 → nameparser-2.5.0}/tests/v2/test_cases.py +23 -6
- nameparser-2.5.0/tests/v2/test_cldr_agreement.py +573 -0
- {nameparser-2.4.1 → nameparser-2.5.0}/tests/v2/test_corpora.py +16 -6
- nameparser-2.5.0/tests/v2/test_corpus_build.py +170 -0
- nameparser-2.5.0/tests/v2/test_design_docs.py +150 -0
- {nameparser-2.4.1 → nameparser-2.5.0}/tests/v2/test_doc_citations.py +93 -47
- {nameparser-2.4.1 → nameparser-2.5.0}/tests/v2/test_doc_spellings.py +1 -1
- {nameparser-2.4.1 → nameparser-2.5.0}/tests/v2/test_facade.py +29 -41
- {nameparser-2.4.1 → nameparser-2.5.0}/tests/v2/test_facade_cases.py +14 -0
- nameparser-2.5.0/tests/v2/test_gnd_corpus.py +301 -0
- {nameparser-2.4.1 → nameparser-2.5.0}/tests/v2/test_lexicon.py +4 -4
- {nameparser-2.4.1 → nameparser-2.5.0}/tests/v2/test_locales.py +11 -10
- nameparser-2.5.0/tests/v2/test_name_usage_tool.py +1156 -0
- {nameparser-2.4.1 → nameparser-2.5.0}/tests/v2/test_order_correspondence.py +19 -19
- {nameparser-2.4.1 → nameparser-2.5.0}/tests/v2/test_parser.py +157 -43
- {nameparser-2.4.1 → nameparser-2.5.0}/tests/v2/test_policy.py +4 -3
- {nameparser-2.4.1 → nameparser-2.5.0}/tests/v2/test_properties.py +154 -46
- {nameparser-2.4.1 → nameparser-2.5.0}/tests/v2/test_regex_sync.py +6 -1
- {nameparser-2.4.1 → nameparser-2.5.0}/tests/v2/test_render.py +82 -42
- {nameparser-2.4.1 → nameparser-2.5.0}/tests/v2/test_rules_doc.py +22 -13
- {nameparser-2.4.1 → nameparser-2.5.0}/tests/v2/test_rules_doc_grammar.py +4 -4
- {nameparser-2.4.1 → nameparser-2.5.0}/tests/v2/test_snapshot.py +1 -1
- nameparser-2.5.0/tests/v2/test_wikidata_corpus.py +429 -0
- nameparser-2.5.0/tests/v2/test_wikidata_native_corpus.py +480 -0
- nameparser-2.5.0/tests/v2/test_zh_surnames_tool.py +145 -0
- nameparser-2.4.1/tests/v2/test_cldr_agreement.py +0 -208
- nameparser-2.4.1/tests/v2/test_name_usage_tool.py +0 -303
- {nameparser-2.4.1 → nameparser-2.5.0}/AUTHORS +0 -0
- {nameparser-2.4.1 → nameparser-2.5.0}/LICENSE +0 -0
- {nameparser-2.4.1 → nameparser-2.5.0}/MANIFEST.in +0 -0
- {nameparser-2.4.1 → nameparser-2.5.0}/nameparser/__init__.py +0 -0
- {nameparser-2.4.1 → nameparser-2.5.0}/nameparser/__main__.py +0 -0
- {nameparser-2.4.1 → nameparser-2.5.0}/nameparser/_locale.py +0 -0
- {nameparser-2.4.1 → nameparser-2.5.0}/nameparser/_pipeline/__init__.py +0 -0
- {nameparser-2.4.1 → nameparser-2.5.0}/nameparser/config/_deprecated.py +0 -0
- {nameparser-2.4.1 → nameparser-2.5.0}/nameparser/config/_invariants.py +0 -0
- {nameparser-2.4.1 → nameparser-2.5.0}/nameparser/config/bound_first_names.py +0 -0
- {nameparser-2.4.1 → nameparser-2.5.0}/nameparser/config/prefixes.py +0 -0
- {nameparser-2.4.1 → nameparser-2.5.0}/nameparser/config/regexes.py +0 -0
- {nameparser-2.4.1 → nameparser-2.5.0}/nameparser/config/surnames.py +0 -0
- {nameparser-2.4.1 → nameparser-2.5.0}/nameparser/locales/__init__.py +0 -0
- {nameparser-2.4.1 → nameparser-2.5.0}/nameparser/locales/ru.py +0 -0
- {nameparser-2.4.1 → nameparser-2.5.0}/nameparser/locales/tr_az.py +0 -0
- {nameparser-2.4.1 → nameparser-2.5.0}/nameparser/parser.py +0 -0
- {nameparser-2.4.1 → nameparser-2.5.0}/nameparser/py.typed +0 -0
- {nameparser-2.4.1 → nameparser-2.5.0}/nameparser/util.py +0 -0
- {nameparser-2.4.1 → nameparser-2.5.0}/nameparser.egg-info/dependency_links.txt +0 -0
- {nameparser-2.4.1 → nameparser-2.5.0}/nameparser.egg-info/requires.txt +0 -0
- {nameparser-2.4.1 → nameparser-2.5.0}/nameparser.egg-info/top_level.txt +0 -0
- {nameparser-2.4.1 → nameparser-2.5.0}/pyproject.toml +0 -0
- {nameparser-2.4.1 → nameparser-2.5.0}/setup.cfg +0 -0
- {nameparser-2.4.1 → nameparser-2.5.0}/tests/__init__.py +0 -0
- {nameparser-2.4.1 → nameparser-2.5.0}/tests/base.py +0 -0
- {nameparser-2.4.1 → nameparser-2.5.0}/tests/conftest.py +0 -0
- {nameparser-2.4.1 → nameparser-2.5.0}/tests/test_bound_given_names.py +0 -0
- {nameparser-2.4.1 → nameparser-2.5.0}/tests/test_brute_force.py +0 -0
- {nameparser-2.4.1 → nameparser-2.5.0}/tests/test_comma_variants.py +0 -0
- {nameparser-2.4.1 → nameparser-2.5.0}/tests/test_conjunctions.py +0 -0
- {nameparser-2.4.1 → nameparser-2.5.0}/tests/test_constants.py +0 -0
- {nameparser-2.4.1 → nameparser-2.5.0}/tests/test_east_slavic_patronymic_order.py +0 -0
- {nameparser-2.4.1 → nameparser-2.5.0}/tests/test_first_name.py +0 -0
- {nameparser-2.4.1 → nameparser-2.5.0}/tests/test_middle_name_as_last.py +0 -0
- {nameparser-2.4.1 → nameparser-2.5.0}/tests/test_nicknames.py +0 -0
- {nameparser-2.4.1 → nameparser-2.5.0}/tests/test_output_format.py +0 -0
- {nameparser-2.4.1 → nameparser-2.5.0}/tests/test_particles.py +0 -0
- {nameparser-2.4.1 → nameparser-2.5.0}/tests/test_python_api.py +0 -0
- {nameparser-2.4.1 → nameparser-2.5.0}/tests/test_turkic_patronymic_order.py +0 -0
- {nameparser-2.4.1 → nameparser-2.5.0}/tests/test_variations.py +0 -0
- {nameparser-2.4.1 → nameparser-2.5.0}/tests/v2/__init__.py +0 -0
- {nameparser-2.4.1 → nameparser-2.5.0}/tests/v2/conftest.py +0 -0
- {nameparser-2.4.1 → nameparser-2.5.0}/tests/v2/pipeline/__init__.py +0 -0
- {nameparser-2.4.1 → nameparser-2.5.0}/tests/v2/pipeline/test_extract.py +0 -0
- {nameparser-2.4.1 → nameparser-2.5.0}/tests/v2/pipeline/test_state.py +0 -0
- {nameparser-2.4.1 → nameparser-2.5.0}/tests/v2/pipeline/test_tokenize.py +0 -0
- {nameparser-2.4.1 → nameparser-2.5.0}/tests/v2/test_cldr_corpus.py +0 -0
- {nameparser-2.4.1 → nameparser-2.5.0}/tests/v2/test_cli.py +0 -0
- {nameparser-2.4.1 → nameparser-2.5.0}/tests/v2/test_config_aliases.py +0 -0
- {nameparser-2.4.1 → nameparser-2.5.0}/tests/v2/test_config_shim.py +0 -0
- {nameparser-2.4.1 → nameparser-2.5.0}/tests/v2/test_contracts.py +0 -0
- {nameparser-2.4.1 → nameparser-2.5.0}/tests/v2/test_differential.py +0 -0
- {nameparser-2.4.1 → nameparser-2.5.0}/tests/v2/test_layering.py +0 -0
- {nameparser-2.4.1 → nameparser-2.5.0}/tests/v2/test_locale.py +0 -0
- {nameparser-2.4.1 → nameparser-2.5.0}/tests/v2/test_reprs.py +0 -0
- {nameparser-2.4.1 → nameparser-2.5.0}/tests/v2/test_types.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: nameparser
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.5.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
|
|
@@ -36,14 +36,13 @@ nameparser parses human names into seven fields — title, given, middle,
|
|
|
36
36
|
family, suffix, nickname, maiden. Results are immutable, configuration is
|
|
37
37
|
composable, and locale packs are opt-in.
|
|
38
38
|
|
|
39
|
-
📣 **nameparser 2.
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
details.
|
|
39
|
+
📣 **nameparser 2.5 is out.** It reads junior and senior written in 45
|
|
40
|
+
languages (``Antonio Pavlović mlađi``), keeps a title written before a
|
|
41
|
+
family comma out of the surname (``Dr. Smith, John``), reads Chinese and
|
|
42
|
+
Japanese credential and office words (``田中社長``), and gives a lone first
|
|
43
|
+
name followed by V, I or X its last name back (``Malcolm X``). The `release
|
|
44
|
+
log <https://nameparser.readthedocs.io/en/latest/release_log.html>`__ has
|
|
45
|
+
the details.
|
|
47
46
|
|
|
48
47
|
Chinese, Japanese and Korean names written in their own scripts are read
|
|
49
48
|
family-first, unspaced Korean names are split against the census surname
|
|
@@ -7,14 +7,13 @@ 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.
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
details.
|
|
10
|
+
📣 **nameparser 2.5 is out.** It reads junior and senior written in 45
|
|
11
|
+
languages (``Antonio Pavlović mlađi``), keeps a title written before a
|
|
12
|
+
family comma out of the surname (``Dr. Smith, John``), reads Chinese and
|
|
13
|
+
Japanese credential and office words (``田中社長``), and gives a lone first
|
|
14
|
+
name followed by V, I or X its last name back (``Malcolm X``). The `release
|
|
15
|
+
log <https://nameparser.readthedocs.io/en/latest/release_log.html>`__ has
|
|
16
|
+
the details.
|
|
18
17
|
|
|
19
18
|
Chinese, Japanese and Korean names written in their own scripts are read
|
|
20
19
|
family-first, unspaced Korean names are split against the census surname
|
|
@@ -1109,7 +1109,7 @@ class Constants:
|
|
|
1109
1109
|
# tests/v2/test_config_shim.py::test_snapshot_field_translation)
|
|
1110
1110
|
# A capitalization_exceptions value that does not spell its key
|
|
1111
1111
|
# is the one DECIDED exception to this method's never-raise
|
|
1112
|
-
# rule (#459, decisions
|
|
1112
|
+
# rule (#459, decisions/R4 and #3-0-reevaluations).
|
|
1113
1113
|
# _normpairs raises _MaskValueError for exactly that shape, and
|
|
1114
1114
|
# the except clause re-spells its offered fix for a v1 caller.
|
|
1115
1115
|
# Caught by TYPE, so every other Lexicon error passes through
|
|
@@ -271,7 +271,7 @@ class HumanName:
|
|
|
271
271
|
"""Re-capitalize the current parse against the bound lexicon.
|
|
272
272
|
force=None reads the bound Constants' render default
|
|
273
273
|
(force_mixed_case_capitalization); the core's capitalized()
|
|
274
|
-
implements the one-case gate (rules
|
|
274
|
+
implements the one-case gate (rules/R5, suffixes left out
|
|
275
275
|
since #492) -- not
|
|
276
276
|
re-implemented here."""
|
|
277
277
|
self._resolve()
|
|
@@ -511,7 +511,7 @@ class HumanName:
|
|
|
511
511
|
# answers -- the same fallback _render._cap_word takes for the
|
|
512
512
|
# same tokens and through the same helper, which is why the
|
|
513
513
|
# helper is imported rather than the predicate rewritten
|
|
514
|
-
# (decisions
|
|
514
|
+
# (decisions/R4 for why one question and not two: whether a
|
|
515
515
|
# word is a connective is a fact the word can answer alone,
|
|
516
516
|
# whether a particle is acting as one is a fact about the part).
|
|
517
517
|
#
|
|
@@ -533,7 +533,7 @@ class HumanName:
|
|
|
533
533
|
and UNJOINED_CONJUNCTION_TAG not in tok.tags)
|
|
534
534
|
|
|
535
535
|
def _split_last(self) -> tuple[list[str], list[str]]:
|
|
536
|
-
# rules
|
|
536
|
+
# rules/R2: "a name part whose every word is particle
|
|
537
537
|
# vocabulary is a part where none of them is doing a
|
|
538
538
|
# particle's work" -- the all-particle guard
|
|
539
539
|
# below is this rule, and predates its statement: v1 assumed a
|
|
@@ -617,7 +617,7 @@ class HumanName:
|
|
|
617
617
|
#
|
|
618
618
|
# Particles are NOT decided per token: _is_particle stays a
|
|
619
619
|
# live vocabulary lookup, as _render._cap_word keeps it --
|
|
620
|
-
# rules
|
|
620
|
+
# rules/R4 draws this boundary per question, not per field.
|
|
621
621
|
self._resolve()
|
|
622
622
|
if tokens is None:
|
|
623
623
|
# STRING PATH. split() rather than split(" ") because
|
|
@@ -700,7 +700,7 @@ class HumanName:
|
|
|
700
700
|
if got or not words or not all(self._is_particle(w)
|
|
701
701
|
for w in words):
|
|
702
702
|
return got
|
|
703
|
-
# rules
|
|
703
|
+
# rules/R3: "except the particles of a part whose every
|
|
704
704
|
# word is one, which are not acting as particles there"
|
|
705
705
|
# -- nothing survived
|
|
706
706
|
# the filter, so the whole group is particles. The
|
|
@@ -129,7 +129,7 @@ _PHRASE_FIELDS = ("given_name_titles", "maiden_markers")
|
|
|
129
129
|
#: it is roman five -- suffix -- where 'V.' stays a middle
|
|
130
130
|
#: initial through that same carve-out; 'Jr。' simply reaches its
|
|
131
131
|
#: entry, and 'Dr。' its title.
|
|
132
|
-
#: decisions
|
|
132
|
+
#: decisions/cjk-full-stops carries the measurements.
|
|
133
133
|
FULL_STOPS = "..。。"
|
|
134
134
|
|
|
135
135
|
|
|
@@ -152,7 +152,7 @@ def _normalize(word: str) -> str:
|
|
|
152
152
|
|
|
153
153
|
NFC, since #322, and only for a non-ASCII word (ASCII is already
|
|
154
154
|
NFC, and this runs per token on the parse path --
|
|
155
|
-
decisions
|
|
155
|
+
decisions/parse-cost). NFD hangul decomposes to jamo and missed
|
|
156
156
|
every vocabulary entry; composing at lookup AND at storage keeps
|
|
157
157
|
the two symmetric, and no shipped entry changes under it (measured
|
|
158
158
|
2026-09-09: fold every string member of every Lexicon.default()
|
|
@@ -175,7 +175,7 @@ def _normalize(word: str) -> str:
|
|
|
175
175
|
word = stripped
|
|
176
176
|
|
|
177
177
|
|
|
178
|
-
# rules
|
|
178
|
+
# rules/P7: "A one-letter word written with its period is an
|
|
179
179
|
# initial and not a particle"
|
|
180
180
|
def _spells_an_initial(n: str, text: str) -> bool:
|
|
181
181
|
"""Whether `text` is ONE letter written with its period ('Ó.',
|
|
@@ -195,7 +195,7 @@ def _spells_an_initial(n: str, text: str) -> bool:
|
|
|
195
195
|
-- its script table is _policy's, which this module may not import
|
|
196
196
|
-- so a caller's one-character particle in such a script, written
|
|
197
197
|
with a period, is vetoed where is_initial would call it no initial
|
|
198
|
-
(decisions
|
|
198
|
+
(decisions/P7). No shipped particle is one character of those
|
|
199
199
|
scripts."""
|
|
200
200
|
return len(n) == 1 and text[-1] == "."
|
|
201
201
|
|
|
@@ -224,7 +224,7 @@ def _fold_words(words: Iterable[str]) -> list[str]:
|
|
|
224
224
|
|
|
225
225
|
map/filter rather than a comprehension: both are C calls where a
|
|
226
226
|
comprehension is a Python frame on 3.11, and this runs on the parse
|
|
227
|
-
path for every name carrying a title run (decisions
|
|
227
|
+
path for every name carrying a title run (decisions/parse-cost).
|
|
228
228
|
"""
|
|
229
229
|
return list(filter(None, map(_normalize, words)))
|
|
230
230
|
|
|
@@ -247,7 +247,7 @@ def _run_addresses_by_given(words: Iterable[str],
|
|
|
247
247
|
|
|
248
248
|
Two sites ask it and they must agree -- post_rules for H1, group
|
|
249
249
|
for the P5 licence -- because a run read two ways is a rule
|
|
250
|
-
contradicting itself (decisions
|
|
250
|
+
contradicting itself (decisions/P5, the 2026-08-22 #369 entry).
|
|
251
251
|
One predicate, so they cannot drift.
|
|
252
252
|
|
|
253
253
|
The WHOLE run's key, or that key's LAST word (#489): a run is
|
|
@@ -542,7 +542,7 @@ def _normpairs(
|
|
|
542
542
|
# punctuation marks which letters are joined into one run
|
|
543
543
|
# (_render._apply_mask reads that) and is never written into
|
|
544
544
|
# the word. A raise, not a warning: a mismatched value used to
|
|
545
|
-
# be SUBSTITUTED for the word, and rules
|
|
545
|
+
# be SUBSTITUTED for the word, and rules/R4: "Repair
|
|
546
546
|
# changes case and nothing else", so there is no reading of
|
|
547
547
|
# one that repair can honor.
|
|
548
548
|
if _alnum(_normalize(v)) != _alnum(normalized_key):
|
|
@@ -582,8 +582,8 @@ class Lexicon:
|
|
|
582
582
|
#: Pre-nominal titles ("dr", "sir", "capt", ...). Full default
|
|
583
583
|
#: list: :data:`~nameparser.config.titles.TITLES`.
|
|
584
584
|
titles: frozenset[str] = frozenset()
|
|
585
|
-
#: Titles whose single
|
|
586
|
-
#: ("sheikh", "sister", ...) rather than a family name. Full
|
|
585
|
+
#: Titles whose single name word, before or after them, reads as a
|
|
586
|
+
#: GIVEN name ("sheikh", "sister", ...) rather than a family name. Full
|
|
587
587
|
#: default list: :data:`~nameparser.config.titles.GIVEN_NAME_TITLES`.
|
|
588
588
|
given_name_titles: frozenset[str] = frozenset()
|
|
589
589
|
#: Post-nominal acronym suffixes, matched with or without periods
|
|
@@ -599,7 +599,7 @@ class Lexicon:
|
|
|
599
599
|
#: outright; written bare, capitals decide in a mixed-case name
|
|
600
600
|
#: ("Jack MA" is the credential, "Jack Ma" keeps his family name)
|
|
601
601
|
#: and otherwise the word counts as a suffix only where a name
|
|
602
|
-
#: word is left beside it ("John Smith ma"), rules.
|
|
602
|
+
#: word is left beside it ("John Smith ma"), rules/S6 and rules/S4. Full
|
|
603
603
|
#: default list:
|
|
604
604
|
#: :data:`~nameparser.config.suffixes.SUFFIX_ACRONYMS_AMBIGUOUS`.
|
|
605
605
|
suffix_acronyms_ambiguous: frozenset[str] = frozenset()
|
|
@@ -644,8 +644,10 @@ class Lexicon:
|
|
|
644
644
|
#: decided by the writing instead and never consults this set, and
|
|
645
645
|
#: neither does a caseless letter (Arabic و), which has no case to
|
|
646
646
|
#: read. A member additionally reports
|
|
647
|
-
#: :attr:`~nameparser.AmbiguityKind.CONJUNCTION_OR_INITIAL
|
|
648
|
-
#:
|
|
647
|
+
#: :attr:`~nameparser.AmbiguityKind.CONJUNCTION_OR_INITIAL` where a
|
|
648
|
+
#: name word of its part follows it (with none, it connects
|
|
649
|
+
#: nothing); a non-member reports nothing, its reading not being in
|
|
650
|
+
#: doubt.
|
|
649
651
|
#: Full default list:
|
|
650
652
|
#: :data:`~nameparser.config.conjunctions.CONJUNCTIONS_AMBIGUOUS`.
|
|
651
653
|
#: Entries need not also be in ``conjunctions``; one that is not is
|
|
@@ -673,8 +675,8 @@ class Lexicon:
|
|
|
673
675
|
#: (:data:`~nameparser.config.surnames.KOREAN_SURNAMES`); Chinese
|
|
674
676
|
#: surnames ship in locales.ZH because Han segmentation is opt-in.
|
|
675
677
|
surnames: frozenset[str] = frozenset()
|
|
676
|
-
#: Honorifics
|
|
677
|
-
#: (#308), matched longest-first: 田中さん splits into 田中 and さん
|
|
678
|
+
#: Honorifics, and credential, office and generational words, that
|
|
679
|
+
#: may be peeled off the END of a name token (#308, #707), matched longest-first: 田中さん splits into 田中 and さん
|
|
678
680
|
#: before the tokens are classified. Every entry must also be a
|
|
679
681
|
#: :attr:`suffix_words` entry -- the peeled tail is claimed by
|
|
680
682
|
#: suffix classification like any other post-nominal. Deliberately
|
|
@@ -41,7 +41,7 @@ class Parser:
|
|
|
41
41
|
vocabulary DECLINES, so a locale pack's surnames always win where
|
|
42
42
|
they match; returning None declines in turn and the token stays
|
|
43
43
|
whole. Two promises narrow when one is supplied (the first is
|
|
44
|
-
rules
|
|
44
|
+
rules/A1's Accepted clause):
|
|
45
45
|
parse-totality gains its one exception -- an exception raised by
|
|
46
46
|
the segmenter propagates, because a user-supplied callable's own
|
|
47
47
|
error is a user-code error, not a content error -- and this Parser
|
|
@@ -220,7 +220,7 @@ class Parser:
|
|
|
220
220
|
# tags are KEPT, "joined" included: a within-piece mark
|
|
221
221
|
# (the Ph. D. merge) is role-blind and right for every
|
|
222
222
|
# role, and a clear was measured and backed out --
|
|
223
|
-
# decisions
|
|
223
|
+
# decisions/R1 (2026-09-06 #511) carries the rest: the
|
|
224
224
|
# between-piece mark that rides onto a non-suffix role, and
|
|
225
225
|
# which views read it. Dropped tokens keep their role: the
|
|
226
226
|
# pass filters them by index and assemble omits them.
|
|
@@ -228,7 +228,7 @@ class Parser:
|
|
|
228
228
|
tok if i in dropped
|
|
229
229
|
else dataclasses.replace(tok, role=role)
|
|
230
230
|
for i, tok in enumerate(state.tokens))
|
|
231
|
-
# rules
|
|
231
|
+
# rules/R1: "a run of post-nominals written with spaces
|
|
232
232
|
# renders with spaces, and one written with commas keeps
|
|
233
233
|
# them" -- the same pass post_rules runs last, over the
|
|
234
234
|
# value's own comma offsets, so the harvest carries exactly
|
|
@@ -346,13 +346,13 @@ def parser_for(*locales: Locale, base: Parser | None = None,
|
|
|
346
346
|
# a subclass with extra mandatory args would break this rewrap
|
|
347
347
|
raise type(exc)(
|
|
348
348
|
f"while applying locale {loc.code!r}: {exc}") from exc
|
|
349
|
-
# rules
|
|
349
|
+
# rules/D1: "constructing a parser that activates division for
|
|
350
350
|
# scripts with no covering surnames and no segmenter warns at
|
|
351
351
|
# construction, naming the dead scripts and each way out"
|
|
352
|
-
# (history: decisions
|
|
353
|
-
# rules
|
|
352
|
+
# (history: decisions/D1)
|
|
353
|
+
# rules/D2: "applying a locale pack wraps any such error with
|
|
354
354
|
# the locale's code, so a stacked configuration names which layer
|
|
355
|
-
# broke" (history: decisions
|
|
355
|
+
# broke" (history: decisions/D2)
|
|
356
356
|
# Construction warnings (the segmenterless-activation check in
|
|
357
357
|
# Parser.__post_init__) re-emit from THIS frame: its stacklevel is
|
|
358
358
|
# sized for direct Parser(...) construction, and through this
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
Consumes: tokens (all roles set), dropped, ambiguities (by index;
|
|
4
4
|
one carrying ``field_tail`` is worded from its word's final role,
|
|
5
|
-
#626)
|
|
5
|
+
#626), and segments, for the part a withdrawn connective report's
|
|
6
|
+
letter ends (rules/P3, #721).
|
|
6
7
|
Produces: a validated ParsedName -- the constructor re-checks every
|
|
7
8
|
invariant (span order/bounds, ambiguity subset), so a pipeline bug
|
|
8
9
|
that would produce an invalid result dies HERE, not in a renderer
|
|
@@ -20,16 +21,16 @@ tests/v2/pipeline/test_assemble.py.
|
|
|
20
21
|
"""
|
|
21
22
|
from __future__ import annotations
|
|
22
23
|
|
|
23
|
-
from nameparser._pipeline._state import FIELD_READING, ParseState
|
|
24
|
+
from nameparser._pipeline._state import FIELD_READING, NAME_ROLES, ParseState
|
|
24
25
|
from nameparser._types import (
|
|
25
26
|
Ambiguity, AmbiguityKind, ParsedName, Role, Token,
|
|
26
27
|
)
|
|
27
28
|
|
|
28
29
|
|
|
29
|
-
# rules
|
|
30
|
+
# rules/A2: "a name with no name content parses to the empty
|
|
30
31
|
# name — every field empty, false as a boolean — while ambiguities
|
|
31
32
|
# born from its punctuation survive on the empty result"
|
|
32
|
-
# (history: decisions
|
|
33
|
+
# (history: decisions/A2)
|
|
33
34
|
def assemble(state: ParseState) -> ParsedName:
|
|
34
35
|
dropped = set(state.dropped)
|
|
35
36
|
final: dict[int, Token] = {}
|
|
@@ -74,6 +75,7 @@ def assemble(state: ParseState) -> ParsedName:
|
|
|
74
75
|
if contentless:
|
|
75
76
|
final = {}
|
|
76
77
|
ambiguities = []
|
|
78
|
+
segment_of: dict[int, tuple[int, ...]] | None = None
|
|
77
79
|
for pending in state.ambiguities:
|
|
78
80
|
materialized = tuple(final[i] for i in pending.indices
|
|
79
81
|
if i in final)
|
|
@@ -85,7 +87,7 @@ def assemble(state: ParseState) -> ParsedName:
|
|
|
85
87
|
# referents did not lose a contest, they were discarded
|
|
86
88
|
# wholesale, and the report still describes the input.
|
|
87
89
|
continue
|
|
88
|
-
# rules
|
|
90
|
+
# rules/A1: "a report names the reading the parse took, so
|
|
89
91
|
# a report whose fork the rest of the parse then resolved to
|
|
90
92
|
# NEITHER branch is withdrawn rather than carried beside a
|
|
91
93
|
# reading it contradicts". classify's connective-or-initial
|
|
@@ -114,6 +116,23 @@ def assemble(state: ParseState) -> ParsedName:
|
|
|
114
116
|
and any(t.role in (Role.SUFFIX, Role.TITLE)
|
|
115
117
|
for t in materialized)):
|
|
116
118
|
continue
|
|
119
|
+
# rules/P3: "unless no name word of its part stands to its
|
|
120
|
+
# right" -- nothing after the letter in its segment, or only
|
|
121
|
+
# suffixes, titles, a maiden clause or a nickname: it connects
|
|
122
|
+
# nothing, so the report goes (#721). Asked here for the reason
|
|
123
|
+
# above: the roles are final. Where the letter is also a
|
|
124
|
+
# numeral the fork read, its own report (rules/S10) is the one
|
|
125
|
+
# left, so one token never carries both. Built only for a
|
|
126
|
+
# connective report, so other names pay nothing.
|
|
127
|
+
if pending.kind is AmbiguityKind.CONJUNCTION_OR_INITIAL:
|
|
128
|
+
if segment_of is None:
|
|
129
|
+
segment_of = {j: seg for seg in state.segments
|
|
130
|
+
for j in seg}
|
|
131
|
+
i = pending.indices[0]
|
|
132
|
+
if not any(j > i and j in final
|
|
133
|
+
and final[j].role in NAME_ROLES
|
|
134
|
+
for j in segment_of.get(i, ())):
|
|
135
|
+
continue
|
|
117
136
|
detail = pending.detail
|
|
118
137
|
if pending.field_tail is not None:
|
|
119
138
|
# A report naming a field, in a name emptied above (no
|