import json
from pathlib import Path
import pytest
import ferro_hgvs
def _variants(n: int) -> list[str]:
assert n >= 3
vs = [f"NM_000088.3:c.{i}A>G" for i in range(1, n + 1)]
vs[1] = "definitely not a variant"
vs[n - 1] = "also::not:valid"
return vs
def _from_list(result) -> list[tuple[bool, str]]:
out: list[tuple[bool, str]] = []
errors = dict(result.errors())
successes = iter(result.successes())
for i in range(result.total()):
if i in errors:
out.append((False, ""))
else:
out.append((True, str(next(successes))))
return out
def _from_stream(stream) -> list[tuple[bool, str]]:
return [(item.ok, str(item.variant) if item.ok else "") for item in stream]
@pytest.fixture
def processor():
return ferro_hgvs.BatchProcessor()
@pytest.mark.parametrize("workers", [0, 1, 2])
def test_streaming_parse_matches_the_list_api(processor, workers):
variants = _variants(50)
expected = _from_list(processor.parse(variants, workers=workers))
streamed = _from_stream(processor.parse_streaming(variants, workers=workers))
assert streamed == expected
@pytest.mark.parametrize("workers", [0, 1, 2])
def test_streaming_normalize_matches_the_list_api(processor, workers):
variants = _variants(50)
expected = _from_list(processor.parse_and_normalize(variants, workers=workers))
streamed = _from_stream(processor.parse_and_normalize_streaming(variants, workers=workers))
assert streamed == expected
def _genome_bp(tmp_path: Path) -> "ferro_hgvs.BatchProcessor":
seq = ["A"] * 5000
for i in range(1000, 4000, 7):
seq[i] = "G"
ref = {
"transcripts": [],
"proteins": {},
"genomic_sequences": {"NC_000001.11": "".join(seq)},
}
path = tmp_path / "genome_ref.json"
path.write_text(json.dumps(ref))
return ferro_hgvs.BatchProcessor(reference_json=str(path))
def _genome_variants(n: int) -> list[str]:
assert n >= 3
vs = [f"NC_000001.11:g.{100 + i}del" for i in range(n)]
vs[1] = "definitely not a variant"
vs[n - 1] = "also::not:valid"
return vs
@pytest.mark.parametrize("workers", [0, 1, 2])
def test_streaming_rederive_matches_the_list_api(tmp_path: Path, workers: int) -> None:
bp = _genome_bp(tmp_path)
variants = _genome_variants(50)
expected = _from_list(bp.parse_and_rederive(variants, workers=workers))
assert any(ok for ok, _ in expected), "expected successful rederivations"
streamed = _from_stream(bp.parse_and_rederive_streaming(variants, workers=workers))
assert streamed == expected
@pytest.mark.parametrize("workers", [0, 1, 2])
def test_streaming_rederive_recommended_form_matches_the_list_api(
tmp_path: Path, workers: int
) -> None:
bp = _genome_bp(tmp_path)
variants = _genome_variants(40)
expected = _from_list(bp.parse_and_rederive(variants, recommended_form=True, workers=workers))
assert any(ok for ok, _ in expected), "expected successful rederivations"
streamed = _from_stream(
bp.parse_and_rederive_streaming(variants, recommended_form=True, workers=workers)
)
assert streamed == expected
def test_streaming_rederive_rejects_zero_grid(processor: "ferro_hgvs.BatchProcessor") -> None:
with pytest.raises(ValueError):
processor.parse_and_rederive_streaming(["NM_000088.3:c.1A>G"], max_grid_cells=0)
def test_failed_items_carry_their_input(processor):
items = list(processor.parse_streaming(["NM_000088.3:c.1A>G", "nope"]))
assert items[0].ok and items[0].input is None and items[0].error is None
assert not items[1].ok
assert items[1].input == "nope"
assert items[1].error and items[1].variant is None
def test_the_input_is_consumed_lazily(processor):
pulled: list[str] = []
def source():
for v in _variants(10):
pulled.append(v)
yield v
stream = processor.parse_streaming(source())
assert pulled == [], "constructing the stream must read nothing"
first = next(stream)
assert first.ok
assert pulled, "the first result must have pulled from the source"
rest = list(stream)
assert len(rest) == 9
assert len(pulled) == 10, "every input read exactly once"
def test_the_first_result_costs_a_bounded_prefix_not_the_whole_stream(processor):
total = 30_000 pulled: list[str] = []
exhausted = False
def source():
nonlocal exhausted
for v in _variants(total):
pulled.append(v)
yield v
exhausted = True
stream = processor.parse_streaming(source())
assert pulled == [], "constructing the stream must read nothing"
next(stream)
after_first = len(pulled)
assert 0 < after_first < total, (
f"the first result must cost a bounded prefix, not the whole stream; "
f"pulled {after_first} of {total}"
)
assert not exhausted, "the source generator must not have run to completion"
consumed = 1 + sum(1 for _ in stream)
assert consumed == total
assert len(pulled) == total, "every input read exactly once"
assert exhausted
def test_the_stream_is_its_own_iterator(processor):
stream = processor.parse_streaming(["NM_000088.3:c.1A>G"])
assert iter(stream) is stream
assert len(list(stream)) == 1
assert list(stream) == []
def test_accepts_any_iterable(processor):
variants = _variants(5)
for source in (variants, iter(variants), (v for v in variants)):
assert len(list(processor.parse_streaming(source))) == 5
def test_empty_input_yields_nothing(processor):
assert list(processor.parse_streaming([])) == []
assert list(processor.parse_and_normalize_streaming([])) == []
assert list(processor.parse_and_rederive_streaming([])) == []
def test_non_string_element_raises(processor):
with pytest.raises(TypeError):
list(processor.parse_streaming(["NM_000088.3:c.1A>G", 42]))
def test_non_iterable_argument_raises_at_the_call(processor):
with pytest.raises(TypeError):
processor.parse_streaming(42)
def test_the_streaming_types_are_exported():
assert "BatchStream" in ferro_hgvs.__all__
assert "BatchItem" in ferro_hgvs.__all__
for sibling in ("BatchProgress", "BatchResult", "BatchProcessor"):
assert sibling in ferro_hgvs.__all__