|
3 | 3 | import functools |
4 | 4 | import importlib.util |
5 | 5 | import re |
| 6 | +import sys |
| 7 | +import types |
6 | 8 | from collections.abc import Callable, Iterable |
7 | 9 | from types import ModuleType |
8 | 10 |
|
@@ -215,6 +217,79 @@ def test_ja_segmenter_without_extra_raises_helpfully() -> None: |
215 | 217 | locales.ja_segmenter() |
216 | 218 |
|
217 | 219 |
|
| 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 | + |
218 | 293 | def test_ja_ranges_stay_in_sync_with_vocab() -> None: |
219 | 294 | # the twin of the zh pin above: ja.py hand-copies the three |
220 | 295 | # Japanese-repertoire scripts' spans for DEVIATES and for the |
|
0 commit comments