From 4072e1380b1ac45760cb087c939c75d6c53a4cec Mon Sep 17 00:00:00 2001 From: xvazquezc Date: Tue, 25 Aug 2026 10:40:57 +1000 Subject: [PATCH] fix: cds IDs to be unique --- gfftk/gff.py | 4 +++- tests/unit/test_gff.py | 40 ++++++++++++++++++++++++++++++++++++++-- 2 files changed, 41 insertions(+), 3 deletions(-) diff --git a/gfftk/gff.py b/gfftk/gff.py index 301d2a3..d58449c 100755 --- a/gfftk/gff.py +++ b/gfftk/gff.py @@ -3414,8 +3414,9 @@ def _sortDict(d): # GFF3 phase is 1 less than flat file current_phase = v["codon_start"][i] - 1 for y in range(0, num_cds): + cds_num = y + 1 gffout.write( - "{:}\t{:}\tCDS\t{:}\t{:}\t.\t{:}\t{:}\tID={:}.cds;Parent={:};\n".format( + "{:}\t{:}\tCDS\t{:}\t{:}\t.\t{:}\t{:}\tID={:}.cds{:};Parent={:};\n".format( v["contig"], new_source, sortedCDS[y][0], @@ -3423,6 +3424,7 @@ def _sortDict(d): v["strand"], current_phase, v["ids"][i], + cds_num, v["ids"][i], ) ) diff --git a/tests/unit/test_gff.py b/tests/unit/test_gff.py index ef5a0b9..42d7b27 100644 --- a/tests/unit/test_gff.py +++ b/tests/unit/test_gff.py @@ -7,6 +7,7 @@ from gfftk.gff import ( _detect_format, + dict2gff3, gff2dict, is_combined_gff_fasta, simplifyGO, @@ -19,6 +20,42 @@ class TestGFFParsing: """Tests for GFF parsing functions.""" + def test_dict2gff3_assigns_unique_cds_ids(self): + """Each CDS segment emitted for a transcript must receive its own ID.""" + with tempfile.NamedTemporaryFile(mode="w", delete=False) as gff_temp: + gff_temp.write("##gff-version 3\n") + gff_temp.write("contig1\ttest\tgene\t1\t12\t.\t+\t.\tID=gene1\n") + gff_temp.write( + "contig1\ttest\tmRNA\t1\t12\t.\t+\t.\tID=mRNA1;Parent=gene1\n" + ) + gff_temp.write("contig1\ttest\texon\t1\t3\t.\t+\t.\tID=exon1;Parent=mRNA1\n") + gff_temp.write("contig1\ttest\texon\t7\t12\t.\t+\t.\tID=exon2;Parent=mRNA1\n") + gff_temp.write("contig1\ttest\tCDS\t1\t3\t.\t+\t0\tID=cds1;Parent=mRNA1\n") + gff_temp.write("contig1\ttest\tCDS\t7\t12\t.\t+\t0\tID=cds2;Parent=mRNA1\n") + gff_name = gff_temp.name + + with tempfile.NamedTemporaryFile(mode="w", delete=False) as fasta_temp: + fasta_temp.write(">contig1\nATGAAATTTTAA\n") + fasta_name = fasta_temp.name + + with tempfile.NamedTemporaryFile(mode="w", delete=False) as output_temp: + output_name = output_temp.name + + try: + annotations = gff2dict(gff_name, fasta_name, debug=False) + dict2gff3(annotations, output=output_name) + with open(output_name) as gff_output: + cds_ids = [ + line.split("ID=", 1)[1].split(";", 1)[0] + for line in gff_output + if "\tCDS\t" in line + ] + assert cds_ids == ["mRNA1.cds1", "mRNA1.cds2"] + finally: + os.unlink(gff_name) + os.unlink(fasta_name) + os.unlink(output_name) + def test_detect_format(self): """Test the _detect_format function.""" # Create a GFF3-like file for testing @@ -402,6 +439,7 @@ def test_longest_orf_gapmm2_alignment(self): if os.path.exists(gff_file): os.remove(gff_file) + def test_longest_orf_exact_boundary(self): """Test _longest_orf correctly processes an ORF that exactly ends on an exon boundary.""" # Create a fasta file @@ -452,5 +490,3 @@ def test_longest_orf_exact_boundary(self): os.remove(fasta_file) if os.path.exists(gff_file): os.remove(gff_file) - -