From 2c491199d0cc16722928c79d4acd34b3baa68ce7 Mon Sep 17 00:00:00 2001 From: David Doty Date: Mon, 10 Aug 2026 10:51:48 -0500 Subject: [PATCH 1/3] fixes #344; fixes #326; consolidate packaging metadata into pyproject.toml Packaging metadata was spread across setup.py (all the real metadata), setup.cfg (a single dead `description_file` key, since setup.py set long_description explicitly), MANIFEST (a checked-in distutils artifact whose own first line says "do NOT edit") and doc/requirements.txt (docs dependencies, kept in sync with install_requires by hand). All of it now lives in one declarative PEP 621 pyproject.toml; the other four files are deleted. Because setup.py is gone, check_pypi_packaging.yml can no longer build with `python setup.py sdist bdist_wheel`, so it moves to `python -m build`, the PEP 517 front end release.yml already uses. That is #326, which this necessarily subsumes. The version stays exactly where it was, the __version__ line in scadnano/scadnano.py, and remains the single place to edit when releasing. What goes away is the hand-rolled parsing of it: setup.py and doc/conf.py each had a copy of an extract_version() that searched for a magic trailing comment and split the line on "=". Now pyproject.toml declares the version dynamic and reads the attribute, and doc/conf.py imports it from the module autodoc is already importing to document. release.yml still reads the line with sed, since it runs before any Python environment exists. The version is deliberately not moved into pyproject.toml. scadnano.py uses __version__ at run time to stamp designs it writes, and CONTRIBUTING.md commits to users being able to copy scadnano.py on its own and import it without installing the package; sourcing the version from installed distribution metadata would break that. Also: - docs dependencies become the `docs` extra, so Read the Docs and the "Docs Check" workflow install one declared list rather than two hand-synced ones; - `tests_require`, which setuptools stopped recognizing and warned about on every build, becomes the `tests` extra; - run_unit_tests.yml keeps installing openpyxl and tabulate directly rather than via the extra, so a packaging break cannot take the unit tests down with it; - publish_to_pypi.txt described a manual `python setup.py sdist` + twine upload that would now conflict with the automated release; it points at that workflow instead; - AutoStaple.md told contributors to run the long-removed `python setup.py install`. Verified against artifacts built from the previous setup.py: the wheel contents are identical (same four modules plus dist-info), and `twine check` passes on both sdist and wheel. Metadata differences are the expected PEP 621/639 forms: Home-page becomes Project-URL, Author/Author-email combine, and License: MIT becomes License-Expression: MIT. One field is genuinely lost, Download-URL, which pointed at a version-interpolated GitHub archive zip; an f-string cannot be expressed in static TOML, and PyPI does not use the field. Also verified: 375 unit tests pass, and the html, epub and latex builders all succeed with warnings treated as errors, with the version correctly resolving to 0.21.1 in the built docs. Co-Authored-By: Claude Opus 5 (1M context) --- .github/workflows/check_pypi_packaging.yml | 4 +- .github/workflows/docs-check.yml | 2 +- .github/workflows/run_unit_tests.yml | 2 +- .gitignore | 3 ++ CONTRIBUTING.md | 8 +-- MANIFEST | 7 --- doc/conf.py | 24 +++------ doc/requirements.txt | 16 ------ publish_to_pypi.txt | 17 ++++-- pyproject.toml | 55 +++++++++++++++++++ readthedocs.yml | 5 +- scadnano/docs/AutoStaple.md | 2 +- scadnano/scadnano.py | 2 +- setup.cfg | 2 - setup.py | 62 ---------------------- 15 files changed, 92 insertions(+), 119 deletions(-) delete mode 100644 MANIFEST delete mode 100644 doc/requirements.txt create mode 100644 pyproject.toml delete mode 100644 setup.cfg delete mode 100644 setup.py diff --git a/.github/workflows/check_pypi_packaging.yml b/.github/workflows/check_pypi_packaging.yml index 3031336f..42f5cd45 100644 --- a/.github/workflows/check_pypi_packaging.yml +++ b/.github/workflows/check_pypi_packaging.yml @@ -16,6 +16,6 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - pip install setuptools wheel + pip install build - name: Build - run: python setup.py sdist bdist_wheel + run: python -m build diff --git a/.github/workflows/docs-check.yml b/.github/workflows/docs-check.yml index c602887b..a52be501 100644 --- a/.github/workflows/docs-check.yml +++ b/.github/workflows/docs-check.yml @@ -13,7 +13,7 @@ jobs: - name: Install Sphinx run: | python -m pip install --upgrade pip - pip install -r doc/requirements.txt + pip install .[docs] - name: Move to docs folder and build run: | cd doc diff --git a/.github/workflows/run_unit_tests.yml b/.github/workflows/run_unit_tests.yml index 4357944c..1be010f3 100644 --- a/.github/workflows/run_unit_tests.yml +++ b/.github/workflows/run_unit_tests.yml @@ -10,7 +10,7 @@ jobs: fail-fast: false matrix: # "3.x" means the newest released Python, so the latest version is always tested even if this - # list is not kept up to date. setup.py requires >= 3.10. + # list is not kept up to date. pyproject.toml requires >= 3.10. python-version: [ "3.10", "3.11", "3.12", "3.13", "3.14" ] steps: diff --git a/.gitignore b/.gitignore index 73dd10a7..3e95fa31 100644 --- a/.gitignore +++ b/.gitignore @@ -25,4 +25,7 @@ __pycache__/ tests_outputs/ .vscode/ dist/ +build/ +# generated by `python setup.py sdist`; not MANIFEST.in, and nothing reads it +MANIFEST .mypy_cache/ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e25fc97b..62f6e19e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -221,14 +221,16 @@ So the steps for committing to the main branch are: - For the Python library repo scadnano-python-package, there is a single source of truth: the `__version__` line near the top of the file [scadnano/scadnano.py](scadnano/scadnano.py) (as `__version__ = "0.9.3"` or something similar). - Keep the trailing `# version line; WARNING: ...` comment intact — `setup.py` finds this line by - searching for that comment, and the release workflow reads the same line. + Keep the format of that line exactly as it is. Three things read it: the release workflow + (with `sed`), `pyproject.toml` (which declares the version `dynamic` and reads the + `__version__` attribute), and `doc/conf.py` (which imports it). None of them tolerate the + line being reformatted. The PATCH version numbers are not always synced between the two repos, but, they should stay synced on MAJOR and MINOR versions. **Note:** right now this isn't quite true since MINOR versions deal with backwards-compatible feature additions, and some features are supported on one but not the other; e.g., modifications can be made in the Python package but not the web interface, and calculating helix rolls/positions from crossovers can be done in the web interface but not the Python package. But post-version-1.0.0, the major and minor versions of the should be enforced. 3. Ensure all unit tests pass. -4. In the Python repo, ensure that the documentation is generated without errors. First, run `pip install sphinx sphinx_rtd_theme`. This installs [Sphinx](https://www.sphinx-doc.org/en/main/), which is the most well-supported documentation generator for Python. (It's not very friendly, the syntax for things like links in docstrings is awkward, but it's well supported, so we use it.) Then, from within the subfolder `doc`, run the command `make html` (or `make.bat html` on Windows), ensure there are no errors, and inspect the documentation it generates in the folder `_build`. +4. In the Python repo, ensure that the documentation is generated without errors. First, run `pip install .[docs]` from the repository root. This installs [Sphinx](https://www.sphinx-doc.org/en/main/), which is the most well-supported documentation generator for Python. (It's not very friendly, the syntax for things like links in docstrings is awkward, but it's well supported, so we use it.) Then, from within the subfolder `doc`, run the command `make html` (or `make.bat html` on Windows), ensure there are no errors, and inspect the documentation it generates in the folder `_build`. 5. Create a PR to merge changes from dev into main. `main` is the default base, so there is nothing to change here. diff --git a/MANIFEST b/MANIFEST deleted file mode 100644 index ec8366ab..00000000 --- a/MANIFEST +++ /dev/null @@ -1,7 +0,0 @@ -# file GENERATED by distutils, do NOT edit -setup.cfg -setup.py -scadnano\__init__.py -scadnano\modifications.py -scadnano\origami_rectangle.py -scadnano\scadnano.py diff --git a/doc/conf.py b/doc/conf.py index 48be79ee..d917f102 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -21,23 +21,13 @@ sys.path.insert(0, os.path.abspath('../scadnano')) # print(sys.path) -# this is ugly, but appears to be standard practice: -# https://stackoverflow.com/questions/17583443/what-is-the-correct-way-to-share-package-version-with-setup-py-and-the-package/17626524#17626524 -def extract_version(filename: str): - with open(filename) as f: - lines = f.readlines() - version_comment = '# version line; WARNING: do not remove or change this line or comment' - for line in lines: - if version_comment in line: - idx = line.index(version_comment) - line_prefix = line[:idx] - parts = line_prefix.split('=') - stripped_parts = [part.strip() for part in parts] - version_str = stripped_parts[-1].replace('"', '') - return version_str - raise AssertionError(f'could not find version in {filename}') - -__version__ = extract_version('../scadnano/scadnano.py') +# autodoc imports this same module in order to document it (see the +# `automodule:: scadnano` directive in index.rst), so ask it for the version +# rather than parsing scadnano.py by hand. The import has to come after the +# sys.path line above, which is what makes it resolvable. +import scadnano # noqa: E402 + +__version__ = scadnano.__version__ # Type "make html" at the command line to generate the documentation. diff --git a/doc/requirements.txt b/doc/requirements.txt deleted file mode 100644 index 70addddc..00000000 --- a/doc/requirements.txt +++ /dev/null @@ -1,16 +0,0 @@ -# Dependencies for building the Sphinx documentation in this directory. -# -# Used by Read the Docs (see readthedocs.yml in the repository root) and by the -# "Docs Check" GitHub Actions workflow, so that both build the docs with the -# same set of packages. - -# Sphinx itself, plus the HTML theme selected in conf.py. -sphinx>=8.0,<10 -alabaster>=1.0,<2 - -# Runtime dependencies of the scadnano package. autodoc imports scadnano to -# extract its docstrings and type hints, so these must be importable even -# though they are not used by Sphinx directly. Keep in sync with -# install_requires in setup.py. -openpyxl>=3.1,<4 -tabulate>=0.9,<1 diff --git a/publish_to_pypi.txt b/publish_to_pypi.txt index 0a6e1deb..afca3aaa 100644 --- a/publish_to_pypi.txt +++ b/publish_to_pypi.txt @@ -1,6 +1,13 @@ -rem https://medium.com/@joel.barmettler/how-to-upload-your-python-package-to-pypi-65edc5fe9c56 +Publishing to PyPI is automated. Merging dev into main triggers +.github/workflows/release.yml, which tags the release, creates the GitHub +release, and publishes to PyPI. See the "Pushing to the repository main branch" +section of CONTRIBUTING.md. -rem !!!change current version number in scadnano.py!!! -python setup.py sdist -rem twine upload dist/* -twine upload dist/scadnano-x.x.x.tar.gz +Do not publish by hand unless that workflow is broken: uploading a version that +the workflow later tries to publish will make it fail. + +The manual equivalent, for reference, is: + +rem !!!bump __version__ in scadnano/scadnano.py first!!! +python -m build +twine upload dist/* diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 00000000..bec0067a --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,55 @@ +# Packaging metadata for the scadnano package, in the standard PEP 621 form. +# +# This replaces setup.py, setup.cfg and MANIFEST. Both .github/workflows/release.yml +# (which publishes to PyPI) and .github/workflows/check_pypi_packaging.yml build +# with `python -m build`, which reads this file. + +[build-system] +requires = ["setuptools>=77"] +build-backend = "setuptools.build_meta" + +[project] +name = "scadnano" +description = "Python scripting library for generating designs readable by scadnano." +readme = { file = "README.md", content-type = "text/markdown; variant=GFM" } +license = "MIT" +license-files = ["LICENSE.txt"] +authors = [{ name = "David Doty", email = "doty@ucdavis.edu" }] +requires-python = ">=3.10" + +# Deliberately minimal; see "Keeping the scadnano package simple for users to +# install" in CONTRIBUTING.md. +dependencies = [ + "openpyxl", + "tabulate", +] + +# Read from the __version__ line in scadnano/scadnano.py, which stays the single +# source of truth for the version. release.yml reads that same line. +dynamic = ["version"] + +[project.urls] +Homepage = "https://github.com/UC-Davis-molecular-computing/scadnano-python-package" +Documentation = "https://scadnano-python-package.readthedocs.io" + +[project.optional-dependencies] +# Building the Sphinx documentation in doc/. Installed by Read the Docs (see +# readthedocs.yml) and by the "Docs Check" workflow, so both use the same +# packages. sphinx and alabaster are the only additions: autodoc imports +# scadnano, whose own dependencies are already covered by `dependencies` above. +docs = [ + "sphinx>=8.0,<10", + "alabaster>=1.0,<2", +] + +# Running the unit tests. Replaces setup.py's `tests_require`, which setuptools +# no longer recognizes. +tests = [ + "openpyxl", +] + +[tool.setuptools] +packages = ["scadnano"] + +[tool.setuptools.dynamic] +version = { attr = "scadnano.scadnano.__version__" } diff --git a/readthedocs.yml b/readthedocs.yml index ee58d18b..b3a39864 100644 --- a/readthedocs.yml +++ b/readthedocs.yml @@ -18,4 +18,7 @@ build: python: install: - - requirements: doc/requirements.txt + - method: pip + path: . + extra_requirements: + - docs diff --git a/scadnano/docs/AutoStaple.md b/scadnano/docs/AutoStaple.md index 4a4c78ed..cd9ee57f 100644 --- a/scadnano/docs/AutoStaple.md +++ b/scadnano/docs/AutoStaple.md @@ -71,7 +71,7 @@ source env/bin/activate 3. Run the following command to install dependencies. ```shell -python setup.py install +pip install -e . ``` 4. Inside the repository root, create a file called `test_autostaple.py` file within the `scadnano` directory. Use the contents below as a starting point. diff --git a/scadnano/scadnano.py b/scadnano/scadnano.py index fa6da998..5498b57f 100644 --- a/scadnano/scadnano.py +++ b/scadnano/scadnano.py @@ -54,7 +54,7 @@ # needed to use forward annotations: https://docs.python.org/3/whatsnew/3.7.html#whatsnew37-pep563 from __future__ import annotations -__version__ = "0.21.1" # version line; WARNING: do not remove or change this line or comment +__version__ = "0.21.1" # version line; WARNING: do not remove this line or change its format import collections import dataclasses diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index 890c070e..00000000 --- a/setup.cfg +++ /dev/null @@ -1,2 +0,0 @@ -[metadata] -description_file = README.md \ No newline at end of file diff --git a/setup.py b/setup.py deleted file mode 100644 index 543c9f12..00000000 --- a/setup.py +++ /dev/null @@ -1,62 +0,0 @@ -#!/usr/bin/env python - -# from distutils.core import setup - -# got some ideas from here: https://medium.com/@joel.barmettler/how-to-upload-your-python-package-to-pypi-65edc5fe9c56 -# -# But apparently setuptools is the replacement for distutils, and distutils was causing problems such as -# not including the README.md file and not formatting it as Markdown on PyPI -# https://setuptools.readthedocs.io/en/latest/setuptools.html - -from setuptools import setup - - -# import scadnano.scadnano_version as sv - - -def extract_version(filename: str): - with open(filename) as f: - lines = f.readlines() - version_comment = '# version line; WARNING: do not remove or change this line or comment' - for line in lines: - if version_comment in line: - idx = line.index(version_comment) - line_prefix = line[:idx] - parts = line_prefix.split('=') - stripped_parts = [part.strip() for part in parts] - version_str = stripped_parts[-1].replace('"', '') - return version_str - raise AssertionError(f'could not find version in {filename}') - - -__version__ = extract_version('scadnano/scadnano.py') - -# read the contents of your README file -from os import path - -this_directory = path.abspath(path.dirname(__file__)) -with open(path.join(this_directory, 'README.md'), encoding='utf-8') as f: - long_description = f.read() - -setup(name='scadnano', - packages=['scadnano'], - version=__version__, - # version='0.8.0', - download_url=f'https://github.com/UC-Davis-molecular-computing/scadnano-python-package/archive/v{__version__}.zip', - # download_url=f'https://github.com/UC-Davis-molecular-computing/scadnano-python-package/archive/v0.7.0.zip', - license='MIT', - description="Python scripting library for generating designs readable by scadnano.", - author="David Doty", - author_email="doty@ucdavis.edu", - url="https://github.com/UC-Davis-molecular-computing/scadnano-python-package", - long_description=long_description, - long_description_content_type='text/markdown; variant=GFM', - python_requires='>=3.10', - install_requires=[ - 'openpyxl', - 'tabulate', - ], - tests_require=[ - 'openpyxl', - ], - ) From 88d0c9a1f1429882a6da659c1c72a9615ef5cfdd Mon Sep 17 00:00:00 2001 From: David Doty Date: Mon, 10 Aug 2026 11:29:58 -0500 Subject: [PATCH 2/3] make pyproject.toml the single source of truth for the version, and drop standalone scadnano.py usage Follows on from the pyproject.toml migration in the previous commit. The version now lives in one place, `version` under [project] in pyproject.toml, and `scadnano.__version__` is read back from the installed distribution's metadata with importlib.metadata. Bumping a release is a one-line edit to pyproject.toml. This makes installation mandatory. scadnano could previously be used by copying scadnano.py into a working directory and importing it with nothing installed; that workflow is now gone, and importing from a bare source checkout raises `PackageNotFoundError: No package metadata was found for scadnano`. The package behaves like any other pip-installed package instead. Version plumbing: - pyproject.toml gains a static `version` and loses `dynamic = ["version"]` and the [tool.setuptools.dynamic] table. Keeping either alongside a static version is a PEP 621 conflict that aborts `python -m build`. - release.yml extracted the version by sed-matching `^__version__ = "..."` in scadnano/scadnano.py. That line no longer holds a literal, so the regex would have matched nothing, the semver guard would have failed, and EVERY release would have died at its first step with nothing tagged or published. It now parses pyproject.toml with tomllib, which is immune to reformatting in a way the regex was not. - scadnano/__init__.py exports __version__ explicitly. `import *` skips names beginning with an underscore, so scadnano.__version__ was not actually reachable for an installed user before this, despite being where anyone would look for it. CI now installs the package, which it never did: - run_unit_tests.yml installed only openpyxl and tabulate and relied on the source tree being importable, i.e. on exactly the workflow being removed here. It installs `-e .[tests]` now; editable so the tests still exercise the checked-out source, which they require anyway since they read data from tests_inputs/ relative to the repo root. - check_pypi_packaging.yml built the artifacts and threw them away. It now installs the built wheel into a clean virtualenv and imports it from a directory containing no source tree, asserting that scadnano.__version__ matches pyproject.toml and that a real design serializes. Running from the repo root would prove nothing, because the flat layout means the checkout shadows the installed package. Documentation and examples assumed the copy workflow: - README.md described two installation methods; the second told users to download scadnano.py onto their PYTHONPATH. Removed, leaving pip as the only path, and the troubleshooting section unnested from the list. That section also claimed xlwt is needed to write Excel files, which has been wrong for some time: the code uses openpyxl, an ordinary declared dependency. - 21 example scripts imported `origami_rectangle` and `modifications` as top-level modules, which only resolves when the package directory itself is on sys.path. They now use `scadnano.origami_rectangle` and `scadnano.modifications`, as does the README's example. - CONTRIBUTING.md dropped the "all required code is in a single file so it can be copied" constraint, and told contributors to skip installing; it now documents `pip install -e .[tests]`. Verified: `python -m build` succeeds and twine check passes on both artifacts; the built wheel installs into a clean virtualenv and imports correctly from a neutral directory reporting 0.21.1; a bare checkout with no install fails loudly as intended; 375 unit tests pass under `pip install -e .[tests]`; the docs build with warnings as errors and resolve the version to 0.21.1; and two rewritten example scripts run to completion. One consequence to be aware of, now documented in CONTRIBUTING.md: because the version comes from installed metadata, bumping it in pyproject.toml without reinstalling leaves the old value in scadnano.__version__, and therefore in the "version" field of any .sc file written before the reinstall. Co-Authored-By: Claude Opus 5 (1M context) --- .github/workflows/check_pypi_packaging.yml | 33 ++ .github/workflows/release.yml | 17 +- .github/workflows/run_unit_tests.yml | 7 +- CONTRIBUTING.md | 39 +- README.md | 90 ++-- ...helix_origami_barrel_from_algoSST_paper.py | 510 +++++++++--------- examples/16_helix_origami_rectangle.py | 22 +- examples/16_helix_origami_rectangle_idt.py | 2 +- examples/16_helix_origami_rectangle_no_seq.py | 22 +- ...helix_origami_rectangle_no_seq_no_twist.py | 24 +- .../16_helix_origami_rectangle_no_twist.py | 22 +- ...gami_rectangle_seed_tiles_grow_from_top.py | 310 +++++------ ...x_origami_rectangle_shifted_seam_no_seq.py | 22 +- examples/2_staple_2_helix_modifications.py | 2 +- ...helix_origami_deletions_insertions_mods.py | 2 +- ...letions_insertions_mods_chained_methods.py | 2 +- examples/56_helix_origami_rectangle.py | 22 +- examples/64_helix_origami_rectangle.py | 22 +- examples/6_helix_6_col_origami_rectangle.py | 24 +- examples/6_helix_origami_rectangle.py | 24 +- ..._origami_rectangle_helices_out_of_order.py | 26 +- examples/draw_strand_move_negative.py | 2 +- examples/many_helices_modifications.py | 2 +- examples/proposal.py | 4 +- examples/relax_helix_rolls.py | 2 +- examples/very_large_origami.py | 2 +- publish_to_pypi.txt | 2 +- pyproject.toml | 17 +- scadnano/__init__.py | 6 +- scadnano/scadnano.py | 11 +- 30 files changed, 668 insertions(+), 624 deletions(-) diff --git a/.github/workflows/check_pypi_packaging.yml b/.github/workflows/check_pypi_packaging.yml index 42f5cd45..e2868fe5 100644 --- a/.github/workflows/check_pypi_packaging.yml +++ b/.github/workflows/check_pypi_packaging.yml @@ -19,3 +19,36 @@ jobs: pip install build - name: Build run: python -m build + + # Everything above only proves the artifacts can be produced. This installs + # the built wheel the way a user would and imports it from a directory with + # no source tree in it, so nothing can be satisfied by the checkout: the + # repo uses a flat layout, so running from the repo root would import + # scadnano/ directly and prove nothing about the wheel. + - name: Install the built wheel and import it as a user would + run: | + python -m venv /tmp/wheel-check + /tmp/wheel-check/bin/pip install --quiet dist/*.whl + mkdir -p /tmp/neutral + cd /tmp/neutral + /tmp/wheel-check/bin/python - <<'PY' + import os, pathlib, sys, tomllib + import scadnano, scadnano.modifications, scadnano.origami_rectangle + + # Quoted heredoc, so the shell expands nothing; read the workspace path + # from the environment instead. + pyproject = pathlib.Path(os.environ["GITHUB_WORKSPACE"]) / "pyproject.toml" + expected = tomllib.loads(pyproject.read_text())["project"]["version"] + + if scadnano.__version__ != expected: + sys.exit( + f"scadnano.__version__ is {scadnano.__version__!r}, but " + f"pyproject.toml declares {expected!r}" + ) + + # Exercising a real design keeps this from passing on an import alone. + design = scadnano.Design(helices=[scadnano.Helix(max_offset=16)], strands=[]) + assert design.to_json(), "design serialized to nothing" + + print(f"ok: installed wheel imports and reports version {expected}") + PY diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a9919db7..6c333c02 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,14 +1,14 @@ name: "release" -# On every push to main: tag v{__version__}, create a GitHub release whose body +# On every push to main: tag v{version}, create a GitHub release whose body # lists commits since the previous release, and publish to PyPI. # # Issues are not touched here. main is the default branch, so GitHub itself # closes any issue referenced by a closing keyword in the commits this push # brings to main, and records the closing commit on the issue. # -# INVARIANT: every push to main is a release. If __version__ in -# scadnano/scadnano.py was not bumped, this workflow FAILS (red X) on purpose. +# INVARIANT: every push to main is a release. If the version in pyproject.toml +# was not bumped, this workflow FAILS (red X) on purpose. # # NEVER substitute a personal access token (PAT) for GITHUB_TOKEN below. # Actions taken with GITHUB_TOKEN do not trigger other workflows, and the @@ -43,12 +43,15 @@ jobs: with: fetch-depth: 0 # full history + all tags, needed for `git log ..HEAD` - - name: Extract version from scadnano/scadnano.py + - name: Extract version from pyproject.toml id: version run: | - version=$(sed -nE 's/^__version__ = "([^"]+)".*$/\1/p' scadnano/scadnano.py | head -1) + # Parsed with tomllib rather than a regex so that comments, quoting + # style and key order in pyproject.toml cannot silently change the + # answer. tomllib is stdlib from 3.11; the runner image ships newer. + version=$(python3 -c 'import tomllib, pathlib; print(tomllib.loads(pathlib.Path("pyproject.toml").read_text())["project"]["version"])') if ! [[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then - echo "::error file=scadnano/scadnano.py::could not extract semantic version from the __version__ line (got: '$version')" + echo "::error file=pyproject.toml::could not extract a semantic version from [project] version (got: '$version')" exit 1 fi echo "version=$version" >> "$GITHUB_OUTPUT" @@ -63,7 +66,7 @@ jobs: echo "Tag v$version already exists at this exact commit: re-run of a partially failed workflow. Skipping release creation." echo "create_release=false" >> "$GITHUB_OUTPUT" else - echo "::error::Tag v$version already exists (at $tag_commit) but this push is $GITHUB_SHA. __version__ in scadnano/scadnano.py was not bumped before merging to main. Bump it on dev and merge dev to main again (see CONTRIBUTING.md); this red X is the intended reminder." + echo "::error::Tag v$version already exists (at $tag_commit) but this push is $GITHUB_SHA. The version in pyproject.toml was not bumped before merging to main. Bump it on dev and merge dev to main again (see CONTRIBUTING.md); this red X is the intended reminder." exit 1 fi else diff --git a/.github/workflows/run_unit_tests.yml b/.github/workflows/run_unit_tests.yml index 1be010f3..8d2f8436 100644 --- a/.github/workflows/run_unit_tests.yml +++ b/.github/workflows/run_unit_tests.yml @@ -21,7 +21,10 @@ jobs: uses: actions/setup-python@v7 with: python-version: ${{ matrix.python-version }} - - name: Install openpyxl,tabulate with pip - run: pip install openpyxl tabulate + # Editable, so the tests still exercise the checked-out source (they read + # data from tests_inputs/ relative to the repo root). The install is what + # registers the distribution metadata that scadnano.__version__ reads. + - name: Install scadnano and test dependencies + run: pip install -e .[tests] - name: Test with unittest run: python -m unittest -v tests/scadnano_tests.py diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 62f6e19e..7f9b1770 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -28,19 +28,23 @@ The scadnano Python package requires at least Python 3.7. See the [README for in Follow the [installation instructions](README.md#installation) to install the correct version of Python if you don't have it already. -It is actually unnecessary for you to install scadnano via pip, so you can skip that step. In developing, you will have a local version of the package that you run and modify. +Install your clone in editable mode from the repository root: `pip install -e .[tests]`. You still run and modify the local source — editable mode means your edits take effect immediately, with no reinstall — but the install is required, because `scadnano.__version__` is read from the installed distribution's metadata. I suggest using a powerful IDE such as [PyCharm](https://www.jetbrains.com/pycharm/download/download-thanks.html). [Visual Studio Code](https://code.visualstudio.com/) is also good with the right plugins. The scadnano Python package uses type hints, and these tools are very helpful in giving static analysis warnings about the code that may represent errors that will manifest at run time. ### Keeping the scadnano package simple for users to install -One goal is to make the package as easy to install as possible, even for users who have trouble installing scadnano via pip. For this reason, we have two self-imposed constraints: +One self-imposed constraint remains: keep package dependencies minimal. scadnano depends only on +[openpyxl](https://pypi.org/project/openpyxl/) and [tabulate](https://pypi.org/project/tabulate/), +both declared in [pyproject.toml](pyproject.toml), and it should stay that way unless there is a +strong reason otherwise. -1. There are minimal package dependencies. scadnano can be run in most circumstances with a standard Python 3.7 (or above) installation. (One exception is the package [xlwt](https://pypi.org/project/xlwt/), which is required to call the method [`Design.write_idt_plate_excel_file()`](https://scadnano-python-package.readthedocs.io/#scadnano.Design.write_idt_plate_excel_file).) - -2. All the required code is in a single file, [scadnano.py](scadnano/scadnano.py). This is one reason an IDE will help, because navigating a large source code file is easier in an IDE. - -These two constraints imply that a user who has trouble installing via pip can simply copy the file scadnano.py into their working directory (or in some directory on their `PYTHONPATH`) and import it as normal. +scadnano is installed as a normal Python package, so `pip install scadnano` is the only supported +way to get it. It used to be possible to copy [scadnano.py](scadnano/scadnano.py) into your working +directory and import it without installing anything, and most of the code is still in that one file, +which is why an IDE helps for navigating it. But that is no longer a supported workflow: the package +reads its own version from the installed distribution's metadata, so importing it requires a real +install. ### git @@ -183,9 +187,9 @@ GitHub proposes it as the base, so there is nothing to change. The workflow that `dev` deliberately skips this one, recognizing it by its `dev` head branch *in this repository* (a contributor's fork may also have a `dev` branch, and those PRs are retargeted normally). -**Every push to `main` is a release.** The release workflow reads `__version__` from -[scadnano/scadnano.py](scadnano/scadnano.py), creates the tag `v{version}`, creates a GitHub release, -and publishes to PyPI. So you **must bump `__version__` on `dev` before merging `dev` into `main`**. +**Every push to `main` is a release.** The release workflow reads `version` from +[pyproject.toml](pyproject.toml), creates the tag `v{version}`, creates a GitHub release, +and publishes to PyPI. So you **must bump the version on `dev` before merging `dev` into `main`**. If you forget, the release workflow fails with a red X and an explanatory message, and nothing is tagged or published; bump the version on `dev` and merge again to recover. (Practically, this means there is no such thing as a casual push to `main` — even a README typo fix rides along with a version @@ -219,12 +223,15 @@ So the steps for committing to the main branch are: MINOR for backwards-compatible feature additions. - For the web interface repo scadnano, this is located at the top of the file https://github.com/UC-Davis-molecular-computing/scadnano/blob/main/lib/src/constants.dart - For the Python library repo scadnano-python-package, there is a single source of truth: the - `__version__` line near the top of the file - [scadnano/scadnano.py](scadnano/scadnano.py) (as `__version__ = "0.9.3"` or something similar). - Keep the format of that line exactly as it is. Three things read it: the release workflow - (with `sed`), `pyproject.toml` (which declares the version `dynamic` and reads the - `__version__` attribute), and `doc/conf.py` (which imports it). None of them tolerate the - line being reformatted. + `version` field under `[project]` in [pyproject.toml](pyproject.toml) + (as `version = "0.9.3"` or something similar). That is the only place to edit. + + Everything else derives from it. `scadnano.__version__` — which is what stamps the version + into every `.sc` file the library writes — is read at import time from the installed + distribution's metadata via `importlib.metadata`, and the release workflow parses + `pyproject.toml` directly. One consequence worth knowing: after bumping the version, run + `pip install -e .` again before running any script whose output you care about, or the + metadata (and therefore the version written into `.sc` files) will still be the old one. The PATCH version numbers are not always synced between the two repos, but, they should stay synced on MAJOR and MINOR versions. **Note:** right now this isn't quite true since MINOR versions deal with backwards-compatible feature additions, and some features are supported on one but not the other; e.g., modifications can be made in the Python package but not the web interface, and calculating helix rolls/positions from crossovers can be done in the web interface but not the Python package. But post-version-1.0.0, the major and minor versions of the should be enforced. diff --git a/README.md b/README.md index 832835e6..4f50bdfa 100644 --- a/README.md +++ b/README.md @@ -85,65 +85,51 @@ If that fails, or reports Python version 3.10 or below, you will have to install ### Installing the scadnano Python package -Once Python is installed, there are two ways you can install the scadnano Python package: +Once Python is installed, use [pip](https://pypi.org/project/pip/) to install the package by executing the following at the command line: -1. pip (recommended) - - Use [pip](https://pypi.org/project/pip/) to install the package by executing the following at the command line: - ```console - pip install scadnano - ``` - - If it worked, you should be able to open a Python interpreter and import the scadnano module: +```console +pip install scadnano +``` - ```console - Python 3.7.9 (default, Aug 31 2020, 17:10:11) [MSC v.1916 64 bit (AMD64)] :: Anaconda, Inc. on win32 - Type "help", "copyright", "credits" or "license" for more information. - >>> import scadnano as sc - >>> print(sc.Domain(helix=1, forward=True, start=0, end=8)) - Domain(, helix=1, forward=True, start=0, end=8) - >>> - ``` +If it worked, you should be able to open a Python interpreter and import the scadnano module: - ### Troubleshooting - If the above does not work for you, here are some things to try. +```console +Python 3.12.5 (main, Sep 11 2024, 12:00:00) [MSC v.1929 64 bit (AMD64)] on win32 +Type "help", "copyright", "credits" or "license" for more information. +>>> import scadnano as sc +>>> print(sc.Domain(helix=1, forward=True, start=0, end=8)) +Domain(, helix=1, forward=True, start=0, end=8) +>>> +``` - If your Python installation does not already have pip installed, you may have to install it. - Executing [this Python script](https://bootstrap.pypa.io/get-pip.py) should work; - see also - https://docs.python.org/3/installing/index.html - or - https://www.liquidweb.com/kb/install-pip-windows/. +Two optional submodules are installed along with it: `scadnano.modifications`, which contains common +DNA modifications such as biotin and Cy3, and `scadnano.origami_rectangle`, which helps create +origami rectangles. - Once pip is installed, or if you believe it is already installed, check your version of `pip` by typing - ``` - pip --version - ``` - It should say something like - ``` - pip 19.3.1 from ...lib\site-packages\pip (python 3.8) - ``` - If the version of Python at the end is Python 3.9 or higher, you are good. If it is version 2.7 or lower, type - ``` - pip3 --version - ``` - If that works and shows Python 3.9 or higher, you are good, but you should type `pip3` in the subsequent instructions instead of `pip`. +### Troubleshooting +If the above does not work for you, here are some things to try. - -2. download +If your Python installation does not already have pip installed, you may have to install it. +Executing [this Python script](https://bootstrap.pypa.io/get-pip.py) should work; +see also +https://docs.python.org/3/installing/index.html +or +https://www.liquidweb.com/kb/install-pip-windows/. - As a simple alternative (in case you run into trouble using pip), you can simply download the scadnano.py file. However, you need to first install two packages that are required by scadnano: Install [openpyxl](https://pypi.org/project/openpyxl/) and [tabulate](https://pypi.org/project/tabulate/) by typing the following at the command line: `pip install openpyxl tabulate`. - - Download and place the following files in your [PYTHONPATH](https://docs.python.org/3/using/cmdline.html#envvar-PYTHONPATH) (e.g., in the same directory as the scripts you are running). **Note:** If you are reading this on the PyPI website or anywhere other than GitHub, the links below won't work. They are relative links intended to be read on the [GitHub README page](https://github.com/UC-Davis-molecular-computing/scadnano-python-package#readme). +Once pip is installed, or if you believe it is already installed, check your version of `pip` by typing +``` +pip --version +``` +It should say something like +``` +pip 19.3.1 from ...lib\site-packages\pip (python 3.8) +``` +If the version of Python at the end is Python 3.10 or higher, you are good. If it is version 2.7 or lower, type +``` +pip3 --version +``` +If that works and shows Python 3.10 or higher, you are good, but you should type `pip3` in the subsequent instructions instead of `pip`. - - *required*: [scadnano.py](scadnano/scadnano.py) - - *optional*: [modifications.py](scadnano/modifications.py); This contains some common DNA modifications such as biotin and Cy3. - - *optional*: [origami_rectangle.py](scadnano/origami_rectangle.py); This can help create origami rectangles, but it is not necessary to use scadnano. - - To download them, right-click on "Raw" near the top and select (in Chrome or Firefox) "Save link as...": - ![](images/download_raw_screenshot.png) - - The scadnano package uses the Python package [xlwt](https://pypi.org/project/xlwt/) to write Excel files, so xlwt must be installed in order to call the method [`Design.write_idt_plate_excel_file()`](https://scadnano-python-package.readthedocs.io/#scadnano.Design.write_idt_plate_excel_file) to export an Excel file with DNA sequences. To install xlwt, type `pip install xlwt` at the command line. (If you instead use pip to install the scadnano package, xlwt will be automatically installed.) @@ -161,7 +147,7 @@ The following Python script produces this design. ```python import scadnano as sc -import modifications as mod +import scadnano.modifications as mod def create_design() -> sc.Design: diff --git a/examples/16_helix_origami_barrel_from_algoSST_paper.py b/examples/16_helix_origami_barrel_from_algoSST_paper.py index 2025f4a0..d2db7c27 100644 --- a/examples/16_helix_origami_barrel_from_algoSST_paper.py +++ b/examples/16_helix_origami_barrel_from_algoSST_paper.py @@ -1,255 +1,255 @@ -import origami_rectangle as rect -import scadnano as sc - - -def create_design() -> sc.Design: - design = rect.create(num_helices=16, num_cols=28, seam_left_column=12, assign_seq=False, - num_flanking_columns=2, - num_flanking_helices=2, edge_staples=False, - scaffold_nick_offset=102) - - # # need this to match original design, but doesn't leave room for left-side adapters - # design.move_strand_offsets(8) - - set_helix_major_tickets(design) - move_top_and_bottom_staples_within_column_boundaries(design) - add_domains_for_barrel_seam(design) - add_toeholds_for_seam_displacement(design) - add_adapters(design) - add_twist_correct_deletions(design) - add_angle_inducing_insertions_deletions(design) - add_tiles_and_assign_dna(design) - assign_dna_to_unzipper_toeholds(design) - - design.assign_m13_to_scaffold() - - return design - - -def set_helix_major_tickets(design: sc.Design) -> None: - major_ticks = [11, 22, 32] - for tick in range(40, 481, 8): - major_ticks.append(tick) - major_ticks.extend([490, 501]) - - for helix in design.helices.values(): - helix.major_ticks = list(major_ticks) - - -def add_twist_correct_deletions(design: sc.Design) -> None: - # I choose between 3 and 4 offset arbitrarily for twist-correction deletions for some reason, - # so they have to be hard-coded. - for col, offset in zip(range(4, 29, 3), [4, 3, 3, 4, 3, 3, 3, 3, 3]): - for helix in range(2, 18): - design.add_deletion(helix, 16 * col + offset) - - -def move_top_and_bottom_staples_within_column_boundaries(design: sc.Design) -> None: - top_staples = design.strands_starting_on_helix(2) - bot_staples = design.strands_starting_on_helix(17) - bot_staples.remove(design.scaffold) - - for top_staple in top_staples: - current_end = top_staple.domains[0].end - design.set_end(top_staple.domains[0], current_end - 8) - - for bot_staple in bot_staples: - current_start = bot_staple.domains[0].start - design.set_start(bot_staple.domains[0], current_start + 8) - - -def add_domains_for_barrel_seam(design: sc.Design) -> None: - top_staples_5p = design.strands_starting_on_helix(2) - top_staples_3p = design.strands_ending_on_helix(2) - bot_staples_5p = design.strands_starting_on_helix(17) - bot_staples_3p = design.strands_ending_on_helix(17) - - # remove scaffold - top_staples_5p = [st for st in top_staples_5p if len(st.domains) <= 3] - top_staples_3p = [st for st in top_staples_3p if len(st.domains) <= 3] - bot_staples_5p = [st for st in bot_staples_5p if len(st.domains) <= 3] - bot_staples_3p = [st for st in bot_staples_3p if len(st.domains) <= 3] - - top_staples_5p.sort(key=lambda stap: stap.offset_5p()) - top_staples_3p.sort(key=lambda stap: stap.offset_3p()) - bot_staples_5p.sort(key=lambda stap: stap.offset_5p()) - bot_staples_3p.sort(key=lambda stap: stap.offset_3p()) - - for top_5p, top_3p, bot_5p, bot_3p in zip(top_staples_5p, top_staples_3p, bot_staples_5p, bot_staples_3p): - ss_top = sc.Domain(helix=2, forward=False, - start=top_5p.first_domain().end, end=top_3p.last_domain().start) - ss_bot = sc.Domain(helix=17, forward=True, - start=bot_3p.last_domain().end, end=bot_5p.first_domain().start) - design.insert_domain(bot_5p, 0, ss_top) - design.insert_domain(top_5p, 0, ss_bot) - - -def add_toeholds_for_seam_displacement(design: sc.Design) -> None: - for helix in [2, 17]: - staples_5p = design.strands_starting_on_helix(helix) - - # remove scaffold - staples_5p = [st for st in staples_5p if len(st.domains) <= 3] - - staples_5p.sort(key=lambda stap: stap.offset_5p()) - - for stap_5p in staples_5p: - toe_ss = sc.Domain(helix=1 if helix == 2 else 18, forward=helix == 2, - start=stap_5p.first_bound_domain().start, - end=stap_5p.first_bound_domain().end) - design.insert_domain(stap_5p, 0, toe_ss) - - -def add_adapters(design: sc.Design) -> None: - # left adapters - left_inside_seed = 48 - left_outside_seed = left_inside_seed - 26 - for bot_helix in range(2, 18, 2): - top_helix = bot_helix - 1 if bot_helix != 2 else 17 - dom_top = sc.Domain(helix=top_helix, forward=True, - start=left_outside_seed, end=left_inside_seed) - dom_bot = sc.Domain(helix=bot_helix, forward=False, - start=left_outside_seed, end=left_inside_seed) - idt = sc.VendorFields(scale='25nm', purification='STD') - adapter = sc.Strand(domains=[dom_bot, dom_top], name=f'adap-left-{top_helix}-{bot_helix}', - vendor_fields=idt) - design.add_strand(adapter) - - # right adapters - right_inside_seed = 464 - right_outside_seed = right_inside_seed + 26 - for bot_helix in range(2, 18, 2): - top_helix = bot_helix - 1 if bot_helix != 2 else 17 - dom_top = sc.Domain(helix=top_helix, forward=True, - start=right_inside_seed, end=right_outside_seed) - dom_bot = sc.Domain(helix=bot_helix, forward=False, - start=right_inside_seed, end=right_outside_seed) - idt = sc.VendorFields(scale='25nm', purification='STD') - adapter = sc.Strand(domains=[dom_top, dom_bot], name=f'adap-right-{top_helix}-{bot_helix}', - vendor_fields=idt) - design.add_strand(adapter) - - -seq_lines = """tile1rot0,ACCAAGAACT TTGTCAACAAT AAACAAATCCA ATCTTTCCGT,25nm,STD -tile2rot0,TTGTCTAGAGT TTGGGATGTT AGTTCTTGGT ATTGTTGACAA,25nm,STD -tile3rot0,TTATCCACGT TTCCTCCTATT ACTCTAGACAA AACATCCCAA,25nm,STD -tile4rot0,AAGGAAGTAGA TTCGAAAGGT ACGTGGATAA AATAGGAGGAA,25nm,STD -tile5rot0,AACCTCGAAT TACCAGATTCT TCTACTTCCTT ACCTTTCGAA,25nm,STD -tile6rot0,AGAATAGTCGT TTGTCAGTGT ATTCGAGGTT AGAATCTGGTA,25nm,STD -tile7rot0,ATCTGCTCAT TCTGATCTCTT ACGACTATTCT ACACTGACAA,25nm,STD -tile8rot0,AATGGATAGGT AGGTGTCTTT ATGAGCAGAT AAGAGATCAGA,25nm,STD -tile9rot0,TCAAGTTCCA TATCCTTAGCA ACCTATCCATT AAAGACACCT,25nm,STD -tile10rot0,AGTGATGATCT TTTAGGCTGT TGGAACTTGA TGCTAAGGATA,25nm,STD -tile11rot0,ACCCATTCAT TTCCTGATACT AGATCATCACT ACAGCCTAAA,25nm,STD -tile12rot0,TGCGTTAAAAT AGATGCGTAT ATGAATGGGT AGTATCAGGAA,25nm,STD -tile13rot0,AACCTTCACA ATCGTCTCATA ATTTTAACGCA ATACGCATCT,25nm,STD -tile14rot0,ATTCAGAGAGT TGGCATGATA TGTGAAGGTT TATGAGACGAT,25nm,STD -tile15rot0,TACCATGCTT TTGACCAATTT ACTCTCTGAAT TATCATGCCA,25nm,STD -tile16rot0,TGGATTTGTTT ACGGAAAGAT AAGCATGGTA AAATTGGTCAA,25nm,STD""".split('\n') - -tile_dna_seqs = [''.join(line.split(',')[1]) for line_no, line in enumerate(seq_lines) if line_no % 2 == 1] - - -# print(tile_dna_seqs) - - -def add_tiles_and_assign_dna(design: sc.Design) -> None: - # left tiles - left_left = 11 - left_right = 32 - for top_helix, seq in zip(range(2, 18, 2), tile_dna_seqs): - bot_helix = top_helix + 1 - ss_top = sc.Domain(helix=top_helix, forward=True, - start=left_left, end=left_right) - ss_bot = sc.Domain(helix=bot_helix, forward=False, - start=left_left, end=left_right) - idt = sc.VendorFields(scale='25nm', purification='STD') - tile = sc.Strand(domains=[ss_bot, ss_top], name=f'tile-left-{top_helix}-{bot_helix}', - color=sc.Color(0, 0, 0), vendor_fields=idt) - design.add_strand(tile) - design.assign_dna(tile, seq) - - # right tiles - right_left = 480 - right_right = 501 - for top_helix, seq in zip(range(2, 18, 2), tile_dna_seqs): - bot_helix = top_helix + 1 - ss_top = sc.Domain(helix=top_helix, forward=True, - start=right_left, end=right_right) - ss_bot = sc.Domain(helix=bot_helix, forward=False, - start=right_left, end=right_right) - idt = sc.VendorFields(scale='25nm', purification='STD') - tile = sc.Strand(domains=[ss_bot, ss_top], name=f'tile-right-{top_helix}-{bot_helix}', - color=sc.Color(0, 0, 0), vendor_fields=idt) - design.add_strand(tile) - design.assign_dna(tile, seq) - - -def add_angle_inducing_insertions_deletions(design) -> None: - # insertion followed by deletion - start = 59 - end = start + (32 * 12) - for helix in [3, 7, 9, 13, 15]: - for offset in range(start, end, 32): - design.add_insertion(helix, offset, 1) - design.add_deletion(helix, offset + 16) - - # deletion followed by insertion - for helix in [4, 6, 10, 12, 16]: - for offset in range(start, end, 32): - design.add_deletion(helix, offset) - design.add_insertion(helix, offset + 16, 1) - - -uz_toes_wc = """ -CACCCCAC -CTTTCCTT -TTCACTAA -ACCAACCC -TCTCTTAA -CTTTCATA -ATAATAAA -AACTCACC -ACTTAATA -CAAATCAC -ACCATCCA -TACTCTAT -ATACCTTC -TTATTCAT -ATCCACAA -ATATTTTT -CCACCTAA -CTAAATTA -ATTACCCC -CACTAACA -ACACACTT -TTTTAATC -ACATTTAA -TCCACATC -CCTACCTT -TCCCTATA -""".split() - - -# above is in order from right to left on helix 1, followed by left to right on helix 18 - -def assign_dna_to_unzipper_toeholds(design: sc.Design) -> None: - uz_toes = [sc.rc(seq) for seq in uz_toes_wc] - - strands_h1 = design.strands_starting_on_helix(1) - strands_h1.sort(key=lambda _strand: _strand.first_domain().offset_5p()) - strands_h1.reverse() - - strands_h18 = design.strands_starting_on_helix(18) - strands_h18.sort(key=lambda _strand: _strand.first_domain().offset_5p()) - - for strand, toe in zip(strands_h1 + strands_h18, uz_toes): - seq = toe + sc.DNA_base_wildcard * (strand.dna_length() - 8) - design.assign_dna(strand, seq) - - -if __name__ == '__main__': - d = create_design() - d.write_scadnano_file(directory='output_designs') - d.write_idt_bulk_input_file(directory='idt') - # d.write_idt_plate_excel_file(directory='idt', export_non_modified_strand_version=True) +import scadnano.origami_rectangle as rect +import scadnano as sc + + +def create_design() -> sc.Design: + design = rect.create(num_helices=16, num_cols=28, seam_left_column=12, assign_seq=False, + num_flanking_columns=2, + num_flanking_helices=2, edge_staples=False, + scaffold_nick_offset=102) + + # # need this to match original design, but doesn't leave room for left-side adapters + # design.move_strand_offsets(8) + + set_helix_major_tickets(design) + move_top_and_bottom_staples_within_column_boundaries(design) + add_domains_for_barrel_seam(design) + add_toeholds_for_seam_displacement(design) + add_adapters(design) + add_twist_correct_deletions(design) + add_angle_inducing_insertions_deletions(design) + add_tiles_and_assign_dna(design) + assign_dna_to_unzipper_toeholds(design) + + design.assign_m13_to_scaffold() + + return design + + +def set_helix_major_tickets(design: sc.Design) -> None: + major_ticks = [11, 22, 32] + for tick in range(40, 481, 8): + major_ticks.append(tick) + major_ticks.extend([490, 501]) + + for helix in design.helices.values(): + helix.major_ticks = list(major_ticks) + + +def add_twist_correct_deletions(design: sc.Design) -> None: + # I choose between 3 and 4 offset arbitrarily for twist-correction deletions for some reason, + # so they have to be hard-coded. + for col, offset in zip(range(4, 29, 3), [4, 3, 3, 4, 3, 3, 3, 3, 3]): + for helix in range(2, 18): + design.add_deletion(helix, 16 * col + offset) + + +def move_top_and_bottom_staples_within_column_boundaries(design: sc.Design) -> None: + top_staples = design.strands_starting_on_helix(2) + bot_staples = design.strands_starting_on_helix(17) + bot_staples.remove(design.scaffold) + + for top_staple in top_staples: + current_end = top_staple.domains[0].end + design.set_end(top_staple.domains[0], current_end - 8) + + for bot_staple in bot_staples: + current_start = bot_staple.domains[0].start + design.set_start(bot_staple.domains[0], current_start + 8) + + +def add_domains_for_barrel_seam(design: sc.Design) -> None: + top_staples_5p = design.strands_starting_on_helix(2) + top_staples_3p = design.strands_ending_on_helix(2) + bot_staples_5p = design.strands_starting_on_helix(17) + bot_staples_3p = design.strands_ending_on_helix(17) + + # remove scaffold + top_staples_5p = [st for st in top_staples_5p if len(st.domains) <= 3] + top_staples_3p = [st for st in top_staples_3p if len(st.domains) <= 3] + bot_staples_5p = [st for st in bot_staples_5p if len(st.domains) <= 3] + bot_staples_3p = [st for st in bot_staples_3p if len(st.domains) <= 3] + + top_staples_5p.sort(key=lambda stap: stap.offset_5p()) + top_staples_3p.sort(key=lambda stap: stap.offset_3p()) + bot_staples_5p.sort(key=lambda stap: stap.offset_5p()) + bot_staples_3p.sort(key=lambda stap: stap.offset_3p()) + + for top_5p, top_3p, bot_5p, bot_3p in zip(top_staples_5p, top_staples_3p, bot_staples_5p, bot_staples_3p): + ss_top = sc.Domain(helix=2, forward=False, + start=top_5p.first_domain().end, end=top_3p.last_domain().start) + ss_bot = sc.Domain(helix=17, forward=True, + start=bot_3p.last_domain().end, end=bot_5p.first_domain().start) + design.insert_domain(bot_5p, 0, ss_top) + design.insert_domain(top_5p, 0, ss_bot) + + +def add_toeholds_for_seam_displacement(design: sc.Design) -> None: + for helix in [2, 17]: + staples_5p = design.strands_starting_on_helix(helix) + + # remove scaffold + staples_5p = [st for st in staples_5p if len(st.domains) <= 3] + + staples_5p.sort(key=lambda stap: stap.offset_5p()) + + for stap_5p in staples_5p: + toe_ss = sc.Domain(helix=1 if helix == 2 else 18, forward=helix == 2, + start=stap_5p.first_bound_domain().start, + end=stap_5p.first_bound_domain().end) + design.insert_domain(stap_5p, 0, toe_ss) + + +def add_adapters(design: sc.Design) -> None: + # left adapters + left_inside_seed = 48 + left_outside_seed = left_inside_seed - 26 + for bot_helix in range(2, 18, 2): + top_helix = bot_helix - 1 if bot_helix != 2 else 17 + dom_top = sc.Domain(helix=top_helix, forward=True, + start=left_outside_seed, end=left_inside_seed) + dom_bot = sc.Domain(helix=bot_helix, forward=False, + start=left_outside_seed, end=left_inside_seed) + idt = sc.VendorFields(scale='25nm', purification='STD') + adapter = sc.Strand(domains=[dom_bot, dom_top], name=f'adap-left-{top_helix}-{bot_helix}', + vendor_fields=idt) + design.add_strand(adapter) + + # right adapters + right_inside_seed = 464 + right_outside_seed = right_inside_seed + 26 + for bot_helix in range(2, 18, 2): + top_helix = bot_helix - 1 if bot_helix != 2 else 17 + dom_top = sc.Domain(helix=top_helix, forward=True, + start=right_inside_seed, end=right_outside_seed) + dom_bot = sc.Domain(helix=bot_helix, forward=False, + start=right_inside_seed, end=right_outside_seed) + idt = sc.VendorFields(scale='25nm', purification='STD') + adapter = sc.Strand(domains=[dom_top, dom_bot], name=f'adap-right-{top_helix}-{bot_helix}', + vendor_fields=idt) + design.add_strand(adapter) + + +seq_lines = """tile1rot0,ACCAAGAACT TTGTCAACAAT AAACAAATCCA ATCTTTCCGT,25nm,STD +tile2rot0,TTGTCTAGAGT TTGGGATGTT AGTTCTTGGT ATTGTTGACAA,25nm,STD +tile3rot0,TTATCCACGT TTCCTCCTATT ACTCTAGACAA AACATCCCAA,25nm,STD +tile4rot0,AAGGAAGTAGA TTCGAAAGGT ACGTGGATAA AATAGGAGGAA,25nm,STD +tile5rot0,AACCTCGAAT TACCAGATTCT TCTACTTCCTT ACCTTTCGAA,25nm,STD +tile6rot0,AGAATAGTCGT TTGTCAGTGT ATTCGAGGTT AGAATCTGGTA,25nm,STD +tile7rot0,ATCTGCTCAT TCTGATCTCTT ACGACTATTCT ACACTGACAA,25nm,STD +tile8rot0,AATGGATAGGT AGGTGTCTTT ATGAGCAGAT AAGAGATCAGA,25nm,STD +tile9rot0,TCAAGTTCCA TATCCTTAGCA ACCTATCCATT AAAGACACCT,25nm,STD +tile10rot0,AGTGATGATCT TTTAGGCTGT TGGAACTTGA TGCTAAGGATA,25nm,STD +tile11rot0,ACCCATTCAT TTCCTGATACT AGATCATCACT ACAGCCTAAA,25nm,STD +tile12rot0,TGCGTTAAAAT AGATGCGTAT ATGAATGGGT AGTATCAGGAA,25nm,STD +tile13rot0,AACCTTCACA ATCGTCTCATA ATTTTAACGCA ATACGCATCT,25nm,STD +tile14rot0,ATTCAGAGAGT TGGCATGATA TGTGAAGGTT TATGAGACGAT,25nm,STD +tile15rot0,TACCATGCTT TTGACCAATTT ACTCTCTGAAT TATCATGCCA,25nm,STD +tile16rot0,TGGATTTGTTT ACGGAAAGAT AAGCATGGTA AAATTGGTCAA,25nm,STD""".split('\n') + +tile_dna_seqs = [''.join(line.split(',')[1]) for line_no, line in enumerate(seq_lines) if line_no % 2 == 1] + + +# print(tile_dna_seqs) + + +def add_tiles_and_assign_dna(design: sc.Design) -> None: + # left tiles + left_left = 11 + left_right = 32 + for top_helix, seq in zip(range(2, 18, 2), tile_dna_seqs): + bot_helix = top_helix + 1 + ss_top = sc.Domain(helix=top_helix, forward=True, + start=left_left, end=left_right) + ss_bot = sc.Domain(helix=bot_helix, forward=False, + start=left_left, end=left_right) + idt = sc.VendorFields(scale='25nm', purification='STD') + tile = sc.Strand(domains=[ss_bot, ss_top], name=f'tile-left-{top_helix}-{bot_helix}', + color=sc.Color(0, 0, 0), vendor_fields=idt) + design.add_strand(tile) + design.assign_dna(tile, seq) + + # right tiles + right_left = 480 + right_right = 501 + for top_helix, seq in zip(range(2, 18, 2), tile_dna_seqs): + bot_helix = top_helix + 1 + ss_top = sc.Domain(helix=top_helix, forward=True, + start=right_left, end=right_right) + ss_bot = sc.Domain(helix=bot_helix, forward=False, + start=right_left, end=right_right) + idt = sc.VendorFields(scale='25nm', purification='STD') + tile = sc.Strand(domains=[ss_bot, ss_top], name=f'tile-right-{top_helix}-{bot_helix}', + color=sc.Color(0, 0, 0), vendor_fields=idt) + design.add_strand(tile) + design.assign_dna(tile, seq) + + +def add_angle_inducing_insertions_deletions(design) -> None: + # insertion followed by deletion + start = 59 + end = start + (32 * 12) + for helix in [3, 7, 9, 13, 15]: + for offset in range(start, end, 32): + design.add_insertion(helix, offset, 1) + design.add_deletion(helix, offset + 16) + + # deletion followed by insertion + for helix in [4, 6, 10, 12, 16]: + for offset in range(start, end, 32): + design.add_deletion(helix, offset) + design.add_insertion(helix, offset + 16, 1) + + +uz_toes_wc = """ +CACCCCAC +CTTTCCTT +TTCACTAA +ACCAACCC +TCTCTTAA +CTTTCATA +ATAATAAA +AACTCACC +ACTTAATA +CAAATCAC +ACCATCCA +TACTCTAT +ATACCTTC +TTATTCAT +ATCCACAA +ATATTTTT +CCACCTAA +CTAAATTA +ATTACCCC +CACTAACA +ACACACTT +TTTTAATC +ACATTTAA +TCCACATC +CCTACCTT +TCCCTATA +""".split() + + +# above is in order from right to left on helix 1, followed by left to right on helix 18 + +def assign_dna_to_unzipper_toeholds(design: sc.Design) -> None: + uz_toes = [sc.rc(seq) for seq in uz_toes_wc] + + strands_h1 = design.strands_starting_on_helix(1) + strands_h1.sort(key=lambda _strand: _strand.first_domain().offset_5p()) + strands_h1.reverse() + + strands_h18 = design.strands_starting_on_helix(18) + strands_h18.sort(key=lambda _strand: _strand.first_domain().offset_5p()) + + for strand, toe in zip(strands_h1 + strands_h18, uz_toes): + seq = toe + sc.DNA_base_wildcard * (strand.dna_length() - 8) + design.assign_dna(strand, seq) + + +if __name__ == '__main__': + d = create_design() + d.write_scadnano_file(directory='output_designs') + d.write_idt_bulk_input_file(directory='idt') + # d.write_idt_plate_excel_file(directory='idt', export_non_modified_strand_version=True) diff --git a/examples/16_helix_origami_rectangle.py b/examples/16_helix_origami_rectangle.py index f409ecb7..f1538b51 100644 --- a/examples/16_helix_origami_rectangle.py +++ b/examples/16_helix_origami_rectangle.py @@ -1,11 +1,11 @@ -import origami_rectangle as rect -import scadnano as sc - - -def create_design() -> sc.Design: - return rect.create(num_helices=16, num_cols=26) - - -if __name__ == '__main__': - design = create_design() - design.write_scadnano_file(directory='output_designs') +import scadnano.origami_rectangle as rect +import scadnano as sc + + +def create_design() -> sc.Design: + return rect.create(num_helices=16, num_cols=26) + + +if __name__ == '__main__': + design = create_design() + design.write_scadnano_file(directory='output_designs') diff --git a/examples/16_helix_origami_rectangle_idt.py b/examples/16_helix_origami_rectangle_idt.py index 92d4ff05..2ac120bb 100644 --- a/examples/16_helix_origami_rectangle_idt.py +++ b/examples/16_helix_origami_rectangle_idt.py @@ -1,4 +1,4 @@ -import origami_rectangle as rect +import scadnano.origami_rectangle as rect import scadnano as sc diff --git a/examples/16_helix_origami_rectangle_no_seq.py b/examples/16_helix_origami_rectangle_no_seq.py index ba0ab8f2..77b63678 100644 --- a/examples/16_helix_origami_rectangle_no_seq.py +++ b/examples/16_helix_origami_rectangle_no_seq.py @@ -1,11 +1,11 @@ -import origami_rectangle as rect -import scadnano as sc - - -def create_design() -> sc.Design: - return rect.create(num_helices=16, num_cols=26, assign_seq=False) - - -if __name__ == '__main__': - design = create_design() - design.write_scadnano_file(directory='output_designs') +import scadnano.origami_rectangle as rect +import scadnano as sc + + +def create_design() -> sc.Design: + return rect.create(num_helices=16, num_cols=26, assign_seq=False) + + +if __name__ == '__main__': + design = create_design() + design.write_scadnano_file(directory='output_designs') diff --git a/examples/16_helix_origami_rectangle_no_seq_no_twist.py b/examples/16_helix_origami_rectangle_no_seq_no_twist.py index 1673c420..be2ba681 100644 --- a/examples/16_helix_origami_rectangle_no_seq_no_twist.py +++ b/examples/16_helix_origami_rectangle_no_seq_no_twist.py @@ -1,12 +1,12 @@ -import origami_rectangle as rect -import scadnano as sc - - -def create_design() -> sc.Design: - return rect.create(num_helices=16, num_cols=26, assign_seq=False, twist_correction_deletion_spacing=3, - twist_correction_start_col=2) - - -if __name__ == '__main__': - design = create_design() - design.write_scadnano_file(directory='output_designs') +import scadnano.origami_rectangle as rect +import scadnano as sc + + +def create_design() -> sc.Design: + return rect.create(num_helices=16, num_cols=26, assign_seq=False, twist_correction_deletion_spacing=3, + twist_correction_start_col=2) + + +if __name__ == '__main__': + design = create_design() + design.write_scadnano_file(directory='output_designs') diff --git a/examples/16_helix_origami_rectangle_no_twist.py b/examples/16_helix_origami_rectangle_no_twist.py index 9cb8536f..38d5ffb1 100644 --- a/examples/16_helix_origami_rectangle_no_twist.py +++ b/examples/16_helix_origami_rectangle_no_twist.py @@ -1,11 +1,11 @@ -import origami_rectangle as rect -import scadnano as sc - - -def create_design() -> sc.Design: - return rect.create(num_helices=16, num_cols=26, assign_seq=True, twist_correction_deletion_spacing=3) - - -if __name__ == '__main__': - design = create_design() - design.write_scadnano_file(directory='output_designs') +import scadnano.origami_rectangle as rect +import scadnano as sc + + +def create_design() -> sc.Design: + return rect.create(num_helices=16, num_cols=26, assign_seq=True, twist_correction_deletion_spacing=3) + + +if __name__ == '__main__': + design = create_design() + design.write_scadnano_file(directory='output_designs') diff --git a/examples/16_helix_origami_rectangle_seed_tiles_grow_from_top.py b/examples/16_helix_origami_rectangle_seed_tiles_grow_from_top.py index d1928396..3ea78ee2 100644 --- a/examples/16_helix_origami_rectangle_seed_tiles_grow_from_top.py +++ b/examples/16_helix_origami_rectangle_seed_tiles_grow_from_top.py @@ -1,155 +1,155 @@ -import origami_rectangle as rect -import scadnano as sc - - -def create_design() -> sc.Design: - design = rect.create(num_helices=16, num_cols=28, seam_left_column=12, assign_seq=False, - num_flanking_columns=2, num_flanking_helices=2, edge_staples=False, - scaffold_nick_offset=102) - - # # need this to match original design, but doesn't leave room for left-side adapters - # design.move_strand_offsets(8) - - set_helix_major_ticks(design) - # move_top_and_bottom_staples_within_column_boundaries(design) - # add_adapters(design) - # add_twist_correct_deletions(design) - # add_tiles_and_assign_dna(design) - - design.assign_m13_to_scaffold() - - return design - - -def set_helix_major_ticks(design: sc.Design) -> None: - major_ticks = [11, 22, 32] - for tick in range(40, 481, 8): - major_ticks.append(tick) - major_ticks.extend([490, 501]) - - for helix in design.helices.values(): - helix.major_ticks = list(major_ticks) - - for _, helix in zip([0, 1, 2], design.helices.values()): - ticks = [11, 22, 32, 40, 48] - tick = 58 - offset = 11 - while tick < 481: - ticks.append(tick) - tick += offset - offset = 10 if offset == 11 else 11 - helix.major_ticks = ticks - - -def add_twist_correct_deletions(design: sc.Design) -> None: - # I choose between 3 and 4 offset arbitrarily for twist-correction deletions for some reason, - # so they have to be hard-coded. - for col, offset in zip(range(4, 29, 3), [4, 3, 3, 4, 3, 3, 3, 3, 3]): - for helix in range(2, 18): - design.add_deletion(helix, 16 * col + offset) - - -def move_top_and_bottom_staples_within_column_boundaries(design: sc.Design) -> None: - top_staples = design.strands_starting_on_helix(2) - bot_staples = design.strands_starting_on_helix(17) - bot_staples.remove(design.scaffold) - - for top_staple in top_staples: - current_end = top_staple.domains[0].end - design.set_end(top_staple.domains[0], current_end - 8) - - for bot_staple in bot_staples: - current_start = bot_staple.domains[0].start - design.set_start(bot_staple.domains[0], current_start + 8) - - -def add_adapters(design: sc.Design) -> None: - # left adapters - left_inside_seed = 48 - left_outside_seed = left_inside_seed - 26 - for bot_helix in range(2, 18, 2): - top_helix = bot_helix - 1 if bot_helix != 2 else 17 - ss_top = sc.Domain(helix=top_helix, forward=True, - start=left_outside_seed, end=left_inside_seed) - ss_bot = sc.Domain(helix=bot_helix, forward=False, - start=left_outside_seed, end=left_inside_seed) - idt = sc.VendorFields(scale='25nm', purification='STD') - adapter = sc.Strand(domains=[ss_bot, ss_top], name=f'adap-left-{top_helix}-{bot_helix}', - vendor_fields=idt) - design.add_strand(adapter) - - # right adapters - right_inside_seed = 464 - right_outside_seed = right_inside_seed + 26 - for bot_helix in range(2, 18, 2): - top_helix = bot_helix - 1 if bot_helix != 2 else 17 - ss_top = sc.Domain(helix=top_helix, forward=True, - start=right_inside_seed, end=right_outside_seed) - ss_bot = sc.Domain(helix=bot_helix, forward=False, - start=right_inside_seed, end=right_outside_seed) - idt = sc.VendorFields(scale='25nm', purification='STD') - adapter = sc.Strand(domains=[ss_top, ss_bot], name=f'adap-right-{top_helix}-{bot_helix}', - vendor_fields=idt) - design.add_strand(adapter) - - -seq_lines = """tile1rot0,ACCAAGAACT TTGTCAACAAT AAACAAATCCA ATCTTTCCGT,25nm,STD -tile2rot0,TTGTCTAGAGT TTGGGATGTT AGTTCTTGGT ATTGTTGACAA,25nm,STD -tile3rot0,TTATCCACGT TTCCTCCTATT ACTCTAGACAA AACATCCCAA,25nm,STD -tile4rot0,AAGGAAGTAGA TTCGAAAGGT ACGTGGATAA AATAGGAGGAA,25nm,STD -tile5rot0,AACCTCGAAT TACCAGATTCT TCTACTTCCTT ACCTTTCGAA,25nm,STD -tile6rot0,AGAATAGTCGT TTGTCAGTGT ATTCGAGGTT AGAATCTGGTA,25nm,STD -tile7rot0,ATCTGCTCAT TCTGATCTCTT ACGACTATTCT ACACTGACAA,25nm,STD -tile8rot0,AATGGATAGGT AGGTGTCTTT ATGAGCAGAT AAGAGATCAGA,25nm,STD -tile9rot0,TCAAGTTCCA TATCCTTAGCA ACCTATCCATT AAAGACACCT,25nm,STD -tile10rot0,AGTGATGATCT TTTAGGCTGT TGGAACTTGA TGCTAAGGATA,25nm,STD -tile11rot0,ACCCATTCAT TTCCTGATACT AGATCATCACT ACAGCCTAAA,25nm,STD -tile12rot0,TGCGTTAAAAT AGATGCGTAT ATGAATGGGT AGTATCAGGAA,25nm,STD -tile13rot0,AACCTTCACA ATCGTCTCATA ATTTTAACGCA ATACGCATCT,25nm,STD -tile14rot0,ATTCAGAGAGT TGGCATGATA TGTGAAGGTT TATGAGACGAT,25nm,STD -tile15rot0,TACCATGCTT TTGACCAATTT ACTCTCTGAAT TATCATGCCA,25nm,STD -tile16rot0,TGGATTTGTTT ACGGAAAGAT AAGCATGGTA AAATTGGTCAA,25nm,STD""".split('\n') - -tile_dna_seqs = [''.join(line.split(',')[1]) for line_no, line in enumerate(seq_lines) if line_no % 2 == 1] - - -# print(tile_dna_seqs) - - -# def add_tiles_and_assign_dna(design): -# # left tiles -# left_left = 11 -# left_right = 32 -# for col, seq in zip(range(2, 18, 2), tile_dna_seqs): -# bot_helix = top_helix + 1 -# ss_top = sc.Domain(helix=top_helix, forward=True, -# start=left_left, end=left_right) -# ss_bot = sc.Domain(helix=bot_helix, forward=False, -# start=left_left, end=left_right) -# tile = sc.Strand(domains=[ss_bot, ss_top], color=sc.Color(0, 0, 0)) -# design.add_strand(tile) -# design.assign_dna(tile, seq) -# -# # right tiles -# right_left = 480 -# right_right = 501 -# for top_helix, seq in zip(range(2, 18, 2), tile_dna_seqs): -# bot_helix = top_helix + 1 -# ss_top = sc.Domain(helix=top_helix, forward=True, -# start=right_left, end=right_right) -# ss_bot = sc.Domain(helix=bot_helix, forward=False, -# start=right_left, end=right_right) -# tile = sc.Strand(domains=[ss_bot, ss_top], color=sc.Color(0, 0, 0)) -# design.add_strand(tile) -# design.assign_dna(tile, seq) - - -def main() -> None: - design = create_design() - design.write_scadnano_file(directory='output_designs') - design.write_idt_bulk_input_file(directory='idt') - design.write_idt_plate_excel_file(directory='idt', use_default_plates=True) - - -if __name__ == '__main__': - main() +import scadnano.origami_rectangle as rect +import scadnano as sc + + +def create_design() -> sc.Design: + design = rect.create(num_helices=16, num_cols=28, seam_left_column=12, assign_seq=False, + num_flanking_columns=2, num_flanking_helices=2, edge_staples=False, + scaffold_nick_offset=102) + + # # need this to match original design, but doesn't leave room for left-side adapters + # design.move_strand_offsets(8) + + set_helix_major_ticks(design) + # move_top_and_bottom_staples_within_column_boundaries(design) + # add_adapters(design) + # add_twist_correct_deletions(design) + # add_tiles_and_assign_dna(design) + + design.assign_m13_to_scaffold() + + return design + + +def set_helix_major_ticks(design: sc.Design) -> None: + major_ticks = [11, 22, 32] + for tick in range(40, 481, 8): + major_ticks.append(tick) + major_ticks.extend([490, 501]) + + for helix in design.helices.values(): + helix.major_ticks = list(major_ticks) + + for _, helix in zip([0, 1, 2], design.helices.values()): + ticks = [11, 22, 32, 40, 48] + tick = 58 + offset = 11 + while tick < 481: + ticks.append(tick) + tick += offset + offset = 10 if offset == 11 else 11 + helix.major_ticks = ticks + + +def add_twist_correct_deletions(design: sc.Design) -> None: + # I choose between 3 and 4 offset arbitrarily for twist-correction deletions for some reason, + # so they have to be hard-coded. + for col, offset in zip(range(4, 29, 3), [4, 3, 3, 4, 3, 3, 3, 3, 3]): + for helix in range(2, 18): + design.add_deletion(helix, 16 * col + offset) + + +def move_top_and_bottom_staples_within_column_boundaries(design: sc.Design) -> None: + top_staples = design.strands_starting_on_helix(2) + bot_staples = design.strands_starting_on_helix(17) + bot_staples.remove(design.scaffold) + + for top_staple in top_staples: + current_end = top_staple.domains[0].end + design.set_end(top_staple.domains[0], current_end - 8) + + for bot_staple in bot_staples: + current_start = bot_staple.domains[0].start + design.set_start(bot_staple.domains[0], current_start + 8) + + +def add_adapters(design: sc.Design) -> None: + # left adapters + left_inside_seed = 48 + left_outside_seed = left_inside_seed - 26 + for bot_helix in range(2, 18, 2): + top_helix = bot_helix - 1 if bot_helix != 2 else 17 + ss_top = sc.Domain(helix=top_helix, forward=True, + start=left_outside_seed, end=left_inside_seed) + ss_bot = sc.Domain(helix=bot_helix, forward=False, + start=left_outside_seed, end=left_inside_seed) + idt = sc.VendorFields(scale='25nm', purification='STD') + adapter = sc.Strand(domains=[ss_bot, ss_top], name=f'adap-left-{top_helix}-{bot_helix}', + vendor_fields=idt) + design.add_strand(adapter) + + # right adapters + right_inside_seed = 464 + right_outside_seed = right_inside_seed + 26 + for bot_helix in range(2, 18, 2): + top_helix = bot_helix - 1 if bot_helix != 2 else 17 + ss_top = sc.Domain(helix=top_helix, forward=True, + start=right_inside_seed, end=right_outside_seed) + ss_bot = sc.Domain(helix=bot_helix, forward=False, + start=right_inside_seed, end=right_outside_seed) + idt = sc.VendorFields(scale='25nm', purification='STD') + adapter = sc.Strand(domains=[ss_top, ss_bot], name=f'adap-right-{top_helix}-{bot_helix}', + vendor_fields=idt) + design.add_strand(adapter) + + +seq_lines = """tile1rot0,ACCAAGAACT TTGTCAACAAT AAACAAATCCA ATCTTTCCGT,25nm,STD +tile2rot0,TTGTCTAGAGT TTGGGATGTT AGTTCTTGGT ATTGTTGACAA,25nm,STD +tile3rot0,TTATCCACGT TTCCTCCTATT ACTCTAGACAA AACATCCCAA,25nm,STD +tile4rot0,AAGGAAGTAGA TTCGAAAGGT ACGTGGATAA AATAGGAGGAA,25nm,STD +tile5rot0,AACCTCGAAT TACCAGATTCT TCTACTTCCTT ACCTTTCGAA,25nm,STD +tile6rot0,AGAATAGTCGT TTGTCAGTGT ATTCGAGGTT AGAATCTGGTA,25nm,STD +tile7rot0,ATCTGCTCAT TCTGATCTCTT ACGACTATTCT ACACTGACAA,25nm,STD +tile8rot0,AATGGATAGGT AGGTGTCTTT ATGAGCAGAT AAGAGATCAGA,25nm,STD +tile9rot0,TCAAGTTCCA TATCCTTAGCA ACCTATCCATT AAAGACACCT,25nm,STD +tile10rot0,AGTGATGATCT TTTAGGCTGT TGGAACTTGA TGCTAAGGATA,25nm,STD +tile11rot0,ACCCATTCAT TTCCTGATACT AGATCATCACT ACAGCCTAAA,25nm,STD +tile12rot0,TGCGTTAAAAT AGATGCGTAT ATGAATGGGT AGTATCAGGAA,25nm,STD +tile13rot0,AACCTTCACA ATCGTCTCATA ATTTTAACGCA ATACGCATCT,25nm,STD +tile14rot0,ATTCAGAGAGT TGGCATGATA TGTGAAGGTT TATGAGACGAT,25nm,STD +tile15rot0,TACCATGCTT TTGACCAATTT ACTCTCTGAAT TATCATGCCA,25nm,STD +tile16rot0,TGGATTTGTTT ACGGAAAGAT AAGCATGGTA AAATTGGTCAA,25nm,STD""".split('\n') + +tile_dna_seqs = [''.join(line.split(',')[1]) for line_no, line in enumerate(seq_lines) if line_no % 2 == 1] + + +# print(tile_dna_seqs) + + +# def add_tiles_and_assign_dna(design): +# # left tiles +# left_left = 11 +# left_right = 32 +# for col, seq in zip(range(2, 18, 2), tile_dna_seqs): +# bot_helix = top_helix + 1 +# ss_top = sc.Domain(helix=top_helix, forward=True, +# start=left_left, end=left_right) +# ss_bot = sc.Domain(helix=bot_helix, forward=False, +# start=left_left, end=left_right) +# tile = sc.Strand(domains=[ss_bot, ss_top], color=sc.Color(0, 0, 0)) +# design.add_strand(tile) +# design.assign_dna(tile, seq) +# +# # right tiles +# right_left = 480 +# right_right = 501 +# for top_helix, seq in zip(range(2, 18, 2), tile_dna_seqs): +# bot_helix = top_helix + 1 +# ss_top = sc.Domain(helix=top_helix, forward=True, +# start=right_left, end=right_right) +# ss_bot = sc.Domain(helix=bot_helix, forward=False, +# start=right_left, end=right_right) +# tile = sc.Strand(domains=[ss_bot, ss_top], color=sc.Color(0, 0, 0)) +# design.add_strand(tile) +# design.assign_dna(tile, seq) + + +def main() -> None: + design = create_design() + design.write_scadnano_file(directory='output_designs') + design.write_idt_bulk_input_file(directory='idt') + design.write_idt_plate_excel_file(directory='idt', use_default_plates=True) + + +if __name__ == '__main__': + main() diff --git a/examples/16_helix_origami_rectangle_shifted_seam_no_seq.py b/examples/16_helix_origami_rectangle_shifted_seam_no_seq.py index cc026779..ffe5c5e8 100644 --- a/examples/16_helix_origami_rectangle_shifted_seam_no_seq.py +++ b/examples/16_helix_origami_rectangle_shifted_seam_no_seq.py @@ -1,11 +1,11 @@ -import origami_rectangle as rect -import scadnano as sc - - -def create_design() -> sc.Design: - return rect.create(num_helices=16, num_cols=26, assign_seq=False, seam_left_column=2) - - -if __name__ == '__main__': - design = create_design() - design.write_scadnano_file(directory='output_designs') +import scadnano.origami_rectangle as rect +import scadnano as sc + + +def create_design() -> sc.Design: + return rect.create(num_helices=16, num_cols=26, assign_seq=False, seam_left_column=2) + + +if __name__ == '__main__': + design = create_design() + design.write_scadnano_file(directory='output_designs') diff --git a/examples/2_staple_2_helix_modifications.py b/examples/2_staple_2_helix_modifications.py index 56bf7412..91f92d26 100644 --- a/examples/2_staple_2_helix_modifications.py +++ b/examples/2_staple_2_helix_modifications.py @@ -1,5 +1,5 @@ import scadnano as sc -import modifications as mod +import scadnano.modifications as mod import dataclasses def create_design() -> sc.Design: diff --git a/examples/2_staple_2_helix_origami_deletions_insertions_mods.py b/examples/2_staple_2_helix_origami_deletions_insertions_mods.py index 2a7b9283..075ac00c 100644 --- a/examples/2_staple_2_helix_origami_deletions_insertions_mods.py +++ b/examples/2_staple_2_helix_origami_deletions_insertions_mods.py @@ -1,5 +1,5 @@ import scadnano as sc -import modifications as mod +import scadnano.modifications as mod def create_design() -> sc.Design: diff --git a/examples/2_staple_2_helix_origami_deletions_insertions_mods_chained_methods.py b/examples/2_staple_2_helix_origami_deletions_insertions_mods_chained_methods.py index c1d1f1e7..885ec0da 100644 --- a/examples/2_staple_2_helix_origami_deletions_insertions_mods_chained_methods.py +++ b/examples/2_staple_2_helix_origami_deletions_insertions_mods_chained_methods.py @@ -1,5 +1,5 @@ import scadnano as sc -import modifications as mod +import scadnano.modifications as mod def create_design() -> sc.Design: diff --git a/examples/56_helix_origami_rectangle.py b/examples/56_helix_origami_rectangle.py index 00a0506c..5d789d81 100644 --- a/examples/56_helix_origami_rectangle.py +++ b/examples/56_helix_origami_rectangle.py @@ -1,11 +1,11 @@ -import origami_rectangle as rect -import scadnano as sc - - -def create_design() -> sc.Design: - return rect.create(num_helices=56, num_cols=8, seam_left_column=4) - - -if __name__ == '__main__': - design = create_design() - design.write_scadnano_file(directory='output_designs') +import scadnano.origami_rectangle as rect +import scadnano as sc + + +def create_design() -> sc.Design: + return rect.create(num_helices=56, num_cols=8, seam_left_column=4) + + +if __name__ == '__main__': + design = create_design() + design.write_scadnano_file(directory='output_designs') diff --git a/examples/64_helix_origami_rectangle.py b/examples/64_helix_origami_rectangle.py index 5a71abcd..ecafb90e 100644 --- a/examples/64_helix_origami_rectangle.py +++ b/examples/64_helix_origami_rectangle.py @@ -1,11 +1,11 @@ -import origami_rectangle as rect -import scadnano as sc - - -def create_design() -> sc.Design: - return rect.create(num_helices=64, num_cols=6) - - -if __name__ == '__main__': - design = create_design() - design.write_scadnano_file(directory='output_designs') +import scadnano.origami_rectangle as rect +import scadnano as sc + + +def create_design() -> sc.Design: + return rect.create(num_helices=64, num_cols=6) + + +if __name__ == '__main__': + design = create_design() + design.write_scadnano_file(directory='output_designs') diff --git a/examples/6_helix_6_col_origami_rectangle.py b/examples/6_helix_6_col_origami_rectangle.py index fe46d09d..294aa59f 100644 --- a/examples/6_helix_6_col_origami_rectangle.py +++ b/examples/6_helix_6_col_origami_rectangle.py @@ -1,12 +1,12 @@ -import origami_rectangle as rect -import scadnano as sc - - -def create_design() -> sc.Design: - design = rect.create(num_helices=6, num_cols=6, num_flanking_columns=0) - return design - - -if __name__ == '__main__': - d = create_design() - d.write_scadnano_file(directory='output_designs') +import scadnano.origami_rectangle as rect +import scadnano as sc + + +def create_design() -> sc.Design: + design = rect.create(num_helices=6, num_cols=6, num_flanking_columns=0) + return design + + +if __name__ == '__main__': + d = create_design() + d.write_scadnano_file(directory='output_designs') diff --git a/examples/6_helix_origami_rectangle.py b/examples/6_helix_origami_rectangle.py index 36d95d63..229d847e 100644 --- a/examples/6_helix_origami_rectangle.py +++ b/examples/6_helix_origami_rectangle.py @@ -1,12 +1,12 @@ -import origami_rectangle as rect -import scadnano as sc - - -def create_design() -> sc.Design: - design = rect.create(num_helices=6, num_cols=10, nick_pattern=rect.staggered, twist_correction_deletion_spacing=3) - return design - - -if __name__ == '__main__': - d = create_design() - d.write_scadnano_file(directory='output_designs') +import scadnano.origami_rectangle as rect +import scadnano as sc + + +def create_design() -> sc.Design: + design = rect.create(num_helices=6, num_cols=10, nick_pattern=rect.staggered, twist_correction_deletion_spacing=3) + return design + + +if __name__ == '__main__': + d = create_design() + d.write_scadnano_file(directory='output_designs') diff --git a/examples/6_helix_origami_rectangle_helices_out_of_order.py b/examples/6_helix_origami_rectangle_helices_out_of_order.py index 8249e5a8..1fa06190 100644 --- a/examples/6_helix_origami_rectangle_helices_out_of_order.py +++ b/examples/6_helix_origami_rectangle_helices_out_of_order.py @@ -1,13 +1,13 @@ -import origami_rectangle as rect -import scadnano as sc - - -def create_design() -> sc.Design: - design = rect.create(num_helices=6, num_cols=10, nick_pattern=rect.staggered, twist_correction_deletion_spacing=3) - design.set_helices_view_order([5,4,3,2,1,0]) - return design - - -if __name__ == '__main__': - d = create_design() - d.write_scadnano_file(directory='output_designs') +import scadnano.origami_rectangle as rect +import scadnano as sc + + +def create_design() -> sc.Design: + design = rect.create(num_helices=6, num_cols=10, nick_pattern=rect.staggered, twist_correction_deletion_spacing=3) + design.set_helices_view_order([5,4,3,2,1,0]) + return design + + +if __name__ == '__main__': + d = create_design() + d.write_scadnano_file(directory='output_designs') diff --git a/examples/draw_strand_move_negative.py b/examples/draw_strand_move_negative.py index 75a8160b..ce2c8077 100644 --- a/examples/draw_strand_move_negative.py +++ b/examples/draw_strand_move_negative.py @@ -1,5 +1,5 @@ import scadnano as sc -import modifications as mod +import scadnano.modifications as mod import dataclasses diff --git a/examples/many_helices_modifications.py b/examples/many_helices_modifications.py index cf92ef82..87ae464e 100644 --- a/examples/many_helices_modifications.py +++ b/examples/many_helices_modifications.py @@ -1,5 +1,5 @@ import scadnano as sc -import modifications as mod +import scadnano.modifications as mod import dataclasses def create_design() -> sc.Design: diff --git a/examples/proposal.py b/examples/proposal.py index 60ea01eb..2f9d7329 100644 --- a/examples/proposal.py +++ b/examples/proposal.py @@ -1,8 +1,8 @@ import math import dataclasses -import origami_rectangle as rect +import scadnano.origami_rectangle as rect import scadnano as sc -import modifications as mod +import scadnano.modifications as mod def create_design() -> sc.Design: diff --git a/examples/relax_helix_rolls.py b/examples/relax_helix_rolls.py index 9e11ac23..ddfbebdc 100644 --- a/examples/relax_helix_rolls.py +++ b/examples/relax_helix_rolls.py @@ -1,5 +1,5 @@ import scadnano as sc -import modifications as mod +import scadnano.modifications as mod import dataclasses diff --git a/examples/very_large_origami.py b/examples/very_large_origami.py index b0961709..67297461 100644 --- a/examples/very_large_origami.py +++ b/examples/very_large_origami.py @@ -1,4 +1,4 @@ -import origami_rectangle as rect +import scadnano.origami_rectangle as rect import scadnano as sc diff --git a/publish_to_pypi.txt b/publish_to_pypi.txt index afca3aaa..c3c3b2de 100644 --- a/publish_to_pypi.txt +++ b/publish_to_pypi.txt @@ -8,6 +8,6 @@ the workflow later tries to publish will make it fail. The manual equivalent, for reference, is: -rem !!!bump __version__ in scadnano/scadnano.py first!!! +rem !!!bump version in pyproject.toml first!!! python -m build twine upload dist/* diff --git a/pyproject.toml b/pyproject.toml index bec0067a..8401963b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,6 +10,12 @@ build-backend = "setuptools.build_meta" [project] name = "scadnano" + +# THE single source of truth for the version. Bump this line and nothing else. +# scadnano.__version__ is read back from the installed distribution metadata at +# run time, and release.yml reads this file to decide the release tag. +version = "0.21.1" + description = "Python scripting library for generating designs readable by scadnano." readme = { file = "README.md", content-type = "text/markdown; variant=GFM" } license = "MIT" @@ -17,17 +23,13 @@ license-files = ["LICENSE.txt"] authors = [{ name = "David Doty", email = "doty@ucdavis.edu" }] requires-python = ">=3.10" -# Deliberately minimal; see "Keeping the scadnano package simple for users to -# install" in CONTRIBUTING.md. +# Deliberately minimal, so that installing scadnano pulls in as little as +# possible. dependencies = [ "openpyxl", "tabulate", ] -# Read from the __version__ line in scadnano/scadnano.py, which stays the single -# source of truth for the version. release.yml reads that same line. -dynamic = ["version"] - [project.urls] Homepage = "https://github.com/UC-Davis-molecular-computing/scadnano-python-package" Documentation = "https://scadnano-python-package.readthedocs.io" @@ -50,6 +52,3 @@ tests = [ [tool.setuptools] packages = ["scadnano"] - -[tool.setuptools.dynamic] -version = { attr = "scadnano.scadnano.__version__" } diff --git a/scadnano/__init__.py b/scadnano/__init__.py index 7e22f1fa..bb0b9dac 100644 --- a/scadnano/__init__.py +++ b/scadnano/__init__.py @@ -1,3 +1,7 @@ from scadnano.scadnano import * from scadnano.modifications import * -from scadnano.origami_rectangle import * \ No newline at end of file +from scadnano.origami_rectangle import * + +# `import *` skips names beginning with an underscore, so __version__ would not +# otherwise be reachable as scadnano.__version__, which is where users expect it. +from scadnano.scadnano import __version__ as __version__ diff --git a/scadnano/scadnano.py b/scadnano/scadnano.py index 5498b57f..689ef06d 100644 --- a/scadnano/scadnano.py +++ b/scadnano/scadnano.py @@ -54,7 +54,16 @@ # needed to use forward annotations: https://docs.python.org/3/whatsnew/3.7.html#whatsnew37-pep563 from __future__ import annotations -__version__ = "0.21.1" # version line; WARNING: do not remove this line or change its format +from importlib.metadata import version as _distribution_version + +# The version lives in pyproject.toml; this reads it back from the metadata of the +# installed distribution, so there is only one place to bump it. Design files +# written by write_scadnano_file() are stamped with this value. +# +# This requires scadnano to be installed (`pip install scadnano`, or +# `pip install -e .` when working from a clone). It is not importable from a bare +# source checkout. +__version__ = _distribution_version("scadnano") import collections import dataclasses From 762374c1810bfbd522179fa9b3d8502411de988d Mon Sep 17 00:00:00 2001 From: David Doty Date: Mon, 10 Aug 2026 11:50:16 -0500 Subject: [PATCH 3/3] assert scadnano.__version__ matches pyproject.toml, so a stale editable install fails loudly The version is declared once, under [project] in pyproject.toml, and read back at import time from the installed distribution's metadata. `pip install -e .` makes the source live but not that metadata: pip writes it once, at install time, into scadnano-.dist-info/METADATA, with the version baked into the directory name. So bumping the version without reinstalling leaves scadnano.__version__ reporting the previous value, and that stale value is what gets written into the "version" field of every .sc file produced in the meantime. Nothing detected that. This test does, at the moment it matters: it compares the declared version against the installed one and fails with an explanation and the command that fixes it. run_unit_tests.yml installs immediately before running the suite, so it cannot fire spuriously in CI. Skipped on Python 3.10, which the test matrix still covers and which predates stdlib tomllib, and skipped when not run from a source checkout, where there is no pyproject.toml to compare against and the installed metadata is authoritative by definition. Verified both directions: passes in the synced state, and with pyproject.toml temporarily bumped to 0.99.0 against an install of 0.21.1 it fails with "pyproject.toml declares version 0.99.0, but the installed scadnano distribution reports 0.21.1". Full suite is 376 tests, all passing. Co-Authored-By: Claude Opus 5 (1M context) --- tests/scadnano_tests.py | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/tests/scadnano_tests.py b/tests/scadnano_tests.py index 1cafa071..6d75dd47 100644 --- a/tests/scadnano_tests.py +++ b/tests/scadnano_tests.py @@ -1,5 +1,6 @@ import dataclasses import os +import pathlib import sys import tempfile import unittest @@ -16,6 +17,11 @@ from scadnano.scadnano import _convert_design_to_oxdna_system +try: + import tomllib # standard library from Python 3.11 +except ModuleNotFoundError: # only on Python 3.10, which the CI matrix still covers + tomllib = None # type: ignore[assignment] + def strand_matching(strands: Iterable[sc.Strand], helix: int, forward: bool, start: int, end: int) -> sc.Strand: """ @@ -36,6 +42,36 @@ def remove_whitespace(sequence: str) -> str: return sequence +class TestVersion(unittest.TestCase): + """ + The version is declared once, as ``version`` under ``[project]`` in pyproject.toml. + ``sc.__version__`` does not read that file: it reads the metadata of the *installed* + distribution, which pip writes once at install time. An editable install keeps the + source live but not that metadata, so bumping the version without reinstalling leaves + ``sc.__version__`` reporting the previous value -- which then gets written into the + "version" field of every .sc file produced. This test turns that silent staleness into + an immediate, self-explanatory failure. + """ + + def test_version_matches_pyproject(self) -> None: + if tomllib is None: + self.skipTest("tomllib requires Python 3.11 or later") + # Tests are run from the repository root; outside a checkout there is nothing to + # compare against, and the installed metadata is authoritative by definition. + pyproject = pathlib.Path("pyproject.toml") + if not pyproject.is_file(): + self.skipTest("not running from a source checkout") + + declared = tomllib.loads(pyproject.read_text(encoding="utf-8"))["project"]["version"] + self.assertEqual( + declared, + sc.__version__, + f"pyproject.toml declares version {declared}, but the installed scadnano " + f"distribution reports {sc.__version__}. If you just bumped the version, " + f"reinstall so the metadata catches up: pip install -e .[tests]", + ) + + class TestCreateStrandChainedMethods(unittest.TestCase): # tests methods for creating strands using chained method notation as in this issue: # https://github.com/UC-Davis-molecular-computing/scadnano-python-package/issues/85