diff --git a/poetry.lock b/poetry.lock index f9c6565..7bfb364 100644 --- a/poetry.lock +++ b/poetry.lock @@ -200,14 +200,14 @@ uvloop = ["uvloop (>=0.15.2)"] [[package]] name = "certifi" -version = "2026.1.4" +version = "2026.2.25" description = "Python package for providing Mozilla's CA Bundle." optional = false python-versions = ">=3.7" groups = ["main", "test"] files = [ - {file = "certifi-2026.1.4-py3-none-any.whl", hash = "sha256:9943707519e4add1115f44c2bc244f782c0249876bf51b6599fee1ffbedd685c"}, - {file = "certifi-2026.1.4.tar.gz", hash = "sha256:ac726dd470482006e014ad384921ed6438c457018f4b3d204aea4281258b2120"}, + {file = "certifi-2026.2.25-py3-none-any.whl", hash = "sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa"}, + {file = "certifi-2026.2.25.tar.gz", hash = "sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7"}, ] [[package]] @@ -575,14 +575,14 @@ tests = ["asttokens (>=2.1.0)", "coverage", "coverage-enable-subprocess", "ipyth [[package]] name = "fastapi" -version = "0.133.0" +version = "0.133.1" description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" optional = false python-versions = ">=3.10" groups = ["main"] files = [ - {file = "fastapi-0.133.0-py3-none-any.whl", hash = "sha256:0a78878483d60702a1dde864c24ab349a1a53ef4db6b6f74f8cd4a2b2bc67d2f"}, - {file = "fastapi-0.133.0.tar.gz", hash = "sha256:b900a2bf5685cdb0647a41d5900bdeafc3a9e8a28ac08c6246b76699e164d60d"}, + {file = "fastapi-0.133.1-py3-none-any.whl", hash = "sha256:658f34ba334605b1617a65adf2ea6461901bdb9af3a3080d63ff791ecf7dc2e2"}, + {file = "fastapi-0.133.1.tar.gz", hash = "sha256:ed152a45912f102592976fde6cbce7dae1a8a1053da94202e51dd35d184fadd6"}, ] [package.dependencies] @@ -1954,6 +1954,26 @@ files = [ [package.dependencies] six = ">=1.5" +[[package]] +name = "python-discovery" +version = "1.1.0" +description = "Python interpreter discovery" +optional = false +python-versions = ">=3.8" +groups = ["dev", "test"] +files = [ + {file = "python_discovery-1.1.0-py3-none-any.whl", hash = "sha256:a162893b8809727f54594a99ad2179d2ede4bf953e12d4c7abc3cc9cdbd1437b"}, + {file = "python_discovery-1.1.0.tar.gz", hash = "sha256:447941ba1aed8cc2ab7ee3cb91be5fc137c5bdbb05b7e6ea62fbdcb66e50b268"}, +] + +[package.dependencies] +filelock = ">=3.15.4" +platformdirs = ">=4.3.6,<5" + +[package.extras] +docs = ["furo (>=2025.12.19)", "sphinx (>=9.1)", "sphinx-autodoc-typehints (>=3.6.3)", "sphinxcontrib-mermaid (>=2)"] +testing = ["covdefaults (>=2.3)", "coverage (>=7.5.4)", "pytest (>=8.3.5)", "pytest-mock (>=3.14)", "setuptools (>=75.1)"] + [[package]] name = "python-multipart" version = "0.0.22" @@ -2604,20 +2624,21 @@ standard = ["colorama (>=0.4) ; sys_platform == \"win32\"", "httptools (>=0.6.3) [[package]] name = "virtualenv" -version = "20.39.0" +version = "21.1.0" description = "Virtual Python Environment builder" optional = false python-versions = ">=3.8" groups = ["dev", "test"] files = [ - {file = "virtualenv-20.39.0-py3-none-any.whl", hash = "sha256:44888bba3775990a152ea1f73f8e5f566d49f11bbd1de61d426fd7732770043e"}, - {file = "virtualenv-20.39.0.tar.gz", hash = "sha256:a15f0cebd00d50074fd336a169d53422436a12dfe15149efec7072cfe817df8b"}, + {file = "virtualenv-21.1.0-py3-none-any.whl", hash = "sha256:164f5e14c5587d170cf98e60378eb91ea35bf037be313811905d3a24ea33cc07"}, + {file = "virtualenv-21.1.0.tar.gz", hash = "sha256:1990a0188c8f16b6b9cf65c9183049007375b26aad415514d377ccacf1e4fb44"}, ] [package.dependencies] distlib = ">=0.3.7,<1" filelock = {version = ">=3.24.2,<4", markers = "python_version >= \"3.10\""} platformdirs = ">=3.9.1,<5" +python-discovery = ">=1" [[package]] name = "watchfiles" diff --git a/src/opencloning/endpoints/primer_design.py b/src/opencloning/endpoints/primer_design.py index 305c6f0..5a04578 100644 --- a/src/opencloning/endpoints/primer_design.py +++ b/src/opencloning/endpoints/primer_design.py @@ -4,6 +4,8 @@ from Bio.Restriction import RestrictionBatch from Bio.SeqUtils import gc_fraction +from opencloning.temp_functions import pydna_primer_to_primer_model + from ..dna_functions import get_invalid_enzyme_names from opencloning_linkml.datamodel import Primer as PrimerModel from opencloning.pydantic_models import PrimerDesignQuery @@ -24,10 +26,11 @@ from ..get_router import get_router from ..ebic.primer_design import ebic_primers from pydantic import BaseModel +import copy router = get_router() -PrimerDesignResponse = create_model('PrimerDesignResponse', primers=(list[PrimerModel], ...)) +PrimerDesignResponse = create_model('PrimerDesignResponse', primers=(list[PrimerModel | None], ...)) def validate_spacers(spacers: list[str] | None, nb_templates: int, circular: bool): @@ -101,7 +104,13 @@ async def primer_design_homologous_recombination( @router.post('/primer_design/gibson_assembly', response_model=PrimerDesignResponse) async def primer_design_gibson_assembly( - pcr_templates: list[PrimerDesignQuery], + pcr_templates: list[PrimerDesignQuery] = Body( + ..., + description='''The templates to design primers for. If the location is not + provided for a pcr_template, primers won't be designed for that part (the part will be included + in the Gibson as is).''', + min_length=1, + ), settings: PrimerDesignSettings = Body(description='Primer design settings.', default_factory=PrimerDesignSettings), spacers: list[str] | None = Body( None, @@ -116,19 +125,26 @@ async def primer_design_gibson_assembly( ), circular: bool = Query(False, description='Whether the assembly is circular.'), ): - """Design primers for Gibson assembly""" + """Design primers for Gibson assembly. If the location is not provided for a pcr_template, primers won't be designed for that part.""" + # Validate the spacers validate_spacers(spacers, len(pcr_templates), circular) templates = list() + amplify_templates = list() for query in pcr_templates: dseqr = read_dsrecord_from_json(query.sequence) - location = query.location.to_biopython_location() - template = location.extract(dseqr) - if not query.forward_orientation: - template = template.reverse_complement() - # For naming the primers - template.name = dseqr.name - template.id = dseqr.id + if query.location is not None: + location = query.location.to_biopython_location() + template = location.extract(dseqr) + if not query.forward_orientation: + template = template.reverse_complement() + # For naming the primers + template.name = dseqr.name + template.id = dseqr.id + amplify_templates.append(True) + else: + template = copy.deepcopy(dseqr) + amplify_templates.append(False) templates.append(template) try: primers = gibson_assembly_primers( @@ -139,11 +155,12 @@ async def primer_design_gibson_assembly( circular, spacers, tm_func=lambda x: primer3_calc_tm(x, settings), + amplify_templates=amplify_templates, ) except ValueError as e: raise HTTPException(400, *e.args) - return {'primers': primers} + return {'primers': [pydna_primer_to_primer_model(primer) if primer is not None else None for primer in primers]} @router.post('/primer_design/simple_pair', response_model=PrimerDesignResponse) @@ -193,7 +210,7 @@ async def primer_design_simple_pair( # This is to my knowledge the only way to get the enzymes rb = RestrictionBatch() try: - fwd, rvs = simple_pair_primers( + primers = simple_pair_primers( template, minimal_hybridization_length, target_tm, @@ -208,7 +225,7 @@ async def primer_design_simple_pair( except ValueError as e: raise HTTPException(400, *e.args) - return {'primers': [fwd, rvs]} + return {'primers': [pydna_primer_to_primer_model(primer) for primer in primers]} @router.post('/primer_design/ebic', response_model=PrimerDesignResponse) diff --git a/src/opencloning/primer_design.py b/src/opencloning/primer_design.py index 85634ad..fe4feb9 100644 --- a/src/opencloning/primer_design.py +++ b/src/opencloning/primer_design.py @@ -1,4 +1,5 @@ from pydna.dseqrecord import Dseqrecord +from pydna.primer import Primer from pydna.design import primer_design, assembly_fragments from Bio.SeqFeature import SimpleLocation from pydna.utils import locations_overlap, shift_location, location_boundaries @@ -8,7 +9,6 @@ from Bio.Data.IUPACData import ambiguous_dna_values as _ambiguous_dna_values from typing import Callable from .primer3_functions import primer3_calc_tm, PrimerDesignSettings -from opencloning_linkml.datamodel import Primer as PrimerModel ambiguous_dna_values = _ambiguous_dna_values.copy() # Remove acgt @@ -92,51 +92,92 @@ def gibson_assembly_primers( spacers: list[str] | None = None, tm_func: Callable[[str], float] = default_tm_func, estimate_function: Callable[[str], float] | None = None, -) -> list[PrimerModel]: - - initial_amplicons = [ - primer_design( - template, - limit=minimal_hybridization_length, - target_tm=target_tm, - tm_func=tm_func, - estimate_function=estimate_function, - ) - for template in templates - ] - - for i, amplicon in enumerate(initial_amplicons): - if None in amplicon.primers(): + amplify_templates: list[bool] | None = None, +) -> list[Primer]: + + if amplify_templates is None: + amplify_templates = [True] * len(templates) + if len(amplify_templates) != len(templates): + raise ValueError('The number of amplify_templates must be the same as the number of templates.') + for prev, next in zip(amplify_templates[:-1], amplify_templates[1:]): + if not prev and not next: + raise ValueError('Two consecutive templates with amplify_templates=False are not allowed.') + if circular and (not amplify_templates[-1] and not amplify_templates[0]): + raise ValueError('Two consecutive templates with amplify_templates=False are not allowed.') + + if len(templates) == 1 and amplify_templates[0] is False: + raise ValueError('amplify_templates cannot be False for a single template.') + + if spacers is not None and not circular: + if spacers[0] != '' and not amplify_templates[0]: + raise ValueError( + 'The first spacer must be empty if the first template is not amplified in linear assembly.' + ) + if spacers[-1] != '' and not amplify_templates[-1]: + raise ValueError('The last spacer must be empty if the last template is not amplified in linear assembly.') + + # For the function assembly_fragments, maxlink is the maximum length of a Dseqrecord to be considered a spacer. + # It's important to check that the amplify_templates=False parts are not longer than maxlink, otherwise, they + # would be considered as spacers. This is perhaps not ideal, as there could be a case, for now we just do it + # like this. + + maxlink = minimal_hybridization_length * 2 + if spacers is not None: + maxlink = max(len(spacer) for spacer in spacers) + + inputs: list[Amplicon | Dseqrecord] = list() + for i, template in enumerate(templates): + if amplify_templates[i]: + inputs.append( + primer_design( + template, + limit=minimal_hybridization_length, + target_tm=target_tm, + tm_func=tm_func, + estimate_function=estimate_function, + ) + ) + else: + if len(template) < maxlink: + raise ValueError( + f'Template {template.name} ({len(template)} bps) is shorter than the longest spacer or 2x the minimal hybridization length.' + ) + inputs.append(template) + + for i, amplicon in enumerate(inputs): + if amplify_templates[i] is True and None in amplicon.primers(): raise ValueError(f'Primers could not be designed for template {templates[i].name}, try changing settings.') - maxlink = 40 if spacers is not None: - maxlink = max(len(spacer) for spacer in spacers) spacers = [Dseqrecord(spacer) for spacer in spacers] - initial_amplicons_with_spacers = [] + inputs_withspacers = [] # For linear assemblies, the first spacer is the first thing if not circular: - initial_amplicons_with_spacers.append(spacers.pop(0)) - for amplicon in initial_amplicons: - initial_amplicons_with_spacers.append(amplicon) - initial_amplicons_with_spacers.append(spacers.pop(0)) - initial_amplicons = initial_amplicons_with_spacers + inputs_withspacers.append(spacers.pop(0)) + for part in inputs: + inputs_withspacers.append(part) + inputs_withspacers.append(spacers.pop(0)) + inputs = inputs_withspacers # Maxlink is used to define what is a spacer or what is a template (see docs) - - assembly_amplicons: list[Amplicon] = assembly_fragments( - initial_amplicons, overlap=homology_length, circular=circular, maxlink=maxlink + inputs = [i for i in inputs if len(i) > 0] + assembly_output: list[Amplicon] = assembly_fragments( + inputs, overlap=homology_length, circular=circular, maxlink=maxlink ) - all_primers = sum((list(amplicon.primers()) for amplicon in assembly_amplicons), []) + all_primers = list() + for i, part in enumerate(assembly_output): + all_primers.extend(list(part.primers() if amplify_templates[i] is True else [None, None])) for i in range(0, len(all_primers), 2): fwd, rvs = all_primers[i : i + 2] + if fwd is None or rvs is None: + continue template = templates[i // 2] template_name = template.name if template.name != 'name' else f'seq_{template.id}' fwd.name = f'{template_name}_fwd' rvs.name = f'{template_name}_rvs' - return [PrimerModel(id=0, name=primer.name, sequence=str(primer.seq)) for primer in all_primers] + return all_primers def sanitize_enzyme_site(site: str) -> str: @@ -158,7 +199,7 @@ def simple_pair_primers( right_enzyme_inverted: bool = False, tm_func: Callable[[str], float] = default_tm_func, estimate_function: Callable[[str], float] | None = None, -) -> tuple[PrimerModel, PrimerModel]: +) -> tuple[Primer, Primer]: """ Design primers to amplify a DNA fragment, if left_enzyme or right_enzyme are set, the primers will be designed to include the restriction enzyme sites. @@ -202,10 +243,7 @@ def simple_pair_primers( fwd_primer_name = f'{template_name}_{left_enzyme}_fwd' if left_enzyme is not None else f'{template_name}_fwd' rvs_primer_name = f'{template_name}_{right_enzyme}_rvs' if right_enzyme is not None else f'{template_name}_rvs' - return ( - PrimerModel(id=0, name=fwd_primer_name, sequence=str(fwd_primer_seq)), - PrimerModel(id=0, name=rvs_primer_name, sequence=str(rvs_primer_seq)), - ) + return (Primer(fwd_primer_seq, name=fwd_primer_name), Primer(rvs_primer_seq, name=rvs_primer_name)) # def gateway_attB_primers( diff --git a/src/opencloning/pydantic_models.py b/src/opencloning/pydantic_models.py index 94728c2..a357c3a 100644 --- a/src/opencloning/pydantic_models.py +++ b/src/opencloning/pydantic_models.py @@ -67,9 +67,11 @@ def all_children_source_ids(self, source_id: int, source_children: list | None = class PrimerDesignQuery(BaseModel): model_config = {'arbitrary_types_allowed': True} sequence: _TextFileSequence - location: SequenceLocationStr + location: SequenceLocationStr | None forward_orientation: bool = True @field_validator('location', mode='before') def parse_location(cls, v): + if v is None: + return None return SequenceLocationStr.field_validator(v) diff --git a/src/opencloning/temp_functions.py b/src/opencloning/temp_functions.py index cc12282..6d50473 100644 --- a/src/opencloning/temp_functions.py +++ b/src/opencloning/temp_functions.py @@ -48,3 +48,7 @@ def restriction_sequence_cut_to_cutsite_tuple( def primer_model_to_pydna_primer(primer_model: PrimerModel) -> PydnaPrimer: return PydnaPrimer(primer_model.sequence, id=str(primer_model.id), name=primer_model.name) + + +def pydna_primer_to_primer_model(pydna_primer: PydnaPrimer) -> PrimerModel: + return PrimerModel(id=0, name=pydna_primer.name, sequence=str(pydna_primer.seq)) diff --git a/tests/test_endpoints_primer_design.py b/tests/test_endpoints_primer_design.py index ac31e70..d4a41ea 100644 --- a/tests/test_endpoints_primer_design.py +++ b/tests/test_endpoints_primer_design.py @@ -275,6 +275,37 @@ def test_gibson_assembly(self): ) self.assertEqual(response.status_code, 422) + def test_gibson_assembly_amplify_templates(self): + to_amplify = Dseqrecord('aagccagaagtgcatttggatccaagtgcctccattttaaatctctcatcttc', name='to_amplify') + to_amplify.add_feature(0, len(to_amplify), label='to_amplify') + spacer_like = Dseqrecord('attaattcctttgaagaagaaattttgggtttgtggtctgagcctaaat', name='spacer_like') + spacer_like.add_feature(0, len(spacer_like), label='spacer_like') + + queries = [ + { + 'sequence': format_sequence_genbank(to_amplify).model_dump(), + 'location': SequenceLocationStr.from_start_and_end(start=0, end=len(to_amplify)), + 'forward_orientation': True, + }, + { + 'sequence': format_sequence_genbank(spacer_like).model_dump(), + 'location': None, + 'forward_orientation': True, + }, + ] + + params = {'homology_length': 20, 'minimal_hybridization_length': 15, 'target_tm': 55, 'circular': True} + + response = client.post( + '/primer_design/gibson_assembly', json={'pcr_templates': queries, 'spacers': None}, params=params + ) + + primers = [PrimerModel.model_validate(p) if p is not None else None for p in response.json()['primers']] + self.assertEqual(primers[0].name, 'to_amplify_fwd') + self.assertEqual(primers[1].name, 'to_amplify_rvs') + self.assertEqual(primers[2], None) + self.assertEqual(primers[3], None) + def test_simple_pair(self): from Bio.Restriction import EcoRI, BamHI diff --git a/tests/test_primer_design.py b/tests/test_primer_design.py index df6c383..74d4d8b 100644 --- a/tests/test_primer_design.py +++ b/tests/test_primer_design.py @@ -9,15 +9,13 @@ from pydna.dseqrecord import Dseqrecord from pydna.amplify import pcr from pydna.parsers import parse -from pydna.assembly2 import Assembly, gibson_overlap +from pydna.assembly2 import Assembly, gibson_overlap, gibson_assembly import pytest from Bio.Data.IUPACData import ambiguous_dna_values -from Bio.Seq import reverse_complement import os from opencloning.primer3_functions import PrimerDesignSettings -from opencloning.temp_functions import primer_model_to_pydna_primer -from opencloning_linkml.datamodel import Primer as PrimerModel +from pydna.primer import Primer test_files = os.path.join(os.path.dirname(__file__), 'test_files') @@ -298,7 +296,7 @@ def test_normal_examples(self): # Check if all primers are instances of PrimerModel for primer in primers: - self.assertIsInstance(primer, PrimerModel) + self.assertIsInstance(primer, Primer) # Check if primer names are correctly formatted for i, primer in enumerate(primers): @@ -312,8 +310,8 @@ def test_normal_examples(self): for i in range(len(templates)): amplicons.append( pcr( - primer_model_to_pydna_primer(primers[i * 2]), - primer_model_to_pydna_primer(primers[i * 2 + 1]), + primers[i * 2], + primers[i * 2 + 1], templates[i], ) ) @@ -344,8 +342,8 @@ def test_normal_examples(self): for i in range(len(templates)): amplicons.append( pcr( - primer_model_to_pydna_primer(primers[i * 2]), - primer_model_to_pydna_primer(primers[i * 2 + 1]), + primers[i * 2], + primers[i * 2 + 1], templates[i], ) ) @@ -381,14 +379,14 @@ def test_primer_with_spacers(self): templates, homology_length, minimal_hybridization_length, target_tm, circular, spacers=spacers ) - self.assertTrue(primers[0].sequence.startswith('TTAAGTACccccAAACAGTA')) - self.assertTrue(reverse_complement(primers[-1].sequence).endswith('TTAAGTACccccAAACAGTA')) + self.assertTrue(primers[0].seq.startswith('TTAAGTACccccAAACAGTA')) + self.assertTrue(primers[-1].seq.reverse_complement().endswith('TTAAGTACccccAAACAGTA')) - self.assertTrue(reverse_complement(primers[1].sequence).endswith('GATTCTATaaaaGTTTACAA')) - self.assertTrue(primers[2].sequence.startswith('GATTCTATaaaaGTTTACAA')) + self.assertTrue(primers[1].seq.reverse_complement().endswith('GATTCTATaaaaGTTTACAA')) + self.assertTrue(primers[2].seq.startswith('GATTCTATaaaaGTTTACAA')) - self.assertTrue(reverse_complement(primers[3].sequence).endswith('AAATGGAAttttAAGGACAA')) - self.assertTrue(primers[4].sequence.startswith('AAATGGAAttttAAGGACAA')) + self.assertTrue(primers[3].seq.reverse_complement().endswith('AAATGGAAttttAAGGACAA')) + self.assertTrue(primers[4].seq.startswith('AAATGGAAttttAAGGACAA')) @pytest.mark.xfail(reason='Waiting on https://github.com/BjornFJohansson/pydna/issues/265') def test_primer_errors(self): @@ -431,6 +429,158 @@ def test_primer_errors(self): except ValueError as e: self.assertEqual(str(e), 'Primers could not be designed for template 1, 2, try changing settings.') + def test_primer_with_amplify_templates(self): + to_amplify = Dseqrecord('aagccagaagtgcatttggatccaagtgcctccattttaaatctctcatcttc', name='to_amplify') + to_amplify.add_feature(0, len(to_amplify), label='to_amplify') + spacer_like = Dseqrecord('attaattcctttgaagaagaaattttgggtttgtggtctgagcctaaat', name='spacer_like') + spacer_like.add_feature(0, len(spacer_like), label='spacer_like') + + primers = gibson_assembly_primers( + [to_amplify, spacer_like], + homology_length=15, + minimal_hybridization_length=15, + target_tm=55, + circular=True, + amplify_templates=[True, False], + ) + + self.assertEqual(len(primers), 4) + self.assertEqual(primers[0].name, 'to_amplify_fwd') + self.assertEqual(primers[1].name, 'to_amplify_rvs') + self.assertTrue(primers[0].seq.startswith('ggtctgagcctaaat')) + self.assertTrue(primers[1].seq.reverse_complement().endswith('attaattcctttgaa')) + self.assertEqual(primers[2], None) + self.assertEqual(primers[3], None) + + # Test linear assembly + primers = gibson_assembly_primers( + [to_amplify, spacer_like], + homology_length=15, + minimal_hybridization_length=15, + target_tm=55, + circular=False, + amplify_templates=[True, False], + spacers=['ATATATA', 'GCGCGCGC', ''], + ) + self.assertEqual(len(primers), 4) + self.assertEqual(primers[2], None) + self.assertEqual(primers[3], None) + pcr_product = pcr(primers[0], primers[1], to_amplify) + + assembly_product = gibson_assembly([pcr_product, spacer_like], limit=15)[0] + self.assertEqual( + str(assembly_product.seq).lower(), + ('ATATATA' + str(to_amplify.seq) + 'GCGCGCGC' + str(spacer_like.seq)).lower(), + ) + + def test_validation_errors(self): + """ + Test the validation errors for the gibson_assembly_primers function. + """ + templates = [ + Dseqrecord('AAACAGTAATACGTTCCTTTTTTATGATGATGGATGACATTCAAAGCACTGATTCTAT'), + Dseqrecord('GTTTACAACGGCAATGAACGTTCCTTTTTTATGATATGCCCAGCTTCATGAAATGGAA'), + Dseqrecord('AAGGACAACGTTCCTTTTTTATGATATATATGGCACAGTATGATCAAAAGTTAAGTAC'), + ] + templates[1].name = 'template_1' + homology_length = 20 + minimal_hybridization_length = 15 + target_tm = 55 + circular = False + + # Not two consecutive templates with amplify_templates=False + with pytest.raises(ValueError) as e: + gibson_assembly_primers( + templates, + homology_length, + minimal_hybridization_length, + target_tm, + circular, + amplify_templates=[True, False, False], + ) + self.assertEqual(str(e.value), 'Two consecutive templates with amplify_templates=False are not allowed.') + + # Also in circular + with pytest.raises(ValueError) as e: + gibson_assembly_primers( + templates, + homology_length, + minimal_hybridization_length, + target_tm, + circular=True, + amplify_templates=[False, True, False], + ) + + # No mismatch in length between templates and amplify_templates + with pytest.raises(ValueError) as e: + gibson_assembly_primers( + templates, + homology_length, + minimal_hybridization_length, + target_tm, + circular, + amplify_templates=[True, False], + ) + self.assertEqual(str(e.value), 'The number of amplify_templates must be the same as the number of templates.') + + # Spacer too long + with pytest.raises(ValueError) as e: + gibson_assembly_primers( + templates, + homology_length, + minimal_hybridization_length, + target_tm, + circular, + spacers=['', 'ACGT' * 100], + amplify_templates=[True, False, True], + ) + self.assertEqual( + str(e.value), + 'Template template_1 (58 bps) is shorter than the longest spacer or 2x the minimal hybridization length.', + ) + + # Single input, amplify_templates=False + with pytest.raises(ValueError) as e: + gibson_assembly_primers( + templates[:1], + homology_length, + minimal_hybridization_length, + target_tm, + circular, + amplify_templates=[False], + ) + self.assertEqual(str(e.value), 'amplify_templates cannot be False for a single template.') + + # First spacer cannot be empty if the first template is not amplified in linear assembly + with pytest.raises(ValueError) as e: + gibson_assembly_primers( + templates[:2], + homology_length, + minimal_hybridization_length, + target_tm, + circular=False, + spacers=['ACGT', '', ''], + amplify_templates=[False, True], + ) + self.assertEqual( + str(e.value), 'The first spacer must be empty if the first template is not amplified in linear assembly.' + ) + + # Last spacer must be empty if the last template is not amplified in linear assembly + with pytest.raises(ValueError) as e: + gibson_assembly_primers( + templates[:2], + homology_length, + minimal_hybridization_length, + target_tm, + circular=False, + spacers=['', '', 'ACGT'], + amplify_templates=[True, False], + ) + self.assertEqual( + str(e.value), 'The last spacer must be empty if the last template is not amplified in linear assembly.' + ) + class TestSimplePairPrimers(TestCase): def test_restriction_enzyme_primers(self): @@ -453,8 +603,8 @@ def test_restriction_enzyme_primers(self): ) # Check that primers contain the correct restriction sites - self.assertTrue(fwd.sequence.startswith('GC' + str(EcoRI.site))) - self.assertTrue(rvs.sequence.startswith('GC' + str(BamHI.site))) + self.assertTrue(fwd.seq.startswith('GC' + str(EcoRI.site))) + self.assertTrue(rvs.seq.startswith('GC' + str(BamHI.site))) # Check that the name is correct self.assertEqual(fwd.name, 'dummy_EcoRI_fwd') @@ -474,22 +624,22 @@ def test_restriction_enzyme_primers(self): template, minimal_hybridization_length, target_tm, left_enzyme, None, filler_bases ) - self.assertTrue(fwd.sequence.startswith('GC' + str(EcoRI.site))) - self.assertFalse(rvs.sequence.startswith('GC' + str(BamHI.site))) + self.assertTrue(fwd.seq.startswith('GC' + str(EcoRI.site))) + self.assertFalse(rvs.seq.startswith('GC' + str(BamHI.site))) # Test with only right enzyme fwd, rvs = simple_pair_primers( template, minimal_hybridization_length, target_tm, None, right_enzyme, filler_bases ) - self.assertFalse(fwd.sequence.startswith('GC' + str(EcoRI.site))) - self.assertTrue(rvs.sequence.startswith('GC' + str(BamHI.site))) + self.assertFalse(fwd.seq.startswith('GC' + str(EcoRI.site))) + self.assertTrue(rvs.seq.startswith('GC' + str(BamHI.site))) # Test with no enzymes fwd, rvs = simple_pair_primers(template, minimal_hybridization_length, target_tm, None, None, filler_bases) - self.assertFalse(fwd.sequence.startswith('GC' + str(EcoRI.site))) - self.assertFalse(rvs.sequence.startswith('GC' + str(BamHI.site))) + self.assertFalse(fwd.seq.startswith('GC' + str(EcoRI.site))) + self.assertFalse(rvs.seq.startswith('GC' + str(BamHI.site))) # Test with enzyme that has ambiguous bases fwd, rvs = simple_pair_primers(template, minimal_hybridization_length, target_tm, AflIII, AflIII, filler_bases) @@ -497,8 +647,8 @@ def test_restriction_enzyme_primers(self): str(AflIII.site).replace('R', ambiguous_dna_values['R'][0]).replace('Y', ambiguous_dna_values['Y'][0]) ) - self.assertTrue(fwd.sequence.startswith('GC' + actual_site)) - self.assertTrue(rvs.sequence.startswith('GC' + actual_site)) + self.assertTrue(fwd.seq.startswith('GC' + actual_site)) + self.assertTrue(rvs.seq.startswith('GC' + actual_site)) # Test spacers on one side only spacers = ['ACGT' * 10, ''] @@ -506,8 +656,8 @@ def test_restriction_enzyme_primers(self): template, minimal_hybridization_length, target_tm, left_enzyme, right_enzyme, filler_bases, spacers=spacers ) - self.assertTrue('GC' + str(left_enzyme.site) + 'ACGT' * 10 in fwd.sequence) - self.assertTrue('GC' + str(right_enzyme.site) in rvs.sequence) + self.assertTrue('GC' + str(left_enzyme.site) + 'ACGT' * 10 in fwd.seq) + self.assertTrue('GC' + str(right_enzyme.site) in rvs.seq) # Both spacers spacers = ['ACGT' * 10, 'ACGT' * 10] @@ -515,8 +665,8 @@ def test_restriction_enzyme_primers(self): template, minimal_hybridization_length, target_tm, left_enzyme, right_enzyme, filler_bases, spacers=spacers ) - self.assertTrue('GC' + str(left_enzyme.site) + 'ACGT' * 10 in fwd.sequence) - self.assertTrue('GC' + str(right_enzyme.site) + 'ACGT' * 10 in rvs.sequence) + self.assertTrue('GC' + str(left_enzyme.site) + 'ACGT' * 10 in fwd.seq) + self.assertTrue('GC' + str(right_enzyme.site) + 'ACGT' * 10 in rvs.seq) # Test with left_enzyme_inverted and right_enzyme_inverted fwd, rvs = simple_pair_primers( @@ -531,8 +681,8 @@ def test_restriction_enzyme_primers(self): right_enzyme_inverted=True, ) - self.assertTrue('GCGAGACC' + 'ACGT' * 10 in fwd.sequence) - self.assertTrue('GCGAGACC' + 'ACGT' * 10 in rvs.sequence) + self.assertTrue('GCGAGACC' + 'ACGT' * 10 in fwd.seq) + self.assertTrue('GCGAGACC' + 'ACGT' * 10 in rvs.seq) def test_without_restriction_enzymes(self): """ @@ -548,8 +698,8 @@ def test_without_restriction_enzymes(self): fwd, rvs = simple_pair_primers(template, minimal_hybridization_length, target_tm, None, None, filler_bases) # Check that primers are correct - self.assertTrue(fwd.sequence.startswith('ATGCA')) - self.assertTrue(rvs.sequence.startswith('GTCAT')) + self.assertTrue(fwd.seq.startswith('ATGCA')) + self.assertTrue(rvs.seq.startswith('GTCAT')) class TestEbicPrimers(TestCase):