Source code for alhambra.glues

from __future__ import annotations

import collections.abc
import copy
from dataclasses import dataclass
from enum import Enum
from typing import (
    Any,
    ClassVar,
    Generic,
    Iterable,
    List,
    Literal,
    Mapping,
    MutableMapping,
    Optional,
    Protocol,
    Type,
    TypeVar,
    Union,
    cast,
)
from warnings import warn

from .classes import UpdateListD
from .seq import Seq

import attrs

[docs]T = TypeVar("T")
[docs]GlueA = TypeVar("GlueA", bound="Glue")
[docs]GlueB = TypeVar("GlueB", bound="Glue")
[docs]class Use(int, Enum):
[docs] UNUSED = 0
[docs] NULL = 1
[docs] INPUT = 2
[docs] OUTPUT = 3
[docs] BOTH = 4
[docs] PERMANENT = 5
[docs] UNSET = 6
@classmethod
[docs] def from_any(cls, other: str | Use | int) -> Use: if isinstance(other, str): return Use[other.upper()] elif isinstance(other, Use): return other else: return Use(other)
[docs] def invert(self) -> Use: if self == Use.UNSET: raise ValueError return Use([0, 1, 3, 2, 4, 5][self.value])
[docs] def merge(self: Use, other: Use) -> Use: if self == other: return self if self == Use.UNSET: return other elif other == Use.UNSET: return self elif self == Use.UNUSED: return other elif other == Use.UNUSED: return self elif (self == Use.INPUT and other == Use.OUTPUT) or ( self == Use.OUTPUT and other == Use.INPUT ): return Use.BOTH elif (self == Use.BOTH and 2 <= other.value <= 4) or ( 2 <= self.value <= 4 and self == Use.BOTH ): return Use.BOTH raise ValueError
[docs] def __or__(self, other: Any) -> Use: raise NotImplementedError
[docs] def __ror__(self, other: Any) -> Use: raise NotImplementedError
[docs]def merge_items(a: Optional[T], b: Optional[T]) -> Optional[T]: if a is None: return b elif b is None: return a else: assert a == b return a
[docs]def _ensure_glue_name(n: str | None) -> str | None: "Ensure the glue name uses *, not /, and is reasonable." if n is not None: if (len(n) >= 1) and n[-1] == "/": n = n[:-1] + "*" return n
@attrs.define()
[docs]class Glue:
[docs] name: Optional[str] = attrs.field( converter=_ensure_glue_name, on_setattr=attrs.setters.convert, default=None )
[docs] note: Optional[str] = attrs.field(default=None)
[docs] use: Use = attrs.field(default=Use.UNSET)
[docs] abstractstrength: Optional[int] = attrs.field(default=None)
"""The stickydesign type of the glue.""" @property
[docs] def etype(self) -> str: raise NotImplementedError
[docs] def _into_complement(self): if self.name is not None: if self.is_complement: self.name = self.name[:-1] else: self.name = self.name + "*"
[docs] def copy(self: T) -> T: return copy.copy(self)
[docs] def ident(self) -> str: if self.name: return self.name else: raise ValueError
@property
[docs] def type(self) -> str: return self.__class__.__name__
[docs] def basename(self) -> str: if self.name is None: raise ValueError if self.is_complement: return self.name[:-1] else: return self.name
@property
[docs] def is_complement(self: Glue) -> bool: if self.name and (self.name[-1] == "*"): return True return False
@property
[docs] def complement(self: GlueA) -> GlueA: c = self.copy() c._into_complement() return c
[docs] def update(self, other: Glue): if type(other) == Glue: self.name = merge_items(self.name, other.name) self.abstractstrength = merge_items( self.abstractstrength, other.abstractstrength ) self.use = Use.merge(self.use, other.use) else: raise NotImplementedError
[docs] def merge(self: Glue, other: Glue) -> Glue: if type(other) == Glue: return Glue( merge_items(self.name, other.name), abstractstrength=merge_items( self.abstractstrength, other.abstractstrength ), use=Use.merge(self.use, other.use), ) else: return other.merge(self)
[docs] def __or__(self, other: Glue) -> Glue: return self.merge(other)
[docs] def __ior__(self, other: Glue): self.update(other)
[docs] def __ror__(self, other: Glue) -> Glue: return self.merge(other)
[docs] def __eq__(self, other: object) -> bool: if isinstance(other, str): other = Glue(other) if not isinstance(other, Glue): return False return self.ident() == other.ident()
[docs] def to_dict(self) -> dict[str, Any]: return { k: v for k in ["name", "use", "note"] if (v := getattr(self, k)) is not None }
@staticmethod
[docs] def from_dict(d: dict[str, Any]) -> Glue: return glue_factory.from_dict(d)
[docs]class GlueFactory:
[docs] types: dict[str, Type[Glue]]
def __init__(self): self.types = {}
[docs] def register(self, c: Type[Glue]): self.types[c.__name__] = c
[docs] def from_dict(self, d: str | dict[str, Any]) -> Glue: if isinstance(d, str): return Glue(d) if "type" in d: c = self.types[d["type"]] del d["type"] return c(**d) else: return Glue(**d)
[docs]glue_factory = GlueFactory()
@attrs.define(init=False)
[docs]class SSGlue(Glue):
[docs] etype: str = "S"
[docs] _sequence: Seq = attrs.field(default=Seq(""))
def __init__( self, name: Optional[str] = None, length: Union[None, int, str, Seq] = None, sequence: Union[None, str, Seq] = None, note: Optional[str] = None, use: Optional[Use] = None, ): if use is None: use = Use.UNSET super(SSGlue, self).__init__(name, note, use) if isinstance(length, int): lseq: Seq | None = Seq("N" * length) elif isinstance(length, str): lseq = Seq(length) elif isinstance(length, Seq): lseq = length else: lseq = None if sequence is not None: if not isinstance(sequence, Seq): sequence = Seq(sequence) self._sequence = sequence | lseq # fixme: better error elif lseq is not None: self._sequence = lseq else: raise ValueError("Must have at least length or sequence.") self.etype = "S" # FIXME: there should be a better way to do this @property
[docs] def dna_length(self) -> int: return self.sequence.dna_length
@property
[docs] def sequence(self) -> Seq: return self._sequence
@sequence.setter def sequence(self, seq: Seq | str | None) -> None: # type: ignore if seq is None: self._sequence = Seq("N" * self.dna_length) return elif not isinstance(seq, Seq): seq = Seq(seq) if self.dna_length is not None: assert seq.dna_length == self.dna_length self._sequence = seq
[docs] def ident(self) -> str: if self.name: return super().ident() if self.sequence: return f"SSGlue_{self.sequence.base_str}" else: raise ValueError
[docs] def _into_complement(self): if self.sequence is not None: self.sequence = self.sequence.revcomp super()._into_complement()
[docs] def update(self, other: Glue | str): if isinstance(other, str): other = Glue(other) if type(other) is Glue: # a base glue: we can merge self.name = merge_items(self.name, other.name) elif type(other) is SSGlue: newname = merge_items(self.name, other.name) newseq = self.sequence | other.sequence self.name = newname self.sequence = newseq else: raise NotImplementedError
[docs] def merge(self, other: Glue | str) -> SSGlue: new = self.copy() new.update(other) return new
[docs] def to_dict(self) -> dict[str, Any]: d = super().to_dict() d["type"] = self.__class__.__name__ d["sequence"] = self.sequence.seq_str return d
@property
[docs] def _shortdesc(self) -> str: r = [] if self.name is not None: r.append(self.name) if (self.sequence is not None) and not self.sequence.is_null: r.append(f"({self.sequence.seq_str})") else: r.append(self.sequence.seq_str) return "".join(r)
glue_factory.register(SSGlue) @attrs.define(init=False)
[docs]class DXGlue(Glue):
[docs] etype: Literal["TD", "DT"] = attrs.field(default="TD")
[docs] fullseq: Seq = attrs.field(default=Seq(""))
[docs] def _into_complement(self): if self.fullseq is not None: self.fullseq = self.fullseq.revcomp super()._into_complement()
def __init__( self, etype, name=None, length=None, sequence: Seq | str | None = None, fullseq=None, use=None, abstractstrength=None, note=None, ): super(DXGlue, self).__init__(name, note, use, abstractstrength) self.etype = etype trial_fseq: Optional[Seq] = None if length: trial_fseq = Seq("N" * (length + 1)) if sequence: if self.etype == "TD": if trial_fseq: trial_fseq |= "N" + sequence else: trial_fseq = Seq("N" + sequence) elif self.etype == "DT": if trial_fseq: trial_fseq |= sequence + "N" else: trial_fseq = Seq(sequence + "N") else: raise ValueError(etype) if fullseq: if trial_fseq: trial_fseq |= fullseq else: trial_fseq = Seq(fullseq) if trial_fseq is None: raise ValueError("Must have a least some information.") self.fullseq = trial_fseq @property
[docs] def fseq(self) -> Optional[str]: if self.fullseq is not None: return self.fullseq.seq_str else: return None
@fseq.setter def fseq(self, value: str): if self.fullseq is not None and len(value) != len(self.fullseq): warn("Changing end length") self.fullseq = Seq(value) @property
[docs] def seq(self) -> Optional[str]: if not self.fseq: return None if self.etype == "TD": return self.fseq[1:] elif self.etype == "DT": return self.fseq[:-1]
@property
[docs] def comp(self): """The complement end sequences of the End, as a string.""" if not self.fullseq: return None if self.etype == "TD": return self.fullseq.revcomp.base_str[1:] elif self.etype == "DT": return self.fullseq.revcomp.base_str[:-1]
[docs] def merge(self, other: Glue) -> DXGlue: out = self.copy() if type(other) not in [Glue, DXGlue]: raise ValueError for k in ["note", "name", "etype", "abstractstrength"]: if (v := getattr(out, k, None)) is not None: if (nv := getattr(other, k, None)) is not None: if nv != v: raise ValueError else: if (nv := getattr(other, k, None)) is not None: setattr(out, k, nv) if isinstance(other, DXGlue): if out.fullseq: out.fullseq = out.fullseq.merge(other.fullseq) if out.use and other.use: out.use = out.use | other.use return out
[docs] def __str__(self): if self.fseq and self.seq and self.comp: if self.etype == "DT": s = self.seq[0] + "-" + self.seq[1:] c = self.comp[0] + "-" + self.comp[1:] elif self.etype == "TD": s = self.seq[:-1] + "-" + self.seq[-1] c = self.comp[:-1] + "-" + self.comp[-1] else: raise ValueError return "<dxend {} ({}{}): {} | {}>".format( self.name, self.etype, len(self.seq), s, c ) else: return "<dxend {} ({})>".format(self.name, getattr(self, "etype", "?"))
[docs]class GlueList(Generic[GlueA], UpdateListD[GlueA]):
[docs] def merge_complements(self) -> None: newitems: dict[str, GlueA] = {} for v in self: c = v.complement kc = c.ident() if kc in self.data: self.data[kc] = cast(GlueA, self.data[kc].merge(c)) else: newitems[kc] = c self.data.update(newitems)
[docs] def merge_glue(self, g: GlueA) -> GlueA: if g.ident() in self.data: g = cast(GlueA, self.data[g.ident()].merge(g)) c = g.complement if c.ident() in self.data: g = cast(GlueA, self.data[c.ident()].complement.merge(g)) return g
[docs] def merge_glue_and_update_list(self, g: GlueA) -> GlueA: kg = g.ident() if kg in self.data: g = cast( GlueA, self.data[kg].merge(g) ) # Glue merges can only constrain type self.data[kg] = g c = g.complement kc = c.ident() if kc in self.data: c = cast(GlueA, self.data[kc].merge(c)) self.data[kc] = c g = c.complement if kg in self.data: self.data[kg] = g return g
[docs] def to_endarrays(self) -> Any: raise NotImplementedError