From 9a969ce3a538f342339d824d5198c5c0ddd06a58 Mon Sep 17 00:00:00 2001 From: David Doty Date: Tue, 25 Aug 2020 16:00:41 -0700 Subject: [PATCH] added loopout labels; bumped version --- scadnano/scadnano.py | 52 +++++++++++++++++++++++++-------------- tests/scadnano_tests.py | 54 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 88 insertions(+), 18 deletions(-) diff --git a/scadnano/scadnano.py b/scadnano/scadnano.py index c63a7396..80f364c0 100644 --- a/scadnano/scadnano.py +++ b/scadnano/scadnano.py @@ -44,7 +44,7 @@ # commented out for now to support Python 3.6, which does not support this feature # from __future__ import annotations -__version__ = "0.11.1" # version line; WARNING: do not remove or change this line or comment +__version__ = "0.11.2" # version line; WARNING: do not remove or change this line or comment import dataclasses from abc import abstractmethod, ABC @@ -1412,7 +1412,8 @@ class Domain(_JSONSerializable): The total number of bases at this offset is num_insertions+1.""" label: Any = None - """Generic "label" object to associate to this :any:`Domain`. + """ + Generic "label" object to associate to this :any:`Domain`. Useful for associating extra information with the :any:`Domain` that will be serialized, for example, for DNA sequence design. It must be an object (e.g., a dict or primitive type such as str or int) @@ -1427,7 +1428,7 @@ class Domain(_JSONSerializable): def __post_init__(self): self._check_start_end() - def __repr__(self): + def __repr__(self) -> str: rep = (f'Domain(helix={self.helix}' f', forward={self.forward}' f', start={self.start}' @@ -1437,17 +1438,17 @@ def __repr__(self): ')' return rep - def __str__(self): + def __str__(self) -> str: return repr(self) def strand(self) -> 'Strand': # remove quotes when Python 3.6 support dropped return self._parent_strand - def set_label(self, label): + def set_label(self, label) -> None: """Sets label of this :any:`Domain`.""" self.label = label - def _check_start_end(self): + def _check_start_end(self) -> None: if self.start >= self.end: raise StrandError(self._parent_strand, f'start = {self.start} must be less than end = {self.end}') @@ -1761,20 +1762,36 @@ class Loopout(_JSONSerializable): length: int """Length (in DNA bases) of this Loopout.""" + label: Any = None + """ + Generic "label" object to associate to this :any:`Loopout`. + + Useful for associating extra information with the :any:`Loopout` that will be serialized, for example, + for DNA sequence design. It must be an object (e.g., a dict or primitive type such as str or int) + that is naturally JSON serializable. (Calling ``json.dumps`` on the object should succeed without + having to specify a custom encoder.) + """ + # not serialized; for efficiency # remove quotes when Python 3.6 support dropped _parent_strand: 'Strand' = field(init=False, repr=False, compare=False, default=None) - def to_json_serializable(self, suppress_indent: bool = True, **kwargs): + def to_json_serializable(self, suppress_indent: bool = True, **kwargs: dict) -> Union[dict, NoIndent]: dct = {loopout_key: self.length} - return NoIndent(dct) + if self.label is not None: + dct[domain_label_key] = self.label + return NoIndent(dct) if suppress_indent else dct - def __repr__(self): + def __repr__(self) -> str: return f'Loopout({self.length})' - def __str__(self): + def __str__(self) -> str: return repr(self) + def set_label(self, label: Any) -> None: + """Sets label of this :any:`Loopout`.""" + self.label = label + @staticmethod def is_loopout() -> bool: """Indicates if this is a :any:`Loopout` (always true). @@ -1819,12 +1836,13 @@ def get_seq_start_idx(self) -> int: return self_seq_idx_start @staticmethod - def from_json(json_map) -> 'Loopout': # remove quotes when Python 3.6 support dropped + def from_json(json_map: dict) -> 'Loopout': # remove quotes when Python 3.6 support dropped # XXX: this should never fail since we detect whether to call this from_json by the presence # of a length key in json_map length_str = mandatory_field(Loopout, json_map, loopout_key) length = int(length_str) - return Loopout(length=length) + label = json_map.get(domain_label_key) + return Loopout(length=length, label=label) _wctable = str.maketrans('ACGTacgt', 'TGCAtgca') @@ -1925,7 +1943,7 @@ def __init__(self, design: 'Design', helix: int, offset: int): self.design: Design = design self.current_helix: int = helix self.current_offset: int = offset - self.loopout_length: Optional[int] = None + # self.loopout_length: Optional[int] = None self._strand: Optional[Strand] = None self.just_moved_to_helix: bool = True self.last_domain: Optional[Domain] = None @@ -1980,8 +1998,9 @@ def loopout(self, helix: int, length: int, offset: Optional[int] = None, move: O Mutually excusive with `offset`. :return: self """ - self.loopout_length = length - return self.cross(helix, offset=offset, move=move) + self.cross(helix, offset=offset, move=move) + self.design.append_domain(self._strand, Loopout(length)) + return self def move(self, delta: int) -> 'StrandBuilder': # remove quotes when Python 3.6 support dropped """ @@ -2047,10 +2066,7 @@ def to(self, offset: int) -> 'StrandBuilder': # remove quotes when Python 3.6 s domain = Domain(helix=self.current_helix, forward=forward, start=start, end=end) self.last_domain = domain if self._strand is not None: - if self.loopout_length is not None: - self.design.append_domain(self._strand, Loopout(self.loopout_length)) self.design.append_domain(self._strand, domain) - self.loopout_length = None else: self._strand = Strand(domains=[domain]) self.design.add_strand(self._strand) diff --git a/tests/scadnano_tests.py b/tests/scadnano_tests.py index 332de0ab..92f3222d 100644 --- a/tests/scadnano_tests.py +++ b/tests/scadnano_tests.py @@ -7,6 +7,8 @@ import json from typing import Iterable +from docutils.nodes import label + import scadnano as sc import scadnano.origami_rectangle as rect import scadnano.modifications as mod @@ -36,6 +38,58 @@ def setUp(self): helices = [sc.Helix(max_offset=100) for _ in range(6)] self.design_6helix = sc.Design(helices=helices, strands=[], grid=sc.square) + def test_strand__loopouts_with_labels(self): + design = self.design_6helix + sb = design.strand(0, 0) + sb.to(10) + sb.loopout(1, 8) + sb.with_domain_label('loop0') + sb.to(5) + sb.with_domain_label('dom1') + sb.cross(2) + sb.to(10) + sb.with_domain_label('dom2') + sb.loopout(3, 12) + sb.with_domain_label('loop1') + sb.to(5) + expected_strand = sc.Strand([ + sc.Domain(0, True, 0, 10), + sc.Loopout(8, label='loop0'), + sc.Domain(1, False, 5, 10, label='dom1'), + sc.Domain(2, True, 5, 10, label='dom2'), + sc.Loopout(12, label='loop1'), + sc.Domain(3, False, 5, 10), + ]) + self.assertEqual(1, len(design.strands)) + self.assertEqual(expected_strand, design.strands[0]) + + def test_strand__loopouts_with_labels_to_json(self): + design = self.design_6helix + sb = design.strand(0, 0) + sb.to(10) + sb.loopout(1, 8) + sb.with_domain_label('loop0') + sb.to(5) + sb.with_domain_label('dom1') + sb.cross(2) + sb.to(10) + sb.with_domain_label('dom2') + sb.loopout(3, 12) + sb.with_domain_label('loop1') + sb.to(5) + design_json_map = design.to_json_serializable(suppress_indent=False) + design_from_json = sc.Design.from_scadnano_json_map(design_json_map) + expected_strand = sc.Strand([ + sc.Domain(0, True, 0, 10), + sc.Loopout(8, label='loop0'), + sc.Domain(1, False, 5, 10, label='dom1'), + sc.Domain(2, True, 5, 10, label='dom2'), + sc.Loopout(12, label='loop1'), + sc.Domain(3, False, 5, 10), + ]) + self.assertEqual(1, len(design_from_json.strands)) + self.assertEqual(expected_strand, design_from_json.strands[0]) + def test_strand__0_0_to_10_cross_1_to_5(self): design = self.design_6helix sb = design.strand(0, 0)