From 6441454ca1379b5f5f0391a56cfdcb3b8a2c4c05 Mon Sep 17 00:00:00 2001 From: youdie006 Date: Wed, 19 Aug 2026 11:07:22 +0900 Subject: [PATCH] Fix KeyError: 'ID' in create_splice_sites for GTF-derived databases FeatureDB.create_splice_sites() uniquifies each new splice-site feature's ID by reading splice_site.attributes["ID"]. GTF-derived features carry gene_id/transcript_id/exon_id but have no ID attribute, so the read raises KeyError: 'ID'. The sibling create_introns() is unaffected because it never touches ID. The interfeatures() helper already guards its own ID rewrite with `if "ID" in ...attributes`, so this applies the same existing convention here. Guard the ID rewrite so it only runs when an ID is present. GTF input no longer crashes; GFF3 input (which has real IDs) keeps its uniquified IDs exactly as before. Fixes #239. --- gffutils/interface.py | 11 +++++++---- gffutils/test/test_1.py | 40 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 4 deletions(-) diff --git a/gffutils/interface.py b/gffutils/interface.py index 74e3706..30522de 100644 --- a/gffutils/interface.py +++ b/gffutils/interface.py @@ -1452,10 +1452,13 @@ def child_gen(): if side == "right": splice_site.start = splice_site.end - 1 - # make ID uniq by adding suffix - splice_site.attributes["ID"] = [ - new_featuretype + "_" + splice_site.attributes["ID"][0] - ] + # make ID uniq by adding suffix. GTF-derived features have + # no ID attribute (only gene_id/transcript_id/exon_id), so + # only rewrite the ID when one is actually present. + if "ID" in splice_site.attributes: + splice_site.attributes["ID"] = [ + new_featuretype + "_" + splice_site.attributes["ID"][0] + ] yield splice_site diff --git a/gffutils/test/test_1.py b/gffutils/test/test_1.py index 1780d99..98eb229 100644 --- a/gffutils/test/test_1.py +++ b/gffutils/test/test_1.py @@ -1259,6 +1259,46 @@ def test_create_splice_sites(): assert observed == expected +def test_create_splice_sites_from_gtf(): + # Regression test for issue #239: create_splice_sites raised + # KeyError: 'ID' on GTF-derived databases. GTF features carry + # gene_id/transcript_id/exon_id but no ID attribute, so the + # ID-uniquification step must not assume an ID is present. + gtfdata = dedent( + """ + chr1 example exon 50 1005 . + . gene_id "gene_1"; transcript_id "gene_0_0"; exon_id "gene_0_0_0"; + chr1 example exon 1850 2500 . + . gene_id "gene_1"; transcript_id "gene_0_0"; exon_id "gene_0_0_1"; + chr1 example exon 3800 4500 . + . gene_id "gene_1"; transcript_id "gene_0_0"; exon_id "gene_0_0_2"; + """ + ) + db = gffutils.create_db(gtfdata, ":memory:", from_string=True) + assert db.dialect["fmt"] == "gtf" + + # A 3-exon transcript has 2 introns, and each intron contributes a + # donor and an acceptor splice site, so 4 splice sites are expected. + splice_sites = list(db.create_splice_sites(exon_featuretype="exon")) + assert len(splice_sites) == 4 + + # On the "+" strand the left side is the donor (5') and the right + # side is the acceptor (3'). + featuretypes = sorted(ss.featuretype for ss in splice_sites) + assert featuretypes == [ + "five_prime_cis_splice_site", + "five_prime_cis_splice_site", + "three_prime_cis_splice_site", + "three_prime_cis_splice_site", + ] + + # Each splice site spans a single intronic base pair. + for ss in splice_sites: + assert ss.end - ss.start == 1 + + # GTF-derived features have no ID attribute, so the splice sites + # should not have one either (and must not crash trying to build it). + for ss in splice_sites: + assert "ID" not in ss.attributes + + if __name__ == "__main__": # this test case fails # test_attributes_modify()