From 0c4036eaef280526673ab695d1f981e78aec4743 Mon Sep 17 00:00:00 2001 From: fdekievit Date: Tue, 7 Jul 2026 14:54:19 +0200 Subject: [PATCH] formatted to satisfy ruff --- pyproject.toml | 4 + .../hamming_distance.py | 45 ++++---- tests/test_hamming_distance.py | 104 +++++++++++------- 3 files changed, 92 insertions(+), 61 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 6883b60..36aa72c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -31,3 +31,7 @@ dev = [ [tool.ruff] line-length = 127 + +[tool.ruff.lint] +select = ["E", "F", "W"] +extend-select = ["W292"] diff --git a/src/hammingdistancecalculator/hamming_distance.py b/src/hammingdistancecalculator/hamming_distance.py index 9169dec..0a41a55 100644 --- a/src/hammingdistancecalculator/hamming_distance.py +++ b/src/hammingdistancecalculator/hamming_distance.py @@ -34,11 +34,16 @@ def hamming_distance(seq1: str, seq2: str) -> int: check_1 = is_valid_dna(seq1) check_2 = is_valid_dna(seq2) if not check_1 and not check_2: - raise ValueError(f'Provided sequences are invalid, only A, C, T, and G nucleotides are allowed. Invalid sequences: {seq1}, {seq2}') + raise ValueError( + f'Provided sequences are invalid, only A, C, T, and G nucleotides are allowed. Invalid sequences: {seq1}, {seq2}') elif not check_1 and check_2: - raise ValueError(f'Provided sequence for sequence 1 is invalid, only A, C, T, and G nucleotides are allowed. Invalid sequence: {seq1}') + raise ValueError( + f'Provided sequence for sequence 1 is invalid, only A, C, T, and G nucleotides are allowed. ' + f'Invalid sequence: {seq1}') elif check_1 and not check_2: - raise ValueError(f'Provided sequence for sequence 2 is invalid, only A, C, T, and G nucleotides are allowed. Invalid sequence: {seq2}') + raise ValueError( + f'Provided sequence for sequence 2 is invalid, only A, C, T, and G nucleotides are allowed. ' + f'Invalid sequence: {seq2}') # break if index have unequal length if len(seq1) != len(seq2): @@ -56,7 +61,6 @@ def hamming_distance(seq1: str, seq2: str) -> int: def is_valid_dna(seq: str) -> bool: """Check if provided dna string contains valid characters - Currently only 'A', 'C', 'T' and 'G' are checked for. Args: @@ -71,7 +75,6 @@ def is_valid_dna(seq: str) -> bool: def rev_comp(dna: str) -> str: """Function which returns the reverse complement of a DNA string - Args: dna: The input DNA string we want to convert @@ -88,7 +91,6 @@ def rev_comp(dna: str) -> str: def is_valid_input_csv(csv_file: Path) -> bool: """Function that tests input csv file given to be in the expected format. - We expect the file to have a header with labels 'label', 'barcode'. We expect no empty labels We expect no empty barcodes @@ -209,19 +211,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)." - ), - outpath: Path = typer.Option( - ".", - "--outpath", - "-o", - help="Directory waar output files worden geschreven (default huidige directory)." - )): - + 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) @@ -251,10 +252,12 @@ def main(input_csv: Path = typer.Argument(help="Pad naar input CSV met kolommen: # write each file with output_path.open('w', newline='') as file_handle: writer = csv.writer(file_handle) - writer.writerow([f"Number of comparisons found with hamming distance {counter}: {len(hamming_distance_dict[counter])}"]) + writer.writerow( + [f"Number of comparisons found with hamming distance {counter}: {len(hamming_distance_dict[counter])}"]) for item in hamming_distance_dict[counter]: - writer.writerow([f"Barcode {item.split('_vs_')[0]} vs barcode {item.split('_vs_')[1]} has hamming distance {counter}"]) + writer.writerow( + [f"Barcode {item.split('_vs_')[0]} vs barcode {item.split('_vs_')[1]} has hamming distance {counter}"]) if __name__ == "__main__": diff --git a/tests/test_hamming_distance.py b/tests/test_hamming_distance.py index 8bf79f6..73cb6bc 100644 --- a/tests/test_hamming_distance.py +++ b/tests/test_hamming_distance.py @@ -11,57 +11,78 @@ ) from typer.testing import CliRunner + # test hamming distance function @pytest.mark.parametrize( "a,b,expected", [ - ("A", "A", 0), # distance of 0, identical letters - ("a", "a", 0), # distance of 0, lowe case - ("a", "A", 0), # distance of 0, mixed case - ("C", "T", 1), # distance of 1, 1 letter difference - ("c", "t", 1), # distance of 1, lower case - ("C", "t", 1), # distance of 1, mixed case - ("ACTGCG", "ACTGCG", 0), # distance of 0, multiple letters - ("actgcg", "actgcg", 0), # distance of 0, lower case - ("ACTGCG", "actgcg", 0), # distance of 0, mixed case - ("AACCGGTT", "TTGGCCAA", 8), # distance of 6, full mismatch - ("aaccttgg", "ttggccaa", 8), # distance of 6, full mismatch - ("AACCGGTT", "ttggccaa", 8), # distance of 6, full mismatch + ("A", "A", 0), # distance of 0, identical letters + ("a", "a", 0), # distance of 0, lowe case + ("a", "A", 0), # distance of 0, mixed case + ("C", "T", 1), # distance of 1, 1 letter difference + ("c", "t", 1), # distance of 1, lower case + ("C", "t", 1), # distance of 1, mixed case + ("ACTGCG", "ACTGCG", 0), # distance of 0, multiple letters + ("actgcg", "actgcg", 0), # distance of 0, lower case + ("ACTGCG", "actgcg", 0), # distance of 0, mixed case + ("AACCGGTT", "TTGGCCAA", 8), # distance of 6, full mismatch + ("aaccttgg", "ttggccaa", 8), # distance of 6, full mismatch + ("AACCGGTT", "ttggccaa", 8), # distance of 6, full mismatch ], ) def test_hamming_distance(a, b, expected): # test normal cases assert hamming_distance(a, b) == expected + @pytest.mark.parametrize( "a,b,expected", [ - ("", "", 0), # empty strings - ("A", "", 0), # unequal length - ("AC", "A", 0), # unequal length - ("A", "AC", 0), # unequal length + ("", "", 0), # empty strings + ("A", "", 0), # unequal length + ("AC", "A", 0), # unequal length + ("A", "AC", 0), # unequal length ], ) def test_hamming_distance_empty(a, b, expected): # test empty strings - with pytest.raises(ValueError, match=r"^Provided sequences are invalid, only A, C, T, and G nucleotides are allowed. Invalid sequences:"): + with pytest.raises(ValueError, match=r"^Provided sequences are invalid, " + r"only A, C, T, and G nucleotides are allowed. Invalid sequences:"): hamming_distance("", "") + # test invalid dna provided @pytest.mark.parametrize( "a,b,error_message", [ - ("QQ", "AA", "Provided sequence for sequence 1 is invalid, only A, C, T, and G nucleotides are allowed. Invalid sequence: QQ"), # Left side invalid - ("AA", "BB", "Provided sequence for sequence 2 is invalid, only A, C, T, and G nucleotides are allowed. Invalid sequence: BB"), # Right side invalid - ("QA", "AA", "Provided sequence for sequence 1 is invalid, only A, C, T, and G nucleotides are allowed. Invalid sequence: QA"), # Left side partially invalid - ("AQ", "AA", "Provided sequence for sequence 1 is invalid, only A, C, T, and G nucleotides are allowed. Invalid sequence: AQ"), # Left side partially invalid - ("AA", "QA", "Provided sequence for sequence 2 is invalid, only A, C, T, and G nucleotides are allowed. Invalid sequence: QA"), # Right side partially invalid - ("AA", "AQ", "Provided sequence for sequence 2 is invalid, only A, C, T, and G nucleotides are allowed. Invalid sequence: AQ"), # Right side partially invalid - ("QQ", "VV", "Provided sequences are invalid, only A, C, T, and G nucleotides are allowed. Invalid sequences: QQ, VV"), # Both sides invalid - ("QA", "VA", "Provided sequences are invalid, only A, C, T, and G nucleotides are allowed. Invalid sequences: QA, VA"), # Both sides invalid - ("QA", "AV", "Provided sequences are invalid, only A, C, T, and G nucleotides are allowed. Invalid sequences: QA, AV"), # Both sides invalid - ("AQ", "VA", "Provided sequences are invalid, only A, C, T, and G nucleotides are allowed. Invalid sequences: AQ, VA"), # Both sides invalid - ("AQ", "AV", "Provided sequences are invalid, only A, C, T, and G nucleotides are allowed. Invalid sequences: AQ, AV"), # Both sides invalid + ("QQ", "AA", + "Provided sequence for sequence 1 is invalid, only A, C, T, and G nucleotides are allowed. Invalid sequence: QQ"), + # Left side invalid + ("AA", "BB", + "Provided sequence for sequence 2 is invalid, only A, C, T, and G nucleotides are allowed. Invalid sequence: BB"), + # Right side invalid + ("QA", "AA", + "Provided sequence for sequence 1 is invalid, only A, C, T, and G nucleotides are allowed. Invalid sequence: QA"), + # Left side partially invalid + ("AQ", "AA", + "Provided sequence for sequence 1 is invalid, only A, C, T, and G nucleotides are allowed. Invalid sequence: AQ"), + # Left side partially invalid + ("AA", "QA", + "Provided sequence for sequence 2 is invalid, only A, C, T, and G nucleotides are allowed. Invalid sequence: QA"), + # Right side partially invalid + ("AA", "AQ", + "Provided sequence for sequence 2 is invalid, only A, C, T, and G nucleotides are allowed. Invalid sequence: AQ"), + # Right side partially invalid + ("QQ", "VV", "Provided sequences are invalid, only A, C, T, and G nucleotides are allowed. Invalid sequences: QQ, VV"), + # Both sides invalid + ("QA", "VA", "Provided sequences are invalid, only A, C, T, and G nucleotides are allowed. Invalid sequences: QA, VA"), + # Both sides invalid + ("QA", "AV", "Provided sequences are invalid, only A, C, T, and G nucleotides are allowed. Invalid sequences: QA, AV"), + # Both sides invalid + ("AQ", "VA", "Provided sequences are invalid, only A, C, T, and G nucleotides are allowed. Invalid sequences: AQ, VA"), + # Both sides invalid + ("AQ", "AV", "Provided sequences are invalid, only A, C, T, and G nucleotides are allowed. Invalid sequences: AQ, AV"), + # Both sides invalid ], ) def test_hamming_distance_invalid_dna(a, b, error_message): @@ -75,20 +96,21 @@ def test_hamming_distance_invalid_dna(a, b, error_message): @pytest.mark.parametrize( "seq,expected", [ - ("A", True), # Single valid character - ("ACTG", True), # All valid characters - ("AAA", True), # Repeated valid characters - ("", False), # Empty string should be invalid - ("ACTGN", False), # Contains 'N' which is not allowed - ("XYZ", False), # Completely invalid characters - ("actg", False), # Lowercase letters should fail (regex expects uppercase) - ("ACTG123", False), # Numbers are not allowed - ("ACTG!", False), # Special characters are not allowed + ("A", True), # Single valid character + ("ACTG", True), # All valid characters + ("AAA", True), # Repeated valid characters + ("", False), # Empty string should be invalid + ("ACTGN", False), # Contains 'N' which is not allowed + ("XYZ", False), # Completely invalid characters + ("actg", False), # Lowercase letters should fail (regex expects uppercase) + ("ACTG123", False), # Numbers are not allowed + ("ACTG!", False), # Special characters are not allowed ] ) def test_is_valid_dna(seq, expected): assert is_valid_dna(seq) == expected + # test rev_comp def test_rev_comp(): # default @@ -97,6 +119,7 @@ def test_rev_comp(): # palindrome assert rev_comp("ATAT") == "ATAT" + # test is_valid_input_csv def test_is_valid_input_csv(): bad_file = 'tests/bad_input.csv' @@ -104,7 +127,8 @@ def test_is_valid_input_csv(): assert is_valid_input_csv(good_file) - with pytest.raises(ValueError, match=re.escape("Line ['S1', ' ACTG', ' invalid extra'] has too many elements, 2 expected.")): + with pytest.raises(ValueError, + match=re.escape("Line ['S1', ' ACTG', ' invalid extra'] has too many elements, 2 expected.")): is_valid_input_csv(bad_file) @@ -117,7 +141,7 @@ def test_load_barcodes_correct(tmp_path): encoding="utf-8") test_barcodes = load_barcodes(test_file) - assert test_barcodes == [("S1", "ACTG"),("S2", "TTTT")] + assert test_barcodes == [("S1", "ACTG"), ("S2", "TTTT")] def test_load_barcodes_strips_whitespace(tmp_path): @@ -318,4 +342,4 @@ def test_cli_missing_required_argument_fails(): result = runner.invoke(cli, []) # Geen input argument assert result.exit_code != 0 - assert "Missing argument" in result.output or "Usage:" in result.output \ No newline at end of file + assert "Missing argument" in result.output or "Usage:" in result.output