diff --git a/.github/workflows/build-wheels.sh b/.github/workflows/build-wheels.sh index d49081c..72dd683 100644 --- a/.github/workflows/build-wheels.sh +++ b/.github/workflows/build-wheels.sh @@ -3,7 +3,7 @@ set -euxo pipefail nullglob -for PY in cp38-cp38 cp39-cp39 cp310-cp310; do +for PY in cp310-cp310 cp311-cp311 cp312-cp312; do PYBIN="/opt/python/${PY}/bin" "${PYBIN}/pip" install maturin "${PYBIN}/maturin" build -i "${PYBIN}/python" --release diff --git a/.github/workflows/python.yml b/.github/workflows/python.yml index f06385c..bf4f615 100644 --- a/.github/workflows/python.yml +++ b/.github/workflows/python.yml @@ -11,7 +11,7 @@ jobs: - uses: actions/checkout@v3 - uses: actions/setup-python@v2 with: - python-version: '3.10' - - uses: abatilo/actions-poetry@v2.1.4 + python-version: '3.14' + - uses: abatilo/actions-poetry@v3.0.2 - run: poetry install - run: poetry run black --check --verbose . \ No newline at end of file diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index b8a1d1a..156d52a 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -26,7 +26,7 @@ jobs: runs-on: macos-latest strategy: matrix: - python-version: ["3.8", "3.9", "3.10"] + python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13", "3.14"] steps: - uses: actions/checkout@v1 - uses: actions-rs/toolchain@v1 @@ -75,7 +75,7 @@ jobs: - uses: actions/download-artifact@v2 # downloads all artifacts into subdir of current directory named artifact name - uses: actions/setup-python@v2 with: - python-version: 3.9 + python-version: 3.14 - name: Publish env: MATURIN_PASSWORD: ${{ secrets.MATURIN_PASSWORD }} diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 80e9423..135b1b8 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -90,8 +90,8 @@ jobs: - uses: actions/checkout@v3 - uses: actions/setup-python@v2 with: - python-version: '3.10' - - uses: abatilo/actions-poetry@v2.1.4 + python-version: '3.14' + - uses: abatilo/actions-poetry@v3.0.2 - run: poetry install - run: poetry run maturin develop --release - run: poetry run pytest diff --git a/Cargo.toml b/Cargo.toml index a5976f5..5ad74e3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,7 +10,7 @@ publish = false lazy_static = "1.4.0" thiserror = "1.0.30" smallvec = "1.8.0" -pyo3 = {version = "0.20.0", features = ["extension-module"], optional = true} +pyo3 = {version = "0.27.1", features = ["extension-module"], optional = true} quickcheck = {version = "1.0.3", optional = true} serde = {version = "1.0", features = ["derive"], optional = true} diff --git a/pyproject.toml b/pyproject.toml index 598901a..b686901 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,10 +9,10 @@ repository = "https://github.com/SecureDNA/quickdna" [tool.poetry.dependencies] -python = "^3.8" +python = "^3.10" [tool.poetry.dev-dependencies] -pytest = "^6.1" +pytest = "^9.0" pylint = "^2.6" flake8 = "^3.9" wheel = "*" diff --git a/src/python_api.rs b/src/python_api.rs index 2546419..49d21c8 100644 --- a/src/python_api.rs +++ b/src/python_api.rs @@ -1,83 +1,75 @@ -#![allow(clippy::borrow_deref_ref)] // TODO: broken clippy lint? - // Copyright 2021-2024 SecureDNA Stiftung (SecureDNA Foundation) - // SPDX-License-Identifier: MIT OR Apache-2.0 +// Copyright 2021-2024 SecureDNA Stiftung (SecureDNA Foundation) +// SPDX-License-Identifier: MIT OR Apache-2.0 -use pyo3::{exceptions::PyValueError, prelude::*, types::PyBytes}; +#[pyo3::pymodule] +mod quickdna { -use crate::{ - errors::TranslationError, - trans_table::{reverse_complement_bytes, TranslationTable}, - Nucleotide, NucleotideAmbiguous, -}; + use pyo3::{exceptions::PyValueError, prelude::*, types::PyBytes}; -impl From for PyErr { - fn from(err: TranslationError) -> PyErr { - PyValueError::new_err(err.to_string()) - } -} - -#[pyfunction] -fn _check_table(table: u8) -> PyResult<()> { - let _ = TranslationTable::try_from(table)?; - Ok(()) -} + use crate::{ + errors::TranslationError, + trans_table::{reverse_complement_bytes, TranslationTable}, + Nucleotide, NucleotideAmbiguous, + }; -/// Translate a bytestring of DNA nucleotides into a bytestring of amino acids. -/// -/// The input string is allowed to contain IUPAC ambiguity codes; ambiguous -/// codons are represented by `X` in the output. -/// -/// * `translate(b"CCNTACACK CATNCNAAT")` returns `b"PYTHXN"` -#[pyfunction] -fn _translate(py: Python, table: u8, dna: &PyBytes) -> PyResult { - let table = TranslationTable::try_from(table)?; - let bytes = table.translate_dna_bytes::(dna.as_bytes())?; - Ok(PyBytes::new(py, &bytes).into()) -} + impl From for PyErr { + fn from(err: TranslationError) -> PyErr { + PyValueError::new_err(err.to_string()) + } + } -/// Translate a bytestring of DNA nucleotides into a bytestring of amino acids. -/// -/// The input string is validated to consist of unambiguous nucleotides (no IUPAC ambiguity codes). -/// -/// * `translate_strict(b"AAACCCTTTGGG")` returns `b"KPFG"` -/// * `translate_strict(b"AAACCCTTTGGN")` is an error. -#[pyfunction] -fn _translate_strict(py: Python, table: u8, dna: &PyBytes) -> PyResult { - let table = TranslationTable::try_from(table)?; - let bytes = table.translate_dna_bytes::(dna.as_bytes())?; - Ok(PyBytes::new(py, &bytes).into()) -} + #[pyfunction] + fn _check_table(table: u8) -> PyResult<()> { + let _ = TranslationTable::try_from(table)?; + Ok(()) + } -/// Get the reverse complement of a bytestring of DNA nucleotides. -/// -/// The input string is allowed to contain IUPAC ambiguity codes. -/// -/// * `reverse_complement(b"AAAAABCCC")` returns `b"GGGVTTTTT"` -#[pyfunction] -fn _reverse_complement(py: Python, dna: &PyBytes) -> PyResult { - let bytes = reverse_complement_bytes::(dna.as_bytes())?; - Ok(PyBytes::new(py, &bytes).into()) -} + /// Translate a bytestring of DNA nucleotides into a bytestring of amino acids. + /// + /// The input string is allowed to contain IUPAC ambiguity codes; ambiguous + /// codons are represented by `X` in the output. + /// + /// * `translate(b"CCNTACACK CATNCNAAT")` returns `b"PYTHXN"` + #[pyfunction] + fn _translate(py: Python, table: u8, dna: Bound<'_, PyBytes>) -> PyResult> { + let table = TranslationTable::try_from(table)?; + let bytes = table.translate_dna_bytes::(dna.as_bytes())?; + Ok(PyBytes::new(py, &bytes).into()) + } -/// Get the reverse complement of a bytestring of DNA nucleotides. -/// -/// The input string is validated to consist of unambiguous nucleotides (no IUPAC ambiguity codes). -/// -/// * `reverse_complement_strict(b"AAAAAACCC")` returns `b"GGGTTTTTT"` -/// * `reverse_complement_strict(b"AAAAAACCN")` is an error. -#[pyfunction] -fn _reverse_complement_strict(py: Python, dna: &PyBytes) -> PyResult { - let bytes = reverse_complement_bytes::(dna.as_bytes())?; - Ok(PyBytes::new(py, &bytes).into()) -} + /// Translate a bytestring of DNA nucleotides into a bytestring of amino acids. + /// + /// The input string is validated to consist of unambiguous nucleotides (no IUPAC ambiguity codes). + /// + /// * `translate_strict(b"AAACCCTTTGGG")` returns `b"KPFG"` + /// * `translate_strict(b"AAACCCTTTGGN")` is an error. + #[pyfunction] + fn _translate_strict(py: Python, table: u8, dna: Bound<'_, PyBytes>) -> PyResult> { + let table = TranslationTable::try_from(table)?; + let bytes = table.translate_dna_bytes::(dna.as_bytes())?; + Ok(PyBytes::new(py, &bytes).into()) + } -#[pymodule] -fn quickdna(_py: Python, m: &PyModule) -> PyResult<()> { - m.add_function(wrap_pyfunction!(_check_table, m)?)?; - m.add_function(wrap_pyfunction!(_translate, m)?)?; - m.add_function(wrap_pyfunction!(_translate_strict, m)?)?; - m.add_function(wrap_pyfunction!(_reverse_complement, m)?)?; - m.add_function(wrap_pyfunction!(_reverse_complement_strict, m)?)?; + /// Get the reverse complement of a bytestring of DNA nucleotides. + /// + /// The input string is allowed to contain IUPAC ambiguity codes. + /// + /// * `reverse_complement(b"AAAAABCCC")` returns `b"GGGVTTTTT"` + #[pyfunction] + fn _reverse_complement(py: Python, dna: Bound<'_, PyBytes>) -> PyResult> { + let bytes = reverse_complement_bytes::(dna.as_bytes())?; + Ok(PyBytes::new(py, &bytes).into()) + } - Ok(()) + /// Get the reverse complement of a bytestring of DNA nucleotides. + /// + /// The input string is validated to consist of unambiguous nucleotides (no IUPAC ambiguity codes). + /// + /// * `reverse_complement_strict(b"AAAAAACCC")` returns `b"GGGTTTTTT"` + /// * `reverse_complement_strict(b"AAAAAACCN")` is an error. + #[pyfunction] + fn _reverse_complement_strict(py: Python, dna: Bound<'_, PyBytes>) -> PyResult> { + let bytes = reverse_complement_bytes::(dna.as_bytes())?; + Ok(PyBytes::new(py, &bytes).into()) + } }