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/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/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 4357944c..8d2f8436 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: @@ -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/.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..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,16 +223,21 @@ 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 trailing `# version line; WARNING: ...` comment intact — `setup.py` finds this line by - searching for that comment, and the release workflow reads the same line. + `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. 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/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/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/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 0a6e1deb..c3c3b2de 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 pyproject.toml first!!! +python -m build +twine upload dist/* diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 00000000..8401963b --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,54 @@ +# 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" + +# 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" +license-files = ["LICENSE.txt"] +authors = [{ name = "David Doty", email = "doty@ucdavis.edu" }] +requires-python = ">=3.10" + +# Deliberately minimal, so that installing scadnano pulls in as little as +# possible. +dependencies = [ + "openpyxl", + "tabulate", +] + +[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"] 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/__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/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..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 or change this line or comment +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 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', - ], - ) 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