From 602575d1e17e5e19a47c73030136a7b2aa4c8b89 Mon Sep 17 00:00:00 2001 From: David Doty Date: Tue, 18 Jul 2023 08:34:37 -0700 Subject: [PATCH] closes #261: change `label` type to `str` --- scadnano/scadnano.py | 220 +++++++++++++++++++++---------------------- 1 file changed, 107 insertions(+), 113 deletions(-) diff --git a/scadnano/scadnano.py b/scadnano/scadnano.py index f69f4f68..2d24a0ca 100644 --- a/scadnano/scadnano.py +++ b/scadnano/scadnano.py @@ -51,10 +51,9 @@ """ # needed to use forward annotations: https://docs.python.org/3/whatsnew/3.7.html#whatsnew37-pep563 -# commented out for now to support Py3.6, which does not support this feature -# from __future__ import annotations +from __future__ import annotations -__version__ = "0.17.8" # version line; WARNING: do not remove or change this line or comment +__version__ = "0.18.0" # version line; WARNING: do not remove or change this line or comment import collections import dataclasses @@ -66,7 +65,7 @@ from builtins import ValueError from dataclasses import dataclass, field, InitVar, replace from typing import Iterator, Tuple, List, Sequence, Iterable, Set, Dict, Union, Optional, Type, cast, Any, \ - TypeVar, Generic, Callable, AbstractSet + TypeVar, Callable, AbstractSet from collections import defaultdict, OrderedDict, Counter import sys import os.path @@ -1555,7 +1554,7 @@ class Helix(_JSONSerializable): """Name of the :any:`HelixGroup` to which this :any:`Helix` belongs.""" # for optimization; list of domains on that Helix - _domains: List['Domain'] = field(default_factory=list) + _domains: List[Domain] = field(default_factory=list) def __post_init__(self) -> None: if self.major_tick_start is None: # type: ignore @@ -1697,7 +1696,7 @@ def calculate_major_ticks(self, grid: Grid) -> List[int]: return list(range(self.major_tick_start, self.max_offset + 1, distance)) @property - def domains(self) -> List['Domain']: + def domains(self) -> List[Domain]: """ Return :any:`Domain`'s on this :any:`Helix`. Assigned when a :any:`Design` is created using this :any:`Helix`. @@ -1746,12 +1745,8 @@ def _is_close(x1: float, x2: float) -> bool: return abs(x1 - x2) < _floating_point_tolerance -DomainLabel = TypeVar('DomainLabel') -StrandLabel = TypeVar('StrandLabel') - - @dataclass -class Domain(_JSONSerializable, Generic[DomainLabel]): +class Domain(_JSONSerializable): """ A maximal portion of a :any:`Strand` that is continguous on a single :any:`Helix`. A :any:`Strand` contains a list of :any:`Domain`'s (and also potentially :any:`Loopout`'s). @@ -1803,15 +1798,11 @@ class Domain(_JSONSerializable, Generic[DomainLabel]): This is used to interoperate with the dsd DNA sequence design package.""" - label: Optional[DomainLabel] = None + label: Optional[str] = None """ - Generic "label" object to associate to this :any:`Domain`. + This can be used to attach a "label" to associate to this :any:`Loopout`. - 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) - that is naturally JSON serializable. (Calling - `json.dumps `_ - on the object should succeed without having to specify a custom encoder.) + See :any:`Strand.label` for examples. """ dna_sequence: Optional[str] = None @@ -1825,7 +1816,7 @@ class Domain(_JSONSerializable, Generic[DomainLabel]): # not serialized; for efficiency # remove quotes when Py3.6 support dropped - _parent_strand: Optional['Strand'] = field(init=False, repr=False, compare=False, default=None) + _parent_strand: Optional[Strand] = field(init=False, repr=False, compare=False, default=None) def __post_init__(self) -> None: self._check_start_end() @@ -1850,7 +1841,7 @@ def to_json_serializable(self, suppress_indent: bool = True, return NoIndent(dct) if suppress_indent else dct @staticmethod - def from_json(json_map: Dict[str, Any]) -> 'Domain': # remove quotes when Py3.6 support dropped + def from_json(json_map: Dict[str, Any]) -> Domain: # remove quotes when Py3.6 support dropped helix = mandatory_field(Domain, json_map, helix_idx_key) forward = mandatory_field(Domain, json_map, forward_key, legacy_keys=legacy_forward_keys) start = mandatory_field(Domain, json_map, start_key) @@ -1890,7 +1881,7 @@ def __repr__(self) -> str: def __str__(self) -> str: return repr(self) if self.name is None else self.name - def strand(self) -> 'Strand': # remove quotes when Py3.6 support dropped + def strand(self) -> Strand: # remove quotes when Py3.6 support dropped """ :return: The :any:`Strand` that contains this :any:`Domain`. """ @@ -1902,7 +1893,7 @@ def set_name(self, name: str) -> None: """Sets name of this :any:`Domain`.""" self.name = name - def set_label(self, label: DomainLabel) -> None: + def set_label(self, label: str) -> None: """Sets label of this :any:`Domain`.""" self.label = label @@ -2086,11 +2077,7 @@ def _between_5p_and_offset(self, offset_to_test: int, offset_edge: int) -> bool: # return ((self.direction == Direction.left and self.start <= offset_to_test < offset_edge) or # (self.direction == Direction.forward and offset_edge < offset_to_test < self.end)) - # The type hint 'Domain' must be in quotes since Domain is not yet defined. - # This is a "forward reference": https://www.python.org/dev/peps/pep-0484/#forward-references - # remove quotes when Py3.6 support dropped - # def overlaps(self, other: Domain) -> bool: - def overlaps(self, other: 'Domain') -> bool: + def overlaps(self, other: Domain) -> bool: r"""Indicates if this :any:`Domain`'s set of offsets (the set :math:`\{x \in \mathbb{N} \mid` ``self.start`` @@ -2105,7 +2092,7 @@ def overlaps(self, other: 'Domain') -> bool: # remove quotes when Py3.6 support dropped # def overlaps_illegally(self, other: Domain): - def overlaps_illegally(self, other: 'Domain') -> bool: + def overlaps_illegally(self, other: Domain) -> bool: r"""Indicates if this :any:`Domain`'s set of offsets (the set :math:`\{x \in \mathbb{N} \mid` ``self.start`` @@ -2120,7 +2107,7 @@ def overlaps_illegally(self, other: 'Domain') -> bool: # remove quotes when Py3.6 support dropped # def compute_overlap(self, other: Domain) -> Tuple[int, int]: - def compute_overlap(self, other: 'Domain') -> Tuple[int, int]: + def compute_overlap(self, other: Domain) -> Tuple[int, int]: """Return [left,right) offset indicating overlap between this Domain and `other`. Return ``(-1,-1)`` if they do not overlap (different helices, or non-overlapping regions @@ -2167,7 +2154,7 @@ def is_3p_domain(self) -> bool: @dataclass -class Loopout(_JSONSerializable, Generic[DomainLabel]): +class Loopout(_JSONSerializable): """Represents a single-stranded loopout on a :any:`Strand`. One could think of a :any:`Loopout` as a type of :any:`Domain`, but none of the fields of @@ -2213,15 +2200,11 @@ class Loopout(_JSONSerializable, Generic[DomainLabel]): This is used to interoperate with the dsd DNA sequence design package. """ - label: Optional[DomainLabel] = None + label: Optional[str] = None """ - Generic "label" object to associate to this :any:`Loopout`. + This can be used to attach a "label" 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.) + See :any:`Strand.label` for examples. """ dna_sequence: Optional[str] = None @@ -2234,7 +2217,7 @@ class Loopout(_JSONSerializable, Generic[DomainLabel]): # not serialized; for efficiency # remove quotes when Py3.6 support dropped - _parent_strand: Optional['Strand'] = field(init=False, repr=False, compare=False, default=None) + _parent_strand: Optional[Strand] = field(init=False, repr=False, compare=False, default=None) def to_json_serializable(self, suppress_indent: bool = True, **kwargs: Any) -> Union[Dict[str, Any], NoIndent]: @@ -2259,7 +2242,7 @@ def from_json(json_map: Dict[str, Any]) -> 'Loopout': # remove quotes when Py3. color = Color.from_json(color_json) return Loopout(length=length, name=name, label=label, color=color) - def strand(self) -> 'Strand': # remove quotes when Py3.6 support dropped + def strand(self) -> Strand: # remove quotes when Py3.6 support dropped """ :return: The :any:`Strand` that contains this :any:`Loopout`. """ @@ -2281,7 +2264,7 @@ def set_name(self, name: str) -> None: """Sets name of this :any:`Loopout`.""" self.name = name - def set_label(self, label: Optional[DomainLabel]) -> None: + def set_label(self, label: Optional[str]) -> None: """Sets label of this :any:`Loopout`.""" self.label = label @@ -2313,7 +2296,7 @@ def get_seq_start_idx(self) -> int: @dataclass -class Extension(_JSONSerializable, Generic[DomainLabel]): +class Extension(_JSONSerializable): # using raw string literal for docstring to avoid warning about backslash r"""Represents a single-stranded extension on either the 3' or 5' end of :any:`Strand`. @@ -2373,15 +2356,12 @@ class Extension(_JSONSerializable, Generic[DomainLabel]): for degrees strictly between 0 and 180. """ - label: Optional[DomainLabel] = None + label: Optional[str] = None + """ - Generic "label" object to associate to this :any:`Extension`. + This can be used to attach a "label" to associate to this :any:`Extension`. - Useful for associating extra information with the :any:`Extension` 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.) + See :any:`Strand.label` for examples. """ name: Optional[str] = None @@ -2399,7 +2379,7 @@ class Extension(_JSONSerializable, Generic[DomainLabel]): # not serialized; for efficiency # remove quotes when Py3.6 support dropped - _parent_strand: Optional['Strand'] = field(init=False, repr=False, compare=False, default=None) + _parent_strand: Optional[Strand] = field(init=False, repr=False, compare=False, default=None) def to_json_serializable(self, suppress_indent: bool = True, **kwargs: Any) \ -> Union[Dict[str, Any], NoIndent]: @@ -2416,7 +2396,7 @@ def dna_length(self) -> int: """Length of this :any:`Extension`; same as field :data:`Extension.num_bases`.""" return self.num_bases - def set_label(self, label: Optional[DomainLabel]) -> None: + def set_label(self, label: Optional[str]) -> None: """Sets label of this :any:`Extension`.""" self.label = label @@ -2556,7 +2536,7 @@ def _check_idt_string_not_none_or_empty(value: str, field_name: str) -> None: raise IllegalDesignError(f'field {field_name} in IDTFields cannot be empty') -class StrandBuilder(Generic[StrandLabel, DomainLabel]): +class StrandBuilder: """ Represents a :any:`Strand` that is being built in an existing :any:`Design`. @@ -2582,25 +2562,24 @@ class StrandBuilder(Generic[StrandLabel, DomainLabel]): strand_builder.as_scaffold() """ - # remove quotes when Py3.6 support dropped - def __init__(self, design: 'Design[StrandLabel, DomainLabel]', helix: int, offset: int): - self.design: Design[StrandLabel, DomainLabel] = design + 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._strand: Optional[Strand[StrandLabel, DomainLabel]] = None + self._strand: Optional[Strand] = None self.just_moved_to_helix: bool = True - self.last_domain: Optional[Domain[DomainLabel]] = None + self.last_domain: Optional[Domain] = None @property - def strand(self) -> 'Strand[StrandLabel, DomainLabel]': + def strand(self) -> Strand: if self._strand is None: raise ValueError('no Strand created yet; make at least one domain first') return self._strand # remove quotes when Py3.6 support dropped def cross(self, helix: int, offset: Optional[int] = None, move: Optional[int] = None) \ - -> 'StrandBuilder[StrandLabel, DomainLabel]': + -> StrandBuilder: """ Add crossover. To have any effect, must be followed by call to :py:meth:`StrandBuilder.to` or :py:meth:`StrandBuilder.move`. @@ -2639,7 +2618,7 @@ def _most_recently_added_substrand_is_extension(self): # remove quotes when Py3.6 support dropped def loopout(self, helix: int, length: int, offset: Optional[int] = None, move: Optional[int] = None) \ - -> 'StrandBuilder[StrandLabel, DomainLabel]': + -> StrandBuilder: """ Like :py:meth:`StrandBuilder.cross`, but creates a :any:`Loopout` instead of a crossover. @@ -2664,7 +2643,7 @@ def extension_3p(self, num_bases: int, display_length: float = default_display_length, display_angle: float = default_display_angle - ) -> 'StrandBuilder[StrandLabel, DomainLabel]': + ) -> StrandBuilder: """ Creates an :any:`Extension` after verifying that it is valid to add an :any:`Extension` to the :any:`Strand` as a 3' :any:`Extension`. @@ -2703,7 +2682,7 @@ def extension_5p(self, num_bases: int, display_length: float = default_display_length, display_angle: float = default_display_angle - ) -> 'StrandBuilder[StrandLabel, DomainLabel]': + ) -> StrandBuilder: """ Creates an :any:`Extension` after verifying that it is valid to add an :any:`Extension` to the :any:`Strand` as a 5' :any:`Extension`. @@ -2728,7 +2707,7 @@ def _verify_extension_5p_is_valid(self): 'Did you mean to create a 3\' extension?') # remove quotes when Py3.6 support dropped - def move(self, delta: int) -> 'StrandBuilder[StrandLabel, DomainLabel]': + def move(self, delta: int) -> StrandBuilder: """ Extends this :any:`StrandBuilder` on the current helix to offset given by the current offset plus `delta`, which adds a new :any:`Domain` to the :any:`Strand` being built. This is a @@ -2751,7 +2730,7 @@ def move(self, delta: int) -> 'StrandBuilder[StrandLabel, DomainLabel]': return self.to(self.current_offset + delta) # remove quotes when Py3.6 support dropped - def to(self, offset: int) -> 'StrandBuilder[StrandLabel, DomainLabel]': + def to(self, offset: int) -> StrandBuilder: """ Extends this :any:`StrandBuilder` on the current helix to offset `offset`, which adds a new :any:`Domain` to the :any:`Strand` being built. This is an @@ -2793,7 +2772,7 @@ def to(self, offset: int) -> 'StrandBuilder[StrandLabel, DomainLabel]': else: raise IllegalDesignError(f'offset {offset} cannot be equal to current offset') - domain: Domain[DomainLabel] = Domain(helix=self.current_helix, forward=forward, start=start, end=end) + domain: Domain = Domain(helix=self.current_helix, forward=forward, start=start, end=end) self.last_domain = domain if self._strand is not None: self.design.append_domain(self._strand, domain) @@ -2813,7 +2792,7 @@ def _most_recently_added_substrand_is_extension_3p(self) -> bool: return len(self._strand.domains) > 1 and self._most_recently_added_substrand_is_extension() # remove quotes when Py3.6 support dropped - def update_to(self, offset: int) -> 'StrandBuilder[StrandLabel, DomainLabel]': + def update_to(self, offset: int) -> StrandBuilder: """ Like :py:meth:`StrandBuilder.to`, but changes the current offset without creating a new :any:`Domain`. So unlike :py:meth:`StrandBuilder.to`, several consecutive calls to @@ -2849,7 +2828,7 @@ def update_to(self, offset: int) -> 'StrandBuilder[StrandLabel, DomainLabel]': return self - def as_circular(self) -> 'StrandBuilder[StrandLabel, DomainLabel]': + def as_circular(self) -> StrandBuilder: """ Makes :any:`Strand` being built circular. @@ -2861,7 +2840,7 @@ def as_circular(self) -> 'StrandBuilder[StrandLabel, DomainLabel]': return self # remove quotes when Py3.6 support dropped - def as_scaffold(self) -> 'StrandBuilder[StrandLabel, DomainLabel]': + def as_scaffold(self) -> StrandBuilder: """ Makes :any:`Strand` being built a scaffold. @@ -2875,7 +2854,7 @@ def as_scaffold(self) -> 'StrandBuilder[StrandLabel, DomainLabel]': def with_idt(self, scale: str = default_idt_scale, purification: str = default_idt_purification, plate: Optional[str] = None, well: Optional[str] = None) \ - -> 'StrandBuilder[StrandLabel, DomainLabel]': + -> StrandBuilder: """ Gives :any:`IDTFields` value to :any:`Strand` being built. Only a name is required; other fields are given reasonable default values. @@ -2897,7 +2876,7 @@ def with_idt(self, scale: str = default_idt_scale, return self # remove quotes when Py3.6 support dropped - def with_modification_5p(self, mod: Modification5Prime) -> 'StrandBuilder[StrandLabel, DomainLabel]': + def with_modification_5p(self, mod: Modification5Prime) -> StrandBuilder: """ Sets Strand being built to have given 5' modification. @@ -2910,7 +2889,7 @@ def with_modification_5p(self, mod: Modification5Prime) -> 'StrandBuilder[Strand return self # remove quotes when Py3.6 support dropped - def with_modification_3p(self, mod: Modification3Prime) -> 'StrandBuilder[StrandLabel, DomainLabel]': + def with_modification_3p(self, mod: Modification3Prime) -> StrandBuilder: """ Sets Strand being built to have given 3' modification. @@ -2924,7 +2903,7 @@ def with_modification_3p(self, mod: Modification3Prime) -> 'StrandBuilder[Strand # remove quotes when Py3.6 support dropped def with_modification_internal(self, idx: int, mod: ModificationInternal, warn_on_no_dna: bool) \ - -> 'StrandBuilder[StrandLabel, DomainLabel]': + -> StrandBuilder: """ Sets Strand being built to have given internal modification. @@ -2939,7 +2918,7 @@ def with_modification_internal(self, idx: int, mod: ModificationInternal, warn_o return self # remove quotes when Py3.6 support dropped - def with_color(self, color: Color) -> 'StrandBuilder[StrandLabel, DomainLabel]': + def with_color(self, color: Color) -> StrandBuilder: """ Sets Strand being built to have given color. @@ -2953,7 +2932,7 @@ def with_color(self, color: Color) -> 'StrandBuilder[StrandLabel, DomainLabel]': # remove quotes when Py3.6 support dropped def with_sequence(self, sequence: str, assign_complement: bool = False) \ - -> 'StrandBuilder[StrandLabel, DomainLabel]': + -> StrandBuilder: """ Assigns `sequence` as DNA sequence of the :any:`Strand` being built. This should be done after the :any:`Strand`'s structure is done being built, e.g., @@ -2975,7 +2954,7 @@ def with_sequence(self, sequence: str, assign_complement: bool = False) \ # remove quotes when Py3.6 support dropped def with_domain_sequence(self, sequence: str, assign_complement: bool = False) \ - -> 'StrandBuilder[StrandLabel, DomainLabel]': + -> StrandBuilder: """ Assigns `sequence` as DNA sequence of the most recently created :any:`Domain` in the :any:`Strand` being built. This should be called immediately after a :any:`Domain` is created @@ -3012,7 +2991,7 @@ def with_domain_sequence(self, sequence: str, assign_complement: bool = False) \ return self # remove quotes when Py3.6 support dropped - def with_domain_color(self, color: Color) -> 'StrandBuilder[StrandLabel, DomainLabel]': + def with_domain_color(self, color: Color) -> StrandBuilder: """ Sets most recent :any:`Domain`/:any:`Loopout`/:any:`Extension` to have given color. @@ -3029,7 +3008,7 @@ def with_domain_color(self, color: Color) -> 'StrandBuilder[StrandLabel, DomainL return self # remove quotes when Py3.6 support dropped - def with_name(self, name: str) -> 'StrandBuilder[StrandLabel, DomainLabel]': + def with_name(self, name: str) -> StrandBuilder: """ Assigns `name` as name of the :any:`Strand` being built. @@ -3046,7 +3025,7 @@ def with_name(self, name: str) -> 'StrandBuilder[StrandLabel, DomainLabel]': return self # remove quotes when Py3.6 support dropped - def with_label(self, label: StrandLabel) -> 'StrandBuilder[StrandLabel, DomainLabel]': + def with_label(self, label: str) -> StrandBuilder: """ Assigns `label` as label of the :any:`Strand` being built. @@ -3063,7 +3042,7 @@ def with_label(self, label: StrandLabel) -> 'StrandBuilder[StrandLabel, DomainLa return self # remove quotes when Py3.6 support dropped - def with_domain_name(self, name: str) -> 'StrandBuilder[StrandLabel, DomainLabel]': + def with_domain_name(self, name: str) -> StrandBuilder: """ Assigns `name` as of the most recently created :any:`Domain` or :any:`Loopout` in the :any:`Strand` being built. This should be called immediately after a :any:`Domain` is created @@ -3088,7 +3067,7 @@ def with_domain_name(self, name: str) -> 'StrandBuilder[StrandLabel, DomainLabel return self # remove quotes when Py3.6 support dropped - def with_domain_label(self, label: DomainLabel) -> 'StrandBuilder[StrandLabel, DomainLabel]': + def with_domain_label(self, label: str) -> StrandBuilder: """ Assigns `label` as label of the most recently created :any:`Domain` or :any:`Loopout` in the :any:`Strand` being built. This should be called immediately after @@ -3118,7 +3097,7 @@ def with_domain_label(self, label: DomainLabel) -> 'StrandBuilder[StrandLabel, D return self def with_deletions(self, - deletions: Union[int, Iterable[int]]) -> 'StrandBuilder[StrandLabel, DomainLabel]': + deletions: Union[int, Iterable[int]]) -> StrandBuilder: """ Assigns `deletions` as the deletion(s) of the most recently created :any:`Domain` the :any:`Strand` being built. This should be called immediately after @@ -3163,7 +3142,7 @@ def with_deletions(self, return self def with_insertions(self, insertions: Union[Tuple[int, int], Iterable[Tuple[int, int]]]) \ - -> 'StrandBuilder[StrandLabel, DomainLabel]': + -> StrandBuilder: """ Assigns `insertions` as the insertion(s) of the most recently created :any:`Domain` the :any:`Strand` being built. This should be called immediately after @@ -3216,7 +3195,7 @@ def with_insertions(self, insertions: Union[Tuple[int, int], Iterable[Tuple[int, @dataclass -class Strand(_JSONSerializable, Generic[StrandLabel, DomainLabel]): +class Strand(_JSONSerializable): """ Represents a single strand of DNA. @@ -3253,7 +3232,7 @@ class Strand(_JSONSerializable, Generic[StrandLabel, DomainLabel]): uses for the scaffold. """ - domains: List[Union[Domain[DomainLabel], Loopout[DomainLabel], Extension[DomainLabel]]] + domains: List[Union[Domain, Loopout, Extension]] """:any:`Domain`'s (or :any:`Loopout`'s) composing this Strand. Each :any:`Domain` is contiguous on a single :any:`Helix` and could be either single-stranded or double-stranded, @@ -3333,29 +3312,44 @@ def dna_sequence(self) -> Optional[str]: This is used to interoperate with the dsd DNA sequence design package. """ - label: Optional[StrandLabel] = None - """Generic "label" object to associate to this :any:`Strand`. - - Useful for associating extra information with the Strand 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.) + label: Optional[str] = None + """ + This can be used to attach a "label" to associate to this :any:`Strand`. + + Useful for associating extra information with the :any:`Strand` that will be serialized, for example, + for DNA sequence design. It can be useful to create "groups" of strands related in some way. + + Prior to version 0.18.0, this was allowed to be an arbitrary JSON-serializable object. + Now it is just a string + (see https://github.com/UC-Davis-molecular-computing/scadnano-python-package/issues/261). + To store more structured data, it is necessary to serialize (convert to a string) the data manually. + For example, if you want to store a list of numbers, you can do so as a string like this: + + .. code-block:: python + + import json + + nums = [1, 2, 3] + strand.label = json.dumps(nums) # stores strand.label as the string '[1, 2, 3]' + + # and to get the structured data back out: + nums = json.loads(strand.label) # nums is now the list [1, 2, 3] """ # not serialized; efficient way to see a list of all domains on a given helix - _helix_idx_domain_map: Dict[int, List[Domain[DomainLabel]]] = field( + _helix_idx_domain_map: Dict[int, List[Domain]] = field( init=False, repr=False, compare=False, default_factory=dict) def __init__(self, - domains: List[Union[Domain[DomainLabel], Loopout[DomainLabel], Extension[DomainLabel]]], + domains: List[Union[Domain, Loopout, Extension]], circular: bool = False, color: Optional[Color] = None, idt: Optional[IDTFields] = None, is_scaffold: bool = False, modification_5p: Optional[Modification5Prime] = None, modification_3p: Optional[Modification3Prime] = None, modifications_int: Optional[Dict[int, ModificationInternal]] = None, name: Optional[str] = None, - label: Optional[StrandLabel] = None, - _helix_idx_domain_map: Dict[int, List[Domain[DomainLabel]]] = None, + label: Optional[str] = None, + _helix_idx_domain_map: Dict[int, List[Domain]] = None, dna_sequence: Optional[str] = None): self.domains = domains self.circular = circular @@ -3413,7 +3407,7 @@ def to_json_serializable(self, suppress_indent: bool = True, **kwargs: Any) -> D return dct @staticmethod - def from_json(json_map: dict) -> 'Strand': # remove quotes when Py3.6 support dropped + def from_json(json_map: dict) -> Strand: # remove quotes when Py3.6 support dropped substrand_jsons = mandatory_field(Strand, json_map, domains_key, legacy_keys=legacy_domains_keys) if len(substrand_jsons) == 0: raise IllegalDesignError(f'{domains_key} list cannot be empty') @@ -3549,7 +3543,7 @@ def set_linear(self) -> None: """ self.set_circular(False) - def set_domains(self, domains: Iterable[Union[Domain[DomainLabel], Loopout[DomainLabel]]]) -> None: + def set_domains(self, domains: Iterable[Union[Domain, Loopout]]) -> None: """ Sets the :any:`Domain`'s/:any:`Loopout`'s of this :any:`Strand` to be `domains`, which can contain a mix of :any:`Domain`'s and :any:`Loopout`'s, @@ -3757,7 +3751,7 @@ def offset_3p(self) -> int: """3' offset of this entire :any:`Strand`, INCLUSIVE.""" return self.last_domain().offset_3p() - def overlaps(self, other: 'Strand') -> bool: # remove quotes when Py3.6 support dropped + def overlaps(self, other: Strand) -> bool: # remove quotes when Py3.6 support dropped """Indicates whether `self` overlaps `other_strand`, meaning that the set of offsets occupied by `self` has nonempty intersection with those occupied by `other_strand`.""" for domain_self in self.bound_domains(): @@ -3766,7 +3760,7 @@ def overlaps(self, other: 'Strand') -> bool: # remove quotes when Py3.6 support return True return False - def assign_dna_complement_from(self, other: 'Strand') -> None: # remove quotes when Py3.6 support dropped + def assign_dna_complement_from(self, other: Strand) -> None: # remove quotes when Py3.6 support dropped """Assuming a DNA sequence has been assigned to `other`, assign its Watson-Crick complement to the portions of this Strand that are bound to `other`. @@ -4024,7 +4018,7 @@ def idt_dna_sequence(self) -> str: return ''.join(ret_list) - def no_modifications_version(self) -> 'Strand': + def no_modifications_version(self) -> Strand: """ :return: version of this :any:`Strand` with no DNA modifications. """ @@ -4974,10 +4968,10 @@ def reverse_complementary(seq1: str, seq2: str, allow_wildcard: bool = False, @dataclass -class Design(_JSONSerializable, Generic[StrandLabel, DomainLabel]): +class Design(_JSONSerializable): """Object representing the entire design of the DNA structure.""" - strands: List[Strand[StrandLabel, DomainLabel]] + strands: List[Strand] """All of the :any:`Strand`'s in this :any:`Design`. Required field.""" @@ -7625,8 +7619,8 @@ def add_nick(self, helix: int, offset: int, forward: bool, new_color: bool = Tru order = domains.index(domain_to_remove) domains_before = domains[:order] domains_after = domains[order + 1:] - domain_left: Domain[DomainLabel] = Domain(helix, forward, domain_to_remove.start, offset) - domain_right: Domain[DomainLabel] = Domain(helix, forward, offset, domain_to_remove.end) + domain_left: Domain = Domain(helix, forward, domain_to_remove.start, offset) + domain_right: Domain = Domain(helix, forward, offset, domain_to_remove.end) # "before" and "after" mean in the 5' --> 3' direction, i.e., if a reverse domain: # <--------] @@ -7688,7 +7682,7 @@ def add_nick(self, helix: int, offset: int, forward: bool, new_color: bool = Tru self.strands.remove(strand) idt_present = strand.idt is not None - strand_before: Strand[StrandLabel, DomainLabel] = Strand( + strand_before: Strand = Strand( domains=domains_before, dna_sequence=seq_before_whole, color=strand.color, @@ -7696,7 +7690,7 @@ def add_nick(self, helix: int, offset: int, forward: bool, new_color: bool = Tru ) color_after = next(self.color_cycler) if new_color else strand.color - strand_after: Strand[StrandLabel, DomainLabel] = Strand( + strand_after: Strand = Strand( domains=domains_after, dna_sequence=seq_after_whole, color=color_after, @@ -7772,11 +7766,11 @@ def ligate(self, helix: int, offset: int, forward: bool) -> None: strand_left = dom_left.strand() strand_right = dom_right.strand() - dom_new: Domain[DomainLabel] = Domain(helix=helix, forward=forward, start=dom_left.start, - end=dom_right.end, - deletions=dom_left.deletions + dom_right.deletions, - insertions=dom_left.insertions + dom_right.insertions, - name=dom_left.name, label=dom_left.label) + dom_new: Domain = Domain(helix=helix, forward=forward, start=dom_left.start, + end=dom_right.end, + deletions=dom_left.deletions + dom_right.deletions, + insertions=dom_left.insertions + dom_right.insertions, + name=dom_left.name, label=dom_left.label) # normalize 5'/3' distinction; below refers to which Strand has the 5'/3' end that will be ligated # So strand_5p is the one whose 3' end will be the 3' end of the whole new Strand @@ -7942,9 +7936,9 @@ def add_half_crossover(self, helix: int, helix2: int, offset: int, forward: bool raise IllegalDesignError( 'cannot add crossover between two strands if one has a DNA sequence ' 'and the other does not') - new_strand: Strand[StrandLabel, DomainLabel] = Strand(domains=new_domains, color=strand_first.color, - dna_sequence=new_dna, idt=strand_first.idt, - is_scaffold=strand1.is_scaffold or strand2.is_scaffold) + new_strand: Strand = Strand(domains=new_domains, color=strand_first.color, + dna_sequence=new_dna, idt=strand_first.idt, + is_scaffold=strand1.is_scaffold or strand2.is_scaffold) # put new strand in place where strand_first was strand_first_idx = self.strands.index(strand_first)