Skip to content

Commit ca51c48

Browse files
authored
Fix #242: expose Ensembl annotation versions on Gene/Transcript/Exon/Protein (#341)
The transcript_version, gene_version, exon_version, and protein_version columns were already loaded into the sqlite database from modern Ensembl GTFs; they were just never surfaced on the Python objects. Each entity now has a prefixed canonical accessor and a generic alias: Gene gene_version / versioned_gene_id version / versioned_id (aliases) Transcript transcript_version / versioned_transcript_id version / versioned_id (aliases) protein -> new Protein view (or None for noncoding) Exon exon_version / versioned_exon_id version / versioned_id (aliases) exon_version is now a constructor kwarg (default None) Protein protein_id, protein_version (primary) id, version, versioned_protein_id, versioned_id (aliases) All version accessors return None when the GTF omits the corresponding attribute (e.g. TAIR), and versioned_id falls back to the unversioned id in that case. Bump version to 2.7.0.
1 parent 90bca3e commit ca51c48

8 files changed

Lines changed: 321 additions & 3 deletions

File tree

pyensembl/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
genome_for_reference_name,
2929
)
3030

31+
from .protein import Protein
3132
from .search import find_nearest_locus
3233
from .sequence_data import SequenceData
3334
from .species import find_species_by_name, check_species_object, normalize_species_name
@@ -43,6 +44,7 @@
4344
"MAX_ENSEMBL_RELEASE",
4445
"Gene",
4546
"Transcript",
47+
"Protein",
4648
"Exon",
4749
"SequenceData",
4850
"find_nearest_locus",

pyensembl/exon.py

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,22 @@
1515

1616

1717
class Exon(Locus):
18-
def __init__(self, exon_id, contig, start, end, strand, gene_name, gene_id):
18+
def __init__(
19+
self,
20+
exon_id,
21+
contig,
22+
start,
23+
end,
24+
strand,
25+
gene_name,
26+
gene_id,
27+
exon_version=None,
28+
):
1929
Locus.__init__(self, contig, start, end, strand)
2030
self.exon_id = exon_id
2131
self.gene_name = gene_name
2232
self.gene_id = gene_id
33+
self.exon_version = exon_version
2334

2435
@property
2536
def id(self):
@@ -28,6 +39,23 @@ def id(self):
2839
"""
2940
return self.exon_id
3041

42+
@property
43+
def version(self):
44+
"""Alias for :attr:`exon_version`."""
45+
return self.exon_version
46+
47+
@property
48+
def versioned_exon_id(self):
49+
"""``exon_id.exon_version`` when available, else ``exon_id``."""
50+
if self.exon_version is None:
51+
return self.exon_id
52+
return "%s.%d" % (self.exon_id, self.exon_version)
53+
54+
@property
55+
def versioned_id(self):
56+
"""Alias for :attr:`versioned_exon_id`."""
57+
return self.versioned_exon_id
58+
3159
def __str__(self):
3260
return (
3361
"Exon(exon_id='%s',"
@@ -69,4 +97,5 @@ def to_dict(self):
6997
state_dict["exon_id"] = self.id
7098
state_dict["gene_name"] = self.gene_name
7199
state_dict["gene_id"] = self.gene_id
100+
state_dict["exon_version"] = self.exon_version
72101
return state_dict

pyensembl/gene.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,42 @@ def to_dict(self):
7979
state_dict["gene_name"] = self.gene_name
8080
return state_dict
8181

82+
@memoized_property
83+
def gene_version(self):
84+
"""
85+
Ensembl annotation version for this gene (int) or None if the GTF
86+
did not provide a gene_version attribute.
87+
"""
88+
if not self.db.column_exists("gene", "gene_version"):
89+
return None
90+
result = self.db.query_one(
91+
select_column_names=["gene_version"],
92+
filter_column="gene_id",
93+
filter_value=self.id,
94+
feature="gene",
95+
required=False,
96+
)
97+
if not result or not result[0]:
98+
return None
99+
return int(result[0])
100+
101+
@property
102+
def version(self):
103+
"""Alias for :attr:`gene_version`."""
104+
return self.gene_version
105+
106+
@property
107+
def versioned_gene_id(self):
108+
"""``gene_id.gene_version`` when a version is available, else ``gene_id``."""
109+
if self.gene_version is None:
110+
return self.gene_id
111+
return "%s.%d" % (self.gene_id, self.gene_version)
112+
113+
@property
114+
def versioned_id(self):
115+
"""Alias for :attr:`versioned_gene_id`."""
116+
return self.versioned_gene_id
117+
82118
@memoized_property
83119
def transcripts(self):
84120
"""

pyensembl/genome.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1064,14 +1064,20 @@ def exon_by_id(self, exon_id):
10641064
"gene_name",
10651065
"gene_id",
10661066
]
1067+
has_exon_version = self.db.column_exists("exon", "exon_version")
1068+
if has_exon_version:
1069+
field_names.append("exon_version")
10671070

1068-
contig, start, end, strand, gene_name, gene_id = self.db.query_one(
1071+
row = self.db.query_one(
10691072
select_column_names=field_names,
10701073
filter_column="exon_id",
10711074
filter_value=exon_id,
10721075
feature="exon",
10731076
distinct=True,
10741077
)
1078+
contig, start, end, strand, gene_name, gene_id = row[:6]
1079+
version_value = row[6] if has_exon_version else None
1080+
exon_version = int(version_value) if version_value else None
10751081

10761082
self._exons[exon_id] = Exon(
10771083
exon_id=exon_id,
@@ -1081,6 +1087,7 @@ def exon_by_id(self, exon_id):
10811087
strand=strand,
10821088
gene_name=gene_name,
10831089
gene_id=gene_id,
1090+
exon_version=exon_version,
10841091
)
10851092

10861093
return self._exons[exon_id]

pyensembl/protein.py

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
# Licensed under the Apache License, Version 2.0 (the "License");
2+
# you may not use this file except in compliance with the License.
3+
# You may obtain a copy of the License at
4+
#
5+
# http://www.apache.org/licenses/LICENSE-2.0
6+
#
7+
# Unless required by applicable law or agreed to in writing, software
8+
# distributed under the License is distributed on an "AS IS" BASIS,
9+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10+
# See the License for the specific language governing permissions and
11+
# limitations under the License.
12+
13+
from serializable import Serializable
14+
15+
16+
class Protein(Serializable):
17+
"""
18+
Lightweight view object exposing the protein identity for a transcript.
19+
Accessed via :attr:`Transcript.protein`.
20+
"""
21+
22+
def __init__(self, protein_id, protein_version=None):
23+
self.protein_id = protein_id
24+
self.protein_version = protein_version
25+
26+
@property
27+
def id(self):
28+
"""Alias for :attr:`protein_id`."""
29+
return self.protein_id
30+
31+
@property
32+
def version(self):
33+
"""Alias for :attr:`protein_version`."""
34+
return self.protein_version
35+
36+
@property
37+
def versioned_protein_id(self):
38+
"""``protein_id.protein_version`` when available, else ``protein_id``."""
39+
if self.protein_version is None:
40+
return self.protein_id
41+
return "%s.%d" % (self.protein_id, self.protein_version)
42+
43+
@property
44+
def versioned_id(self):
45+
"""Alias for :attr:`versioned_protein_id`."""
46+
return self.versioned_protein_id
47+
48+
def __eq__(self, other):
49+
return (
50+
other.__class__ is Protein
51+
and self.protein_id == other.protein_id
52+
and self.protein_version == other.protein_version
53+
)
54+
55+
def __hash__(self):
56+
return hash((self.protein_id, self.protein_version))
57+
58+
def __str__(self):
59+
return "Protein(protein_id='%s', protein_version=%s)" % (
60+
self.protein_id,
61+
self.protein_version,
62+
)
63+
64+
def __repr__(self):
65+
return str(self)
66+
67+
def to_dict(self):
68+
return {
69+
"protein_id": self.protein_id,
70+
"protein_version": self.protein_version,
71+
}

pyensembl/transcript.py

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -531,6 +531,42 @@ def three_prime_utr_sequence(self):
531531
return None
532532
return self.sequence[self.last_stop_codon_spliced_offset + 1 :]
533533

534+
@memoized_property
535+
def transcript_version(self):
536+
"""
537+
Ensembl annotation version for this transcript (int) or None if the
538+
GTF did not provide a transcript_version attribute.
539+
"""
540+
if not self.db.column_exists("transcript", "transcript_version"):
541+
return None
542+
result = self.db.query_one(
543+
select_column_names=["transcript_version"],
544+
filter_column="transcript_id",
545+
filter_value=self.id,
546+
feature="transcript",
547+
required=False,
548+
)
549+
if not result or not result[0]:
550+
return None
551+
return int(result[0])
552+
553+
@property
554+
def version(self):
555+
"""Alias for :attr:`transcript_version`."""
556+
return self.transcript_version
557+
558+
@property
559+
def versioned_transcript_id(self):
560+
"""``transcript_id.transcript_version`` when available, else ``transcript_id``."""
561+
if self.transcript_version is None:
562+
return self.transcript_id
563+
return "%s.%d" % (self.transcript_id, self.transcript_version)
564+
565+
@property
566+
def versioned_id(self):
567+
"""Alias for :attr:`versioned_transcript_id`."""
568+
return self.versioned_transcript_id
569+
534570
@memoized_property
535571
def protein_id(self):
536572
result_tuple = self.db.query_one(
@@ -546,6 +582,32 @@ def protein_id(self):
546582
else:
547583
return None
548584

585+
@memoized_property
586+
def protein(self):
587+
"""
588+
:class:`Protein` view object for this transcript's encoded protein,
589+
or ``None`` if this transcript is non-coding.
590+
"""
591+
from .protein import Protein
592+
593+
if not self.protein_id:
594+
return None
595+
protein_version = None
596+
if self.db.column_exists("CDS", "protein_version"):
597+
result = self.db.query_one(
598+
select_column_names=["protein_version"],
599+
filter_column="transcript_id",
600+
filter_value=self.id,
601+
feature="CDS",
602+
distinct=True,
603+
required=False,
604+
)
605+
if result and result[0]:
606+
protein_version = int(result[0])
607+
return Protein(
608+
protein_id=self.protein_id, protein_version=protein_version
609+
)
610+
549611
@memoized_property
550612
def protein_sequence(self):
551613
if self.protein_id:

pyensembl/version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
__version__ = "2.6.13"
1+
__version__ = "2.7.0"
22

33
def print_version():
44
print(f"v{__version__}")

0 commit comments

Comments
 (0)