Skip to content

Commit 6b0eae0

Browse files
derek73claude
andcommitted
Pin the ja adapter against a stub so coverage sees it (#272)
The adapter's guard stack ran only in the extra-gated integration tests, which the coverage-bearing CI job never executes -- so codecov/patch read locales/ja.py at 55% while ja-extra proved it green. A stubbed namedivider module exercises every branch in the plain suite, including the two defensive paths the real 0.4.x library cannot reach, plus the gbdt selection. Also pins the two remaining uncovered new lines: parser_for's base type check and Segmentation's confidence TYPE error. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 3b53076 commit 6b0eae0

3 files changed

Lines changed: 83 additions & 0 deletions

File tree

tests/v2/test_locales.py

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
import functools
44
import importlib.util
55
import re
6+
import sys
7+
import types
68
from collections.abc import Callable, Iterable
79
from types import ModuleType
810

@@ -215,6 +217,79 @@ def test_ja_segmenter_without_extra_raises_helpfully() -> None:
215217
locales.ja_segmenter()
216218

217219

220+
class _FakeDivided:
221+
"""The shape of namedivider's DividedName, minus namedivider —
222+
lets the adapter's guard stack run in the plain (no-extra) suite,
223+
including the two defensive branches the real 0.4.x library can
224+
never reach (reconstruction failure, an empty side)."""
225+
226+
def __init__(self, family: str, given: str, score: float) -> None:
227+
self.family, self.given, self.score = family, given, score
228+
229+
230+
def _fake_namedivider(monkeypatch: pytest.MonkeyPatch,
231+
divide: "Callable[[str], _FakeDivided]") -> list[str]:
232+
"""Install a stub namedivider module and return the gbdt-call log
233+
(empty unless GBDTNameDivider was constructed)."""
234+
constructed: list[str] = []
235+
236+
class _Basic:
237+
def __init__(self) -> None:
238+
constructed.append("basic")
239+
240+
def divide_name(self, text: str) -> _FakeDivided:
241+
return divide(text)
242+
243+
class _GBDT(_Basic):
244+
def __init__(self) -> None:
245+
constructed.append("gbdt")
246+
247+
stub = types.ModuleType("namedivider")
248+
stub.BasicNameDivider = _Basic # type: ignore[attr-defined]
249+
stub.GBDTNameDivider = _GBDT # type: ignore[attr-defined]
250+
monkeypatch.setitem(sys.modules, "namedivider", stub)
251+
return constructed
252+
253+
254+
def test_ja_adapter_guard_stack_against_a_stub(
255+
monkeypatch: pytest.MonkeyPatch) -> None:
256+
# The adapter's whole contract without the extra: this is what the
257+
# coverage-bearing CI job sees, so the guards are pinned here and
258+
# not only behind the nameparser[ja] integration tests.
259+
divide = _fake_namedivider(
260+
monkeypatch, lambda text: _FakeDivided(text[:2], text[2:], 0.5))
261+
seg = locales.ja_segmenter()
262+
assert divide == ["basic"]
263+
answer = seg("山田太郎")
264+
assert answer is not None
265+
assert answer.splits == (2,) and answer.confidence == 0.5
266+
assert seg("김민준") is None # outside the repertoire
267+
assert seg("林") is None # namedivider raises below 2
268+
269+
270+
def test_ja_adapter_defensive_branches_against_a_stub(
271+
monkeypatch: pytest.MonkeyPatch) -> None:
272+
# reconstruction failure -> decline; an empty side -> the stated
273+
# "confidently undivided" opinion; an out-of-range score -> clamped
274+
# so Segmentation's [0, 1] validation cannot raise mid-parse
275+
_fake_namedivider(
276+
monkeypatch, lambda text: _FakeDivided("外", "れ", 0.5))
277+
assert locales.ja_segmenter()("山田太郎") is None
278+
_fake_namedivider(
279+
monkeypatch, lambda text: _FakeDivided(text, "", 1.0000001))
280+
whole = locales.ja_segmenter()("山田太郎")
281+
assert whole is not None
282+
assert whole.splits == () and whole.confidence == 1.0
283+
284+
285+
def test_ja_adapter_gbdt_flag_selects_the_gbdt_divider(
286+
monkeypatch: pytest.MonkeyPatch) -> None:
287+
divide = _fake_namedivider(
288+
monkeypatch, lambda text: _FakeDivided(text[:1], text[1:], 1.0))
289+
locales.ja_segmenter(gbdt=True)
290+
assert divide == ["gbdt"]
291+
292+
218293
def test_ja_ranges_stay_in_sync_with_vocab() -> None:
219294
# the twin of the zh pin above: ja.py hand-copies the three
220295
# Japanese-repertoire scripts' spans for DEVIATES and for the

tests/v2/test_parser.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -634,6 +634,11 @@ def seg(text: str) -> Segmentation | None:
634634
assert p.segmenter is seg
635635

636636

637+
def test_parser_for_rejects_a_non_parser_base() -> None:
638+
with pytest.raises(TypeError, match="base must be a Parser"):
639+
parser_for(locales.get("zh"), base=5) # type: ignore[arg-type]
640+
641+
637642
def test_parser_for_takes_a_segmenter_keyword() -> None:
638643
def seg(text: str) -> Segmentation | None:
639644
return None

tests/v2/test_types.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,9 @@ def test_segmentation_validates_splits() -> None:
8787
Segmentation(("2",)) # type: ignore[arg-type]
8888
with pytest.raises(ValueError, match="confidence"):
8989
Segmentation((2,), confidence=1.5)
90+
# wrong TYPE is the TypeError branch, distinct from the range check
91+
with pytest.raises(TypeError, match="confidence"):
92+
Segmentation((2,), confidence="high") # type: ignore[arg-type]
9093

9194

9295
def test_segmentation_rejects_mappings_and_non_iterables() -> None:

0 commit comments

Comments
 (0)