Skip to content

Commit b85bab7

Browse files
committed
Switch from None to raising exceptions in HCV.
1 parent d5da55b commit b85bab7

File tree

6 files changed

+119
-102
lines changed

6 files changed

+119
-102
lines changed

.idea/pyvdrm.iml

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyvdrm/asi2.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from functools import reduce, total_ordering
66
from pyparsing import (Literal, nums, Word, Forward, Optional, Regex,
77
infixNotation, delimitedList, opAssoc, ParseException)
8-
from pyvdrm.drm import AsiExpr, AsiBinaryExpr, AsiUnaryExpr, DRMParser
8+
from pyvdrm.drm import AsiExpr, AsiBinaryExpr, DRMParser
99
from pyvdrm.vcf import MutationSet
1010

1111

@@ -79,7 +79,7 @@ class Negate(AsiExpr):
7979
def __call__(self, mutations):
8080
child_score = self.children[0](mutations)
8181
if child_score is None:
82-
return Score(True, []) # TODO: propagate negative residues
82+
return Score(True, []) # TODO: propagate negative residues
8383
return Score(not child_score.score, child_score.residues)
8484

8585

pyvdrm/drm.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@ class AsiParseError(Exception):
88
pass
99

1010

11+
class MissingPositionError(Exception):
12+
pass
13+
14+
1115
class DRMParser(metaclass=ABCMeta):
1216
"""abstract class for DRM rule parsers/evaluators"""
1317

pyvdrm/hcvr.py

Lines changed: 46 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -4,46 +4,22 @@
44

55
from functools import reduce, total_ordering
66
from pyparsing import (Literal, nums, Word, Forward, Optional, Regex,
7-
infixNotation, delimitedList, opAssoc, alphas, ParseException)
8-
from pyvdrm.drm import AsiExpr, AsiBinaryExpr, AsiUnaryExpr, DRMParser
7+
infixNotation, delimitedList, opAssoc, ParseException)
8+
9+
from pyvdrm.drm import MissingPositionError
10+
from pyvdrm.drm import AsiExpr, AsiBinaryExpr, DRMParser
911
from pyvdrm.vcf import MutationSet
1012

13+
1114
def update_flags(fst, snd):
1215
for k in snd:
1316
if k in fst:
1417
fst[k].append(snd[k])
1518
else:
16-
fst[k] = snd[k] # this chould be achieved with a defaultdict
19+
fst[k] = snd[k] # this could be achieved with a defaultdict
1720
return fst
1821

1922

20-
def maybe_foldl(func, noneable):
21-
"""Safely fold a function over a potentially empty list of
22-
potentially null values"""
23-
if noneable is None:
24-
return None
25-
clean = [x for x in noneable if x is not None]
26-
if not clean:
27-
return None
28-
return reduce(func, clean)
29-
30-
31-
def maybe_map(func, noneable):
32-
if noneable is None:
33-
return None
34-
r_list = []
35-
for x in noneable:
36-
if x is None:
37-
continue
38-
result = func(x)
39-
if result is None:
40-
continue
41-
r_list.append(result)
42-
if not r_list:
43-
return None
44-
return r_list
45-
46-
4723
@total_ordering
4824
class Score(object):
4925
"""Encapsulate a score and the residues that support it"""
@@ -65,13 +41,15 @@ def __init__(self, score, residues, flags=None):
6541

6642
def __add__(self, other):
6743
flags = update_flags(self.flags, other.flags)
68-
return Score(self.score + other.score, self.residues | other.residues,
69-
flags)
44+
return Score(self.score + other.score,
45+
self.residues | other.residues,
46+
flags)
7047

7148
def __sub__(self, other):
7249
flags = update_flags(self.flags, other.flags)
73-
return Score(self.score - other.score, self.residues | other.residues,
74-
flags)
50+
return Score(self.score - other.score,
51+
self.residues | other.residues,
52+
flags)
7553

7654
def __repr__(self):
7755
return "Score({!r}, {!r})".format(self.score, self.residues)
@@ -166,11 +144,11 @@ def __call__(self, mutations):
166144
if len(self.children) == 4:
167145
operation, _, flag, _ = self.children
168146
flags[flag] = []
169-
score = 0 # should be None
147+
score = 0 # should be None
170148

171149
elif len(self.children) == 3:
172150
operation, minus, score = self.children
173-
if minus != '-': # this is parsing the expression twice, refactor
151+
if minus != '-': # this is parsing the expression twice, refactor
174152
raise ValueError
175153
score = -1 * int(score)
176154

@@ -197,10 +175,22 @@ class ScoreList(AsiExpr):
197175
def __call__(self, mutations):
198176
operation, *rest = self.children
199177
if operation == 'MAX':
200-
return maybe_foldl(max, [f(mutations) for f in rest])
201-
202-
# the default operation is sum
203-
return maybe_foldl(lambda x, y: x+y, [f(mutations) for f in self.children])
178+
terms = rest
179+
func = max
180+
else:
181+
# the default operation is sum
182+
terms = self.children
183+
func = sum
184+
scores = [f(mutations) for f in terms]
185+
matched_scores = [score.score for score in scores if score.score]
186+
residues = reduce(lambda x, y: x | y,
187+
(score.residues for score in scores))
188+
flags = {}
189+
for score in scores:
190+
flags.update(score.flags)
191+
return Score(bool(matched_scores) and func(matched_scores),
192+
residues,
193+
flags)
204194

205195

206196
class SelectFrom(AsiExpr):
@@ -215,14 +205,12 @@ def __call__(self, mutations):
215205
operation, *rest = self.children
216206
# the head of the arg list must be an equality expression
217207

218-
scored = list(maybe_map(lambda f: f(mutations), rest))
219-
passing = len(scored)
208+
scored = [f(mutations) for f in rest]
209+
passing = sum(bool(score.score) for score in scored)
220210

221-
if operation(passing):
222-
return Score(True, maybe_foldl(
223-
lambda x, y: x.residues.union(y.residues), scored))
224-
else:
225-
return None
211+
return Score(operation(passing),
212+
reduce(lambda x, y: x | y,
213+
(item.residues for item in scored)))
226214

227215

228216
class AsiScoreCond(AsiExpr):
@@ -232,7 +220,7 @@ class AsiScoreCond(AsiExpr):
232220

233221
def __call__(self, args):
234222
"""Score conditions evaluate a list of expressions and sum scores"""
235-
return maybe_foldl(lambda x, y: x+y, map(lambda x: x(args), self.children))
223+
return sum((f(args) for f in self.children), Score(False, set()))
236224

237225

238226
class AsiMutations(object):
@@ -241,21 +229,26 @@ class AsiMutations(object):
241229
def __init__(self, _label=None, _pos=None, args=None):
242230
"""Initialize set of mutations from a potentially ambiguous residue
243231
"""
244-
self.mutations = args and MutationSet(''.join(args))
232+
self.mutations = MutationSet(''.join(args))
245233

246234
def __repr__(self):
247235
if self.mutations is None:
248236
return "AsiMutations()"
249237
return "AsiMutations(args={!r})".format(str(self.mutations))
250238

251239
def __call__(self, env):
240+
is_found = False
252241
for mutation_set in env:
242+
is_found |= mutation_set.pos == self.mutations.pos
253243
intersection = self.mutations.mutations & mutation_set.mutations
254244
if len(intersection) > 0:
255245
return Score(True, intersection)
256246

257-
# the mutationset has no members in the environment
258-
return None
247+
if not is_found:
248+
# Some required positions were not found in the environment.
249+
raise MissingPositionError('Missing position {}.'.format(
250+
self.mutations.pos))
251+
return Score(False, set())
259252

260253

261254
class HCVR(DRMParser):
@@ -305,8 +298,8 @@ def parser(self, rule):
305298
selectstatement = select + select_quantifier + from_ + residue_list
306299
selectstatement.setParseAction(SelectFrom)
307300

308-
bool_ = Literal('TRUE').suppress().setParseAction(BoolTrue) |\
309-
Literal('FALSE').suppress().setParseAction(BoolFalse)
301+
bool_ = (Literal('TRUE').suppress().setParseAction(BoolTrue) |
302+
Literal('FALSE').suppress().setParseAction(BoolFalse))
310303

311304
booleancondition = Forward()
312305
condition = residue | excludestatement | selectstatement | bool_

pyvdrm/tests/test_asi2.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import os
12
import unittest
23

34
from pyparsing import ParseException
@@ -128,7 +129,9 @@ def test_parse_exception_multiline(self):
128129

129130
class TestActualRules(unittest.TestCase):
130131
def test_hivdb_rules_parse(self):
131-
for line in open("pyvdrm/tests/HIVDB.rules"):
132+
folder = os.path.dirname(__file__)
133+
rules_file = os.path.join(folder, 'HIVDB.rules')
134+
for line in open(rules_file):
132135
r = ASI2(line)
133136
self.assertEqual(line, r.rule)
134137

0 commit comments

Comments
 (0)