Skip to content

Commit 65b6093

Browse files
derek73claude
andcommitted
Simplify: hoist the JA union, merge factory closures, trim duplicated prose
* Hoist _JA_SCRIPTS into _policy.py beside _SCRIPT_RANGES: the union was defined twice (_pipeline/_vocab.py frozenset, locales/ja.py tuple) with nothing pinning their membership equality -- the same drift class this branch already deleted at span level. One tuple now serves the kana license, the ja pack's DEVIATES, and the adapter's repertoire guard; the katakana rationale moves onto the ja predicates it explains, and _vocab's naming-defense comment (guarding a constraint that never existed) goes away. * Merge _script_matcher's twin closures: one compiled [cls]+ pattern with the bound method chosen by `whole` -- search over [cls]+ is exactly contains-any, so the second pattern and closure bought nothing. * Reorder ja adapter guards: the O(1) length check now leads the chain, so routine one-char tokens from spaced input ("林 太郎") no longer pay the regex fullmatch and the 〆 scan (measured 4x) before being declined. All guards return the same None, so behavior is unchanged. * Trim four prose duplications: the 〆 paragraph's copy of the 々 closer in _policy's table comment (plus an orphan line wrap), the triple-stated ordering point on _SCRIPT_MATCHERS, locales.rst's restatement of the marker-classification contract the factory docstring already carries, and the twice-narrated namedivider mis-cut measurement in test_locales.py (the authoritative account stays at ja.py's decline comment). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent dbd1675 commit 65b6093

5 files changed

Lines changed: 51 additions & 69 deletions

File tree

docs/locales.rst

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -283,11 +283,8 @@ by ``tests/v2/test_locales.py``:
283283
not — when in doubt, ``DEVIATES`` should say yes. A pack whose
284284
scope is a *script* builds the predicate with ``_script_matcher``
285285
from ``nameparser/_policy.py``, the way ``zh`` and ``ja`` do —
286-
never by compiling its own character ranges: the table in
287-
``_policy.py`` is the single source for script codepoint spans, and
288-
the contract test reads any module-level ``re.Pattern`` in a pack
289-
as a marker regex needing branch coverage (a pack that imports
290-
``re`` without exposing one fails outright).
286+
never by compiling its own character ranges: the factory's
287+
docstring explains how the pack-contract test enforces this.
291288
#. Add a rotator list to ``tests/v2/test_locales.py``. Every pack needs
292289
one, but what it has to contain follows from how the pack declares
293290
its scope. A pack declaring by *marker regex* (``ru``, ``tr_az``)

nameparser/_pipeline/_vocab.py

Lines changed: 7 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@
1313
from collections.abc import Callable, Iterable
1414

1515
from nameparser._lexicon import Lexicon, _normalize
16-
from nameparser._policy import Script, _SCRIPT_RANGES, _script_matcher
16+
from nameparser._policy import (Script, _JA_SCRIPTS, _SCRIPT_RANGES,
17+
_script_matcher)
1718

1819
# Ported verbatim from v1 (nameparser/config/regexes.py "initial") minus
1920
# its empty-string alternative -- WorkToken text is never empty. Kept in
@@ -39,26 +40,15 @@
3940
# compiled regex wins by 3-9x, and by roughly 70x on long tokens.)
4041
# Derived, never hand-written -- even the class construction goes
4142
# through the shared factory: one wholly-of predicate per script, in
42-
# the table's own key order (a dict comprehension over _SCRIPT_RANGES
43-
# preserves it, so single_script's FIRST-covering-entry rule still
44-
# describes what it does; the disjointness requirement that makes
45-
# that well-defined is stated with the table in _policy).
43+
# the table's key order -- the FIRST-covering-entry rule _classify
44+
# documents.
4645
_SCRIPT_MATCHERS: dict[Script, Callable[[str], bool]] = {
4746
script: _script_matcher(script, whole=True)
4847
for script in _SCRIPT_RANGES
4948
}
5049

51-
#: The Japanese repertoire: the union effective_script's kana license
52-
#: quantifies over -- HAN, HIRAGANA, KATAKANA (HANGUL simply omitted).
53-
#: A frozenset, not the tuple this started as: resolve_script_set
54-
#: below is the "later task that needs membership" the tuple's
55-
#: original comment anticipated. Membership doesn't care about order,
56-
#: and the matcher built below doesn't either (a regex character
57-
#: class matches the same set regardless of the order its ranges are
58-
#: written in).
59-
_JA_SCRIPTS = frozenset({Script.HAN, Script.HIRAGANA, Script.KATAKANA})
60-
# Not _wholly_japanese: that name belongs to the ja pack's own
61-
# predicate over the same union (nameparser/locales/ja.py).
50+
# The whole-token matcher over _policy's _JA_SCRIPTS union, backing
51+
# effective_script's kana license.
6252
_wholly_ja = _script_matcher(*_JA_SCRIPTS, whole=True)
6353

6454

@@ -261,6 +251,6 @@ def resolve_script_set(scripts: Iterable[Script]) -> Script | None:
261251
found = frozenset(scripts)
262252
if len(found) <= 1:
263253
return next(iter(found), None)
264-
if found <= _JA_SCRIPTS:
254+
if found.issubset(_JA_SCRIPTS):
265255
return Script.HIRAGANA
266256
return None

nameparser/_policy.py

Lines changed: 16 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -67,8 +67,8 @@ class Script(StrEnum):
6767
# below (the table lives here rather than in the pipeline because
6868
# packs must not import the pipeline).
6969
# HAN: the ideographic iteration mark U+3005 and the shime mark
70-
# U+3006, the URO plus Extension
71-
# A, the compatibility block, and the supplementary-plane block
70+
# U+3006, the URO plus Extension A, the compatibility block, and the
71+
# supplementary-plane block
7272
# (Ext B-I + CJK Compat Ideographs Supplement, 0x20000-0x323AF) --
7373
# rare surnames are the biggest real source of supplementary-plane
7474
# hanzi in personal names (e.g. 𠮷田's 𠮷, U+20BB7), so leaving them
@@ -92,8 +92,6 @@ class Script(StrEnum):
9292
# Shimeki, 〆谷 Shimetani, 〆野) -- its other uses (the envelope
9393
# closing mark, 〆切) never reach a name parser -- and it appears in
9494
# no other script's names.
95-
# Leaving it out made 〆木 a mixed-script token: the name reversed and
96-
# never gated into segmentation.
9795
# HANGUL: precomposed syllables only -- modern Korean
9896
# text never writes names as bare jamo.
9997
# HIRAGANA/KATAKANA (#272): the two kana blocks, each in full. There
@@ -138,6 +136,12 @@ class Script(StrEnum):
138136
Script.KATAKANA: ((0x30A0, 0x30FF),),
139137
}
140138

139+
#: The Japanese repertoire: the three scripts Japanese names draw on.
140+
#: The kana license (_pipeline/_vocab.py's effective_script), the ja
141+
#: pack's DEVIATES, and the segmenter adapter's repertoire guard all
142+
#: quantify over this one union (HANGUL simply omitted).
143+
_JA_SCRIPTS = (Script.HAN, Script.HIRAGANA, Script.KATAKANA)
144+
141145

142146
def _script_matcher(*scripts: Script,
143147
whole: bool = False) -> Callable[[str], bool]:
@@ -167,17 +171,14 @@ def _script_matcher(*scripts: Script,
167171
cls = "".join(f"\\U{lo:08x}-\\U{hi:08x}"
168172
for script in scripts
169173
for lo, hi in _SCRIPT_RANGES[script])
170-
if whole:
171-
pattern = re.compile(f"[{cls}]+")
172-
173-
def wholly(text: str) -> bool:
174-
return pattern.fullmatch(text) is not None
175-
return wholly
176-
single = re.compile(f"[{cls}]")
177-
178-
def contains(text: str) -> bool:
179-
return single.search(text) is not None
180-
return contains
174+
# one pattern serves both modes: fullmatch of [cls]+ is wholly-of,
175+
# and search over [cls]+ is exactly contains-any
176+
pattern = re.compile(f"[{cls}]+")
177+
match = pattern.fullmatch if whole else pattern.search
178+
179+
def matcher(text: str) -> bool:
180+
return match(text) is not None
181+
return matcher
181182

182183

183184
# Order-spec constants (#270). Each reads as its contents because roles

nameparser/locales/ja.py

Lines changed: 20 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,8 @@
5959

6060
from nameparser._lexicon import Lexicon
6161
from nameparser._locale import Locale
62-
from nameparser._policy import PolicyPatch, Script, _script_matcher
62+
from nameparser._policy import (PolicyPatch, Script, _JA_SCRIPTS,
63+
_script_matcher)
6364
from nameparser._types import Segmentation
6465

6566
if TYPE_CHECKING:
@@ -72,21 +73,18 @@
7273
segment_scripts=frozenset({Script.HAN, Script.HIRAGANA})),
7374
)
7475

75-
#: The scripts DEVIATES and the adapter quantify over. KATAKANA is
76-
#: here even though the pack never ACTIVATES it: a katakana-bearing
77-
#: token can still be a kana-licensed composite the pack acts on
78-
#: (山田エミ) -- DEVIATES must declare it, and the adapter must not
79-
#: decline it. A pure-katakana token never reaches THE ADAPTER:
80-
#: KATAKANA is in no activation set, so the stage's own gate stops it
81-
#: before the segmenter is consulted. DEVIATES still declares one,
82-
#: and must: it is a predicate over any name at all, called before
83-
#: any gate runs, and over-declaring is its safe direction by design.
84-
_JA_SCRIPTS = (Script.HAN, Script.HIRAGANA, Script.KATAKANA)
85-
86-
# Both compiled by _policy's factory from the shared codepoint table,
87-
# so the pack carries no spans of its own to drift out of sync: the
88-
# contains-any form backs DEVIATES, the whole-token form is the
89-
# adapter's repertoire guard.
76+
# Both compiled by _policy's factory from the shared codepoint table
77+
# and its _JA_SCRIPTS union, so the pack carries no spans of its own
78+
# to drift out of sync: the contains-any form backs DEVIATES, the
79+
# whole-token form is the adapter's repertoire guard. Both match
80+
# KATAKANA even though the pack never ACTIVATES it: a katakana-bearing
81+
# token can still be a kana-licensed composite the pack acts on
82+
# (山田エミ) -- DEVIATES must declare it, and the adapter must not
83+
# decline it. A pure-katakana token never reaches THE ADAPTER:
84+
# KATAKANA is in no activation set, so the stage's own gate stops it
85+
# before the segmenter is consulted. DEVIATES still declares one,
86+
# and must: it is a predicate over any name at all, called before
87+
# any gate runs, and over-declaring is its safe direction by design.
9088
_has_japanese = _script_matcher(*_JA_SCRIPTS)
9189
_wholly_japanese = _script_matcher(*_JA_SCRIPTS, whole=True)
9290

@@ -160,6 +158,12 @@ def ja_segmenter(*, gbdt: bool = False) -> Segmenter:
160158
else namedivider.BasicNameDivider())
161159

162160
def segment(text: str) -> Segmentation | None:
161+
# namedivider RAISES below two characters ("Name length needs
162+
# at least 2 chars"), and a segmenter's exceptions propagate by
163+
# contract, so a one-character token -- routine in spaced input
164+
# like "林 太郎" -- must be declined here, not passed on.
165+
if len(text) < 2:
166+
return None
163167
# segment_scripts UNIONS under pack composition, so this can
164168
# receive tokens of any other activated script (hangul, under
165169
# the default policy). Decline anything outside the Japanese
@@ -181,12 +185,6 @@ def segment(text: str) -> Segmentation | None:
181185
# the safe direction and mid-token 〆 is vanishingly rare.
182186
if "〆" in text:
183187
return None
184-
# namedivider RAISES below two characters ("Name length needs
185-
# at least 2 chars"), and a segmenter's exceptions propagate by
186-
# contract, so a one-character token -- routine in spaced input
187-
# like "林 太郎" -- must be declined here, not passed on.
188-
if len(text) < 2:
189-
return None
190188
result = divider.divide_name(text)
191189
if result.family + result.given != text:
192190
return None # defensive: never trust reconstruction

tests/v2/test_locales.py

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -276,12 +276,9 @@ def test_ja_adapter_guard_stack_against_a_stub(
276276
def test_ja_adapter_declines_shime_tokens(
277277
monkeypatch: pytest.MonkeyPatch) -> None:
278278
# Since U+3006 classifies as Han, 〆木太郎 is wholly Japanese and
279-
# reaches the adapter -- but namedivider 0.4.x has no knowledge of
280-
# the shime mark: measured, its rule path cuts every attested 〆
281-
# surname at offset 1 (〆木太郎 -> 〆 | 木太郎, score 1.0,
282-
# algorithm='rule'), a wrong family arriving with NO SEGMENTATION
283-
# report because 1.0 clears the floor. The adapter declines these
284-
# tokens before the divider is ever consulted.
279+
# reaches the adapter, but the divider is never consulted for 〆
280+
# tokens -- see the decline comment in ja.py for the measured
281+
# mis-cut it prevents.
285282
calls: list[str] = []
286283

287284
def divide(text: str) -> _FakeDivided:
@@ -491,10 +488,9 @@ def test_ja_divides_an_astral_kanji_name() -> None:
491488

492489
@_needs_ja
493490
def test_namedivider_still_miscuts_shime_names() -> None:
494-
# The canary behind the adapter's 〆 decline: namedivider 0.4.x's
495-
# rule path cuts every attested 〆 surname at offset 1 with
496-
# confidence 1.0. When this fails, namedivider learned the mark --
497-
# revisit the decline in ja.py rather than deleting this test.
491+
# The canary behind the adapter's 〆 decline: when this fails,
492+
# namedivider learned the mark -- revisit the decline in ja.py
493+
# rather than deleting this test.
498494
import namedivider
499495
result = namedivider.BasicNameDivider().divide_name("〆木太郎")
500496
assert (result.family, result.given) == ("〆", "木太郎")

0 commit comments

Comments
 (0)