nameparser 2.0.0rc1__tar.gz → 2.0.0rc2__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.0rc1/nameparser.egg-info → nameparser-2.0.0rc2}/PKG-INFO +1 -1
- {nameparser-2.0.0rc1 → nameparser-2.0.0rc2}/nameparser/__init__.py +2 -0
- {nameparser-2.0.0rc1 → nameparser-2.0.0rc2}/nameparser/_config_shim.py +44 -2
- {nameparser-2.0.0rc1 → nameparser-2.0.0rc2}/nameparser/_facade.py +6 -1
- {nameparser-2.0.0rc1 → nameparser-2.0.0rc2}/nameparser/_lexicon.py +70 -7
- {nameparser-2.0.0rc1 → nameparser-2.0.0rc2}/nameparser/_parser.py +63 -1
- {nameparser-2.0.0rc1 → nameparser-2.0.0rc2}/nameparser/_policy.py +61 -13
- {nameparser-2.0.0rc1 → nameparser-2.0.0rc2}/nameparser/_types.py +91 -39
- {nameparser-2.0.0rc1 → nameparser-2.0.0rc2}/nameparser/_version.py +1 -1
- {nameparser-2.0.0rc1 → nameparser-2.0.0rc2}/nameparser/config/suffixes.py +0 -7
- {nameparser-2.0.0rc1 → nameparser-2.0.0rc2}/nameparser/config/titles.py +3 -1
- {nameparser-2.0.0rc1 → nameparser-2.0.0rc2/nameparser.egg-info}/PKG-INFO +1 -1
- {nameparser-2.0.0rc1 → nameparser-2.0.0rc2}/tests/test_titles.py +13 -0
- {nameparser-2.0.0rc1 → nameparser-2.0.0rc2}/tests/v2/test_config_shim.py +39 -0
- {nameparser-2.0.0rc1 → nameparser-2.0.0rc2}/tests/v2/test_facade.py +17 -0
- {nameparser-2.0.0rc1 → nameparser-2.0.0rc2}/tests/v2/test_lexicon.py +94 -1
- {nameparser-2.0.0rc1 → nameparser-2.0.0rc2}/tests/v2/test_locales.py +29 -1
- {nameparser-2.0.0rc1 → nameparser-2.0.0rc2}/tests/v2/test_parser.py +122 -1
- {nameparser-2.0.0rc1 → nameparser-2.0.0rc2}/tests/v2/test_policy.py +40 -0
- {nameparser-2.0.0rc1 → nameparser-2.0.0rc2}/tests/v2/test_properties.py +15 -5
- nameparser-2.0.0rc2/tests/v2/test_reprs.py +133 -0
- {nameparser-2.0.0rc1 → nameparser-2.0.0rc2}/tests/v2/test_types.py +66 -0
- nameparser-2.0.0rc1/tests/v2/test_reprs.py +0 -68
- {nameparser-2.0.0rc1 → nameparser-2.0.0rc2}/AUTHORS +0 -0
- {nameparser-2.0.0rc1 → nameparser-2.0.0rc2}/LICENSE +0 -0
- {nameparser-2.0.0rc1 → nameparser-2.0.0rc2}/MANIFEST.in +0 -0
- {nameparser-2.0.0rc1 → nameparser-2.0.0rc2}/README.rst +0 -0
- {nameparser-2.0.0rc1 → nameparser-2.0.0rc2}/nameparser/__main__.py +0 -0
- {nameparser-2.0.0rc1 → nameparser-2.0.0rc2}/nameparser/_locale.py +0 -0
- {nameparser-2.0.0rc1 → nameparser-2.0.0rc2}/nameparser/_pipeline/__init__.py +0 -0
- {nameparser-2.0.0rc1 → nameparser-2.0.0rc2}/nameparser/_pipeline/_assemble.py +0 -0
- {nameparser-2.0.0rc1 → nameparser-2.0.0rc2}/nameparser/_pipeline/_assign.py +0 -0
- {nameparser-2.0.0rc1 → nameparser-2.0.0rc2}/nameparser/_pipeline/_classify.py +0 -0
- {nameparser-2.0.0rc1 → nameparser-2.0.0rc2}/nameparser/_pipeline/_extract.py +0 -0
- {nameparser-2.0.0rc1 → nameparser-2.0.0rc2}/nameparser/_pipeline/_group.py +0 -0
- {nameparser-2.0.0rc1 → nameparser-2.0.0rc2}/nameparser/_pipeline/_post_rules.py +0 -0
- {nameparser-2.0.0rc1 → nameparser-2.0.0rc2}/nameparser/_pipeline/_segment.py +0 -0
- {nameparser-2.0.0rc1 → nameparser-2.0.0rc2}/nameparser/_pipeline/_state.py +0 -0
- {nameparser-2.0.0rc1 → nameparser-2.0.0rc2}/nameparser/_pipeline/_tokenize.py +0 -0
- {nameparser-2.0.0rc1 → nameparser-2.0.0rc2}/nameparser/_pipeline/_vocab.py +0 -0
- {nameparser-2.0.0rc1 → nameparser-2.0.0rc2}/nameparser/_render.py +0 -0
- {nameparser-2.0.0rc1 → nameparser-2.0.0rc2}/nameparser/config/__init__.py +0 -0
- {nameparser-2.0.0rc1 → nameparser-2.0.0rc2}/nameparser/config/_invariants.py +0 -0
- {nameparser-2.0.0rc1 → nameparser-2.0.0rc2}/nameparser/config/bound_first_names.py +0 -0
- {nameparser-2.0.0rc1 → nameparser-2.0.0rc2}/nameparser/config/capitalization.py +0 -0
- {nameparser-2.0.0rc1 → nameparser-2.0.0rc2}/nameparser/config/conjunctions.py +0 -0
- {nameparser-2.0.0rc1 → nameparser-2.0.0rc2}/nameparser/config/maiden_markers.py +0 -0
- {nameparser-2.0.0rc1 → nameparser-2.0.0rc2}/nameparser/config/prefixes.py +0 -0
- {nameparser-2.0.0rc1 → nameparser-2.0.0rc2}/nameparser/config/regexes.py +0 -0
- {nameparser-2.0.0rc1 → nameparser-2.0.0rc2}/nameparser/locales/__init__.py +0 -0
- {nameparser-2.0.0rc1 → nameparser-2.0.0rc2}/nameparser/locales/ru.py +0 -0
- {nameparser-2.0.0rc1 → nameparser-2.0.0rc2}/nameparser/locales/tr_az.py +0 -0
- {nameparser-2.0.0rc1 → nameparser-2.0.0rc2}/nameparser/parser.py +0 -0
- {nameparser-2.0.0rc1 → nameparser-2.0.0rc2}/nameparser/py.typed +0 -0
- {nameparser-2.0.0rc1 → nameparser-2.0.0rc2}/nameparser/util.py +0 -0
- {nameparser-2.0.0rc1 → nameparser-2.0.0rc2}/nameparser.egg-info/SOURCES.txt +0 -0
- {nameparser-2.0.0rc1 → nameparser-2.0.0rc2}/nameparser.egg-info/dependency_links.txt +0 -0
- {nameparser-2.0.0rc1 → nameparser-2.0.0rc2}/nameparser.egg-info/top_level.txt +0 -0
- {nameparser-2.0.0rc1 → nameparser-2.0.0rc2}/pyproject.toml +0 -0
- {nameparser-2.0.0rc1 → nameparser-2.0.0rc2}/setup.cfg +0 -0
- {nameparser-2.0.0rc1 → nameparser-2.0.0rc2}/tests/__init__.py +0 -0
- {nameparser-2.0.0rc1 → nameparser-2.0.0rc2}/tests/base.py +0 -0
- {nameparser-2.0.0rc1 → nameparser-2.0.0rc2}/tests/conftest.py +0 -0
- {nameparser-2.0.0rc1 → nameparser-2.0.0rc2}/tests/test_bound_first_names.py +0 -0
- {nameparser-2.0.0rc1 → nameparser-2.0.0rc2}/tests/test_brute_force.py +0 -0
- {nameparser-2.0.0rc1 → nameparser-2.0.0rc2}/tests/test_capitalization.py +0 -0
- {nameparser-2.0.0rc1 → nameparser-2.0.0rc2}/tests/test_comma_variants.py +0 -0
- {nameparser-2.0.0rc1 → nameparser-2.0.0rc2}/tests/test_conjunctions.py +0 -0
- {nameparser-2.0.0rc1 → nameparser-2.0.0rc2}/tests/test_constants.py +0 -0
- {nameparser-2.0.0rc1 → nameparser-2.0.0rc2}/tests/test_east_slavic_patronymic_order.py +0 -0
- {nameparser-2.0.0rc1 → nameparser-2.0.0rc2}/tests/test_first_name.py +0 -0
- {nameparser-2.0.0rc1 → nameparser-2.0.0rc2}/tests/test_initials.py +0 -0
- {nameparser-2.0.0rc1 → nameparser-2.0.0rc2}/tests/test_middle_name_as_last.py +0 -0
- {nameparser-2.0.0rc1 → nameparser-2.0.0rc2}/tests/test_nicknames.py +0 -0
- {nameparser-2.0.0rc1 → nameparser-2.0.0rc2}/tests/test_output_format.py +0 -0
- {nameparser-2.0.0rc1 → nameparser-2.0.0rc2}/tests/test_prefixes.py +0 -0
- {nameparser-2.0.0rc1 → nameparser-2.0.0rc2}/tests/test_python_api.py +0 -0
- {nameparser-2.0.0rc1 → nameparser-2.0.0rc2}/tests/test_suffixes.py +0 -0
- {nameparser-2.0.0rc1 → nameparser-2.0.0rc2}/tests/test_turkic_patronymic_order.py +0 -0
- {nameparser-2.0.0rc1 → nameparser-2.0.0rc2}/tests/test_variations.py +0 -0
- {nameparser-2.0.0rc1 → nameparser-2.0.0rc2}/tests/v2/__init__.py +0 -0
- {nameparser-2.0.0rc1 → nameparser-2.0.0rc2}/tests/v2/cases.py +0 -0
- {nameparser-2.0.0rc1 → nameparser-2.0.0rc2}/tests/v2/conftest.py +0 -0
- {nameparser-2.0.0rc1 → nameparser-2.0.0rc2}/tests/v2/pipeline/__init__.py +0 -0
- {nameparser-2.0.0rc1 → nameparser-2.0.0rc2}/tests/v2/pipeline/test_assemble.py +0 -0
- {nameparser-2.0.0rc1 → nameparser-2.0.0rc2}/tests/v2/pipeline/test_assign.py +0 -0
- {nameparser-2.0.0rc1 → nameparser-2.0.0rc2}/tests/v2/pipeline/test_classify.py +0 -0
- {nameparser-2.0.0rc1 → nameparser-2.0.0rc2}/tests/v2/pipeline/test_extract.py +0 -0
- {nameparser-2.0.0rc1 → nameparser-2.0.0rc2}/tests/v2/pipeline/test_group.py +0 -0
- {nameparser-2.0.0rc1 → nameparser-2.0.0rc2}/tests/v2/pipeline/test_post_rules.py +0 -0
- {nameparser-2.0.0rc1 → nameparser-2.0.0rc2}/tests/v2/pipeline/test_segment.py +0 -0
- {nameparser-2.0.0rc1 → nameparser-2.0.0rc2}/tests/v2/pipeline/test_state.py +0 -0
- {nameparser-2.0.0rc1 → nameparser-2.0.0rc2}/tests/v2/pipeline/test_tokenize.py +0 -0
- {nameparser-2.0.0rc1 → nameparser-2.0.0rc2}/tests/v2/pipeline/test_vocab.py +0 -0
- {nameparser-2.0.0rc1 → nameparser-2.0.0rc2}/tests/v2/test_benchmark.py +0 -0
- {nameparser-2.0.0rc1 → nameparser-2.0.0rc2}/tests/v2/test_cases.py +0 -0
- {nameparser-2.0.0rc1 → nameparser-2.0.0rc2}/tests/v2/test_cli.py +0 -0
- {nameparser-2.0.0rc1 → nameparser-2.0.0rc2}/tests/v2/test_contracts.py +0 -0
- {nameparser-2.0.0rc1 → nameparser-2.0.0rc2}/tests/v2/test_facade_cases.py +0 -0
- {nameparser-2.0.0rc1 → nameparser-2.0.0rc2}/tests/v2/test_layering.py +0 -0
- {nameparser-2.0.0rc1 → nameparser-2.0.0rc2}/tests/v2/test_locale.py +0 -0
- {nameparser-2.0.0rc1 → nameparser-2.0.0rc2}/tests/v2/test_regex_sync.py +0 -0
- {nameparser-2.0.0rc1 → nameparser-2.0.0rc2}/tests/v2/test_render.py +0 -0
|
@@ -20,6 +20,7 @@ from nameparser._policy import (
|
|
|
20
20
|
PolicyPatch,
|
|
21
21
|
)
|
|
22
22
|
from nameparser._types import (
|
|
23
|
+
STABLE_TAGS,
|
|
23
24
|
Ambiguity,
|
|
24
25
|
AmbiguityKind,
|
|
25
26
|
ParsedName,
|
|
@@ -33,6 +34,7 @@ __all__ = [
|
|
|
33
34
|
"HumanName",
|
|
34
35
|
# v2 core
|
|
35
36
|
"Span", "Role", "Token", "Ambiguity", "AmbiguityKind", "ParsedName",
|
|
37
|
+
"STABLE_TAGS",
|
|
36
38
|
"Lexicon", "Policy", "PolicyPatch", "PatronymicRule", "UNSET",
|
|
37
39
|
"GIVEN_FIRST", "FAMILY_FIRST", "FAMILY_FIRST_GIVEN_LAST",
|
|
38
40
|
"DEFAULT_NICKNAME_DELIMITERS", "Locale",
|
|
@@ -33,6 +33,28 @@ from nameparser._policy import PatronymicRule, Policy
|
|
|
33
33
|
from nameparser.util import lc
|
|
34
34
|
|
|
35
35
|
|
|
36
|
+
#: The eight multi-word entries the pre-2.0 DEFAULT vocabulary shipped.
|
|
37
|
+
#: Provably inert in every release (they can never match; see the
|
|
38
|
+
#: 2.0.0 release log), so dropping them from a restored legacy pickle
|
|
39
|
+
#: changes no parse -- and keeps the multi-word warning from firing
|
|
40
|
+
#: eight times, with wrong advice, at library-internal lines, on the
|
|
41
|
+
#: first parse after a supported 1.3/1.4 pickle upgrade.
|
|
42
|
+
#:
|
|
43
|
+
#: Gated on ALL EIGHT being present across the two fields -- the
|
|
44
|
+
#: signature of a pre-2.0 blob, which froze the complete shipped set.
|
|
45
|
+
#: A round-trip of 2.0-era state that carries FEWER than all eight
|
|
46
|
+
#: keeps them (a user who deliberately re-added one or two does not
|
|
47
|
+
#: match the signature, and .copy() never subtracts either). The trade:
|
|
48
|
+
#: a 2.0 user who re-adds ALL eight exact strings is indistinguishable
|
|
49
|
+
#: from a legacy blob and loses them on the next unpickle.
|
|
50
|
+
_LEGACY_DEAD_ENTRIES = {
|
|
51
|
+
"titles": frozenset({"chargé d'affaires"}),
|
|
52
|
+
"suffix_acronyms": frozenset({
|
|
53
|
+
"leed ap", "nicet i", "nicet ii", "nicet iii", "nicet iv",
|
|
54
|
+
"psm i", "psm ii"}),
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
|
|
36
58
|
def _reject_bare_str_or_bytes(value: object, expected: str) -> None:
|
|
37
59
|
# A bare string is an iterable of its characters, so e.g. SetManager('dr')
|
|
38
60
|
# would silently shred it into {'d', 'r'} instead of raising -- shared by
|
|
@@ -1103,10 +1125,30 @@ class Constants:
|
|
|
1103
1125
|
self.__init__() # type: ignore[misc] # defaults, then overlay
|
|
1104
1126
|
# (managers re-wrapped below so _on_change points at THIS
|
|
1105
1127
|
# instance, not whatever produced the incoming state)
|
|
1128
|
+
managers: dict[str, SetManager] = {}
|
|
1106
1129
|
for name in _SET_FIELDS:
|
|
1107
1130
|
if name in state:
|
|
1108
|
-
|
|
1109
|
-
state[name], _on_change=self._bump)
|
|
1131
|
+
managers[name] = SetManager(
|
|
1132
|
+
state[name], _on_change=self._bump) # type: ignore[arg-type]
|
|
1133
|
+
# SetManager normalized on construction, so the frozen 1.3/1.4
|
|
1134
|
+
# vocabulary's dead entries are matchable in their normalized
|
|
1135
|
+
# spelling here. Subtract only when ALL EIGHT are present --
|
|
1136
|
+
# the pre-2.0 signature; a 2.0 user who re-added one or two
|
|
1137
|
+
# keeps them through a round-trip (see _LEGACY_DEAD_ENTRIES).
|
|
1138
|
+
legacy = all(
|
|
1139
|
+
name in managers and entry in managers[name]
|
|
1140
|
+
for name, entries in _LEGACY_DEAD_ENTRIES.items()
|
|
1141
|
+
for entry in entries
|
|
1142
|
+
)
|
|
1143
|
+
for name, manager in managers.items():
|
|
1144
|
+
if legacy:
|
|
1145
|
+
# Reach past the public discard() deliberately: this is
|
|
1146
|
+
# part of restoring the state, not a mutation of it, and
|
|
1147
|
+
# must not bump the generation of an instance that is
|
|
1148
|
+
# still being built.
|
|
1149
|
+
manager._elements -= _LEGACY_DEAD_ENTRIES.get(
|
|
1150
|
+
name, frozenset())
|
|
1151
|
+
object.__setattr__(self, name, manager)
|
|
1110
1152
|
if "capitalization_exceptions" in state:
|
|
1111
1153
|
object.__setattr__(
|
|
1112
1154
|
self, "capitalization_exceptions", TupleManager(
|
|
@@ -335,6 +335,8 @@ class HumanName:
|
|
|
335
335
|
else:
|
|
336
336
|
raise TypeError(
|
|
337
337
|
f"{member} must be a str, list, or None, got {value!r}")
|
|
338
|
+
# v1 setters stay on replace(): revise()'s vocabulary tags would
|
|
339
|
+
# change v1 parity
|
|
338
340
|
self._parsed = self._parsed.replace(
|
|
339
341
|
**{_V2_FIELD.get(member, member): joined})
|
|
340
342
|
|
|
@@ -616,7 +618,10 @@ class HumanName:
|
|
|
616
618
|
"slicing a HumanName was removed in 2.0 (#258); access "
|
|
617
619
|
"the named attributes instead"
|
|
618
620
|
)
|
|
619
|
-
|
|
621
|
+
# Role is a StrEnum, so Role members (and the plain 'given'/
|
|
622
|
+
# 'family' strings) reach here too -- translate to the v1
|
|
623
|
+
# spelling the facade actually exposes as attributes.
|
|
624
|
+
return getattr(self, _V1_SPELLING.get(key, key))
|
|
620
625
|
|
|
621
626
|
def as_dict(self, include_empty: bool = True) -> dict[str, str]:
|
|
622
627
|
"""The seven v1-named components as a dict; include_empty=False
|
|
@@ -9,9 +9,11 @@ from __future__ import annotations
|
|
|
9
9
|
|
|
10
10
|
import dataclasses
|
|
11
11
|
import functools
|
|
12
|
+
import sys
|
|
13
|
+
import warnings
|
|
12
14
|
from collections.abc import Iterable, Mapping
|
|
13
15
|
from dataclasses import dataclass, field
|
|
14
|
-
from types import MappingProxyType
|
|
16
|
+
from types import FrameType, MappingProxyType
|
|
15
17
|
from typing import cast
|
|
16
18
|
|
|
17
19
|
#: Vocabulary set fields, in declaration order. add()/remove() operate
|
|
@@ -112,7 +114,27 @@ def _reject_buffer(value: object, label: str, plural: str) -> None:
|
|
|
112
114
|
)
|
|
113
115
|
|
|
114
116
|
|
|
115
|
-
def
|
|
117
|
+
def _warn_dead_entry(message: str) -> None:
|
|
118
|
+
# A fixed stacklevel always lands on library internals: the call
|
|
119
|
+
# depth differs per entry point (Lexicon(), add(), unpickle,
|
|
120
|
+
# dataclasses.replace, and the v1 shim's lazy snapshot -- built on
|
|
121
|
+
# the first parse after a Constants mutation, several facade frames
|
|
122
|
+
# below the user's own add()). Walk out of this module (and
|
|
123
|
+
# dataclasses' replace frames, and the facade layer that builds
|
|
124
|
+
# lexicons on the caller's behalf) so the warning points at the
|
|
125
|
+
# caller's own line.
|
|
126
|
+
level = 2
|
|
127
|
+
frame: FrameType | None = sys._getframe(1)
|
|
128
|
+
while frame is not None and frame.f_globals.get("__name__") in (
|
|
129
|
+
__name__, "dataclasses",
|
|
130
|
+
"nameparser._config_shim", "nameparser._facade"):
|
|
131
|
+
frame, level = frame.f_back, level + 1
|
|
132
|
+
warnings.warn(message, UserWarning, stacklevel=level)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _normset(
|
|
136
|
+
entries: Iterable[str], field_name: str, warn: bool = True,
|
|
137
|
+
) -> frozenset[str]:
|
|
116
138
|
# Reject a bare str before iterating: iterating "dr" would silently
|
|
117
139
|
# yield the single characters {'d', 'r'} -- the set(str) footgun on
|
|
118
140
|
# the primary customization surface.
|
|
@@ -159,6 +181,25 @@ def _normset(entries: Iterable[str], field_name: str) -> frozenset[str]:
|
|
|
159
181
|
f"Lexicon.{field_name} entry {w!r} normalizes to empty "
|
|
160
182
|
f"(lowercase + strip periods/whitespace leaves nothing)"
|
|
161
183
|
)
|
|
184
|
+
# Every field but given_name_titles is matched one word at a
|
|
185
|
+
# time, so a multi-word entry can never match -- the library
|
|
186
|
+
# itself shipped eight such dead entries for years (repaired
|
|
187
|
+
# 2026-07-26). Warn, never raise: an inert entry produces
|
|
188
|
+
# nothing, and the given_name_titles precedent says a raise
|
|
189
|
+
# here costs working configurations (see __post_init__).
|
|
190
|
+
# warn=False is _edit()'s pass (both ops): add() warns via the
|
|
191
|
+
# new instance's __post_init__; remove() stores nothing, so
|
|
192
|
+
# warning there would name entries the caller is trying to get
|
|
193
|
+
# RID of, with "split it" advice that makes no sense for a
|
|
194
|
+
# no-op.
|
|
195
|
+
if (warn and field_name != "given_name_titles"
|
|
196
|
+
# interior whitespace test; split() covers all Unicode
|
|
197
|
+
# whitespace
|
|
198
|
+
and n != "".join(n.split())):
|
|
199
|
+
_warn_dead_entry(
|
|
200
|
+
f"Lexicon.{field_name} entries are matched one word at "
|
|
201
|
+
f"a time; multi-word entry {w!r} can never match. "
|
|
202
|
+
f"Split it into separate entries")
|
|
162
203
|
normalized.add(n)
|
|
163
204
|
return frozenset(normalized)
|
|
164
205
|
|
|
@@ -214,6 +255,14 @@ def _normpairs(
|
|
|
214
255
|
f"empty (lowercase + strip periods/whitespace leaves "
|
|
215
256
|
f"nothing)"
|
|
216
257
|
)
|
|
258
|
+
# capitalized() looks words up one at a time (the _WORD regex
|
|
259
|
+
# never yields spaces), so a multi-word key is unreachable.
|
|
260
|
+
# interior whitespace test; split() covers all Unicode whitespace
|
|
261
|
+
if normalized_key != "".join(normalized_key.split()):
|
|
262
|
+
_warn_dead_entry(
|
|
263
|
+
f"capitalization_exceptions keys are matched one word "
|
|
264
|
+
f"at a time; multi-word key {k!r} can never match. "
|
|
265
|
+
f"Split it into per-word entries")
|
|
217
266
|
deduped[normalized_key] = v
|
|
218
267
|
return tuple(sorted(deduped.items()))
|
|
219
268
|
|
|
@@ -226,9 +275,12 @@ class Lexicon:
|
|
|
226
275
|
:meth:`empty`, derive variants with :meth:`add` / :meth:`remove` /
|
|
227
276
|
``|`` (union), and pass the result to ``Parser(lexicon=...)``.
|
|
228
277
|
Entries are normalized at construction -- lowercased, edge periods
|
|
229
|
-
stripped -- so matching is case-insensitive.
|
|
230
|
-
|
|
231
|
-
|
|
278
|
+
stripped -- so matching is case-insensitive. Vocabulary entries are
|
|
279
|
+
single words -- a multi-word entry warns at construction and can
|
|
280
|
+
never match (``given_name_titles``, matched as a space-joined run,
|
|
281
|
+
is the one exception). Field docs below show examples, not full
|
|
282
|
+
contents; inspect any field's shipped vocabulary directly, e.g.
|
|
283
|
+
``Lexicon.default().conjunctions``."""
|
|
232
284
|
|
|
233
285
|
#: Pre-nominal titles ("dr", "sir", "capt", ...). Full default
|
|
234
286
|
#: list: :data:`~nameparser.config.titles.TITLES`.
|
|
@@ -256,7 +308,8 @@ class Lexicon:
|
|
|
256
308
|
particles: frozenset[str] = frozenset()
|
|
257
309
|
#: Subset of particles that can also BE a given name: a leading
|
|
258
310
|
#: one reads as given and records a particle-or-given ambiguity
|
|
259
|
-
#: ("Van Johnson"). No constant of its own
|
|
311
|
+
#: ("Van Johnson", but also "Van Buren"). No constant of its own
|
|
312
|
+
#: -- the default derives
|
|
260
313
|
#: as particles minus
|
|
261
314
|
#: :data:`~nameparser.config.prefixes.NON_FIRST_NAME_PREFIXES`
|
|
262
315
|
#: (which marks the opposite, never-given subset).
|
|
@@ -479,7 +532,17 @@ class Lexicon:
|
|
|
479
532
|
f"{', '.join(_VOCAB_FIELDS)}"
|
|
480
533
|
)
|
|
481
534
|
current: frozenset[str] = getattr(self, name)
|
|
482
|
-
|
|
535
|
+
# warn=False: this pass only computes the new set membership,
|
|
536
|
+
# never stores it directly. add() still warns exactly once,
|
|
537
|
+
# from the replaced instance's own __post_init__ -> _normset;
|
|
538
|
+
# remove() never reaches __post_init__ with the dead entry
|
|
539
|
+
# (it is subtracted out here), so it stays silent -- correct,
|
|
540
|
+
# since a removal stores nothing a warning could be about.
|
|
541
|
+
# That silence covers the entry BEING REMOVED only: a
|
|
542
|
+
# different multi-word entry still stored re-warns from the
|
|
543
|
+
# derived instance's __post_init__, since the warning is
|
|
544
|
+
# per-construction by design.
|
|
545
|
+
normalized = _normset(words, name, warn=False)
|
|
483
546
|
updates[name] = (current | normalized if op == "add"
|
|
484
547
|
else current - normalized)
|
|
485
548
|
# mypy's dataclasses.replace() typing checks a **dict's single
|
|
@@ -19,7 +19,10 @@ from nameparser._pipeline import run
|
|
|
19
19
|
from nameparser._pipeline._assemble import assemble
|
|
20
20
|
from nameparser._pipeline._state import ParseState
|
|
21
21
|
from nameparser._policy import UNSET, Policy, PolicyPatch, apply_patch
|
|
22
|
-
from nameparser._types import
|
|
22
|
+
from nameparser._types import (
|
|
23
|
+
FOLDED_TAG, ParsedName, Token, _guarded_getstate, _guarded_setstate,
|
|
24
|
+
_validated_field_strings,
|
|
25
|
+
)
|
|
23
26
|
|
|
24
27
|
|
|
25
28
|
@dataclass(frozen=True, slots=True)
|
|
@@ -75,6 +78,65 @@ class Parser:
|
|
|
75
78
|
policy=self.policy)
|
|
76
79
|
return assemble(run(state))
|
|
77
80
|
|
|
81
|
+
# -- editing ----------------------------------------------------------
|
|
82
|
+
|
|
83
|
+
def revise(self, name: ParsedName, **fields: str) -> ParsedName:
|
|
84
|
+
""":meth:`ParsedName.replace` with this parser's vocabulary:
|
|
85
|
+
each value is tokenized and classified by a full sub-parse, so
|
|
86
|
+
the stable tags survive and the tag-driven views
|
|
87
|
+
(family_particles, initials(), the suffix join) behave as if
|
|
88
|
+
the text had been parsed. The value is classified ON ITS OWN,
|
|
89
|
+
though -- a word whose reading depends on surrounding context
|
|
90
|
+
may classify differently than it would in place (a standalone
|
|
91
|
+
"B. S." reads as initials, not a suffix run). The sub-parse's
|
|
92
|
+
role choices and ambiguities are discarded -- every harvested
|
|
93
|
+
token takes the named field's role -- and its structural
|
|
94
|
+
behavior applies: delimiter characters do not become tokens,
|
|
95
|
+
and a mid-value maiden marker is consumed as in parsing.
|
|
96
|
+
Tokens are synthetic (span=None); original is unchanged; a
|
|
97
|
+
value with no name content (empty, whitespace, or punctuation
|
|
98
|
+
only) clears the field; ambiguities referencing replaced
|
|
99
|
+
tokens are dropped."""
|
|
100
|
+
if not isinstance(name, ParsedName):
|
|
101
|
+
raise TypeError(f"revise() takes a ParsedName, got {name!r}")
|
|
102
|
+
replaced = _validated_field_strings(fields)
|
|
103
|
+
harvested = {
|
|
104
|
+
role: tuple(
|
|
105
|
+
Token(t.text, None, role, t.tags - {FOLDED_TAG})
|
|
106
|
+
for t in self.parse(value).tokens)
|
|
107
|
+
for role, value in replaced.items()
|
|
108
|
+
}
|
|
109
|
+
return name._with_field_tokens(harvested)
|
|
110
|
+
|
|
111
|
+
# -- comparison -------------------------------------------------------
|
|
112
|
+
|
|
113
|
+
def matches(self, a: str | ParsedName, b: str | ParsedName) -> bool:
|
|
114
|
+
"""Component-wise case-insensitive comparison of two names,
|
|
115
|
+
parsing str arguments with THIS parser.
|
|
116
|
+
:meth:`ParsedName.matches` parses its str argument with the
|
|
117
|
+
DEFAULT parser instead -- for names parsed with a custom
|
|
118
|
+
Parser, use this method."""
|
|
119
|
+
if isinstance(a, str):
|
|
120
|
+
a = self.parse(a)
|
|
121
|
+
elif not isinstance(a, ParsedName):
|
|
122
|
+
raise TypeError(f"matches() takes str or ParsedName, got {a!r}")
|
|
123
|
+
if isinstance(b, str):
|
|
124
|
+
b = self.parse(b)
|
|
125
|
+
elif not isinstance(b, ParsedName):
|
|
126
|
+
raise TypeError(f"matches() takes str or ParsedName, got {b!r}")
|
|
127
|
+
return a.comparison_key() == b.comparison_key()
|
|
128
|
+
|
|
129
|
+
# -- rendering delegates ----------------------------------------------
|
|
130
|
+
|
|
131
|
+
def capitalized(self, name: ParsedName, *,
|
|
132
|
+
force: bool = False) -> ParsedName:
|
|
133
|
+
""":meth:`ParsedName.capitalized` under THIS parser's lexicon.
|
|
134
|
+
The no-argument form of that method uses the DEFAULT lexicon --
|
|
135
|
+
for names parsed with a custom Parser, use this method."""
|
|
136
|
+
if not isinstance(name, ParsedName):
|
|
137
|
+
raise TypeError(f"capitalized() takes a ParsedName, got {name!r}")
|
|
138
|
+
return name.capitalized(self.lexicon, force=force)
|
|
139
|
+
|
|
78
140
|
|
|
79
141
|
@functools.cache
|
|
80
142
|
def _default_parser() -> Parser:
|
|
@@ -45,7 +45,32 @@ FAMILY_FIRST = (Role.FAMILY, Role.GIVEN, Role.MIDDLE)
|
|
|
45
45
|
#: ``Policy(name_order=...)`` values.
|
|
46
46
|
FAMILY_FIRST_GIVEN_LAST = (Role.FAMILY, Role.MIDDLE, Role.GIVEN)
|
|
47
47
|
|
|
48
|
-
|
|
48
|
+
_ORDER_CONSTANT_NAMES: dict[tuple[Role, ...], str] = {
|
|
49
|
+
GIVEN_FIRST: "GIVEN_FIRST",
|
|
50
|
+
FAMILY_FIRST: "FAMILY_FIRST",
|
|
51
|
+
FAMILY_FIRST_GIVEN_LAST: "FAMILY_FIRST_GIVEN_LAST",
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _order_repr(value: tuple[Role, ...]) -> str:
|
|
56
|
+
# Unreachable via Policy's constructor (its __post_init__ restricts
|
|
57
|
+
# name_order to the three named orders) but REACHABLE via
|
|
58
|
+
# PolicyPatch, which defers name_order validation to apply time by
|
|
59
|
+
# design -- value may hold non-Role, even unhashable, elements. A
|
|
60
|
+
# value smuggled in through __setstate__ (which validates layout,
|
|
61
|
+
# not values) can also be a non-tuple container or not iterable at
|
|
62
|
+
# all. repr must never raise, so the named-lookup path is taken
|
|
63
|
+
# only for a TUPLE whose every element is confirmed a Role;
|
|
64
|
+
# everything else renders via repr(value). (The annotation states
|
|
65
|
+
# the Policy-side truth; the PolicyPatch call site passes
|
|
66
|
+
# getattr-Any.)
|
|
67
|
+
if isinstance(value, tuple) and all(isinstance(r, Role) for r in value):
|
|
68
|
+
named = _ORDER_CONSTANT_NAMES.get(value)
|
|
69
|
+
if named is not None:
|
|
70
|
+
return named
|
|
71
|
+
return "(" + ", ".join(r.name for r in value) + ")"
|
|
72
|
+
return repr(value)
|
|
73
|
+
|
|
49
74
|
|
|
50
75
|
# Single source for the migration hint raised by both Policy and
|
|
51
76
|
# PolicyPatch when patronymic_rules gets a non-iterable (True is the
|
|
@@ -225,6 +250,10 @@ class Policy:
|
|
|
225
250
|
def __post_init__(self) -> None:
|
|
226
251
|
_reject_bare_string_order(self.name_order)
|
|
227
252
|
order = tuple(_require_iterable(self.name_order, "name_order"))
|
|
253
|
+
# Sole rejection point for plain-string tuples: Role is a StrEnum,
|
|
254
|
+
# so the named-order membership check below compares EQUAL for
|
|
255
|
+
# ("given", "middle", "family") -- do not remove this loop as
|
|
256
|
+
# redundant.
|
|
228
257
|
for element in order:
|
|
229
258
|
if not isinstance(element, Role):
|
|
230
259
|
raise TypeError(
|
|
@@ -326,28 +355,32 @@ class Policy:
|
|
|
326
355
|
def __repr__(self) -> str:
|
|
327
356
|
# Bounded: only fields that deviate from the default are shown
|
|
328
357
|
# (design rule, see nameparser._types module docstring).
|
|
329
|
-
constant_names = {
|
|
330
|
-
GIVEN_FIRST: "GIVEN_FIRST",
|
|
331
|
-
FAMILY_FIRST: "FAMILY_FIRST",
|
|
332
|
-
FAMILY_FIRST_GIVEN_LAST: "FAMILY_FIRST_GIVEN_LAST",
|
|
333
|
-
}
|
|
334
358
|
parts = []
|
|
335
359
|
for f in dataclasses.fields(self):
|
|
336
360
|
value = getattr(self, f.name)
|
|
337
361
|
if value == f.default:
|
|
338
362
|
continue
|
|
339
363
|
if f.name == "name_order":
|
|
340
|
-
|
|
341
|
-
# the fallback is unreachable via the constructor; kept
|
|
342
|
-
# because repr must never raise (e.g. a smuggled
|
|
343
|
-
# __setstate__ value -- layout is validated, values not).
|
|
344
|
-
order_repr = constant_names.get(
|
|
345
|
-
value, "(" + ", ".join(r.name for r in value) + ")")
|
|
346
|
-
parts.append(f"name_order={order_repr}")
|
|
364
|
+
parts.append(f"name_order={_order_repr(value)}")
|
|
347
365
|
else:
|
|
348
366
|
parts.append(f"{f.name}={value!r}")
|
|
349
367
|
return f"Policy({', '.join(parts)})"
|
|
350
368
|
|
|
369
|
+
# -- editing ------------------------------------------------------
|
|
370
|
+
|
|
371
|
+
def patched(self, patch: PolicyPatch) -> Policy:
|
|
372
|
+
"""Fold a :class:`PolicyPatch` onto this Policy and return the
|
|
373
|
+
combined Policy. Set-valued fields union with the patch's;
|
|
374
|
+
scalar fields are overridden by the patch; UNSET fields are
|
|
375
|
+
left alone. Patch VALUES are validated here (Policy's
|
|
376
|
+
constructor re-runs on the result), not at patch construction
|
|
377
|
+
-- see PolicyPatch. The maiden-wins canonicalization applies
|
|
378
|
+
to the combined result exactly as if it had been constructed
|
|
379
|
+
directly."""
|
|
380
|
+
if not isinstance(patch, PolicyPatch):
|
|
381
|
+
raise TypeError(f"patched() takes a PolicyPatch, got {patch!r}")
|
|
382
|
+
return apply_patch(self, patch)
|
|
383
|
+
|
|
351
384
|
|
|
352
385
|
class _Unset(Enum):
|
|
353
386
|
UNSET = auto()
|
|
@@ -432,6 +465,21 @@ class PolicyPatch:
|
|
|
432
465
|
# class docstring ("Values are validated when the patch is
|
|
433
466
|
# applied... not at patch construction").
|
|
434
467
|
|
|
468
|
+
def __repr__(self) -> str:
|
|
469
|
+
# Bounded: only fields the patch actually sets are shown; UNSET
|
|
470
|
+
# fields are omitted (design rule, see nameparser._types module
|
|
471
|
+
# docstring -- the sibling of Policy's deviation-only repr).
|
|
472
|
+
parts = []
|
|
473
|
+
for f in dataclasses.fields(self):
|
|
474
|
+
value = getattr(self, f.name)
|
|
475
|
+
if value is UNSET:
|
|
476
|
+
continue
|
|
477
|
+
if f.name == "name_order":
|
|
478
|
+
parts.append(f"name_order={_order_repr(value)}")
|
|
479
|
+
else:
|
|
480
|
+
parts.append(f"{f.name}={value!r}")
|
|
481
|
+
return f"PolicyPatch({', '.join(parts)})"
|
|
482
|
+
|
|
435
483
|
|
|
436
484
|
def apply_patch(policy: Policy, patch: PolicyPatch) -> Policy:
|
|
437
485
|
"""Fold a PolicyPatch onto a Policy. Policy.__post_init__ re-runs via
|
|
@@ -24,12 +24,15 @@ if TYPE_CHECKING:
|
|
|
24
24
|
from nameparser._parser import Parser
|
|
25
25
|
|
|
26
26
|
|
|
27
|
-
class Role(
|
|
27
|
+
class Role(StrEnum):
|
|
28
28
|
"""The seven fields of a parsed name, one per :class:`Token`.
|
|
29
29
|
Declaration order is the canonical field order everywhere
|
|
30
|
-
(``as_dict()``, ``comparison_key()``, rendering).
|
|
31
|
-
:
|
|
32
|
-
|
|
30
|
+
(``as_dict()``, ``comparison_key()``, rendering). A StrEnum, like
|
|
31
|
+
:class:`AmbiguityKind`: members ARE their string values, so
|
|
32
|
+
``token.role == "given"`` compares directly and
|
|
33
|
+
``str(Role.GIVEN) == "given"``. Members order as strings, so
|
|
34
|
+
``sorted()`` yields alphabetical order -- iterate ``Role`` itself
|
|
35
|
+
for the canonical order."""
|
|
33
36
|
|
|
34
37
|
# Declaration order IS the canonical field order (conventions §3):
|
|
35
38
|
# every listing of the seven fields anywhere derives from this.
|
|
@@ -74,10 +77,16 @@ class Span(NamedTuple):
|
|
|
74
77
|
)
|
|
75
78
|
|
|
76
79
|
|
|
77
|
-
#:
|
|
78
|
-
#:
|
|
79
|
-
#:
|
|
80
|
-
#:
|
|
80
|
+
#: The four :attr:`Token.tags` values that are stable API.
|
|
81
|
+
#: "particle" marks a word from the particle vocabulary ("de", "van")
|
|
82
|
+
#: wherever it lands -- including a given-name "Van" -- so combine it
|
|
83
|
+
#: with Role.FAMILY (as family_particles does) to get actual family
|
|
84
|
+
#: particles; "conjunction" a joining word ("and", "y"); "initial" an
|
|
85
|
+
#: initial-shaped word ("J.", "Q");
|
|
86
|
+
#: "joined" a continuation of the previous token within one merged
|
|
87
|
+
#: piece ("Ph." + "D."), which the suffix view joins with a space
|
|
88
|
+
#: instead of ", ". Every other tag is namespaced ("vocab:...") and is
|
|
89
|
+
#: unstable debugging provenance -- never match against those.
|
|
81
90
|
STABLE_TAGS = frozenset({"particle", "conjunction", "initial", "joined"})
|
|
82
91
|
|
|
83
92
|
#: The one sanctioned view-reorder marker (namespaced = unstable API).
|
|
@@ -154,10 +163,10 @@ class Token:
|
|
|
154
163
|
span: Span | None
|
|
155
164
|
#: The field this token belongs to.
|
|
156
165
|
role: Role
|
|
157
|
-
#: Classification labels. Exactly the four
|
|
158
|
-
#: ("particle", "conjunction",
|
|
159
|
-
#: namespaced tags like "vocab:..."
|
|
160
|
-
#: provenance -- never match against them.
|
|
166
|
+
#: Classification labels. Exactly the four members of
|
|
167
|
+
#: :data:`~nameparser.STABLE_TAGS` ("particle", "conjunction",
|
|
168
|
+
#: "initial", "joined") are API; namespaced tags like "vocab:..."
|
|
169
|
+
#: are unstable debugging provenance -- never match against them.
|
|
161
170
|
tags: frozenset[str] = frozenset()
|
|
162
171
|
|
|
163
172
|
# in the class body so @dataclass(slots=True) keeps them
|
|
@@ -253,9 +262,10 @@ class AmbiguityKind(StrEnum):
|
|
|
253
262
|
#: Smith B"). Which name part was declined depends on position and
|
|
254
263
|
#: ``name_order``, so ``detail`` names it rather than the kind.
|
|
255
264
|
SUFFIX_OR_NAME = "suffix-or-name"
|
|
256
|
-
#: A leading ambiguous particle was read as a given name --
|
|
257
|
-
#:
|
|
258
|
-
#:
|
|
265
|
+
#: A leading ambiguous particle was read as a given name -- the
|
|
266
|
+
#: right call for "Van Johnson" (the actor's given name), the
|
|
267
|
+
#: wrong one for a bare "Van Buren" (the presidential surname);
|
|
268
|
+
#: the two-word shape cannot distinguish them.
|
|
259
269
|
PARTICLE_OR_GIVEN = "particle-or-given"
|
|
260
270
|
#: A nickname/maiden delimiter opened without closing (or closed
|
|
261
271
|
#: without opening); the text was kept as literal name content, so
|
|
@@ -317,12 +327,31 @@ class Ambiguity:
|
|
|
317
327
|
return f"Ambiguity({self.kind.value!r}: {texts})"
|
|
318
328
|
|
|
319
329
|
|
|
330
|
+
def _validated_field_strings(fields: dict[str, str]) -> dict[Role, str]:
|
|
331
|
+
"""Shared by ParsedName.replace and Parser.revise: validate a
|
|
332
|
+
**fields mapping of role names to replacement strings and key it
|
|
333
|
+
by Role. TypeErrors match replace()'s historical wording."""
|
|
334
|
+
by_value = {role.value: role for role in Role}
|
|
335
|
+
for key, value in fields.items():
|
|
336
|
+
if key not in by_value:
|
|
337
|
+
raise TypeError(
|
|
338
|
+
f"unknown field {key!r}; expected one of "
|
|
339
|
+
f"{', '.join(by_value)}"
|
|
340
|
+
)
|
|
341
|
+
if not isinstance(value, str):
|
|
342
|
+
raise TypeError(
|
|
343
|
+
f"field {key!r} must be a str, got {value!r}"
|
|
344
|
+
)
|
|
345
|
+
return {by_value[k]: v for k, v in fields.items()}
|
|
346
|
+
|
|
347
|
+
|
|
320
348
|
@dataclass(frozen=True, slots=True)
|
|
321
349
|
class ParsedName:
|
|
322
350
|
"""The immutable result of parsing one name string. Read the seven
|
|
323
351
|
fields as strings (``.given``, ``.family``, ...); inspect structure
|
|
324
352
|
through :attr:`tokens` / :meth:`tokens_for`; correct a parse with
|
|
325
|
-
:meth:`replace` (returns a new value
|
|
353
|
+
:meth:`replace` (returns a new value; Parser.revise is the
|
|
354
|
+
tag-preserving form); produce output with
|
|
326
355
|
:meth:`render`, :meth:`initials`, :meth:`capitalized`, or ``str()``.
|
|
327
356
|
|
|
328
357
|
Constructor-enforced invariants: spans ascending, non-overlapping,
|
|
@@ -492,10 +521,14 @@ class ParsedName:
|
|
|
492
521
|
|
|
493
522
|
# -- structured access ----------------------------------------------
|
|
494
523
|
|
|
495
|
-
def tokens_for(self, role: Role) -> tuple[Token, ...]:
|
|
524
|
+
def tokens_for(self, role: Role | str) -> tuple[Token, ...]:
|
|
525
|
+
"""The tokens of one field, in document order. Takes a Role
|
|
526
|
+
member or its string value; anything else raises ValueError
|
|
527
|
+
naming the valid roles."""
|
|
528
|
+
role = _coerce_enum(role, Role, "Role", "roles")
|
|
496
529
|
return tuple(t for t in self.tokens if t.role is role)
|
|
497
530
|
|
|
498
|
-
def as_dict(self, include_empty: bool = True) -> dict[str, str]:
|
|
531
|
+
def as_dict(self, *, include_empty: bool = True) -> dict[str, str]:
|
|
499
532
|
# _text_for handles the suffix ", "-join (single-role SUFFIX call)
|
|
500
533
|
d = {role.value: self._text_for(role) for role in Role}
|
|
501
534
|
if not include_empty:
|
|
@@ -509,35 +542,49 @@ class ParsedName:
|
|
|
509
542
|
synthetic tokens (span=None). Whitespace-splits each value; an
|
|
510
543
|
empty value clears the field. original is unchanged (provenance).
|
|
511
544
|
Ambiguities referencing replaced tokens are dropped.
|
|
512
|
-
"""
|
|
513
|
-
by_value = {role.value: role for role in Role}
|
|
514
|
-
for key, value in fields.items():
|
|
515
|
-
if key not in by_value:
|
|
516
|
-
raise TypeError(
|
|
517
|
-
f"unknown field {key!r}; expected one of "
|
|
518
|
-
f"{', '.join(by_value)}"
|
|
519
|
-
)
|
|
520
|
-
if not isinstance(value, str):
|
|
521
|
-
raise TypeError(
|
|
522
|
-
f"field {key!r} must be a str, got {value!r}"
|
|
523
|
-
)
|
|
524
|
-
|
|
525
|
-
def synthetic(value: str, role: Role) -> list[Token]:
|
|
526
|
-
return [Token(word, None, role) for word in value.split()]
|
|
527
545
|
|
|
528
|
-
|
|
546
|
+
Replacement tokens carry NO tags, so tag-driven views degrade:
|
|
547
|
+
family_particles empties, particles regain their initials, and
|
|
548
|
+
a multi-word suffix is comma-joined. Parser.revise() is the
|
|
549
|
+
tag-preserving alternative.
|
|
550
|
+
"""
|
|
551
|
+
replaced = _validated_field_strings(fields)
|
|
552
|
+
synthetic = {
|
|
553
|
+
role: tuple(Token(word, None, role) for word in value.split())
|
|
554
|
+
for role, value in replaced.items()
|
|
555
|
+
}
|
|
556
|
+
return self._with_field_tokens(synthetic)
|
|
557
|
+
|
|
558
|
+
def _with_field_tokens(
|
|
559
|
+
self, replaced: Mapping[Role, tuple[Token, ...]],
|
|
560
|
+
) -> ParsedName:
|
|
561
|
+
"""Shared tail of replace()/Parser.revise(): splice each role's
|
|
562
|
+
replacement tokens in at the role's first position (appended in
|
|
563
|
+
canonical order when the role had no tokens); drop ambiguities
|
|
564
|
+
whose referents were replaced."""
|
|
565
|
+
# Private contract, made self-enforcing: a token filed under a
|
|
566
|
+
# key that is not its own role is the one way this shared tail
|
|
567
|
+
# could build a semantically wrong ParsedName (the splice keys
|
|
568
|
+
# on the mapping, the views key on the token).
|
|
569
|
+
for role, toks in replaced.items():
|
|
570
|
+
for tok in toks:
|
|
571
|
+
if tok.role is not role:
|
|
572
|
+
raise ValueError(
|
|
573
|
+
f"replacement token {tok.text!r} has role "
|
|
574
|
+
f"{tok.role.value}, not {role.value}"
|
|
575
|
+
)
|
|
529
576
|
new_tokens: list[Token] = []
|
|
530
577
|
emitted: set[Role] = set()
|
|
531
578
|
for tok in self.tokens:
|
|
532
579
|
if tok.role in replaced:
|
|
533
580
|
if tok.role not in emitted:
|
|
534
|
-
new_tokens.extend(
|
|
581
|
+
new_tokens.extend(replaced[tok.role])
|
|
535
582
|
emitted.add(tok.role)
|
|
536
583
|
continue
|
|
537
584
|
new_tokens.append(tok)
|
|
538
585
|
for role in Role:
|
|
539
586
|
if role in replaced and role not in emitted:
|
|
540
|
-
new_tokens.extend(
|
|
587
|
+
new_tokens.extend(replaced[role])
|
|
541
588
|
kept = tuple(
|
|
542
589
|
amb for amb in self.ambiguities
|
|
543
590
|
if all(t in new_tokens for t in amb.tokens)
|
|
@@ -557,7 +604,10 @@ class ParsedName:
|
|
|
557
604
|
parser: Parser | None = None) -> bool:
|
|
558
605
|
"""Component-wise case-insensitive comparison (the semantic
|
|
559
606
|
layer; __eq__ stays strict). A str argument is parsed with
|
|
560
|
-
`parser`, or the
|
|
607
|
+
`parser`, or with the DEFAULT parser when None -- if this name
|
|
608
|
+
came from a custom Parser, pass that parser (or use
|
|
609
|
+
Parser.matches); otherwise the comparison silently runs under
|
|
610
|
+
the wrong configuration."""
|
|
561
611
|
if isinstance(other, str):
|
|
562
612
|
import nameparser._parser as _parser
|
|
563
613
|
active = parser if parser is not None else _parser._default_parser()
|
|
@@ -600,7 +650,9 @@ class ParsedName:
|
|
|
600
650
|
force: bool = False) -> ParsedName:
|
|
601
651
|
"""Case-fixing transform -> new ParsedName, same spans, new
|
|
602
652
|
token texts. Needs a lexicon for capitalization_exceptions and
|
|
603
|
-
particle rules; None uses the
|
|
604
|
-
|
|
653
|
+
particle rules; None uses the DEFAULT lexicon -- if this name
|
|
654
|
+
came from a custom Parser, pass its lexicon or use
|
|
655
|
+
Parser.capitalized. force=False preserves mixed-case input
|
|
656
|
+
(v1 parity). Idempotent."""
|
|
605
657
|
import nameparser._render as _render
|
|
606
658
|
return _render.capitalized(self, lexicon, force=force)
|
|
@@ -5,5 +5,5 @@ VERSION = (2, 0, 0)
|
|
|
5
5
|
#: PEP 440 pre-release/dev segment appended to the numeric version, or
|
|
6
6
|
#: "" for a final release. Joined WITHOUT a dot ("2.0.0rc1", not
|
|
7
7
|
#: "2.0.0.rc1"); setuptools reads __version__ as the package version.
|
|
8
|
-
PRE_RELEASE = "
|
|
8
|
+
PRE_RELEASE = "rc2"
|
|
9
9
|
__version__ = ".".join(map(str, VERSION)) + PRE_RELEASE
|