Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -172,3 +172,6 @@ cython_debug/

# PyPI configuration file
.pypirc

*.DS_Store
src/.DS_Store
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "HammingdistanceCalculator"
version = "0.1.0"
version = "0.2.0"
Comment thread
fdekievit marked this conversation as resolved.
description = "Script that calculates the hamming distance between a list of barcodes."
readme = "README.md"
authors = [
Expand Down
Binary file removed src/.DS_Store
Binary file not shown.
31 changes: 21 additions & 10 deletions src/hammingdistancecalculator/hamming_distance.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import pathlib
import re
import typer
import csv
Expand Down Expand Up @@ -210,12 +209,18 @@ def compare_sample_barcode_list(sample_barcode_list: list) -> dict:
# main script
@cli.command()
def main(input_csv: Path = typer.Argument(help="Pad naar input CSV met kolommen: label,barcode"),
max_distance: int = typer.Option(
1,
"--max-distance",
"-d",
help="Maximale hamming distance waarvoor output wordt geschreven (default op 1, wat 0 & 1 schrijft)."
)):
max_distance: int = typer.Option(
1,
"--max-distance",
"-d",
help="Maximale hamming distance waarvoor output wordt geschreven (default op 1, wat 0 & 1 schrijft)."
),
outpath: Path = typer.Option(
".",
"--outpath",
"-o",
help="Directory waar output files worden geschreven (default huidige directory)."
)):

# read input
sample_barcode_list = load_barcodes(input_csv)
Expand All @@ -233,9 +238,15 @@ def main(input_csv: Path = typer.Argument(help="Pad naar input CSV met kolommen:
for key, value in compare_dict.items():
hamming_distance_dict[value].append(key)

# write output files for all distances of 0 to max_distance (2 by default)
for counter in range(max_distance):
output_path = pathlib.Path(f"hamming_distance_{counter}.txt")
# verify output path exists to write to
outpath.mkdir(parents=True, exist_ok=True)

# add 1 to the max distance because when you want a range of 1, you want to loop 'including' 1, not 'until' 1
effective_max_distance = max_distance + 1

# write output files for all distances of 0 to effective_max_distance (2 by default)
for counter in range(effective_max_distance):
output_path = outpath / f"hamming_distance_{counter}.txt"

# write each file
with output_path.open('w', newline='') as file_handle:
Expand Down
157 changes: 141 additions & 16 deletions tests/test_hamming_distance.py
Comment thread
BasMonkey marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import pytest
import re
from hammingdistancecalculator.hamming_distance import (
cli,
hamming_distance,
is_valid_dna,
rev_comp,
is_valid_input_csv,
load_barcodes,
compare_sample_barcode_list
)
from typer.testing import CliRunner

# test hamming distance function
@pytest.mark.parametrize(
Expand Down Expand Up @@ -153,28 +155,13 @@ def test_compare_sample_barcode_list_with_mocked_tpqm(mocker):
progress_mock = mocker.MagicMock()
tqdm_mock = mocker.MagicMock()
tqdm_mock.return_value = progress_mock
# tqdm_mock.return_value.__enter__.return_value = progress_mock

# patch the function we want to mock
mocker.patch("hammingdistancecalculator.hamming_distance.tqdm", tqdm_mock)

# store the return function so we can compare to the expected values
return_value = compare_sample_barcode_list(test_set)

#
# # compare expected keys to return value
# expected_keys = {
# "S1_vs_S2",
# "S1_vs_revcomp_S2",
# "S1_vs_S3",
# "S1_vs_revcomp_S3",
# "S2_vs_S3",
# "S2_vs_revcomp_S3"
# }
#
# # check the keys are correct
# assert set(return_value.keys()) == expected_keys

# compare expected values to return values
expected_values = {
"S1_vs_S2": 1,
Expand All @@ -193,4 +180,142 @@ def test_compare_sample_barcode_list_with_mocked_tpqm(mocker):

# create tqdm & update it while going over the items
# N = 3 -> total = N * (N - 1) = 6
tqdm_mock.assert_called_once_with(total=6, desc="Compare_barcodes", unit="cmp")
tqdm_mock.assert_called_once_with(total=6, desc="Compare_barcodes", unit="cmp")


def test_cli_outpath_parameter_creates_directory(tmp_path):
"""Test dat --outpath directory automatisch maakt"""
input_file = tmp_path / "input.csv"
input_file.write_text("label,barcode\nS1,ACTG\nS2,ACCG", encoding="utf-8")

output_dir = tmp_path / "custom_output"
assert not output_dir.exists()

runner = CliRunner()
result = runner.invoke(cli, [str(input_file), "--outpath", str(output_dir)])

assert result.exit_code == 0
assert output_dir.exists(), "Directory werd niet aangemaakt!"


def test_cli_max_distance_plus_one_logic(tmp_path):
"""Test dat max_distance+1 correct werkt (effectieve range telt 1 op bij max)"""
input_file = tmp_path / "data.csv"
input_file.write_text("label,barcode\nS1,AAAA\nS2,CCCC", encoding="utf-8")

output_dir = tmp_path / "max_test"

runner = CliRunner()
result = runner.invoke(cli, [str(input_file), "-d", "2", "-o", str(output_dir)])

assert result.exit_code == 0

# max_distance=2, effectiveMaxDistance=3 → files 0, 1, 2 (3 files totaal)
created_files = sorted([f.name for f in output_dir.glob("hamming_distance_*.txt")])
expected_files = ["hamming_distance_0.txt", "hamming_distance_1.txt", "hamming_distance_2.txt"]

assert created_files == expected_files, f"Verwacht {expected_files}, kreeg {created_files}"


def test_cli_both_short_and_long_flags_work(tmp_path):
"""Test dat zowel -d/-o als --max-distance/--outpath werken"""
input_file = tmp_path / "test.csv"
input_file.write_text("label,barcode\nX,ATGC\nY,ATGA", encoding="utf-8")

output_dir = tmp_path / "flags_test"

runner = CliRunner()
# Gebruik korte flags
result = runner.invoke(cli, [str(input_file), "-d", "1", "-o", str(output_dir)])

assert result.exit_code == 0
assert len(list(output_dir.glob("*.txt"))) > 0


def test_cli_default_behavior_without_options(tmp_path):
"""Test default gedrag met expliciete . directory"""
input_file = tmp_path / "default.csv"
input_file.write_text("label,barcode\nA,ACTG\nB,ACCG", encoding="utf-8")

output_dir = tmp_path / "dot_output"

runner = CliRunner()
result = runner.invoke(cli, [str(input_file), "-o", str(output_dir)])

assert result.exit_code == 0
assert (output_dir / "hamming_distance_0.txt").exists()


def test_cli_help_shows_all_options():
"""Test dat --help beide opties toont"""
runner = CliRunner()
result = runner.invoke(cli, ["--help"])

assert result.exit_code == 0
assert "--outpath" in result.stdout or "-o" in result.stdout
assert "--max-distance" in result.stdout or "-d" in result.stdout


def test_cli_multiple_barcodes_different_distances(tmp_path):
"""Test met diverse afstanden tussen barcodes"""
input_file = tmp_path / "multi.csv"
# AAAA vs AAAA = dist 0
# AAAA vs AAAA = dist 0
# AAAA vs AAAG = dist 1
# AAAA vs GGTC = dist 4
input_file.write_text("label,barcode\nM,AAAA\nN,AAAA\nO,AAAG\nP,GGTC", encoding="utf-8")

output_dir = tmp_path / "mixed_distances"

runner = CliRunner()
result = runner.invoke(cli, [str(input_file), "-d", "5", "-o", str(output_dir)])

assert result.exit_code == 0

# Minstens dist 0 en 1 moeten bestaan
assert (output_dir / "hamming_distance_0.txt").exists()
assert (output_dir / "hamming_distance_1.txt").exists()


def test_cli_output_content_verification(tmp_path):
"""Test dat output file inhoud correct is"""
input_file = tmp_path / "verify.csv"
input_file.write_text("label,barcode\nFirst,AAAA\nSecond,AAAA", encoding="utf-8")

output_dir = tmp_path / "content_check"

runner = CliRunner()
result = runner.invoke(cli, [str(input_file), "--outpath", str(output_dir)])

assert result.exit_code == 0

dist0 = output_dir / "hamming_distance_0.txt"
content = dist0.read_text()

# Check juiste format (LET OP: "vs" met spaties, NIET "_vs_"!)
assert "Number of comparisons found with hamming distance 0:" in content
assert "Barcode First vs barcode Second" in content
assert "has hamming distance 0" in content


def test_cli_nonexistent_parent_directory_created(tmp_path):
"""Test nested directory (parents=True)"""
input_file = tmp_path / "input.csv"
input_file.write_text("label,barcode\nS1,ACTG", encoding="utf-8")

output_dir = tmp_path / "level1" / "level2" / "deep_output"

runner = CliRunner()
result = runner.invoke(cli, [str(input_file), "-o", str(output_dir)])

assert result.exit_code == 0
assert output_dir.exists(), "Nested directories werden niet aangemaakt!"


def test_cli_missing_required_argument_fails():
"""Test dat ontbrekend verplicht argument een error geeft"""
runner = CliRunner()
result = runner.invoke(cli, []) # Geen input argument

assert result.exit_code != 0
assert "Missing argument" in result.output or "Usage:" in result.output
Loading
Loading