Skip to content

Commit 5ecc1aa

Browse files
author
Enes Ak
committed
chore: Phase 2.8 grooming — couple CLI data_types to _DATATYPE_TO_PROFILE, reject inverted BED intervals, guard wgs+capture_bed at API level, drop unused fixture
Four fixes flagged by final cumulative review: - CLI `valid_data_types` now derived from `_DATATYPE_TO_PROFILE.keys()` to prevent future drift between CLI guard and API mapping. - `load_capture_bed` raises `CaptureBedError` on inverted intervals (start > end) rather than silently accepting rows that match zero positions — quiet correctness failure prevented. - `Annotator.__init__` raises `ValueError` on `data_type='wgs'` with a `capture_bed` path, matching the CLI's existing rejection. - Unused `sma_panel_bed_empty` fixture removed (YAGNI). Two new tests cover Fix 2 and Fix 3. Test count 159 → 161.
1 parent 18d7445 commit 5ecc1aa

6 files changed

Lines changed: 37 additions & 12 deletions

File tree

src/locusguard/api.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,10 @@ def __init__(
7474
self._reference_fasta = reference_fasta
7575
self._data_type = data_type
7676
self._capture_bed = capture_bed
77+
if data_type == "wgs" and capture_bed is not None:
78+
raise ValueError(
79+
"capture_bed is only valid with data_type 'wes' or 'panel' — got 'wgs'"
80+
)
7781
self._profile_name = _DATATYPE_TO_PROFILE.get(data_type)
7882

7983
def annotate_vcf(

src/locusguard/capture_bed.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,11 @@ def load_capture_bed(path: Path) -> list[CaptureRegion]:
8484
f"Line {line_num}: start/end must be integers,"
8585
f" got '{parts[1]}' / '{parts[2]}'"
8686
) from e
87+
if start > end:
88+
raise CaptureBedError(
89+
f"Line {line_num}: start ({start}) > end ({end})"
90+
f" — BED intervals must be non-inverted"
91+
)
8792
regions.append(CaptureRegion(chrom=chrom, start=start, end=end))
8893
return regions
8994

src/locusguard/cli/annotate.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
import typer
99
from rich.console import Console
1010

11-
from locusguard.api import Annotator
11+
from locusguard.api import _DATATYPE_TO_PROFILE, Annotator
1212
from locusguard.config import load_config
1313
from locusguard.config.schema import LocusConfig
1414
from locusguard.io.reference import ReferenceNotFoundError, resolve_reference_fasta
@@ -68,10 +68,9 @@ def annotate(
6868
LOCUS_STATUS, LOCUS_EVIDENCE, LOCUS_KEY fields on variants inside configured
6969
locus regions.
7070
"""
71-
valid_data_types = {"wgs", "wes", "panel"}
72-
if data_type not in valid_data_types:
71+
if data_type not in _DATATYPE_TO_PROFILE:
7372
raise typer.BadParameter(
74-
f"--data-type must be one of: {', '.join(sorted(valid_data_types))}"
73+
f"--data-type must be one of: {', '.join(sorted(_DATATYPE_TO_PROFILE))}"
7574
)
7675
if data_type == "wgs" and capture_bed is not None:
7776
raise typer.BadParameter(

tests/conftest.py

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -318,11 +318,3 @@ def sma_panel_bed_missing_psv(tmp_path: Path) -> Path:
318318
bed = tmp_path / "panel_missing.bed"
319319
bed.write_text("chr5\t12000\t13500\n")
320320
return bed
321-
322-
323-
@pytest.fixture
324-
def sma_panel_bed_empty(tmp_path: Path) -> Path:
325-
"""Synthetic BED with zero regions (just track/comment lines)."""
326-
bed = tmp_path / "panel_empty.bed"
327-
bed.write_text("track name=empty_panel\n# nothing to see here\n")
328-
return bed

tests/unit/test_api.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,3 +127,20 @@ def test_annotator_capture_bed_defaults_to_none():
127127
data_type="wgs",
128128
)
129129
assert annotator._capture_bed is None
130+
131+
132+
def test_annotator_rejects_wgs_with_capture_bed():
133+
"""Annotator API rejects data_type='wgs' + capture_bed combination
134+
(matches the CLI's behavior for the library-level API)."""
135+
from pathlib import Path
136+
137+
import pytest
138+
139+
from locusguard.api import Annotator
140+
with pytest.raises(ValueError, match="capture_bed is only valid"):
141+
Annotator(
142+
configs=[],
143+
reference_fasta=Path("/fake/ref.fa"),
144+
data_type="wgs",
145+
capture_bed=Path("/fake/cap.bed"),
146+
)

tests/unit/test_capture_bed.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,3 +180,11 @@ def test_compute_empty_regions() -> None:
180180
cfg = _make_locus_config([("psv1", 150)])
181181
cov = compute_psv_coverage(cfg, [])
182182
assert cov == PsvCoverage(covered=[], missing=["psv1"], fraction_covered=0.0)
183+
184+
185+
def test_load_inverted_interval_raises(tmp_path: Path) -> None:
186+
"""start > end is rejected as a malformed BED row."""
187+
p = tmp_path / "t.bed"
188+
p.write_text("chr5\t5000\t100\n")
189+
with pytest.raises(CaptureBedError, match="start .* > end"):
190+
load_capture_bed(p)

0 commit comments

Comments
 (0)