From 702e0d437ab57c4e33d323b9df593c440271bba1 Mon Sep 17 00:00:00 2001 From: Joao Cardoso Date: Tue, 10 Mar 2015 13:30:51 +0100 Subject: [PATCH 01/10] Mutations and Features use the same zero-based interval. All tests passing. --- co/component.py | 44 ++++++++++++++++++++++---------------------- co/feature.py | 2 +- co/mutation.py | 4 +--- 3 files changed, 24 insertions(+), 26 deletions(-) diff --git a/co/component.py b/co/component.py index 940ad7f..10517a3 100644 --- a/co/component.py +++ b/co/component.py @@ -2,7 +2,6 @@ from functools import reduce import logging -from Bio import Alphabet from Bio.Seq import Seq import operator from Bio.SeqFeature import FeatureLocation, SeqFeature @@ -10,7 +9,7 @@ import six from co.difference import Diff -from co.feature import Feature, ComponentFeatureSet, Source +from co.feature import Feature, ComponentFeatureSet from co.translation import OverlapError, MutableTranslationTable @@ -174,7 +173,10 @@ def _translate_feature(feature, component, tt=None): # TODO create a "broken reference" object and re-attach here return feature._shift(offset, component) - def mutate(self, mutations, strict=True): + def _reallocate(self, feature, start, stop): + return feature._move(start, stop) + + def mutate(self, mutations, strict=True, reposition_func=None, translate_function=None): """ Creates a copy of this :class:`Component` and applies all ``mutations`` in order. @@ -195,6 +197,13 @@ def mutate(self, mutations, strict=True): handle. """ + + if reposition_func is None: + reposition_func = self._reallocate + + if translate_function is None: + translate_function = Component._translate_feature + component = Component(seq=self._seq, parent=self) # TODO use __new__ instead features = component.features tt = MutableTranslationTable(size=len(self._seq)) @@ -252,8 +261,6 @@ def mutate(self, mutations, strict=True): logging.info('features in mutation: {}'.format(affected_features)) for feature in affected_features | changed_features: - # logging.debug(list(range(len(self.sequence)))) - # logging.debug(list(tt)) logging.info('{} with sequence "{}" affected by {}.'.format(feature, feature.seq, mutation)) if feature.end < mutation.start or feature.start > mutation.end: @@ -264,31 +271,24 @@ def mutate(self, mutations, strict=True): except KeyError: features.remove(feature) - # TODO find untranslated equivalents and replace when all is over. if mutation.start > feature.start: - if mutation.end < feature.end - 1 or mutation.size == 0: # mutation properly contained in feature. + if mutation.end < feature.end or mutation.size == 0: # mutation properly contained in feature. logging.debug('FMMF from {} to {}({})'.format( feature.start, # tt.ge(feature.start), feature.end, # tt.le(feature.end), feature.end - mutation.size + mutation.new_size)) - changed_features.add(feature._move(feature.start, - feature.end - mutation.size + mutation.new_size)) + changed_features.add(reposition_func(feature, feature.start, + feature.end - mutation.size + mutation.new_size)) else: - logging.debug('FMFM from {} to {}'.format( - tt[feature.start], - tt[mutation.start - 1])) # tt[mutation.start] ? - - changed_features.add(feature._move(feature.start, mutation.start)) - else: # mutation.start >= feature.start + logging.debug('FMFM from {} to {}'.format( tt[feature.start], tt[mutation.start - 1])) - if mutation.end < feature.end - 1: - logging.debug('MFMF from {} to {}'.format(tt[mutation.end + 1], tt[feature.end])) + changed_features.add(reposition_func(feature, feature.start, mutation.start)) + else: # if mutation.start >= feature.start + if mutation.end < feature.end: + logging.debug('MFMF from {} to {}'.format(tt[mutation.end], tt[feature.end-1])) - changed_features.add(feature._move(mutation.end + 1, feature.end)) - else: - pass # feature removed and not replaced. - logging.debug('MFFM feature removed') + changed_features.add(reposition_func(feature, mutation.end, feature.end)) logging.debug('applying mutation: at %s("%s") translating from %s("%s") delete %s bases and insert "%s"', translated_start, @@ -328,7 +328,7 @@ def mutate(self, mutations, strict=True): for feature in changed_features: logging.debug('changed: %s', feature) - translated_feature = Component._translate_feature(feature, component, tt) + translated_feature = translate_function(feature, component, tt) features.add(translated_feature) logging.debug('translating %s: %s(%s) "%s" -> %s(%s) "%s"', diff --git a/co/feature.py b/co/feature.py index 6e958db..c165eca 100644 --- a/co/feature.py +++ b/co/feature.py @@ -251,7 +251,7 @@ def __iter__(self): if self.parent_feature_set: # NOTE: this is where caching should kick in on any inherited implementation. keep_features = (f for f in self.parent_feature_set if f not in self.removed_features) translated_features = (self.component._translate_feature(f, self.component) for f in keep_features) - return heapq.merge({f.data for f in self._features}, translated_features) + return heapq.merge(sorted(f.data for f in self._features), sorted(translated_features)) else: return super(ComponentFeatureSet, self).__iter__() diff --git a/co/mutation.py b/co/mutation.py index feff8cc..63897f2 100644 --- a/co/mutation.py +++ b/co/mutation.py @@ -65,9 +65,7 @@ def end(self): # FIXME *dangerous* needs review. """ Computed end coordinate of the deletion. Use with caution. """ - if self.size in (0, 1): - return self.position - return self.position + self.size - 1 + return self.position + self.size @property def new_size(self): From fd01524d1f9856e087fd22196c8d71a9ffd409b8 Mon Sep 17 00:00:00 2001 From: Joao Cardoso Date: Tue, 10 Mar 2015 15:37:40 +0100 Subject: [PATCH 02/10] Remove translate_function --- co/component.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/co/component.py b/co/component.py index 10517a3..8450949 100644 --- a/co/component.py +++ b/co/component.py @@ -176,7 +176,7 @@ def _translate_feature(feature, component, tt=None): def _reallocate(self, feature, start, stop): return feature._move(start, stop) - def mutate(self, mutations, strict=True, reposition_func=None, translate_function=None): + def mutate(self, mutations, strict=True, reposition_func=None, ): """ Creates a copy of this :class:`Component` and applies all ``mutations`` in order. @@ -201,9 +201,6 @@ def mutate(self, mutations, strict=True, reposition_func=None, translate_functio if reposition_func is None: reposition_func = self._reallocate - if translate_function is None: - translate_function = Component._translate_feature - component = Component(seq=self._seq, parent=self) # TODO use __new__ instead features = component.features tt = MutableTranslationTable(size=len(self._seq)) @@ -281,7 +278,7 @@ def mutate(self, mutations, strict=True, reposition_func=None, translate_functio changed_features.add(reposition_func(feature, feature.start, feature.end - mutation.size + mutation.new_size)) else: - logging.debug('FMFM from {} to {}'.format( tt[feature.start], tt[mutation.start - 1])) + logging.debug('FMFM from {} to {}'.format(tt[feature.start], tt[mutation.start - 1])) changed_features.add(reposition_func(feature, feature.start, mutation.start)) else: # if mutation.start >= feature.start @@ -328,7 +325,7 @@ def mutate(self, mutations, strict=True, reposition_func=None, translate_functio for feature in changed_features: logging.debug('changed: %s', feature) - translated_feature = translate_function(feature, component, tt) + translated_feature = self._translate_feature(feature, component, tt) features.add(translated_feature) logging.debug('translating %s: %s(%s) "%s" -> %s(%s) "%s"', From 8be764e177b701fd7ba409b625b02ac155fca10a Mon Sep 17 00:00:00 2001 From: Joao Cardoso Date: Tue, 10 Mar 2015 16:10:08 +0100 Subject: [PATCH 03/10] Let the translation be handled by feature._shift --- co/component.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/co/component.py b/co/component.py index 8450949..e4bf882 100644 --- a/co/component.py +++ b/co/component.py @@ -176,7 +176,7 @@ def _translate_feature(feature, component, tt=None): def _reallocate(self, feature, start, stop): return feature._move(start, stop) - def mutate(self, mutations, strict=True, reposition_func=None, ): + def mutate(self, mutations, strict=True, reposition_func=None): """ Creates a copy of this :class:`Component` and applies all ``mutations`` in order. From 1427e12571f9ba2b0193d5b1631cfd1c7e0515ef Mon Sep 17 00:00:00 2001 From: lyschoening Date: Tue, 10 Mar 2015 16:15:17 +0100 Subject: [PATCH 04/10] Add feature.FeatureProxy --- co/feature.py | 64 ++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 63 insertions(+), 1 deletion(-) diff --git a/co/feature.py b/co/feature.py index c165eca..93e284e 100644 --- a/co/feature.py +++ b/co/feature.py @@ -224,6 +224,68 @@ def __gt__(self, other): return False +class FeatureProxy(object): + + def __init__(self, feature, component): + if isinstance(feature, FeatureProxy): + self._feature = feature.feature + self._component = feature.origin + else: + self._feature = feature + self._component = component + + if self._feature.component == component: + self.location = feature.location + else: + self.location = self._translate_location(feature, component) + + def __getattr__(self, item): + return getattr(self.feature, item) + + def __lt__(self, other): + if self.start < other.start: + return True + if self.start > other.start: + return False + if self.end < other.end: + return True + return False + + def __gt__(self, other): + if self.start > other.start: + return True + if self.start < other.start: + return False + if self.end > other.end: + return True + return False + + @staticmethod + def _translate_location(feature, component, tt=None): + if tt is None: + tt = component.tt(feature.component) + + offset = tt[feature.location.start] - feature.location.start + return feature.location._shift(offset) + + @property + def feature(self): + return self._feature + + @property + def origin(self): + return self._feature.component + + @property + def start(self): + return self.location.start + + @property + def end(self): + return self.location.end + + + class ComponentFeatureSet(FeatureSet): """ An extended version of :class:`FeatureSet` that binds to a :class:`Component` and inherits from any @@ -250,7 +312,7 @@ def __init__(self, component, removed_features=None, feature_class=None): def __iter__(self): if self.parent_feature_set: # NOTE: this is where caching should kick in on any inherited implementation. keep_features = (f for f in self.parent_feature_set if f not in self.removed_features) - translated_features = (self.component._translate_feature(f, self.component) for f in keep_features) + translated_features = (FeatureProxy(f, self.component) for f in keep_features) return heapq.merge(sorted(f.data for f in self._features), sorted(translated_features)) else: return super(ComponentFeatureSet, self).__iter__() From 389a6966bdd48538db8c9f8ec07f67b5703c96fe Mon Sep 17 00:00:00 2001 From: lyschoening Date: Tue, 10 Mar 2015 17:49:52 +0100 Subject: [PATCH 05/10] Refactor mutation; 1 test is failing --- co/component.py | 102 +++++++++++++++++++++++++--------------------- co/feature.py | 31 +++++++------- co/translation.py | 5 +++ 3 files changed, 76 insertions(+), 62 deletions(-) diff --git a/co/component.py b/co/component.py index 8450949..82c5724 100644 --- a/co/component.py +++ b/co/component.py @@ -6,13 +6,17 @@ import operator from Bio.SeqFeature import FeatureLocation, SeqFeature from Bio.SeqRecord import SeqRecord +from intervaltree import IntervalTree +import itertools import six from co.difference import Diff from co.feature import Feature, ComponentFeatureSet -from co.translation import OverlapError, MutableTranslationTable +from co.translation import OverlapError, MutableTranslationTable, shift_feature_location +# logging.basicConfig(level=logging.DEBUG) + class Component(object): """ .. attribute:: features @@ -139,7 +143,7 @@ def combine(cls, *components, **kwargs): offset = 0 for component in components: for feature in component.features: - combined.features.add(feature._shift(offset)) #move(offset + feature.position, component=combined)) + combined.features.add(Feature.translate(feature, offset, combined)) #move(offset + feature.position, component=combined)) offset += len(component) else: offset = 0 @@ -159,24 +163,22 @@ def combine(cls, *components, **kwargs): def parent(self): return self._parent - @staticmethod - def _translate_feature(feature, component, tt=None): - if tt is None: - tt = component.tt(feature.component) - # - # logging.debug('TRANSLATE {}'.format(feature)) - # logging.debug('TRANSLATE T {}'.format(list(enumerate(tt)))) - - offset = tt[feature.location.start] - feature.location.start - - # FIXME does not include ref and ref_db! - # TODO create a "broken reference" object and re-attach here - return feature._shift(offset, component) - - def _reallocate(self, feature, start, stop): - return feature._move(start, stop) - - def mutate(self, mutations, strict=True, reposition_func=None, ): + # @staticmethod + # def _translate_feature(feature, component, tt=None): + # if tt is None: + # tt = component.tt(feature.component) + # # + # # logging.debug('TRANSLATE {}'.format(feature)) + # # logging.debug('TRANSLATE T {}'.format(list(enumerate(tt)))) + # + # offset = tt[feature.location.start] - feature.location.start + # + # # FIXME does not include ref and ref_db! + # # TODO create a "broken reference" object and re-attach here + # return feature._shift(offset, component) + + + def mutate(self, mutations, strict=True, transform=None): """ Creates a copy of this :class:`Component` and applies all ``mutations`` in order. @@ -197,17 +199,19 @@ def mutate(self, mutations, strict=True, reposition_func=None, ): handle. """ - - if reposition_func is None: - reposition_func = self._reallocate + if transform is None: + transform = Feature.transform component = Component(seq=self._seq, parent=self) # TODO use __new__ instead features = component.features + tt = MutableTranslationTable(size=len(self._seq)) sequence = self.seq.tomutable() - changed_features = set() + changed_features = IntervalTree() + + # TODO check that mutations do not overlap # check that all mutations are in range: for mutation in mutations: @@ -227,7 +231,7 @@ def mutate(self, mutations, strict=True, reposition_func=None, ): if strict: translated_start = tt[mutation.position] - else: + else: # TODO get rid of non-strict mode try: translated_start = tt.ge(mutation.position) logging.debug('translated start: nonstrict=%s; strict=%s', translated_start, tt[mutation.position]) @@ -257,35 +261,36 @@ def mutate(self, mutations, strict=True, reposition_func=None, ): logging.debug('new features: {}'.format(list(features))) logging.info('features in mutation: {}'.format(affected_features)) - for feature in affected_features | changed_features: - logging.info('{} with sequence "{}" affected by {}.'.format(feature, feature.seq, mutation)) + for feature_start, feature_end, feature in itertools.chain( + changed_features.search(mutation.start, mutation.end + 1), # process these first, as we are adding to changed features in second step + ((feature.start, feature.end, feature) for feature in affected_features)): + assert not (feature_end < mutation.start or feature_start > mutation.end) - if feature.end < mutation.start or feature.start > mutation.end: - continue + logging.info('{} with sequence "{}" affected by {}.'.format(feature, feature.seq, mutation)) + # TODO move this into a previous loop: try: - changed_features.remove(feature) - except KeyError: + changed_features.removei(feature_start, feature_end, feature) + except ValueError: features.remove(feature) - if mutation.start > feature.start: - if mutation.end < feature.end or mutation.size == 0: # mutation properly contained in feature. + if mutation.start > feature_start: + if mutation.end < feature_end or mutation.size == 0: # mutation properly contained in feature. logging.debug('FMMF from {} to {}({})'.format( - feature.start, # tt.ge(feature.start), - feature.end, # tt.le(feature.end), - feature.end - mutation.size + mutation.new_size)) + feature_start, # tt.ge(feature.start), + feature_end, # tt.le(feature.end), + feature_end - mutation.size + mutation.new_size)) - changed_features.add(reposition_func(feature, feature.start, - feature.end - mutation.size + mutation.new_size)) + changed_features.addi(feature_start, + feature_end - mutation.size + mutation.new_size, + feature) else: - logging.debug('FMFM from {} to {}'.format(tt[feature.start], tt[mutation.start - 1])) - - changed_features.add(reposition_func(feature, feature.start, mutation.start)) + logging.debug('FMFM from {} to {}'.format(tt[feature_start], tt[mutation.start - 1])) + changed_features.addi(feature_start, mutation.start, feature) else: # if mutation.start >= feature.start - if mutation.end < feature.end: - logging.debug('MFMF from {} to {}'.format(tt[mutation.end], tt[feature.end-1])) - - changed_features.add(reposition_func(feature, mutation.end, feature.end)) + if mutation.end < feature_end: + logging.debug('MFMF from {} to {}'.format(tt[mutation.end], tt[feature_end-1])) + changed_features.addi(mutation.end, feature_end, feature) logging.debug('applying mutation: at %s("%s") translating from %s("%s") delete %s bases and insert "%s"', translated_start, @@ -323,9 +328,12 @@ def mutate(self, mutations, strict=True, reposition_func=None, ): component._mutations_tt = tt component.features = features - for feature in changed_features: + for new_start, new_end, feature in changed_features: + new_location = FeatureLocation(new_start, new_end, strand=feature.location.strand) + new_location = shift_feature_location(tt, new_location) + logging.debug('changed: %s', feature) - translated_feature = self._translate_feature(feature, component, tt) + translated_feature = transform(feature, new_location, component) features.add(translated_feature) logging.debug('translating %s: %s(%s) "%s" -> %s(%s) "%s"', diff --git a/co/feature.py b/co/feature.py index 93e284e..bf67349 100644 --- a/co/feature.py +++ b/co/feature.py @@ -165,23 +165,24 @@ def __eq__(self, other): self.ref == other.ref and \ self.ref_db == other.ref_db - def _move_to_location(self, location, component=None): + @classmethod + def transform(cls, feature, location, component): return Feature(location=location, - component=component or self.component, - type=self.type, - location_operator=self.location_operator, - id=self.id, - qualifiers=dict(self.qualifiers.items())) - + component=component or feature.component, + type=feature.type, + location_operator=feature.location_operator, + id=feature.id, + qualifiers=dict(feature.qualifiers.items())) # TODO refs? - def _move(self, start, end): - # TODO ref - new_location = FeatureLocation(start, end, strand=self.location.strand) - return self._move_to_location(new_location) - - def _shift(self, offset, component=None): - return self._move_to_location(self.location._shift(offset), component) + @classmethod + def translate(cls, feature, offset, component=None): + return Feature(location=feature.location._shift(offset), + component=component or feature.component, + type=feature.type, + location_operator=feature.location_operator, + id=feature.id, + qualifiers=dict(feature.qualifiers.items())) @property def seq(self): @@ -350,7 +351,7 @@ def remove(self, feature): def overlap(self, start, end, include_inherited=True): """ - Returns an iterator over all features in the collection that overlap the given range. + Returns a set of all features in the collection that overlap the given range. :param int start: overlap region start :param int end: overlap region end diff --git a/co/translation.py b/co/translation.py index 6f6b3a1..0cfb32a 100644 --- a/co/translation.py +++ b/co/translation.py @@ -487,3 +487,8 @@ def substitute(self, position, size, strict=True): :raises OverlapError: in various edge cases involving overlapping mutations, particularly in `strict` mode. """ self._insert_gap(position, size, size, strict) + + +def shift_feature_location(tt, location): + offset = tt[location.start] - location.start + return location._shift(offset) From 72fc89917a1c55274e16f1925e6fbe354e05b45d Mon Sep 17 00:00:00 2001 From: lyschoening Date: Tue, 10 Mar 2015 18:12:16 +0100 Subject: [PATCH 06/10] Simplify loop in mutate() --- co/component.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/co/component.py b/co/component.py index 82c5724..e35cb12 100644 --- a/co/component.py +++ b/co/component.py @@ -261,19 +261,22 @@ def mutate(self, mutations, strict=True, transform=None): logging.debug('new features: {}'.format(list(features))) logging.info('features in mutation: {}'.format(affected_features)) - for feature_start, feature_end, feature in itertools.chain( + for interval in itertools.chain( changed_features.search(mutation.start, mutation.end + 1), # process these first, as we are adding to changed features in second step - ((feature.start, feature.end, feature) for feature in affected_features)): - assert not (feature_end < mutation.start or feature_start > mutation.end) - - logging.info('{} with sequence "{}" affected by {}.'.format(feature, feature.seq, mutation)) + affected_features): + assert not (interval.end < mutation.start or interval.start > mutation.end) # TODO move this into a previous loop: try: - changed_features.removei(feature_start, feature_end, feature) + changed_features.remove(interval) + feature = interval.data except ValueError: + feature = interval features.remove(feature) + feature_start = interval.start + feature_end = interval.end + if mutation.start > feature_start: if mutation.end < feature_end or mutation.size == 0: # mutation properly contained in feature. logging.debug('FMMF from {} to {}({})'.format( From fa1146c7c18b5b75a16ded627b25630a92802109 Mon Sep 17 00:00:00 2001 From: lyschoening Date: Wed, 11 Mar 2015 11:37:42 +0100 Subject: [PATCH 07/10] Fix AttributeError --- co/component.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/co/component.py b/co/component.py index e35cb12..d9438bc 100644 --- a/co/component.py +++ b/co/component.py @@ -262,20 +262,21 @@ def mutate(self, mutations, strict=True, transform=None): logging.info('features in mutation: {}'.format(affected_features)) for interval in itertools.chain( - changed_features.search(mutation.start, mutation.end + 1), # process these first, as we are adding to changed features in second step + changed_features.search(mutation.start, mutation.end + 1), affected_features): - assert not (interval.end < mutation.start or interval.start > mutation.end) + # assert not (interval.end < mutation.start or interval.start > mutation.end) # TODO move this into a previous loop: try: changed_features.remove(interval) feature = interval.data + feature_start = interval.begin + feature_end = interval.end except ValueError: feature = interval features.remove(feature) - - feature_start = interval.start - feature_end = interval.end + feature_start = feature.start + feature_end = feature.end if mutation.start > feature_start: if mutation.end < feature_end or mutation.size == 0: # mutation properly contained in feature. From 500733a7977c4a85efa979e36619bb24572e0eaf Mon Sep 17 00:00:00 2001 From: lyschoening Date: Wed, 11 Mar 2015 11:41:12 +0100 Subject: [PATCH 08/10] Fix improper overlap search of changed_features in mutate() --- co/component.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/co/component.py b/co/component.py index d9438bc..67d8c68 100644 --- a/co/component.py +++ b/co/component.py @@ -262,9 +262,8 @@ def mutate(self, mutations, strict=True, transform=None): logging.info('features in mutation: {}'.format(affected_features)) for interval in itertools.chain( - changed_features.search(mutation.start, mutation.end + 1), + changed_features.search(mutation.start - 1, mutation.end), affected_features): - # assert not (interval.end < mutation.start or interval.start > mutation.end) # TODO move this into a previous loop: try: @@ -278,6 +277,8 @@ def mutate(self, mutations, strict=True, transform=None): feature_start = feature.start feature_end = feature.end + assert not (feature_end < mutation.start or feature_start > mutation.end) + if mutation.start > feature_start: if mutation.end < feature_end or mutation.size == 0: # mutation properly contained in feature. logging.debug('FMMF from {} to {}({})'.format( From 7fc1d7b53d7670ac81a77ae44e78e9980ddc1932 Mon Sep 17 00:00:00 2001 From: lyschoening Date: Wed, 11 Mar 2015 13:32:41 +0100 Subject: [PATCH 09/10] Add more tests for FeatureProxy (one failing) --- co/component.py | 29 ++++++++++------------------- co/feature.py | 11 +++++++++-- co/translation.py | 10 ++++++++++ tests/test_component.py | 36 ++++++++++++++++++++++++++++++++++++ 4 files changed, 65 insertions(+), 21 deletions(-) diff --git a/co/component.py b/co/component.py index 67d8c68..6be4c77 100644 --- a/co/component.py +++ b/co/component.py @@ -12,7 +12,7 @@ from co.difference import Diff from co.feature import Feature, ComponentFeatureSet -from co.translation import OverlapError, MutableTranslationTable, shift_feature_location +from co.translation import OverlapError, MutableTranslationTable, shift_feature_location, TranslationTableChain # logging.basicConfig(level=logging.DEBUG) @@ -121,7 +121,14 @@ def seq(self): def tt(self, ancestor=None): if ancestor in (None, self._parent): return self._mutations_tt - raise KeyError("Translation table from {} to {} cannot be accessed.".format(repr(self), repr(ancestor))) + + try: + ancestors = list(self.get_lineage()) + i = ancestors.index(ancestor) + ancestors = [self] + ancestors[:i] + return TranslationTableChain(a.tt() for a in ancestors) + except ValueError: + raise KeyError("Translation table from {} to {} cannot be accessed.".format(repr(self), repr(ancestor))) @classmethod def combine(cls, *components, **kwargs): @@ -158,26 +165,10 @@ def combine(cls, *components, **kwargs): return combined - @property def parent(self): return self._parent - # @staticmethod - # def _translate_feature(feature, component, tt=None): - # if tt is None: - # tt = component.tt(feature.component) - # # - # # logging.debug('TRANSLATE {}'.format(feature)) - # # logging.debug('TRANSLATE T {}'.format(list(enumerate(tt)))) - # - # offset = tt[feature.location.start] - feature.location.start - # - # # FIXME does not include ref and ref_db! - # # TODO create a "broken reference" object and re-attach here - # return feature._shift(offset, component) - - def mutate(self, mutations, strict=True, transform=None): """ Creates a copy of this :class:`Component` and applies all ``mutations`` in order. @@ -202,7 +193,7 @@ def mutate(self, mutations, strict=True, transform=None): if transform is None: transform = Feature.transform - component = Component(seq=self._seq, parent=self) # TODO use __new__ instead + component = self.__class__(seq=self._seq, parent=self) features = component.features tt = MutableTranslationTable(size=len(self._seq)) diff --git a/co/feature.py b/co/feature.py index bf67349..162ceda 100644 --- a/co/feature.py +++ b/co/feature.py @@ -228,12 +228,12 @@ def __gt__(self, other): class FeatureProxy(object): def __init__(self, feature, component): + self._component = component + if isinstance(feature, FeatureProxy): self._feature = feature.feature - self._component = feature.origin else: self._feature = feature - self._component = component if self._feature.component == component: self.location = feature.location @@ -261,6 +261,9 @@ def __gt__(self, other): return True return False + def __repr__(self): + return 'Proxy({})/{}'.format(self.location, repr(self._feature)) + @staticmethod def _translate_location(feature, component, tt=None): if tt is None: @@ -277,6 +280,10 @@ def feature(self): def origin(self): return self._feature.component + @property + def component(self): + return self._component + @property def start(self): return self.location.start diff --git a/co/translation.py b/co/translation.py index 0cfb32a..ebc642a 100644 --- a/co/translation.py +++ b/co/translation.py @@ -489,6 +489,16 @@ def substitute(self, position, size, strict=True): self._insert_gap(position, size, size, strict) +class TranslationTableChain(object): + def __init__(self, tables): + self.tables = tuple(tables) + + def __getitem__(self, position): + for table in reversed(self.tables): + position = table[position] + return position + + def shift_feature_location(tt, location): offset = tt[location.start] - location.start return location._shift(offset) diff --git a/tests/test_component.py b/tests/test_component.py index f0f9f37..c192326 100644 --- a/tests/test_component.py +++ b/tests/test_component.py @@ -128,6 +128,42 @@ def test_quickstart_feature_inherit(self): Feature(new_slogan, FeatureLocation(10, 20))], list(new_slogan.features)) self.assertEqual(['Co', 'components'], [str(f.seq) for f in new_slogan.features]) + def test_mutate_inherit_feature_twice(self): + + source = Component('potatoapplecarrot', features=[ + SeqFeature(FeatureLocation(6, 11), type='fruit'), + SeqFeature(FeatureLocation(11, 17), type='vegetable')]) + + self.assertEqual(Seq('apple'), next(iter(source.features)).seq) + c2 = source.mutate([DEL(7, 2)]) + + self.assertEqual('potatoalecarrot', str(c2.seq)) + self.assertEqual({Seq('ale'), Seq('carrot')}, {f.seq for f in c2.features}) + + c3 = c2.mutate([INS(7, 'pp'), INS(6, 'pine')]) + + self.assertEqual('potatopineapplecarrot', str(c3.seq)) + self.assertEqual({Seq('apple'), Seq('carrot')}, {f.seq for f in c3.features}) + self.assertEqual({Seq('apple'), Seq('carrot')}, {c3.seq[f.start:f.end] for f in c3.features}) + + + # FIXME this test fails: + @unittest.SkipTest + def test_inherit_feature_shift(self): + source = Component('12345aaaaa67890', features=[ + SeqFeature(FeatureLocation(5, 10), type='foo')]) + + self.assertEqual(Seq('aaaaa'), next(iter(source.features)).seq) + + c2 = source.mutate([INS(1, 'XXX')]) + self.assertEqual(Seq('aaaaa'), next(iter(c2.features)).seq) + self.assertEqual({Seq('aaaaa')}, {c2.seq[f.start:f.end] for f in c2.features}) + + c3 = c2.mutate([]) + print(c2.features) + print(c3.features) # why is this empty? + self.assertEqual(Seq('aaaaa'), next(iter(c3.features)).seq) + self.assertEqual({Seq('aaaaa')}, {c3.seq[f.start:f.end] for f in c3.features}) @unittest.SkipTest def test_mutate_break_source(self): From ade82e5f1780791f81b393de46369aa5b9f7fa56 Mon Sep 17 00:00:00 2001 From: Joao Cardoso Date: Wed, 11 Mar 2015 16:07:34 +0100 Subject: [PATCH 10/10] Fix constructor to keep the same feature_class --- co/component.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/co/component.py b/co/component.py index 67d8c68..7dc8701 100644 --- a/co/component.py +++ b/co/component.py @@ -202,7 +202,7 @@ def mutate(self, mutations, strict=True, transform=None): if transform is None: transform = Feature.transform - component = Component(seq=self._seq, parent=self) # TODO use __new__ instead + component = Component(seq=self._seq, parent=self, feature_class=self.features._feature_class) # TODO use __new__ instead features = component.features tt = MutableTranslationTable(size=len(self._seq))