Skip to content

Commit 2e61fc0

Browse files
authored
Merge pull request #40 from SynBioDex/develop
prepare 1.0a10 release
2 parents e51335d + cbb58c2 commit 2e61fc0

22 files changed

Lines changed: 1051 additions & 592 deletions

.github/workflows/python-app.yml

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,25 @@ on:
1212

1313
jobs:
1414
build:
15-
1615
runs-on: ubuntu-latest
17-
16+
strategy:
17+
matrix:
18+
# Default builds are on Ubuntu
19+
os: [ubuntu-latest]
20+
python-version: [3.7, 3.8, 3.9, pypy-3.7]
21+
include:
22+
# Also test on macOS and Windows using latest Python 3
23+
- os: macos-latest
24+
python-version: 3.x
25+
- os: windows-latest
26+
python-version: 3.x
27+
1828
steps:
1929
- uses: actions/checkout@v2
20-
- name: Set up Python 3.9
30+
- name: Set up Python ${{ matrix.python-version }}
2131
uses: actions/setup-python@v2
2232
with:
23-
python-version: 3.9
33+
python-version: ${{ matrix.python-version }}
2434
- name: Install dependencies
2535
run: |
2636
python -m pip install --upgrade pip

sbol_utilities/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
# No shared content for package: import sub packages

sbol_utilities/calculate_sequences.py

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,13 @@
11
import argparse
22
import logging
3-
from typing import Union
3+
from typing import List, Tuple, Union
44

55
import sbol3
66

7-
from sbol_utilities.helper_functions import type_to_standard_extension, is_plasmid, id_sort
7+
from sbol_utilities.helper_functions import is_plasmid
8+
from sbol_utilities.workarounds import type_to_standard_extension, id_sort
89

910

10-
###############################################################
11-
# Collect all components of type DNA and resolve sequences until blocked
12-
1311
def resolved_dna_component(component: sbol3.Component) -> bool:
1412
""" Check if a DNA component still needs its sequence calculated
1513
@@ -19,7 +17,7 @@ def resolved_dna_component(component: sbol3.Component) -> bool:
1917
return sbol3.SBO_DNA in component.types and len(component.sequences) > 0
2018

2119

22-
def order_subcomponents(component: sbol3.Component) -> Union[tuple[list[sbol3.Feature], bool], None]:
20+
def order_subcomponents(component: sbol3.Component) -> Union[Tuple[List[sbol3.Feature], bool], None]:
2321
"""Attempt to find a sorted order of features in an SBOL Component, so its sequence can be calculated from theirs
2422
Conduct the sort by walking through one meet relation at a time (excepting a circular component)
2523
@@ -68,7 +66,7 @@ def order_subcomponents(component: sbol3.Component) -> Union[tuple[list[sbol3.Fe
6866
return (order if not unordered else None), circular
6967

7068
# assumes already ordered
71-
def ready_to_resolve(component: sbol3.Component, resolved: list[str]):
69+
def ready_to_resolve(component: sbol3.Component, resolved: List[str]):
7270
return all(isinstance(f,sbol3.SubComponent) and str(f.instance_of) in resolved for f in component.features)
7371

7472
def compute_sequence(component: sbol3.Component) -> sbol3.Sequence:
@@ -99,7 +97,7 @@ def compute_sequence(component: sbol3.Component) -> sbol3.Sequence:
9997
# Entry point function
10098

10199
# Takes a list of targets, all of which should be in the same input document, and expands within that document
102-
def calculate_sequences(doc: sbol3.Document) -> list[sbol3.Sequence]:
100+
def calculate_sequences(doc: sbol3.Document) -> List[sbol3.Sequence]:
103101
"""Attempt to calculate missing sequences of Components from their features
104102
105103
:param doc: Document where sequences will be calculated

sbol_utilities/component.py

Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
1+
from typing import Dict, Iterable, List, Union, Set, Optional
2+
3+
import sbol3
4+
import tyto
5+
6+
from sbol_utilities.workarounds import get_parent, id_sort
7+
8+
9+
# TODO: consider allowing return of LocalSubComponent and ExternallyDefined
10+
def contained_components(roots: Union[sbol3.TopLevel, Iterable[sbol3.TopLevel]]) -> Set[sbol3.Component]:
11+
"""Find the set of all SBOL Components contained within the roots or their children
12+
This will explore via Collection.member relations and Component.feature relations
13+
14+
:param roots: single TopLevel or iterable collection of TopLevel objects to explore
15+
:return: set of Components found, including roots
16+
"""
17+
if isinstance(roots, sbol3.TopLevel):
18+
roots = [roots]
19+
explored = set() # set being built via traversal
20+
21+
# subfunction for walking containment tree
22+
def walk_tree(obj: sbol3.TopLevel):
23+
if obj not in explored:
24+
explored.add(obj)
25+
if isinstance(obj, sbol3.Component):
26+
for f in (f.instance_of.lookup() for f in obj.features if isinstance(f, sbol3.SubComponent)):
27+
walk_tree(f)
28+
elif isinstance(obj, sbol3.Collection):
29+
for m in obj.members:
30+
walk_tree(m.lookup())
31+
for r in roots:
32+
walk_tree(r)
33+
# filter result for containers:
34+
return {c for c in explored if isinstance(c, sbol3.Component)}
35+
36+
37+
def ensure_singleton_feature(system: sbol3.Component, target: Union[sbol3.Feature, sbol3.Component]):
38+
"""Return a feature associated with the target, i.e., the target itself if a feature, or a SubComponent
39+
If the target is not already in the system, add it.
40+
Raises ValueError if given a Component with multiple instances
41+
42+
:return: associated feature
43+
"""
44+
if isinstance(target, sbol3.Feature): # features are returned directly
45+
if target not in system.features:
46+
system.features.append(target)
47+
return target
48+
instances = [f for f in system.features if isinstance(f, sbol3.SubComponent) and f.instance_of == target.identity]
49+
if len(instances) == 1: # if there is precisely one SubComponent, return it
50+
return instances[0]
51+
elif not len(instances): # if there are no SubComponents, add one
52+
return add_feature(system, target)
53+
else: # if there are multiple SubComponents, raise an exception
54+
raise ValueError(f'Ambiguous reference: {len(instances)} instances of {target.identity} in {system.identity}')
55+
56+
57+
def ensure_singleton_system(system: Optional[sbol3.Component], *features: Union[sbol3.Feature, sbol3.Component])\
58+
-> sbol3.Component:
59+
"""Check that the system referred to is unambiguous. Raises ValueError if there are multiple or zero systems
60+
61+
:param system: Optional explicit specification of system
62+
:param features: features in the same system or components to be referenced from it
63+
:return: Component for the identified system
64+
"""
65+
systems = set(filter(None,(get_parent(f) for f in features if isinstance(f, sbol3.Feature))))
66+
if system:
67+
systems |= {system}
68+
if len(systems) == 1:
69+
system = systems.pop()
70+
if not isinstance(system, sbol3.Component):
71+
raise ValueError(f'Could not find system, instead found {system}')
72+
return system
73+
elif not systems:
74+
raise ValueError(f'Could not find system: no features in {features}')
75+
else:
76+
raise ValueError(f'Multiple systems referred to: {systems}')
77+
78+
79+
def add_feature(component: sbol3.Component, to_add: Union[sbol3.Feature, sbol3.Component]) -> sbol3.Feature:
80+
"""Pass-through adder for adding a Feature to a Component for allowing slightly more compact code.
81+
Note that unlike ensure_singleton_feature, this allows adding multiple instances
82+
83+
:param component: Component to add the Feature to
84+
:param to_add: Feature or Component to be added to system. Components will be wrapped in a SubComponent Feature
85+
:return: feature added (SubComponent if to_add was a Component)
86+
"""
87+
if isinstance(to_add, sbol3.Component):
88+
to_add = sbol3.SubComponent(to_add)
89+
component.features.append(to_add)
90+
return to_add
91+
92+
93+
def contains(container: Union[sbol3.Feature, sbol3.Component], contained: Union[sbol3.Feature, sbol3.Component],
94+
system: Optional[sbol3.Component] = None) -> sbol3.Feature:
95+
"""Assert a topological containment constraint between two features (e.g., a promoter contained in a plasmid)
96+
Implicitly identifies system and creates/adds features as necessary
97+
98+
:param container: containing feature
99+
:param contained: feature that is contained
100+
:param system: optional explicit statement of system
101+
:return: contained feature
102+
"""
103+
# transform implicit arguments into explicit
104+
system = ensure_singleton_system(system, container, contained)
105+
container = ensure_singleton_feature(system, container)
106+
contained = ensure_singleton_feature(system, contained)
107+
# add a containment relation
108+
system.constraints.append(sbol3.Constraint(sbol3.SBOL_CONTAINS, subject=container, object=contained))
109+
return contained
110+
111+
112+
def order(five_prime: Union[sbol3.Feature, sbol3.Component], three_prime: Union[sbol3.Feature, sbol3.Component],
113+
system: Optional[sbol3.Component] = None) -> sbol3.Feature:
114+
"""Assert a topological ordering constraint between two features (e.g., a CDS followed by a terminator)
115+
Implicitly identifies system and creates/adds features as necessary
116+
117+
:param five_prime: containing feature
118+
:param three_prime: feature that is contained
119+
:param system: optional explicit statement of system
120+
:return: three_prime feature
121+
"""
122+
# transform implicit arguments into explicit
123+
system = ensure_singleton_system(system, five_prime, three_prime)
124+
five_prime = ensure_singleton_feature(system, five_prime)
125+
three_prime = ensure_singleton_feature(system, three_prime)
126+
# add a containment relation
127+
system.constraints.append(sbol3.Constraint(sbol3.SBOL_MEETS, subject=five_prime, object=three_prime))
128+
return three_prime
129+
130+
131+
def regulate(five_prime: Union[sbol3.Feature, sbol3.Component], target: Union[sbol3.Feature, sbol3.Component],
132+
system: Optional[sbol3.Component] = None) -> sbol3.Feature:
133+
"""Connect a 5' regulatory region to control the expression of a 3' target region
134+
Note: this function is an alias for "order"
135+
136+
:param five_prime: Regulatory region to place upstream of target
137+
:param target: region to be regulated (e.g., a CDS or ncRNA)
138+
:param system: optional explicit statement of system
139+
:return: target feature
140+
"""
141+
return order(five_prime, target, system)
142+
143+
144+
def constitutive(target: Union[sbol3.Feature, sbol3.Component], system: Optional[sbol3.Component] = None)\
145+
-> sbol3.Feature:
146+
"""Add a constitutive promoter regulating the target feature
147+
148+
:param target: 5' region for promoter to regulate
149+
:param system: optional explicit statement of system
150+
:return: newly created constitutive promoter
151+
"""
152+
# transform implicit arguments into explicit
153+
system = ensure_singleton_system(system, target)
154+
target = ensure_singleton_feature(system, target)
155+
156+
# create a constitutive promoter and use it to regulate the target
157+
promoter = add_feature(system, sbol3.LocalSubComponent([sbol3.SBO_DNA], roles=[tyto.SO.constitutive_promoter]))
158+
regulate(promoter, target)
159+
160+
# also add the promoter into any containers that hold the target
161+
# TODO: add lookups for constraints like we have for interactions
162+
containers = [c.subject for c in system.constraints
163+
if c.restriction == sbol3.SBOL_CONTAINS and c.object == target.identity]
164+
for c in containers:
165+
contains(c.lookup(), promoter)
166+
167+
return promoter
168+
169+
170+
def add_interaction(interaction_type: str,
171+
participants: Dict[Union[sbol3.Feature, sbol3.Component], str],
172+
system: sbol3.Component = None,
173+
name: str = None) -> sbol3.Interaction:
174+
"""Compact function for creation of an interaction
175+
Implicitly identifies system and creates/adds features as necessary
176+
177+
:param interaction_type: SBO type of interaction to be to be added
178+
:param participants: dictionary assigning features/components to roles for participations
179+
:param system: system to add interaction to
180+
:param name: name for the interaction
181+
:return: interaction
182+
"""
183+
# transform implicit arguments into explicit
184+
system = ensure_singleton_system(system, *participants.keys())
185+
participations = [sbol3.Participation([r], ensure_singleton_feature(system, p)) for p, r in participants.items()]
186+
# make and return interaction
187+
interaction = sbol3.Interaction([interaction_type], participations=participations, name=name)
188+
system.interactions.append(interaction)
189+
return interaction
190+
191+
192+
def in_role(interaction: sbol3.Interaction, role: str) -> sbol3.Feature:
193+
"""Find the (precisely one) feature with a given role in the interaction
194+
195+
:param interaction: interaction to search
196+
:param role: role to search for
197+
:return Feature playing that role
198+
"""
199+
feature_participation = [p for p in interaction.participations if role in p.roles]
200+
if len(feature_participation) != 1:
201+
raise ValueError(f'Role can be in 1 participant: found {len(feature_participation)} in {interaction.identity}')
202+
return feature_participation[0].participant.lookup()
203+
204+
205+
def all_in_role(interaction: sbol3.Interaction, role: str) -> List[sbol3.Feature]:
206+
"""Find the features with a given role in the interaction
207+
208+
:param interaction: interaction to search
209+
:param role: role to search for
210+
:return sorted list of Features playing that role
211+
"""
212+
return id_sort([p.participant.lookup() for p in interaction.participations if role in p.roles])

sbol_utilities/excel_to_sbol.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,9 @@
66

77
import sbol3
88
import openpyxl
9-
from .helper_functions import toplevel_named, strip_sbol2_version, type_to_standard_extension, is_plasmid, \
10-
tyto_lookup_with_caching, string_to_display_id, url_to_identity, strip_filetype_suffix
9+
from .helper_functions import toplevel_named, strip_sbol2_version, is_plasmid, string_to_display_id, url_to_identity, \
10+
strip_filetype_suffix
11+
from .workarounds import type_to_standard_extension, tyto_lookup_with_caching
1112

1213
BASIC_PARTS_COLLECTION = 'BasicParts'
1314
COMPOSITE_PARTS_COLLECTION = 'CompositeParts'
@@ -130,6 +131,8 @@ def row_to_basic_part(doc: sbol3.Document, row, basic_parts: sbol3.Collection, l
130131
name = row[config['basic_name_col']].value
131132
if name is None:
132133
return # skip lines without names
134+
else:
135+
name = name.strip() # make sure we're discarding whitespace
133136
raw_role = row[config['basic_role_col']].value
134137
try: # look up with tyto; if fail, leave blank or add to description
135138
role = (tyto_lookup_with_caching(raw_role) if raw_role else None)
@@ -183,7 +186,7 @@ def row_to_basic_part(doc: sbol3.Document, row, basic_parts: sbol3.Collection, l
183186
if circular:
184187
component.types.append(sbol3.SO_CIRCULAR)
185188
if sequence:
186-
sbol_seq = sbol3.Sequence(f'{display_id}_sequence', encoding=sbol3.IUPAC_DNA_ENCODING, elements=sequence)
189+
sbol_seq = sbol3.Sequence(f'{component.identity}_sequence', encoding=sbol3.IUPAC_DNA_ENCODING, elements=sequence)
187190
doc.add(sbol_seq)
188191
component.sequences.append(sbol_seq.identity)
189192

0 commit comments

Comments
 (0)