Skip to content

Commit

Permalink
added loopout labels; bumped version
Browse files Browse the repository at this point in the history
  • Loading branch information
dave-doty committed Aug 25, 2020
1 parent 23d5a1c commit 9a969ce
Show file tree
Hide file tree
Showing 2 changed files with 88 additions and 18 deletions.
52 changes: 34 additions & 18 deletions scadnano/scadnano.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand All @@ -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}'
Expand All @@ -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}')
Expand Down Expand Up @@ -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).
Expand Down Expand Up @@ -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')
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
"""
Expand Down Expand Up @@ -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)
Expand Down
54 changes: 54 additions & 0 deletions tests/scadnano_tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down

0 comments on commit 9a969ce

Please sign in to comment.