diff --git a/src/pydna/dseq.py b/src/pydna/dseq.py index 3addefa9..b7a9a5ae 100644 --- a/src/pydna/dseq.py +++ b/src/pydna/dseq.py @@ -29,8 +29,9 @@ from pydna.utils import cseguid as _cseg from pydna.utils import rc as _rc from pydna.utils import flatten as _flatten -from pydna.common_sub_strings import common_sub_strings as _common_sub_strings +from pydna.utils import cuts_overlap as _cuts_overlap +from pydna.common_sub_strings import common_sub_strings as _common_sub_strings from Bio.Restriction import RestrictionBatch as _RestrictionBatch from Bio.Restriction import CommOnly @@ -1517,7 +1518,8 @@ def right_end_position(self) -> tuple[int, int]: return len(self), len(self) - self.watson_ovhg() def apply_cut(self, left_cut, right_cut): - + if _cuts_overlap(left_cut, right_cut, len(self)): + raise ValueError("Cuts overlap") left_watson, left_crick = left_cut[0] if left_cut is not None else self.left_end_position() ovhg = left_cut[1].ovhg if left_cut is not None else self.ovhg right_watson, right_crick = right_cut[0] if right_cut is not None else self.right_end_position() diff --git a/src/pydna/utils.py b/src/pydna/utils.py index f66d797b..da2a1b38 100644 --- a/src/pydna/utils.py +++ b/src/pydna/utils.py @@ -800,6 +800,29 @@ def eq(*args, **kwargs): same = False return same +def cuts_overlap(left_cut, right_cut, seq_len): + # Special cases: + if left_cut is None or right_cut is None or left_cut == right_cut: + return False + + # This block of code would not be necessary if the cuts were + # initially represented like this + (left_watson, _), enz1 = left_cut + (right_watson, _), enz2 = right_cut + left_crick = left_watson - enz1.ovhg + right_crick = right_watson - enz2.ovhg + if left_crick >= seq_len: + left_crick -= seq_len + left_watson -= seq_len + if right_crick >= seq_len: + right_crick -= seq_len + right_watson -= seq_len + + # Convert into ranges x and y and see if ranges overlap + x = sorted([left_watson, left_crick]) + y = sorted([right_watson, right_crick]) + return (x[1] > y[0]) != (y[1] < x[0]) + if __name__ == "__main__": cached = _os.getenv("pydna_cached_funcs", "") diff --git a/tests/test_module_dseq.py b/tests/test_module_dseq.py index 830c184b..a3602e8e 100644 --- a/tests/test_module_dseq.py +++ b/tests/test_module_dseq.py @@ -847,9 +847,37 @@ def test_apply_cut(): EcoRI_cut_2 = ((11, 15), type('DynamicClass', (), {'ovhg': -4})()) assert seq.apply_cut(EcoRI_cut, EcoRI_cut_2) == Dseq.from_full_sequence_and_overhangs('AATTCaaGAATT', watson_ovhg=-4, crick_ovhg=-4) - # TODO: Overlapping cuts should return an error - EcoRI_cut_2 = ((4, 8), type('DynamicClass', (), {'ovhg': -4})()) - assert seq.apply_cut(EcoRI_cut, EcoRI_cut_2) + # Overlapping cuts should return an error + seq = Dseq('aaGAATTCaa', circular=True) + first_cuts = [ + ((3, 7), type('DynamicClass', (), {'ovhg': -4})()), + ((7, 3), type('DynamicClass', (), {'ovhg': 4})()), + # Spanning the origin + ((9, 8), type('DynamicClass', (), {'ovhg': -8})()), + ((8, 9), type('DynamicClass', (), {'ovhg': 8})()), + ] + overlapping_cuts = [ + ((4, 8), type('DynamicClass', (), {'ovhg': -4})()), + ((2, 6), type('DynamicClass', (), {'ovhg': -4})()), + ((2, 8), type('DynamicClass', (), {'ovhg': -4})()), + ((8, 4), type('DynamicClass', (), {'ovhg': 4})()), + ((6, 2), type('DynamicClass', (), {'ovhg': 4})()), + ((8, 2), type('DynamicClass', (), {'ovhg': 4})()), + # Spanning the origin + ((7, 6), type('DynamicClass', (), {'ovhg': -8})()), + ((6, 7), type('DynamicClass', (), {'ovhg': 8})()), + ] + + for first_cut in first_cuts: + for second_cut in overlapping_cuts: + try: + seq.apply_cut(first_cut, second_cut) + except ValueError as e: + assert e.args[0] == 'Cuts overlap' + else: + print(first_cut, second_cut) + assert False, 'Expected ValueError' + if __name__ == "__main__": pytest.main([__file__, "-vv", "-s"])