Skip to content

Commit dbd1675

Browse files
derek73claude
andcommitted
Tighten comments and pin review-found gaps (PR #303 review)
Comment corrections: - _policy.py: scope the 〆 justification to personal names -- the mark is a general-purpose Japanese symbol (〆切, the envelope closing mark); only its within-names use is surname-only - _policy.py: "a pack must not import re" -> "a range-declaring pack" (marker packs like ru/tr_az import re legitimately, by exposing module-level patterns) - _policy.py: the regex-sync sweep scans the pipeline modules PLUS _render, and only private (underscore-prefixed) module-level patterns -- say so - _policy.py: replace the hand-copy PR-history parenthetical with the durable constraint (the table lives here because packs must not import the pipeline) - locales/ja.py: reflow two ragged comment wraps (wording unchanged) - locales/ja.py: acknowledge the blanket 〆 decline's deliberate breadth -- mid-token 〆 (山〆太郎) measures 0.375 on the kanji_feature path and WOULD have carried a SEGMENTATION report; swept in anyway because decline is the safe direction New pins: - test_locales.py guard-stack stub test: assert seg("Yamada太郎") is None -- kills a mutation dropping whole=True from _wholly_japanese, which survived the whole suite - test_locales.py pack-contents tests: _zh/_ja.DEVIATES("𠮷田") -- pins the astral extent of the pack predicates, which a BMP-only hand-rolled replacement would otherwise survive - cases.py ja_shime_with_kana_given ("〆木 ひろ") -- the kana license composing with the widened Han span, previously untested together - test_policy.py test_every_script_member_has_a_range_entry -- a future Script member without a _SCRIPT_RANGES entry is accepted by Policy validation yet never classified; this turns that silent no-op into a failure - test_locales.py test_namedivider_still_miscuts_shime_names -- the self-expiring canary behind the adapter's 〆 decline; when namedivider learns the mark this fails and the decline gets revisited instead of lasting forever Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent fbb32ab commit dbd1675

5 files changed

Lines changed: 61 additions & 28 deletions

File tree

nameparser/_policy.py

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -64,8 +64,8 @@ class Script(StrEnum):
6464
# it -- _pipeline/_vocab.py compiles its per-script patterns from it,
6565
# and it is importable from the pipeline and the locale packs alike,
6666
# so the packs' predicates build on it too, through _script_matcher
67-
# below (the hand-copies this replaced dated from when the table
68-
# lived in the pipeline, which packs must not import).
67+
# below (the table lives here rather than in the pipeline because
68+
# packs must not import the pipeline).
6969
# HAN: the ideographic iteration mark U+3005 and the shime mark
7070
# U+3006, the URO plus Extension
7171
# A, the compatibility block, and the supplementary-plane block
@@ -87,9 +87,11 @@ class Script(StrEnum):
8787
# U+3006 〆 (the shime mark) extends that singleton to a two-codepoint
8888
# span on a DIFFERENT justification: unlike 々, 〆 is Script=Common
8989
# under UAX #24, so this is the table deliberately reaching PAST the
90-
# Script property, not around a block boundary -- justified because 〆
91-
# functions solely as a component of Japanese surnames (〆木 Shimeki,
92-
# 〆谷 Shimetani, 〆野) and appears in no other script's names.
90+
# Script property, not around a block boundary -- justified because
91+
# within personal names 〆 appears solely in Japanese surnames (〆木
92+
# Shimeki, 〆谷 Shimetani, 〆野) -- its other uses (the envelope
93+
# closing mark, 〆切) never reach a name parser -- and it appears in
94+
# no other script's names.
9395
# Leaving it out made 〆木 a mixed-script token: the name reversed and
9496
# never gated into segmentation.
9597
# HANGUL: precomposed syllables only -- modern Korean
@@ -153,12 +155,13 @@ def _script_matcher(*scripts: Script,
153155
holding a module-level re.Pattern as a marker pack needing rotator
154156
branch coverage, and its registry gate goes further -- a pack that
155157
so much as IMPORTS re without exposing such a pattern fails
156-
"imports re but exposes no module-level pattern" -- so a pack must
157-
not import re at all; predicates built here keep the packs
158-
invisible to that sweep by construction (and spare _vocab's
159-
derived matchers a declaration row in tests/v2/test_regex_sync.py's
160-
completeness sweep, which scans the pipeline modules for
161-
module-level patterns)."""
158+
"imports re but exposes no module-level pattern" -- so a
159+
range-declaring pack must not import re at all; predicates built
160+
here keep the packs invisible to that sweep by construction (and
161+
spare _vocab's derived matchers a declaration row in
162+
tests/v2/test_regex_sync.py's completeness sweep, which scans the
163+
pipeline modules (plus _render) for private module-level
164+
patterns)."""
162165
if not scripts:
163166
raise ValueError("_script_matcher needs at least one Script")
164167
cls = "".join(f"\\U{lo:08x}-\\U{hi:08x}"

nameparser/locales/ja.py

Lines changed: 18 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -76,12 +76,11 @@
7676
#: here even though the pack never ACTIVATES it: a katakana-bearing
7777
#: token can still be a kana-licensed composite the pack acts on
7878
#: (山田エミ) -- DEVIATES must declare it, and the adapter must not
79-
#: decline it. A
80-
#: pure-katakana token never reaches THE ADAPTER: KATAKANA is in no
81-
#: activation set, so the stage's own gate stops it before the
82-
#: segmenter is consulted. DEVIATES still declares one, and must: it
83-
#: is a predicate over any name at all, called before any gate runs,
84-
#: and over-declaring is its safe direction by design.
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.
8584
_JA_SCRIPTS = (Script.HAN, Script.HIRAGANA, Script.KATAKANA)
8685

8786
# Both compiled by _policy's factory from the shared codepoint table,
@@ -129,15 +128,13 @@ def ja_segmenter(*, gbdt: bool = False) -> Segmenter:
129128
The adapter declines -- returns None, leaving the token whole --
130129
for text outside the Japanese repertoire, for text bearing the
131130
shime mark 〆 (namedivider 0.4.x's rule path cuts it in the wrong
132-
place), for text too short to divide, for any answer that fails to
133-
reconstruct its input, and for
134-
any score outside [0, 1] by more than float noise (a divider
135-
scoring 87.0 is broken, and its division is worth no more than its
136-
score). An
137-
answer that puts the whole token on one side comes back as
138-
"confidently one token" rather than a decline: the divider read it
139-
and found nothing to cut, which is not the same as having no
140-
opinion.
131+
place), for text too short to divide, for any answer that fails
132+
to reconstruct its input, and for any score outside [0, 1] by
133+
more than float noise (a divider scoring 87.0 is broken, and its
134+
division is worth no more than its score). An answer that puts
135+
the whole token on one side comes back as "confidently one token"
136+
rather than a decline: the divider read it and found nothing to
137+
cut, which is not the same as having no opinion.
141138
Answers arrive with namedivider's own confidence, which the parsing
142139
stage compares against its floor to decide whether to report a
143140
SEGMENTATION ambiguity: namedivider scores a rule-based division
@@ -176,7 +173,12 @@ def segment(text: str) -> Segmentation | None:
176173
# 〆野), so a wrong family would arrive with no SEGMENTATION
177174
# report. Decline instead: the family-first ORDER fix for 〆
178175
# stands regardless, and division can return when a divider
179-
# knows the mark.
176+
# knows the mark. The blanket check is deliberately broader
177+
# than that rule-path failure: mid-token 〆 (山〆太郎) comes
178+
# back 山 | 〆太郎 at 0.375 on the kanji_feature path
179+
# (measured), a low-confidence answer that WOULD have carried
180+
# a SEGMENTATION report -- swept in anyway, because decline is
181+
# the safe direction and mid-token 〆 is vanishingly rare.
180182
if "〆" in text:
181183
return None
182184
# namedivider RAISES below two characters ("Name length needs

tests/v2/cases.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -638,6 +638,11 @@ def __post_init__(self) -> None:
638638
notes="wholly-Han lone token under the family-first entry: "
639639
"the whole token lands in family, unsegmented by "
640640
"default"),
641+
Case("ja_shime_with_kana_given", "〆木 ひろ",
642+
{"family": "〆木", "given": "ひろ"},
643+
classification="fix(shime-mark)",
644+
notes="the kana license composes with the widened Han span: "
645+
"〆木 reads as kanji beside a kana given name"),
641646
Case("ja_nakaguro_inside_a_nickname",
642647
"山田 太郎 (マイケル・ジャクソン)",
643648
{"family": "山田", "given": "太郎",

tests/v2/test_locales.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,7 @@ def test_zh_pack_contents() -> None:
125125
assert "欧阳" in locales.ZH.lexicon.surnames # compound
126126
assert "歐陽" in locales.ZH.lexicon.surnames # traditional
127127
assert _zh.DEVIATES("毛泽东")
128+
assert _zh.DEVIATES("𠮷田") # astral Han declared
128129
assert not _zh.DEVIATES("John Smith")
129130
assert not _zh.DEVIATES("김민준") # HAN alone, not a wider union
130131

@@ -205,6 +206,7 @@ def test_ja_pack_contents() -> None:
205206
assert locales.JA.policy.script_orders is UNSET
206207
assert locales.JA.lexicon == Lexicon.empty()
207208
assert _ja.DEVIATES("山田太郎")
209+
assert _ja.DEVIATES("𠮷田") # astral Han declared
208210
# katakana declared (see the pack's repertoire note)
209211
assert _ja.DEVIATES("マイケル")
210212
assert _ja.DEVIATES("みなみ") # hiragana declared
@@ -266,6 +268,8 @@ def test_ja_adapter_guard_stack_against_a_stub(
266268
assert answer is not None
267269
assert answer.splits == (2,) and answer.confidence == 0.5
268270
assert seg("김민준") is None # outside the repertoire
271+
# contains JA chars but is not WHOLLY JA
272+
assert seg("Yamada太郎") is None
269273
assert seg("林") is None # namedivider raises below 2
270274

271275

@@ -485,6 +489,18 @@ def test_ja_divides_an_astral_kanji_name() -> None:
485489
assert [a.kind for a in n.ambiguities] == [AmbiguityKind.SEGMENTATION]
486490

487491

492+
@_needs_ja
493+
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.
498+
import namedivider
499+
result = namedivider.BasicNameDivider().divide_name("〆木太郎")
500+
assert (result.family, result.given) == ("〆", "木太郎")
501+
assert float(result.score) == 1.0
502+
503+
488504
@_needs_ja
489505
def test_ja_stacked_on_zh_pays_the_zh_packs_documented_cost() -> None:
490506
# Stacking composes MECHANICALLY (the test above) but not for free,

tests/v2/test_policy.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from nameparser._policy import (
77
DEFAULT_SCRIPT_ORDERS, FAMILY_FIRST, FAMILY_FIRST_GIVEN_LAST,
88
GIVEN_FIRST, PatronymicRule, Policy, PolicyPatch, Script, UNSET,
9-
_script_matcher, apply_patch,
9+
_SCRIPT_RANGES, _script_matcher, apply_patch,
1010
)
1111
from nameparser._types import Role
1212

@@ -100,6 +100,13 @@ def test_script_matcher_reaches_astral_han() -> None:
100100
assert _script_matcher(Script.HAN, whole=True)("𠮷田")
101101

102102

103+
def test_every_script_member_has_a_range_entry() -> None:
104+
# a member added to the enum alone would be accepted by
105+
# Policy(segment_scripts=...) yet never classified by
106+
# single_script -- a silent no-op until this fails
107+
assert set(_SCRIPT_RANGES) == set(Script)
108+
109+
103110
def test_patronymic_rules_coerce_and_reject() -> None:
104111
p = Policy(patronymic_rules=frozenset({"east-slavic"})) # type: ignore[arg-type]
105112
assert p.patronymic_rules == frozenset({PatronymicRule.EAST_SLAVIC})

0 commit comments

Comments
 (0)