From dcb2061018986387272d6c2219488a0830176848 Mon Sep 17 00:00:00 2001 From: Philip Tzou Date: Mon, 11 Aug 2025 09:37:57 -0700 Subject: [PATCH 1/5] Test infrastructure updates --- AGENTS.md | 5 +- Makefile | 12 +- Pipfile | 1 + Pipfile.lock | 66 ++++++- README.md | 3 +- requirements.txt | 4 +- tests/component/data/hiv1_reference.fasta | 163 ++++++++++++++++++ tests/component/data/samplesmall.fas | 81 +++++++++ .../features/codon_alignment.feature | 6 + tests/component/steps/codon_steps.py | 92 ++++++++++ tests/component/steps/run_codon_alignment.py | 73 ++++++++ tests/{ => unit}/conftest.py | 2 +- tests/{ => unit}/test_aa_position.py | 0 tests/{ => unit}/test_blosum62.py | 0 tests/{ => unit}/test_cigar.py | 0 tests/{ => unit}/test_cli.py | 0 tests/{ => unit}/test_codon_alignment.py | 0 tests/{ => unit}/test_codonutils.py | 0 tests/{ => unit}/test_entry.py | 0 tests/{ => unit}/test_group_by_codons.py | 0 tests/{ => unit}/test_iupac.py | 0 tests/{ => unit}/test_message.py | 0 tests/{ => unit}/test_modifier.py | 0 tests/{ => unit}/test_na_position.py | 0 tests/{ => unit}/test_paf.py | 0 tests/{ => unit}/test_parsers.py | 0 tests/{ => unit}/test_processor.py | 0 tests/{ => unit}/test_sanitize_sequence.py | 0 tests/{ => unit}/test_save_fasta.py | 0 tests/{ => unit}/test_save_json.py | 0 tests/{ => unit}/test_sequence.py | 0 tests/{ => unit}/test_trim_by_ref.py | 0 tests/{ => unit}/test_version.py | 0 33 files changed, 495 insertions(+), 13 deletions(-) create mode 100644 tests/component/data/hiv1_reference.fasta create mode 100644 tests/component/data/samplesmall.fas create mode 100644 tests/component/features/codon_alignment.feature create mode 100644 tests/component/steps/codon_steps.py create mode 100644 tests/component/steps/run_codon_alignment.py rename tests/{ => unit}/conftest.py (99%) rename tests/{ => unit}/test_aa_position.py (100%) rename tests/{ => unit}/test_blosum62.py (100%) rename tests/{ => unit}/test_cigar.py (100%) rename tests/{ => unit}/test_cli.py (100%) rename tests/{ => unit}/test_codon_alignment.py (100%) rename tests/{ => unit}/test_codonutils.py (100%) rename tests/{ => unit}/test_entry.py (100%) rename tests/{ => unit}/test_group_by_codons.py (100%) rename tests/{ => unit}/test_iupac.py (100%) rename tests/{ => unit}/test_message.py (100%) rename tests/{ => unit}/test_modifier.py (100%) rename tests/{ => unit}/test_na_position.py (100%) rename tests/{ => unit}/test_paf.py (100%) rename tests/{ => unit}/test_parsers.py (100%) rename tests/{ => unit}/test_processor.py (100%) rename tests/{ => unit}/test_sanitize_sequence.py (100%) rename tests/{ => unit}/test_save_fasta.py (100%) rename tests/{ => unit}/test_save_json.py (100%) rename tests/{ => unit}/test_sequence.py (100%) rename tests/{ => unit}/test_trim_by_ref.py (100%) rename tests/{ => unit}/test_version.py (100%) diff --git a/AGENTS.md b/AGENTS.md index 74d2c0c..c9ce499 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -6,7 +6,7 @@ This repo uses automation agents (local or CI) to keep code healthy and consiste - **Package management**: use `pipenv` for environment and dependency management, but maintain `pyproject.toml` (preferred) and keep `setup.py` / `setup.cfg` in sync if present. - **Python**: runtime support starts at **Python 3.11**; develop and run CI on **Python 3.13**. - **Static checks**: enforce `mypy` and `flake8` on all tracked Python files. -- **Tests**: run `pytest` with `pytest-cov`; fail if coverage drops below the configured threshold. +- **Tests**: run `pytest tests/unit --cov=postalign` and `behave tests/component` (which downloads minimap2 2.17); fail if coverage drops below the configured threshold. - **Mocks**: use `unittest.mock`; avoid `monkeypatch` or plain stubs. - **Coverage pragmas**: annotate unavoidable no-op statements with `# pragma: no cover` and a brief justification. File-wide pragmas are not @@ -41,7 +41,8 @@ pipenv run flake8 . pipenv run mypy . # Tests + coverage -pipenv run pytest --cov=postalign --cov-report=term-missing +pipenv run pytest tests/unit --cov=postalign --cov-report=term-missing +pipenv run behave tests/component # Update Pipfile.lock pipenv lock --dev --clear diff --git a/Makefile b/Makefile index 4864921..9665706 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,14 @@ requirements.txt: Pipfile.lock @pipenv requirements --from-pipfile > requirements.txt - @sed -i '' '/^-e \.$$/d' requirements.txt +@sed -i.bak -- '/^-e \.$/d' requirements.txt && rm -f requirements.txt.bak + +test-unit: + @pytest tests/unit + +test-component: + @behave tests/component + +test: test-unit test-component build-docker-builder: requirements.txt @docker pull ubuntu:18.04 @@ -33,4 +41,4 @@ dist/postalign_linux-amd64.tar.gz: dist/linux-amd64 dist: dist/postalign_linux-amd64.tar.gz build: dist -.PHONY: build-docker-builder push-docker-builder +.PHONY: test-unit test-component test build-docker-builder push-docker-builder diff --git a/Pipfile b/Pipfile index 7ca26e0..05737f0 100644 --- a/Pipfile +++ b/Pipfile @@ -10,6 +10,7 @@ mypy = "*" flake8 = "*" pytest = "*" pytest-cov = "*" +behave = "*" [packages] more-itertools = "*" diff --git a/Pipfile.lock b/Pipfile.lock index c157eb4..c64b358 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "b7e026aaf72b897c53851a168145143e46c073196457d50f9bdb6f815797e6f7" + "sha256": "95f5c4dee270345d9c1f7d7fb54885ef140ba1534916865e111ea5636e84b7a3" }, "pipfile-spec": 6, "requires": { @@ -95,11 +95,11 @@ }, "markdown-it-py": { "hashes": [ - "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", - "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb" + "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", + "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3" ], - "markers": "python_version >= '3.8'", - "version": "==3.0.0" + "markers": "python_version >= '3.10'", + "version": "==4.0.0" }, "mdurl": { "hashes": [ @@ -285,6 +285,23 @@ "markers": "python_version >= '3.8'", "version": "==3.0.0" }, + "behave": { + "hashes": [ + "sha256:657ee8c167af716e6ab7b817100bb427a2674440d431751af47af9ffd8a90b57", + "sha256:b32ec1a1ed67f23adc007c1cb7ee31ac1c939638d30c8e3e27a00d9ddb063c09" + ], + "index": "pypi", + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'", + "version": "==1.3.0" + }, + "colorama": { + "hashes": [ + "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", + "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5, 3.6'", + "version": "==0.4.6" + }, "coverage": { "extras": [ "toml" @@ -382,6 +399,22 @@ "markers": "python_version >= '3.9'", "version": "==7.10.3" }, + "cucumber-expressions": { + "hashes": [ + "sha256:86230d503cdda7ef35a1f2072a882d7d57c740aa4c163c82b07f039b6bc60c42", + "sha256:86ce41bf28ee520408416f38022e5a083d815edf04a0bd1dae46d474ca597c60" + ], + "markers": "python_version >= '3.8' and python_version < '4.0'", + "version": "==18.0.1" + }, + "cucumber-tag-expressions": { + "hashes": [ + "sha256:b60aa2cdbf9ac43e28d9b0e4fd49edf9f09d5d941257d2912f5228f9d166c023", + "sha256:f94404b656831c56a3815da5305ac097003884d2ae64fa51f5f4fad82d97e583" + ], + "markers": "python_version >= '2.7'", + "version": "==6.2.0" + }, "decorator": { "hashes": [ "sha256:65f266143752f734b0a7cc83c46f4618af75b8c5911b00ccb61d0ac9b6da0360", @@ -517,6 +550,21 @@ "markers": "python_version >= '3.8'", "version": "==25.0" }, + "parse": { + "hashes": [ + "sha256:967095588cb802add9177d0c0b6133b5ba33b1ea9007ca800e526f42a85af558", + "sha256:b41d604d16503c79d81af5165155c0b20f6c8d6c559efa66b4b695c3e5a0a0ce" + ], + "version": "==1.20.2" + }, + "parse-type": { + "hashes": [ + "sha256:5e1ec10440b000c3f818006033372939e693a9ec0176f446d9303e4db88489a6", + "sha256:83d41144a82d6b8541127bf212dd76c7f01baff680b498ce8a4d052a7a5bce4c" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1'", + "version": "==0.6.4" + }, "parso": { "hashes": [ "sha256:a418670a20291dacd2dddc80c377c5c3791378ee1e8d12bffc35420643d43f18", @@ -648,6 +696,14 @@ "markers": "python_version >= '3.9'", "version": "==80.9.0" }, + "six": { + "hashes": [ + "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", + "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'", + "version": "==1.17.0" + }, "stack-data": { "hashes": [ "sha256:836a778de4fec4dcd1dcd89ed8abff8a221f58308462e1c4aa2a3cf30148f0b9", diff --git a/README.md b/README.md index 7b70398..4d9bccf 100644 --- a/README.md +++ b/README.md @@ -44,7 +44,8 @@ Linting, type checking and tests: ```bash pipenv run flake8 . pipenv run mypy . -pipenv run pytest --cov=postalign --cov-report=term-missing +pipenv run pytest tests/unit --cov=postalign --cov-report=term-missing +pipenv run behave tests/component # downloads minimap2 2.17 automatically ``` The project currently relies on a minimal `setup.py` for building Cython diff --git a/requirements.txt b/requirements.txt index b5262de..4534c4a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,6 +3,6 @@ cython==3.1.2; python_version >= '3.8' more-itertools==10.7.0; python_version >= '3.9' orjson==3.11.1; python_version >= '3.9' pafpy==0.2.0; python_version >= '3.6' and python_version < '4.0' +rich==14.1.0; python_full_version >= '3.8.0' +typer==0.16.0; python_version >= '3.7' types-setuptools==80.9.0.20250809; python_version >= '3.9' -rich==14.1.0; python_version >= '3.9' -typer==0.16.0; python_version >= '3.9' diff --git a/tests/component/data/hiv1_reference.fasta b/tests/component/data/hiv1_reference.fasta new file mode 100644 index 0000000..3dcdd2b --- /dev/null +++ b/tests/component/data/hiv1_reference.fasta @@ -0,0 +1,163 @@ +>HXB2_x_ConsensusB +tggaagggctaattcactcccaacgaagacaagatatccttgatctgtggatctaccaca +cacaaggctacttccctgattGgcagaactacacaccagggccagggatcagatatccac +tgacctttggatggtgctacaagctagtaccagttgagccagagaagttagaagaagcca +acaaaggagagaacaccagcttgttacaccctgtgagcctgcatggaatggatgacccgg +agagagaagtgttagagtggaggtttgacagccgcctagcatttcatcacatggcccgag +agctgcatccggagtacttcaagaactgctgacatcgagcttgctacaagggactttccg +ctggggactttccagggaggcgtggcctgggcgggactggggagtggcgagccctcagat +cctgcatataagcagctgctttttgcctgtactgggtctctctggttagaccagatctga +gcctgggagctctctggctaactagggaacccactgcttaagcctcaataaagcttgcct +tgagtgcttcaagtagtgtgtgcccgtctgttgtgtgactctggtaactagagatccctc +agacccttttagtcagtgtggaaaatctctagcagtggcgcccgaacagggacctgaaag +cgaaagggaaaccagaggagctctctcgacgcaggactcggcttgctgaagcgcgcacgg +caagaggcgaggggcggcgactggtgagtacgccaaaaattttgactagcggaggctaga +aggagagagATGGGTGCGAGAGCGTCAGTATTAAGCGGGGGAGAATTAGATAGATGGGAA +AAAATTCGGTTAAGGCCAGGGGGAAAGAAAAAATATAAATTAAAACATATAGTATGGGCA +AGCAGGGAGCTAGAACGATTCGCAGTTAATCCTGGCCTGTTAGAAACATCAGAAGGCTGT +AGACAAATACTGGGACAGCTACAACCATCCCTTCAGACAGGATCAGAAGAACTTAGATCA +TTATATAATACAGTAGCAACCCTCTATTGTGTGCATCAAAGGATAGAGGTAAAAGACACC +AAGGAAGCTTTAGAGAAGATAGAGGAAGAGCAAAACAAAAGTAAGAAAAAAGCACAGCAA +GCAGCAGCTGACACAGGAAACAGCAGCCAGGTCAGCCAAAATTACCCTATAGTGCAGAAC +CTCCAGGGGCAAATGGTACATCAGGCCATATCACCTAGAACTTTAAATGCATGGGTAAAA +GTAGTAGAAGAGAAGGCTTTCAGCCCAGAAGTAATACCCATGTTTTCAGCATTATCAGAA +GGAGCCACCCCACAAGATTTAAACACCATGCTAAACACAGTGGGGGGACATCAAGCAGCC +ATGCAAATGTTAAAAGAGACCATCAATGAGGAAGCTGCAGAATGGGATAGATTGCATCCA +GTGCATGCAGGGCCTATTGCACCAGGCCAGATGAGAGAACCAAGGGGAAGTGACATAGCA +GGAACTACTAGTACCCTTCAGGAACAAATAGGATGGATGACAAATAATCCACCTATCCCA +GTAGGAGAAATCTATAAAAGATGGATAATCCTGGGATTAAATAAAATAGTAAGAATGTAT +AGCCCTACCAGCATTCTGGACATAAGACAAGGACCAAAGGAACCCTTTAGAGACTATGTA +GACCGGTTCTATAAAACTCTAAGAGCCGAGCAAGCTTCACAGGAGGTAAAAAATTGGATG +ACAGAAACCTTGTTGGTCCAAAATGCGAACCCAGATTGTAAGACTATTTTAAAAGCATTG +GGACCAGCAGCTACACTAGAAGAAATGATGACAGCATGTCAGGGAGTGGGAGGACCCGGC +CATAAAGCAAGAGTTTTGGCTGAAGCAATGAGCCAAGTAACAAATTCAGCTACCATAATG +ATGCAGAGAGGCAATTTTAGGAACCAAAGAAAGACTGTTAAGTGTTTCAATTGTGGCAAA +GAAGGGCACATAGCCAAAAATTGCAGGGCCCCTAGGAAAAAGGGCTGTTGGAAATGTGGA +AAGGAAGGACACCAAATGAAAGATTGTACTGAGAGACAGGCTAATTTTTTAGGGAAGATC +TGGCCTTCCCACAAGGGAAGGCCAGGGAATTTTCTTCAGAGCAGACCAGAGCCAACAGCC +CCACCAGAAGAGAGCTTCAGGTTTGGGGAAGAGACAACAACTCCCTCTCAGAAGCAGGAG +CCGATAGACAAGGAACTGTATCCTTTAGCTTCCCTCAGATCACTCTTTGGCAACGACCCC +TCGTCACAATAAAGATAGGGGGGCAACTAAAGGAAGCTCTATTAGATACAGGAGCAGATG +ATACAGTATTAGAAGAAATGAATTTGCCAGGAAGATGGAAACCAAAAATGATAGGGGGAA +TTGGAGGTTTTATCAAAGTAAGACAGTATGATCAGATACTCATAGAAATCTGTGGACATA +AAGCTATAGGTACAGTATTAGTAGGACCTACACCTGTCAACATAATTGGAAGAAATCTGT +TGACTCAGATTGGTTGCACTTTAAATTTTCCCATTAGTCCTATTGAAACTGTACCAGTAA +AATTAAAGCCAGGAATGGATGGCCCAAAAGTTAAACAATGGCCATTGACAGAAGAAAAAA +TAAAAGCATTAGTAGAAATTTGTACAGAAATGGAAAAGGAAGGGAAAATTTCAAAAATTG +GGCCTGAAAATCCATACAATACTCCAGTATTTGCCATAAAGAAAAAAGACAGTACTAAAT +GGAGAAAATTAGTAGATTTCAGAGAACTTAATAAGAGAACTCAAGACTTCTGGGAAGTTC +AATTAGGAATACCACATCCCGCAGGGTTAAAAAAGAAAAAATCAGTAACAGTACTGGATG +TGGGTGATGCATATTTTTCAGTTCCCTTAGATAAAGACTTCAGGAAGTATACTGCATTTA +CCATACCTAGTATAAACAATGAGACACCAGGGATTAGATATCAGTACAATGTGCTTCCAC +AGGGATGGAAAGGATCACCAGCAATATTCCAAAGTAGCATGACAAAAATCTTAGAGCCTT +TTAGAAAACAAAATCCAGACATAGTTATCTATCAATACATGGATGATTTGTATGTAGGAT +CTGACTTAGAAATAGGGCAGCATAGAACAAAAATAGAGGAACTGAGACAACATCTGTTGA +GGTGGGGATTTACCACACCAGACAAAAAACATCAGAAAGAACCTCCATTCCTTTGGATGG +GTTATGAACTCCATCCTGATAAATGGACAGTACAGCCTATAGTGCTGCCAGAAAAAGACA +GCTGGACTGTCAATGACATACAGAAGTTAGTGGGAAAATTGAATTGGGCAAGTCAGATTT +ATGCAGGGATTAAAGTAAAGCAATTATGTAAACTCCTTAGGGGAACCAAAGCACTAACAG +AAGTAATACCACTAACAGAAGAAGCAGAGCTAGAACTGGCAGAAAACAGGGAGATTCTAA +AAGAACCAGTACATGGAGTGTATTATGACCCATCAAAAGACTTAATAGCAGAAATACAGA +AGCAGGGGCAAGGCCAATGGACATATCAAATTTATCAAGAGCCATTTAAAAATCTGAAAA +CAGGAAAGTATGCAAGAATGAGGGGTGCCCACACTAATGATGTAAAACAATTAACAGAGG +CAGTGCAAAAAATAGCCACAGAAAGCATAGTAATATGGGGAAAGACTCCTAAATTTAAAC +TACCCATACAAAAAGAAACATGGGAAGCATGGTGGACAGAGTATTGGCAAGCCACCTGGA +TTCCTGAGTGGGAGTTTGTCAATACCCCTCCCTTAGTGAAATTATGGTACCAGTTAGAGA +AAGAACCCATAGTAGGAGCAGAAACTTTCTATGTAGATGGGGCAGCTAATAGGGAGACTA +AATTAGGAAAAGCAGGATATGTTACTGACAGAGGAAGACAAAAAGTTGTCTCCCTAACTG +ACACAACAAATCAGAAGACTGAGTTACAAGCAATTCATCTAGCTTTGCAGGATTCGGGAT +TAGAAGTAAACATAGTAACAGACTCACAATATGCATTAGGAATCATTCAAGCACAACCAG +ATAAAAGTGAATCAGAGTTAGTCAGTCAAATAATAGAGCAGTTAATAAAAAAGGAAAAGG +TCTACCTGGCATGGGTACCAGCACACAAAGGAATTGGAGGAAATGAACAAGTAGATAAAT +TAGTCAGTGCTGGAATCAGGAAAGTACTATTTTTAGATGGAATAGATAAGGCCCAAGAAG +AACATGAGAAATATCACAGTAATTGGAGAGCAATGGCTAGTGATTTTAACCTGCCACCTG +TAGTAGCAAAAGAAATAGTAGCCAGCTGTGATAAATGTCAGCTAAAAGGAGAAGCCATGC +ATGGACAAGTAGACTGTAGTCCAGGAATATGGCAACTAGATTGTACACATTTAGAAGGAA +AAATTATCCTGGTAGCAGTTCATGTAGCCAGTGGATATATAGAAGCAGAAGTTATTCCAG +CAGAGACAGGGCAGGAAACAGCATACTTTCTCTTAAAATTAGCAGGAAGATGGCCAGTAA +AAACAATACATACAGACAATGGCAGCAATTTCACCAGTACTACGGTTAAGGCCGCCTGTT +GGTGGGCAGGGATCAAGCAGGAATTTGGCATTCCCTACAATCCCCAAAGTCAAGGAGTAG +TAGAATCTATGAATAAAGAATTAAAGAAAATTATAGGACAGGTAAGAGATCAGGCTGAAC +ATCTTAAGACAGCAGTACAAATGGCAGTATTCATCCACAATTTTAAAAGAAAAGGGGGGA +TTGGGGGGTACAGTGCAGGGGAAAGAATAGTAGACATAATAGCAACAGACATACAAACTA +AAGAATTACAAAAACAAATTACAAAAATTCAAAATTTTCGGGTTTATTACAGGGACAGCA +GAGATCCACTTTGGAAAGGACCAGCAAAGCTTCTCTGGAAAGGTGAAGGGGCAGTAGTAA +TACAAGATAATAGTGACATAAAAGTAGTGCCAAGAAGAAAAGCAAAGATCATTAGGGATT +ATGGAAAACAGATGGCAGGTGATGATTGTGTGGCAAGTAGACAGGATGAGGATTAGaaca +tggaaaagtttagtaaaacaccatatgtatgtttcagggaaagctaggggatggttttat +agacatcactatgaaagccctcatccaagaataagttcagaagtacacatcccactaggg +gatgctagattggtaataacaacatattggggtctgcatacaggagaaagagactggcat +ttgggtcagggagtctccatagaatggaggaaaaagagatatagcacacaagtagaccct +gaactagcagaccaactaattcatctgtattactttgactgtttttcagactctgctata +agaaaggccttattaggacacatagttagccctaggtgtgaatatcaagcaggacataac +aaggtaggatctctacaatacttggcactagcagcattaataacaccaaaaaagataaag +ccacctttgcctagtgttacgaaactgacagaggatagatggaacaagccccagaagacc +aagggccacagagggagccacacaatgaatggacactagagcttttagaggagcttaaga +atgaagctgttagacattttcctaggatttggctccatggcttagggcaacatatctatg +aaacttatggggatacttgggcaggagtggaagccataataagaattctgcaacaactgc +tgtttatccattttcagaattgggtgtcgacatagcagaataggcgttactcgacagagg +agagcaagaaatggagccagtagatcctagactagagccctggaagcatccaggaagtca +gcctaaaactgcttgtaccaattgctattgtaaaaagtgttgctttcattgccaagtttg +tttcataacaaaagccttaggcatctcctatggcaggaagaagcggagacagcgacgaag +agctcatcagaacagtcagactcatcaagcttctctatcaaagcagtaagtagtacatgt +aacgcaacctataccaatagtagcaatagtagcattagtagtagcaataataatagcaat +agttgtgtggtccatagtaatcatagaatataggaaaatattaagacaaagaaaaataga +caggttaattgatagactaatagaaagagcagaagacagtggcaatgagagtgaaggaga +aatatcagcacttgtggagatgggggtggagatggggcaccatgctccttgggatgttga +tgatctgtagtgctacagaaaaattgtgggtcacagtctattatggggtacctgtgtgga +aggaagcaaccaccactctattttgtgcatcagatgctaaagcatatgatacagaggtac +ataatgtttgggccacacatgcctgtgtacccacagaccccaacccacaagaagtagtat +tggtaaatgtgacagaaaattttaacatgtggaaaaatgacatggtagaacagatgcatg +aggatataatcagtttatgggatcaaagcctaaagccatgtgtaaaattaaccccactct +gtgttagtttaaagtgcactgatttgaagaatgatactaataccaatagtagtagcggga +gaatgataatggagaaaggagagataaaaaactgctctttcaatatcagcacaagcataa +gaggtaaggtgcagaaagaatatgcatttttttataaacttgatataataccaatagata +atgatactaccagctataagttgacaagttgtaacacctcagtcattacacaggcctgtc +caaaggtatcctttgagccaattcccatacattattgtgccccggctggttttgcgattc +taaaatgtaataataagacgttcaatggaacaggaccatgtacaaatgtcagcacagtac +aatgtacacatggaattaggccagtagtatcaactcaactgctgttaaatggcagtctag +cagaagaagaggtagtaattagatctgtcaatttcacggacaatgctaaaaccataatag +tacagctgaacacatctgtagaaattaattgtacaagacccaacaacaatacaagaaaaa +gaatccgtatccagagaggaccagggagagcatttgttacaataggaaaaataggaaata +tgagacaagcacattgtaacattagtagagcaaaatggaataacactttaaaacagatag +ctagcaaattaagagaacaatttggaaataataaaacaataatctttaagcaatcctcag +gaggggacccagaaattgtaacgcacagttttaattgtggaggggaatttttctactgta +attcaacacaactgtttaatagtacttggtttaatagtacttggagtactgaagggtcaa +ataacactgaaggaagtgacacaatcaccctcccatgcagaataaaacaaattataaaca +tgtggcagaaagtaggaaaagcaatgtatgcccctcccatcagtggacaaattagatgtt +catcaaatattacagggctgctattaacaagagatggtggtaatagcaacaatgagtccg +agatcttcagacctggaggaggagatatgagggacaattggagaagtgaattatataaat +ataaagtagtaaaaattgaaccattaggagtagcacccaccaaggcaaagagaagagtgg +tgcagagagaaaaaagagcagtgggaataggagctttgttccttgggttcttgggagcag +caggaagcactatgggcgcagcctcaatgacgctgacggtacaggccagacaattattgt +ctggtatagtgcagcagcagaacaatttgctgagggctattgaggcgcaacagcatctgt +tgcaactcacagtctggggcatcaagcagctccaggcaagaatcctggctgtggaaagat +acctaaaggatcaacagctcctggggatttggggttgctctggaaaactcatttgcacca +ctgctgtgccttggaatgctagttggagtaataaatctctggaacagatttggaatcaca +cgacctggatggagtgggacagagaaattaacaattacacaagcttaatacactccttaa +ttgaagaatcgcaaaaccagcaagaaaagaatgaacaagaattattggaattagataaat +gggcaagtttgtggaattggtttaacataacaaattggctgtggtatataaaattattca +taatgatagtaggaggcttggtaggtttaagaatagtttttgctgtactttctatagtga +atagagttaggcagggatattcaccattatcgtttcagacccacctcccaaccccgaggg +gacccgacaggcccgaaggaatCgaagaagaaggtggagagagagacagagacagatcca +ttcgattagtgaacggatccttggcacttatctgggacgatctgcggagcctgtgcctct +tcagctaccaccgcttgagagacttactcttgattgtaacgaggattgtggaacttctgg +gacgcagggggtgggaagccctcaaatattggtggaatctcctacagtattggagtcagg +aactaaagaatagtgctgttagcttgctcaatgccacagccatagcagtagctgagggga +cagatagggttatagaagtagtacaaggagcttgtagagctattcgccacatacctagaa +gaataagacagggcttggaaaggattttgctataagatgggtggcaagtggtcaaaaagt +agtgtgattggatggcctactgtaagggaaagaatgagacgagctgagccagcagcagat +agggtgggagcagcatctcgagacctggaaaaacatggagcaatcacaagtagcaataca +gcagctaccaatgctgcttgtgcctggctagaagcacaagaggaggaggaggtgggtttt +ccagtcacacctcaggtacctttaagaccaatgacttacaaggcagctgtagatcttagc +cactttttaaaagaaaaggggggactggaagggctaattcactcccaaagaagacaagat +atccttgatctgtggatctaccacacacaaggctacttccctgattGgcagaactacaca +ccagggccaggggtcagatatccactgacctttggatggtgctacaagctagtaccagtt +gagccagataagatagaagaggccaataaaggagagaacaccagcttgttacaccctgtg +agcctgcatgggatggatgacccggagagagaagtgttagagtggaggtttgacagccgc +ctagcatttcatcacgtggcccgagagctgcatccggagtacttcaagaactgctgacat +cgagcttgctacaagggactttccgctggggactttccagggaggcgtggcctgggcggg +actggggagtggcgagccctcagatcctgcatataagcagctgctttttgcctgtactgg +gtctctctggttagaccagatctgagcctgggagctctctggctaactagggaacccact +gcttaagcctcaataaagcttgccttgagtgcttcaagtagtgtgtgcccgtctgttgtg +tgactctggtaactagagatccctcagacccttttagtcagtgtggaaaatctctagca diff --git a/tests/component/data/samplesmall.fas b/tests/component/data/samplesmall.fas new file mode 100644 index 0000000..2180ec1 --- /dev/null +++ b/tests/component/data/samplesmall.fas @@ -0,0 +1,81 @@ +>DQ995848 +SAGGATTGGGGACCCTGCGCTGAACATGGAGAACATCACATCAGGATTCCTAGGACCCCTGCTCGTGTTA +CAGGCGGGGTTTTTCTTGTTGACAAGAATCCTCACAATACCGCAGAGTCTAGACTCGTGGTGGACTTCTC +TCAATTTTCTAGGGGGAACCACCGTGTGTCTTGGCCAAAATTCGCAGTCCCCAACCTCCAATCACTCACC +AACCTCCTGTCCTCCAACTTGTCCTGGTTATCGCTGGATGTGTCTGCGGCGTTTTATCATCTTCCTCTTC +ATCCTGCTGCTATGCCTCATCTTCTTGTTGGTTCTTCTGGACTATCAGGGTATGTTGCCCGTCTGTCCTC +TGATTCCAGGATCTTCAACCACCAGCGCGGGACCATGCAGAACCTGCACGACTACTGCTCAAGGAACCTC +TATGTATCCCTCCTGTTGCTGTACCAAACCTTCGGACGGAAATTGCACCTGTATTCCCATCCCATCATCC +TGGGCTTTCGGAAAATTCCTATGGGAGTGGGCCTCAGCCCGTTTCTCATGGCTCAGTTTTCTAGTGCCAT +TTGTTCAGTGGTTCGTAGGGCTTTCCCCCACTGTTTGGCTTTCAGTTATGTGGATGATGTGGTATTGGGG +GCCAAGTCTGTACAGCACCTTGAGTCCCTTTTTACCGCTGTTACCAATTTTCTTTTGTCTTTGGGTATAC +ATTTAAACCCTAACAAAACTAAAAGATGGGGTTACTCTTTAAATTTCATGGGCTATGTCATTGGATGTTA +TGGGTCATTGCCACAAGATCACATCATACAGAAAATCAAAGAATGTTTTAGRAAACTTCCTGTTAACT + +>FJ518811 +GAAGACTGGGGACCCTGTACCGAACATGGAGAACATCGCATCAGGACTCCTAAGACCCCTGCTCGTGTTAC +AGGCGGGGTTTTTCTTGTTGACAAAAATCCTCACAATACCACAGAGTCTAGACTCGTGGTGGACTTCTCTC +AATTTTCTAGGGGGAACACCCGTGTGTCTTGGCCAAAATTCGCAGTCCCAAATCTCCAGTCACTCACCAAC +CTGCTGTCCTCCAATTTGTCCTGGTTATCGCTGGATGTGTCTGCGGCGTTTTATCATCTTCCTCTGCATCC +TGCTGCTATGCCTCATCTTCTTGTTGGTTCTTCTGGACTATCAAGGTATGTTGCCCGTTTGTCCTCTAATT +CCAGGATCATCAACAACCAGCACCGGACCATGCAAGGCCTGCACGACTCCTGCTCAAGGAACCTCTATGTT +TCCCTCATGTTGCTGTACAAAACCTACGGACGGAAACTGCACCTGTATTCCCATCCCATCATCTTCGGCTT +TCGCAAAATACCTATGGGAGTGGGCCTCAGTCCGTTTCTCTTGACTCAGTTTACTAGTGCCATTTGTTCAG +TGGTTCGTAGGGCTTTCCCCCACTGTCTGGCTTTCAGTTATATGGATGATGTGGTATTGGGGGCCAAGTCT +GTACAACATCTTGAGTCCCTTTATGCCGCTGTTACCAATTTTCTTTTGTCTTTGGGTATACATTTAACCCC +TCACAAAACAAAAAGATGGGGATATTCCCTTAACTTTATGGGATATGTAATTGGGAGTTGGGGCACATTGC +CACAGGAACATATTGTACAAAAAATCAAAATGTGTTTTAGGAAACTTCCTGTAAACAGGCCTATTGATTGG +AAAGTATGTCAACGAATTGTGGGTCTTTTGGGGTTTGCCGCCCCTTTCACACAATGTGGATATCCTGCTTT +AATGCCTTTATATGCATGTATACAAGCAAAACAGGCTTTTACTTTCTCGCCAACTTACAAGGCCTTTCTCA +GTAAACAGTATCTGAACCTTTACCCCGTTGCTCGGCAA + +>AY373430 +GAGGATTGGGGACCCTGCGCTGAACATGGAGAACATCACATCAGGATTCCTAGGACCCCTTCTAGTGTTAC +AGGCGGGGTTTTTCTTGTTGACAAGAATCCTCACAATACCGCAGAGTCTAGACTCGTGGTGGACTTCTCTC +AATTTTCTAGGGGGAACTACCGTGTGTCTTGGCCAAAATTCGCAGTCCCCAACCTCCAATCACTCACCAAC +CTCCTGTCCTCCAACTTGTCCTGGTTATCGCTGGATGTGTCTGCGGCGTTTTATCATCTTCCTCTTCATCC +TGCTGCTATGCCTCATCTTCTTGTTGGTTCTTCTGGACTATCAAGGTATGTTGCCCGTTTGTCCTCTAATT +CCAGGATCCTCAACCACCAGCACGGGACCATGCAGAACCTGCACGACTCCTGCTCAAGGAACCTCTATGTA +TCCCTCCTGTTGCTGTACCAAACCTTCGGACGGAAATTGCACCTGTATTCCCATCCCATCATCCTGGGCTT +TCGGAAAATTCCTATGGGAGTGGGCCTCAGCCCGTTTCTCCTGGCTCAGTTTACTAGTGCCATTTGTTCAG +TGGTTCGTAGGGCTTTCCCCCACTGTTTGGCTTTCAGTTATATGGATGATGTGGAATTGGGGGCCAAGTCT +GTACAGCAACTTGAGTCCCTTTTTACCGCTGTTACCAATTTTCTTTTGTCTTTGGGTATACATTTAAACCC +TAACAAAACAAAAAGATGGGGTTACTCTCTACATTTCATGGGCTATGTCATTGGATGTTATGGGTCCTTGC +CACAAGAACACATCATACAAAAAATCAAAGAATGTTTTAGAAAACTTCCTATTAACAGGCCTATTGATTGG +AAAGTATGTCAAAGAATTGTGGGTCTTTTGGGTTTTGCTGCCCCTTTTACACAATGTGGTTATCCTGCTTT +AATGCCCTTGTATGCATGTATTCAATCTAAGCAGGCTTTCACTTTCTCGCCAACTTACAAGGCCTTTCTGT +GTAAACAATACCTGAACCTTTACCCCGTTGCCCGGCAA + +>GU456684 +GAGGATTGGGGACCCTGCGCTGAACATGGAGAACATCACATCAGGACTCCTAGGACCCCTGCTCGTATTAC +AGGCGGGGTTTTTCTTGTTGACAAGAATCCTCACAATACCGCAGAGTCTAGACTCGTGGTGGACTTCTCTC +AATTTTCTAGGGGGAACTACCGTGTGTCTTGGCCAAAATTCGCAGTCCCCAACCTCCAATCACTCACCAAC +CTCCTGTCCTCCAACTTGTCCTGGTTATCGCTGGATGTGTCTGCGGCGTTTTATCATCTTCCTCTTCATCC +TGCTGCTATGCCTCATCTTCTTGTTGGTTCTTCTGGACTATCAAGGTATGTTGCCCGTTTGTCCTCTAATT +CCAGGATCGTCAACCACAAGCACGGGACCATGCAGAACCTGCACGACTCCTGCTCAAGGAACCTCTATGTA +TCCCTCCTGTTGCTGTACCAAACCTTCGGACGGAAATTGCACCTGTATTCCCATCCCATCATCCTGGGCTT +TCGGAAAATTCCTATGGGAGTGGGCCTCAGCCCGTTTCTCCTGGCTCAGTTTACTAGTGCCATTTGTTCAG +TGGTTCGTAGGGCTTTCCCCCACTGTTTGGCTTTCAGTTATATGGATGATGTGGTATTGGGGGCCAACTCT +GTACAACATCTTGAAGCCCTTTTTACCGCTGTTACCAATTTTCTTTTGTCTCTGGGCATACATTTAAACCC +TAACAAAACGAAAAGATGGGGTTACTCTTTACATTTTATGGGCTATGTCATTGGATGTCATGGGTCATTGC +CACAAGATCACATCATACAGAAAATCAAAGAATGTTTTAGAAAACTTCCTGTTAACAGGCCTATAGATTGG +AAAGTCTGTCAACGTATTGTGGGTCTTTTGGGTTTTGCTGCCCCTTTTACACAATGTGGGTATCCTGCTTT +AAAGCCCTTATATGCATGTATTCAATCTAAGCAGGCTTTCACTTTCTCGCCAACTTACAAGACCTTTCTGT +GTAAACAATACCTGAACCTTTACCCCGTTGCCCGGCAA + +>FJ622881 +GAGGACTGGGGACCCTGCACCGAACATGGAGAACACAACATCAGGATTCCTAGGACCCCTGCTCGTGTTA +CAGGCGGGGTTTTTCTTGTTGACAAGAATCCTCACAATACCACAGAGTCTAGACTCGTGGTGGACTTCTC +TCAATTTTCTAGGGGGAGCACCCACGTGTCCTGGCCAAAATTCGCAGTCCCCAACCTCCAATCACTCACC +AACCTCTTGTCCTCCAATTTGTCCTGGCTATCGCTGGATGTGTCTGCGGCGTTTTATCATATTCCTCTTC +ATCCTGCTGCTATGCCTCATCTTCTTGTTGGTTCTTCTGGACTACCAAGGTATGTTGCCCGTTTGTCCTC +TACTTCCAGGAACATCAACTACCAGCACGGGACCATGCAAGACCTGCACAATTCCTGCTCAAGGAACCTC +TATGTTTCCCTCTTGTTGCTGTACAAAACCTTCGGACGGAAACTGCACTTGTATTCCCATCCCATCATCC +TGGGCTTTCGCAAGATTCCTATGGGAGTGGGCCTCAGTCCGTTTCTCCTGGCTCAGTTTACTAGTGCCAT +TTGTTCAGTGGTTCGCAGGGCTTTCCCCCACTGTTTGGCTTTCAGTTATATGGATGATGTGGTATTGGGG +GCCAAGTCTGTACAACATCTTGAGTCCCTTTTTACCTCTATTACCAATTTTCTTTTGTCTTTGGGTATAC +ATTTGAACCCTAGTAAAACCAAACGTTGGGGCTACTCCCTTAACTTCATGGGATATGTAATTGGAAGTTG +GGGTACTTTACCGCAGGAACATATTGTACTAAAAATCAAGCAATGTTTTCGAAAACTGCCTGTAAATAGA +CCTATTGATTGGAAAGTATGTCAAAGAATTGTGGGTCTTTTAGGCTTTGCTGCCCCTTTTACCCAATGTG +GCTATCCTGCCTTAATGCCTTTATATGCATGTATACACTCTAAGCAGGCTTTCACTTTCTCGCCAACTTA +CAAGGCCTTTCTGTGTAAACAATATATGAACCTTTACCCCGTTGCCCGGCAA diff --git a/tests/component/features/codon_alignment.feature b/tests/component/features/codon_alignment.feature new file mode 100644 index 0000000..d62d6cf --- /dev/null +++ b/tests/component/features/codon_alignment.feature @@ -0,0 +1,6 @@ +Feature: Codon alignment on real sequences + Scenario: Align sample sequences to reference + Given sample sequences "tests/component/data/samplesmall.fas" and reference "tests/component/data/hiv1_reference.fasta" + And minimap2 from "https://github.com/lh3/minimap2/releases/download/v2.17/minimap2-2.17_x64-linux.tar.bz2" is available + When they are codon-aligned with options "-k 6 -w 3 --score-N 0 --secondary no" from 790 to 2085 + Then the number of aligned pairs is 5 diff --git a/tests/component/steps/codon_steps.py b/tests/component/steps/codon_steps.py new file mode 100644 index 0000000..fc4867e --- /dev/null +++ b/tests/component/steps/codon_steps.py @@ -0,0 +1,92 @@ +"""Behave steps for codon alignment component tests.""" + +from __future__ import annotations + +import os +import subprocess +import sys +import tarfile +import tempfile +import urllib.request +import ssl +from pathlib import Path +from typing import Any + +from behave import given, then, when # type: ignore[import-untyped] + + +@given('sample sequences "{seqs}" and reference "{ref}"') +def given_sequences(context: Any, seqs: str, ref: str) -> None: + """Store input sequence and reference paths. + + :param context: Behave scenario context. + :param seqs: Path to sample sequences file. + :param ref: Path to reference sequence file. + """ + context.seqs_path = Path(seqs) + context.ref_path = Path(ref) + + +@given('minimap2 from "{url}" is available') +def given_minimap2(context: Any, url: str) -> None: + """Download and prepare the minimap2 binary. + + :param context: Behave scenario context. + :param url: URL to the minimap2 tarball. + """ + tmpdir = Path(tempfile.mkdtemp()) + archive = tmpdir / 'minimap2.tar.bz2' + ssl_ctx = ssl.create_default_context() + ssl_ctx.check_hostname = False + ssl_ctx.verify_mode = ssl.CERT_NONE + with urllib.request.urlopen(url, context=ssl_ctx) as resp: + with open(archive, 'wb') as out: + out.write(resp.read()) + with tarfile.open(archive, 'r:bz2') as tar: + tar.extractall(tmpdir) + binary = next(tmpdir.glob('*/minimap2')) + binary.chmod(0o755) + context.minimap2 = str(binary) + context.tmpdir = tmpdir + + +@when('they are codon-aligned with options "{opts}" from {start:d} to {end:d}') +def when_codon_aligned(context: Any, opts: str, start: int, end: int) -> None: + """Run the codon-alignment pipeline via the CLI. + + :param context: Behave scenario context. + :param opts: Options to pass to minimap2. + :param start: Reference start position. + :param end: Reference end position. + """ + fd, path = tempfile.mkstemp(suffix='.fasta') + os.close(fd) + output = Path(path) + context.output_path = output + script = Path(__file__).with_name('run_codon_alignment.py') + cmd = [ + sys.executable, + str(script), + str(context.seqs_path), + str(context.ref_path), + context.minimap2, + str(output), + opts, + str(start), + str(end), + ] + env = os.environ.copy() + env['PATH'] = f"{Path(context.minimap2).parent}:{env['PATH']}" + subprocess.run(cmd, check=True, env=env) + + +@then('the number of aligned pairs is {count:d}') +def then_check_count(context: Any, count: int) -> None: + """Verify the expected number of sequence pairs were produced. + + :param context: Behave scenario context. + :param count: Expected number of alignment pairs. + """ + with open(context.output_path) as handle: + seqs = sum(1 for line in handle if line.startswith('>')) // 2 + assert seqs == count, f"Expected {count} pairs, got {seqs}" diff --git a/tests/component/steps/run_codon_alignment.py b/tests/component/steps/run_codon_alignment.py new file mode 100644 index 0000000..04574bd --- /dev/null +++ b/tests/component/steps/run_codon_alignment.py @@ -0,0 +1,73 @@ +"""Execute codon alignment pipeline for component tests.""" + +from __future__ import annotations + +import sys +from pathlib import Path +from typing import Any, Callable, List +import inspect + +import typer + +if not hasattr(typer.Typer, "result_callback"): + def result_callback( + self: Any, *args: Any, **kwargs: Any + ) -> Callable[[Callable[..., Any]], Callable[..., Any]]: + def decorator(func: Callable[..., Any]) -> Callable[..., Any]: + return func + + return decorator + + typer.Typer.result_callback = result_callback # type: ignore[attr-defined] + +if "multiple" not in inspect.signature(typer.Option).parameters: + _orig_option = typer.Option + + def Option(*args: Any, **kwargs: Any) -> Any: # type: ignore[override] + kwargs.pop("multiple", None) + return _orig_option(*args, **kwargs) + +typer.Option = Option # type: ignore[assignment] + +sys.path.insert(0, str(Path(__file__).resolve().parents[3])) +from postalign.parsers import minimap2 # noqa: E402 +from postalign.processors.codon_alignment import codon_alignment # noqa: E402 +from postalign.models.sequence import NAPosition # noqa: E402 +from postalign.models import Message # noqa: E402 + + +def main() -> None: + """Run codon alignment and write pairwise FASTA output. + + Expected arguments: + seqs_path, ref_path, minimap2_bin, output_path, opts, start, end. + """ + seqs_path = Path(sys.argv[1]) + ref_path = Path(sys.argv[2]) + minimap2_bin = sys.argv[3] + output_path = Path(sys.argv[4]) + opts = sys.argv[5] + start = int(sys.argv[6]) + end = int(sys.argv[7]) + + messages: List[Message] = [] + with open(seqs_path) as seqs, open(ref_path) as ref: + iterator = minimap2.load( + seqs, + ref, + NAPosition, + messages, + minimap2_execute=[minimap2_bin, *opts.split()], + ) + processor = codon_alignment( + min_gap_distance=15, ref_start=start, ref_end=end + ) + pairs = list(processor(iterator, messages)) + with open(output_path, "w") as out: + for refseq, seq in pairs: + out.write(f">{refseq.header}\n{refseq.seqtext_as_str}\n") + out.write(f">{seq.header}\n{seq.seqtext_as_str}\n") + + +if __name__ == "__main__": + main() diff --git a/tests/conftest.py b/tests/unit/conftest.py similarity index 99% rename from tests/conftest.py rename to tests/unit/conftest.py index c3eb58c..e90107c 100644 --- a/tests/conftest.py +++ b/tests/unit/conftest.py @@ -13,7 +13,7 @@ import pytest -PROJECT_ROOT = Path(__file__).resolve().parents[1] +PROJECT_ROOT = Path(__file__).resolve().parents[2] if str(PROJECT_ROOT) not in sys.path: sys.path.insert(0, str(PROJECT_ROOT)) diff --git a/tests/test_aa_position.py b/tests/unit/test_aa_position.py similarity index 100% rename from tests/test_aa_position.py rename to tests/unit/test_aa_position.py diff --git a/tests/test_blosum62.py b/tests/unit/test_blosum62.py similarity index 100% rename from tests/test_blosum62.py rename to tests/unit/test_blosum62.py diff --git a/tests/test_cigar.py b/tests/unit/test_cigar.py similarity index 100% rename from tests/test_cigar.py rename to tests/unit/test_cigar.py diff --git a/tests/test_cli.py b/tests/unit/test_cli.py similarity index 100% rename from tests/test_cli.py rename to tests/unit/test_cli.py diff --git a/tests/test_codon_alignment.py b/tests/unit/test_codon_alignment.py similarity index 100% rename from tests/test_codon_alignment.py rename to tests/unit/test_codon_alignment.py diff --git a/tests/test_codonutils.py b/tests/unit/test_codonutils.py similarity index 100% rename from tests/test_codonutils.py rename to tests/unit/test_codonutils.py diff --git a/tests/test_entry.py b/tests/unit/test_entry.py similarity index 100% rename from tests/test_entry.py rename to tests/unit/test_entry.py diff --git a/tests/test_group_by_codons.py b/tests/unit/test_group_by_codons.py similarity index 100% rename from tests/test_group_by_codons.py rename to tests/unit/test_group_by_codons.py diff --git a/tests/test_iupac.py b/tests/unit/test_iupac.py similarity index 100% rename from tests/test_iupac.py rename to tests/unit/test_iupac.py diff --git a/tests/test_message.py b/tests/unit/test_message.py similarity index 100% rename from tests/test_message.py rename to tests/unit/test_message.py diff --git a/tests/test_modifier.py b/tests/unit/test_modifier.py similarity index 100% rename from tests/test_modifier.py rename to tests/unit/test_modifier.py diff --git a/tests/test_na_position.py b/tests/unit/test_na_position.py similarity index 100% rename from tests/test_na_position.py rename to tests/unit/test_na_position.py diff --git a/tests/test_paf.py b/tests/unit/test_paf.py similarity index 100% rename from tests/test_paf.py rename to tests/unit/test_paf.py diff --git a/tests/test_parsers.py b/tests/unit/test_parsers.py similarity index 100% rename from tests/test_parsers.py rename to tests/unit/test_parsers.py diff --git a/tests/test_processor.py b/tests/unit/test_processor.py similarity index 100% rename from tests/test_processor.py rename to tests/unit/test_processor.py diff --git a/tests/test_sanitize_sequence.py b/tests/unit/test_sanitize_sequence.py similarity index 100% rename from tests/test_sanitize_sequence.py rename to tests/unit/test_sanitize_sequence.py diff --git a/tests/test_save_fasta.py b/tests/unit/test_save_fasta.py similarity index 100% rename from tests/test_save_fasta.py rename to tests/unit/test_save_fasta.py diff --git a/tests/test_save_json.py b/tests/unit/test_save_json.py similarity index 100% rename from tests/test_save_json.py rename to tests/unit/test_save_json.py diff --git a/tests/test_sequence.py b/tests/unit/test_sequence.py similarity index 100% rename from tests/test_sequence.py rename to tests/unit/test_sequence.py diff --git a/tests/test_trim_by_ref.py b/tests/unit/test_trim_by_ref.py similarity index 100% rename from tests/test_trim_by_ref.py rename to tests/unit/test_trim_by_ref.py diff --git a/tests/test_version.py b/tests/unit/test_version.py similarity index 100% rename from tests/test_version.py rename to tests/unit/test_version.py From 4bfbdaf8646335bd1f928c9bc285d261080ff156 Mon Sep 17 00:00:00 2001 From: Philip Tzou Date: Mon, 11 Aug 2025 12:00:59 -0700 Subject: [PATCH 2/5] Use CLI pipeline for codon alignment tests --- README.md | 2 +- ...iv1_reference.fasta => hiv1_reference.fas} | 0 .../features/codon_alignment.feature | 4 +- tests/component/steps/codon_steps.py | 83 ++++++++++++++----- tests/component/steps/run_codon_alignment.py | 73 ---------------- 5 files changed, 63 insertions(+), 99 deletions(-) rename tests/component/data/{hiv1_reference.fasta => hiv1_reference.fas} (100%) delete mode 100644 tests/component/steps/run_codon_alignment.py diff --git a/README.md b/README.md index 4d9bccf..2b334a8 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,7 @@ The typical workflow runs minimap2 to generate alignments and writes the post‑processed result as JSON: ```bash -pipenv run post-align -i reads.fasta -r ref.fasta -o result.json -f MINIMAP2 \ +pipenv run post-align -i reads.fas -r ref.fas -o result.json -f MINIMAP2 \ save-json ``` diff --git a/tests/component/data/hiv1_reference.fasta b/tests/component/data/hiv1_reference.fas similarity index 100% rename from tests/component/data/hiv1_reference.fasta rename to tests/component/data/hiv1_reference.fas diff --git a/tests/component/features/codon_alignment.feature b/tests/component/features/codon_alignment.feature index d62d6cf..5228d18 100644 --- a/tests/component/features/codon_alignment.feature +++ b/tests/component/features/codon_alignment.feature @@ -1,6 +1,6 @@ Feature: Codon alignment on real sequences Scenario: Align sample sequences to reference - Given sample sequences "tests/component/data/samplesmall.fas" and reference "tests/component/data/hiv1_reference.fasta" - And minimap2 from "https://github.com/lh3/minimap2/releases/download/v2.17/minimap2-2.17_x64-linux.tar.bz2" is available + Given sample sequences "tests/component/data/samplesmall.fas" and reference "tests/component/data/hiv1_reference.fas" + And minimap2 is available When they are codon-aligned with options "-k 6 -w 3 --score-N 0 --secondary no" from 790 to 2085 Then the number of aligned pairs is 5 diff --git a/tests/component/steps/codon_steps.py b/tests/component/steps/codon_steps.py index fc4867e..57794f9 100644 --- a/tests/component/steps/codon_steps.py +++ b/tests/component/steps/codon_steps.py @@ -3,17 +3,44 @@ from __future__ import annotations import os -import subprocess -import sys +import ssl import tarfile import tempfile import urllib.request -import ssl from pathlib import Path -from typing import Any +from typing import Any, Callable +import inspect +import typer from behave import given, then, when # type: ignore[import-untyped] +if not hasattr(typer.Typer, "result_callback"): + def result_callback( + self: Any, *args: Any, **kwargs: Any + ) -> Callable[[Callable[..., Any]], Callable[..., Any]]: + def decorator(func: Callable[..., Any]) -> Callable[..., Any]: + return func + return decorator + typer.Typer.result_callback = result_callback # type: ignore[attr-defined] + +if "multiple" not in inspect.signature(typer.Option).parameters: + _orig_option = typer.Option + + def Option(*args: Any, **kwargs: Any) -> Any: # type: ignore[override] + kwargs.pop("multiple", None) + return _orig_option(*args, **kwargs) + typer.Option = Option # type: ignore[assignment] + +from postalign.cli import AlignmentFormat, process_pipeline +from postalign.processors.codon_alignment import codon_alignment +from postalign.processors.save_fasta import save_fasta + + +MINIMAP2_URL = ( + "https://github.com/lh3/minimap2/releases/download/" + "v2.17/minimap2-2.17_x64-linux.tar.bz2" +) + @given('sample sequences "{seqs}" and reference "{ref}"') def given_sequences(context: Any, seqs: str, ref: str) -> None: @@ -27,19 +54,18 @@ def given_sequences(context: Any, seqs: str, ref: str) -> None: context.ref_path = Path(ref) -@given('minimap2 from "{url}" is available') -def given_minimap2(context: Any, url: str) -> None: +@given('minimap2 is available') +def given_minimap2(context: Any) -> None: """Download and prepare the minimap2 binary. :param context: Behave scenario context. - :param url: URL to the minimap2 tarball. """ tmpdir = Path(tempfile.mkdtemp()) archive = tmpdir / 'minimap2.tar.bz2' ssl_ctx = ssl.create_default_context() ssl_ctx.check_hostname = False ssl_ctx.verify_mode = ssl.CERT_NONE - with urllib.request.urlopen(url, context=ssl_ctx) as resp: + with urllib.request.urlopen(MINIMAP2_URL, context=ssl_ctx) as resp: with open(archive, 'wb') as out: out.write(resp.read()) with tarfile.open(archive, 'r:bz2') as tar: @@ -59,25 +85,36 @@ def when_codon_aligned(context: Any, opts: str, start: int, end: int) -> None: :param start: Reference start position. :param end: Reference end position. """ - fd, path = tempfile.mkstemp(suffix='.fasta') + fd, path = tempfile.mkstemp(suffix='.fas') os.close(fd) output = Path(path) context.output_path = output - script = Path(__file__).with_name('run_codon_alignment.py') - cmd = [ - sys.executable, - str(script), - str(context.seqs_path), - str(context.ref_path), - context.minimap2, - str(output), - opts, - str(start), - str(end), + processors = [ + codon_alignment(ref_start=start, ref_end=end), + save_fasta(pairwise=True, modifiers=False), ] - env = os.environ.copy() - env['PATH'] = f"{Path(context.minimap2).parent}:{env['PATH']}" - subprocess.run(cmd, check=True, env=env) + old_path = os.environ['PATH'] + os.environ['PATH'] = f"{Path(context.minimap2).parent}:{old_path}" + try: + with ( + open(context.seqs_path) as seqs, + open(context.ref_path) as ref, + open(output, 'w') as out, + ): + process_pipeline( + processors, + seqs, + None, + out, + AlignmentFormat.MINIMAP2, + ref, + True, + True, + False, + opts, + ) + finally: + os.environ['PATH'] = old_path @then('the number of aligned pairs is {count:d}') diff --git a/tests/component/steps/run_codon_alignment.py b/tests/component/steps/run_codon_alignment.py deleted file mode 100644 index 04574bd..0000000 --- a/tests/component/steps/run_codon_alignment.py +++ /dev/null @@ -1,73 +0,0 @@ -"""Execute codon alignment pipeline for component tests.""" - -from __future__ import annotations - -import sys -from pathlib import Path -from typing import Any, Callable, List -import inspect - -import typer - -if not hasattr(typer.Typer, "result_callback"): - def result_callback( - self: Any, *args: Any, **kwargs: Any - ) -> Callable[[Callable[..., Any]], Callable[..., Any]]: - def decorator(func: Callable[..., Any]) -> Callable[..., Any]: - return func - - return decorator - - typer.Typer.result_callback = result_callback # type: ignore[attr-defined] - -if "multiple" not in inspect.signature(typer.Option).parameters: - _orig_option = typer.Option - - def Option(*args: Any, **kwargs: Any) -> Any: # type: ignore[override] - kwargs.pop("multiple", None) - return _orig_option(*args, **kwargs) - -typer.Option = Option # type: ignore[assignment] - -sys.path.insert(0, str(Path(__file__).resolve().parents[3])) -from postalign.parsers import minimap2 # noqa: E402 -from postalign.processors.codon_alignment import codon_alignment # noqa: E402 -from postalign.models.sequence import NAPosition # noqa: E402 -from postalign.models import Message # noqa: E402 - - -def main() -> None: - """Run codon alignment and write pairwise FASTA output. - - Expected arguments: - seqs_path, ref_path, minimap2_bin, output_path, opts, start, end. - """ - seqs_path = Path(sys.argv[1]) - ref_path = Path(sys.argv[2]) - minimap2_bin = sys.argv[3] - output_path = Path(sys.argv[4]) - opts = sys.argv[5] - start = int(sys.argv[6]) - end = int(sys.argv[7]) - - messages: List[Message] = [] - with open(seqs_path) as seqs, open(ref_path) as ref: - iterator = minimap2.load( - seqs, - ref, - NAPosition, - messages, - minimap2_execute=[minimap2_bin, *opts.split()], - ) - processor = codon_alignment( - min_gap_distance=15, ref_start=start, ref_end=end - ) - pairs = list(processor(iterator, messages)) - with open(output_path, "w") as out: - for refseq, seq in pairs: - out.write(f">{refseq.header}\n{refseq.seqtext_as_str}\n") - out.write(f">{seq.header}\n{seq.seqtext_as_str}\n") - - -if __name__ == "__main__": - main() From ed968bddae9a3f51e855c18f4529d8c563682cb3 Mon Sep 17 00:00:00 2001 From: Philip Tzou Date: Mon, 11 Aug 2025 12:01:04 -0700 Subject: [PATCH 3/5] Support complex Typer option types --- postalign/processors/codon_alignment.py | 70 ++++++++++++++++--------- tests/component/steps/codon_steps.py | 11 +--- tests/unit/test_codon_alignment.py | 13 +++++ 3 files changed, 60 insertions(+), 34 deletions(-) diff --git a/postalign/processors/codon_alignment.py b/postalign/processors/codon_alignment.py index 44ff6e9..072eb6c 100644 --- a/postalign/processors/codon_alignment.py +++ b/postalign/processors/codon_alignment.py @@ -1,5 +1,6 @@ """Processor performing codon alignment.""" +import inspect import re import typer import cython # type: ignore @@ -614,29 +615,67 @@ def parse_gap_placement_score(value: str) -> dict[ def gap_placement_score_callback( ctx: typer.Context, param: typer.CallbackParam, - value: tuple[str, ...], + value: tuple[str, ...] | list[str] | str | None, ) -> dict[int, dict[tuple[int, int], int]]: """Parse ``--gap-placement-score`` arguments. - :param ctx: Typer context. + The option may be provided multiple times depending on Typer's + capabilities. When ``multiple`` is supported the values arrive as a + tuple or list; otherwise Typer supplies a single comma-delimited string. + + :param ctx: Typer context (unused). :param param: Callback parameter definition. - :param value: Tuple of score specifications. + :param value: Sequence or string of score specifications. :returns: Nested mapping of gap placement scores. - :raises typer.BadParameter: On invalid input value. + :raises typer.BadParameter: On invalid input value or metadata. """ if not param.name: raise typer.BadParameter( 'Internal error (gap_placement_score_callback:1)' ) + if value is None or value == "" or value == (): # no scores provided + return {} + if isinstance(value, (tuple, list)): + raw = ",".join(value) + else: + raw = value try: result: dict[ int, dict[tuple[int, int], int] - ] = parse_gap_placement_score(','.join(value)) + ] = parse_gap_placement_score(raw) return result - except ValueError as exp: + except ValueError as exp: # pragma: no cover - defensive raise typer.BadParameter(str(exp)) +_GAP_PLACEMENT_HELP = ( + 'Bonus (positive number) or penalty (negative number) for gaps ' + 'appear at certain NA position (relative to the WHOLE ref seq) ' + 'in the ref seq (ins) or target seq (del). For example, ' + '204ins:-5 is a -5 penalty designate to a gap with any size ' + 'gap in ref seq after NA position 204 (AA position 68). ' + '2041/12del:10 is a +10 score for a 12 NAs size ' + '(4 codons) gap in target seq at NA position 2041, ' + 'equivalent to deletion at 681, 682, 683 and 684 AA position. ' + 'Multiple scores can be delimited by commas, such as ' + '204ins:-5,2041/12del:10.' +) + +if 'multiple' in inspect.signature(typer.Option).parameters: + _GAP_PLACEMENT_OPTION = typer.Option( + (), '--gap-placement-score', + callback=gap_placement_score_callback, + multiple=True, + help=_GAP_PLACEMENT_HELP, + ) # type: ignore[call-overload] +else: # pragma: no cover - executed on Typer <0.15 + _GAP_PLACEMENT_OPTION = typer.Option( + None, '--gap-placement-score', + callback=gap_placement_score_callback, + help=_GAP_PLACEMENT_HELP, + ) + + @cli.command('codon-alignment') def codon_alignment( min_gap_distance: Annotated[ @@ -666,24 +705,7 @@ def codon_alignment( # Indel NAPos NASize Score # v v v v dict[int, dict[tuple[int, int], int]], - typer.Option( - (), '--gap-placement-score', - callback=gap_placement_score_callback, - multiple=True, - help=( - 'Bonus (positive number) or penalty (negative number) for gaps' - ' appear at certain NA position ' - '(relative to the WHOLE ref seq) ' - 'in the ref seq (ins) or target seq (del). For example, ' - '204ins:-5 is a -5 penalty designate to a gap with any size ' - 'gap in ref seq after NA position 204 (AA position 68). ' - '2041/12del:10 is a +10 score for a 12 NAs size' - ' (4 codons) gap in target seq at NA position 2041,' - ' equivalent to deletion at 681, 682, 683 and 684 AA position.' - ' Multiple scores can be delimited by commas, such as ' - '204ins:-5,2041/12del:10.' - ), - ), # type: ignore[call-overload] + _GAP_PLACEMENT_OPTION, ] = {}, ref_start: Annotated[int, typer.Argument()] = 1, ref_end: Annotated[int, typer.Argument()] = -1, diff --git a/tests/component/steps/codon_steps.py b/tests/component/steps/codon_steps.py index 57794f9..00dcccb 100644 --- a/tests/component/steps/codon_steps.py +++ b/tests/component/steps/codon_steps.py @@ -10,9 +10,8 @@ from pathlib import Path from typing import Any, Callable -import inspect import typer -from behave import given, then, when # type: ignore[import-untyped] +from behave import given, then, when # type: ignore[import-not-found,import-untyped] if not hasattr(typer.Typer, "result_callback"): def result_callback( @@ -23,14 +22,6 @@ def decorator(func: Callable[..., Any]) -> Callable[..., Any]: return decorator typer.Typer.result_callback = result_callback # type: ignore[attr-defined] -if "multiple" not in inspect.signature(typer.Option).parameters: - _orig_option = typer.Option - - def Option(*args: Any, **kwargs: Any) -> Any: # type: ignore[override] - kwargs.pop("multiple", None) - return _orig_option(*args, **kwargs) - typer.Option = Option # type: ignore[assignment] - from postalign.cli import AlignmentFormat, process_pipeline from postalign.processors.codon_alignment import codon_alignment from postalign.processors.save_fasta import save_fasta diff --git a/tests/unit/test_codon_alignment.py b/tests/unit/test_codon_alignment.py index 6d662b2..ae27bfa 100644 --- a/tests/unit/test_codon_alignment.py +++ b/tests/unit/test_codon_alignment.py @@ -436,3 +436,16 @@ def test_gap_placement_score_callback_invalid_value() -> None: param.name = "gps" with pytest.raises(typer.BadParameter): gap_placement_score_callback(ctx, param, ("204foo",)) + + +def test_gap_placement_score_callback_str_value() -> None: + """String values should be accepted when Typer lacks ``multiple``.""" + + ctx = MagicMock() + param = MagicMock() + param.name = "gap_placement_score" + result = gap_placement_score_callback( + ctx, param, "204ins:-5,2041/12del:10" + ) + assert result[REFGAP][(204, 0)] == -5 + assert result[SEQGAP][(2041, 12)] == 10 From aec7a5faa78631b0c5a0afa7badf8640af7f0661 Mon Sep 17 00:00:00 2001 From: Philip Tzou Date: Mon, 11 Aug 2025 16:37:29 -0700 Subject: [PATCH 4/5] Simplify gap placement score option --- postalign/processors/codon_alignment.py | 75 +++++-------------------- tests/component/steps/codon_steps.py | 6 +- tests/unit/test_codon_alignment.py | 36 +----------- 3 files changed, 20 insertions(+), 97 deletions(-) diff --git a/postalign/processors/codon_alignment.py b/postalign/processors/codon_alignment.py index 072eb6c..8bd651d 100644 --- a/postalign/processors/codon_alignment.py +++ b/postalign/processors/codon_alignment.py @@ -1,6 +1,5 @@ """Processor performing codon alignment.""" -import inspect import re import typer import cython # type: ignore @@ -610,44 +609,6 @@ def parse_gap_placement_score(value: str) -> dict[ return scores -@cython.ccall -@cython.returns(dict) -def gap_placement_score_callback( - ctx: typer.Context, - param: typer.CallbackParam, - value: tuple[str, ...] | list[str] | str | None, -) -> dict[int, dict[tuple[int, int], int]]: - """Parse ``--gap-placement-score`` arguments. - - The option may be provided multiple times depending on Typer's - capabilities. When ``multiple`` is supported the values arrive as a - tuple or list; otherwise Typer supplies a single comma-delimited string. - - :param ctx: Typer context (unused). - :param param: Callback parameter definition. - :param value: Sequence or string of score specifications. - :returns: Nested mapping of gap placement scores. - :raises typer.BadParameter: On invalid input value or metadata. - """ - if not param.name: - raise typer.BadParameter( - 'Internal error (gap_placement_score_callback:1)' - ) - if value is None or value == "" or value == (): # no scores provided - return {} - if isinstance(value, (tuple, list)): - raw = ",".join(value) - else: - raw = value - try: - result: dict[ - int, dict[tuple[int, int], int] - ] = parse_gap_placement_score(raw) - return result - except ValueError as exp: # pragma: no cover - defensive - raise typer.BadParameter(str(exp)) - - _GAP_PLACEMENT_HELP = ( 'Bonus (positive number) or penalty (negative number) for gaps ' 'appear at certain NA position (relative to the WHOLE ref seq) ' @@ -661,20 +622,6 @@ def gap_placement_score_callback( '204ins:-5,2041/12del:10.' ) -if 'multiple' in inspect.signature(typer.Option).parameters: - _GAP_PLACEMENT_OPTION = typer.Option( - (), '--gap-placement-score', - callback=gap_placement_score_callback, - multiple=True, - help=_GAP_PLACEMENT_HELP, - ) # type: ignore[call-overload] -else: # pragma: no cover - executed on Typer <0.15 - _GAP_PLACEMENT_OPTION = typer.Option( - None, '--gap-placement-score', - callback=gap_placement_score_callback, - help=_GAP_PLACEMENT_HELP, - ) - @cli.command('codon-alignment') def codon_alignment( @@ -701,12 +648,12 @@ def codon_alignment( ), ] = 10, gap_placement_score: Annotated[ - # For NASize, 0 means any size - # Indel NAPos NASize Score - # v v v v - dict[int, dict[tuple[int, int], int]], - _GAP_PLACEMENT_OPTION, - ] = {}, + list[str] | None, + typer.Option( + None, '--gap-placement-score', + help=_GAP_PLACEMENT_HELP, + ), + ] = None, ref_start: Annotated[int, typer.Argument()] = 1, ref_end: Annotated[int, typer.Argument()] = -1, # XXX: see https://github.com/cython/cython/issues/2753 @@ -721,11 +668,17 @@ def codon_alignment( :param min_gap_distance: Minimal nucleotide gap distance of the output. :param window_size: Amino acid window size for finding optimal placement. :param gap_placement_score: Bonus or penalty scores for gaps at specific - positions. + positions, expressed as repeated ``--gap-placement-score`` options in + the form ``[/](ins|del):``. :param ref_start: Start position relative to reference sequence. :param ref_end: End position relative to reference sequence. :raises typer.BadParameter: If provided positions are invalid. """ + if gap_placement_score: + gap_scores = parse_gap_placement_score(','.join(gap_placement_score)) + else: + gap_scores = {REFGAP: {}, SEQGAP: {}} + if ref_start < 1: raise typer.BadParameter( f'argument :{ref_start} must be not less than 1' @@ -762,7 +715,7 @@ def processor( seq, min_gap_distance, window_size, - gap_placement_score, + gap_scores, ref_start, my_ref_end ) diff --git a/tests/component/steps/codon_steps.py b/tests/component/steps/codon_steps.py index 00dcccb..7170bab 100644 --- a/tests/component/steps/codon_steps.py +++ b/tests/component/steps/codon_steps.py @@ -11,7 +11,11 @@ from typing import Any, Callable import typer -from behave import given, then, when # type: ignore[import-not-found,import-untyped] +from behave import ( # type: ignore[import-not-found,import-untyped] + given, + then, + when, +) if not hasattr(typer.Typer, "result_callback"): def result_callback( diff --git a/tests/unit/test_codon_alignment.py b/tests/unit/test_codon_alignment.py index ae27bfa..46eee72 100644 --- a/tests/unit/test_codon_alignment.py +++ b/tests/unit/test_codon_alignment.py @@ -2,7 +2,7 @@ from __future__ import annotations -from unittest.mock import MagicMock, patch +from unittest.mock import patch import pytest import typer @@ -19,7 +19,6 @@ find_first_gap, move_gaps_to_center, parse_gap_placement_score, - gap_placement_score_callback, calc_match_score, separate_gaps_from_nas, paired_find_best_matches, @@ -53,16 +52,6 @@ def test_parse_gap_placement_score_skips_empty() -> None: assert scores[SEQGAP][(205, 0)] == 3 -def test_gap_placement_score_callback_missing_name() -> None: - """Missing parameter name should raise :class:`BadParameter`.""" - - ctx = MagicMock() - param = MagicMock() - param.name = None - with pytest.raises(typer.BadParameter): - gap_placement_score_callback(ctx, param, ()) - - def test_codon_alignment_invalid_ref_start() -> None: """Invalid reference start should raise :class:`BadParameter`.""" @@ -426,26 +415,3 @@ def test_codon_alignment_processor_dispatches() -> None: ca.assert_called_once() assert result[0] == (empty_ref, empty_seq) assert result[1] == (full_ref, full_seq) - - -def test_gap_placement_score_callback_invalid_value() -> None: - """Invalid callback values should raise :class:`BadParameter`.""" - - ctx = MagicMock() - param = MagicMock() - param.name = "gps" - with pytest.raises(typer.BadParameter): - gap_placement_score_callback(ctx, param, ("204foo",)) - - -def test_gap_placement_score_callback_str_value() -> None: - """String values should be accepted when Typer lacks ``multiple``.""" - - ctx = MagicMock() - param = MagicMock() - param.name = "gap_placement_score" - result = gap_placement_score_callback( - ctx, param, "204ins:-5,2041/12del:10" - ) - assert result[REFGAP][(204, 0)] == -5 - assert result[SEQGAP][(2041, 12)] == 10 From b91393151ea5ddea0c3b1316120325cc60772d00 Mon Sep 17 00:00:00 2001 From: Philip Tzou Date: Mon, 11 Aug 2025 17:38:38 -0700 Subject: [PATCH 5/5] Fix requirements.txt --- AGENTS.md | 22 ++++------------------ Makefile | 3 +-- requirements.txt | 1 - 3 files changed, 5 insertions(+), 21 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index c9ce499..76289d3 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -54,10 +54,10 @@ make requirements.txt ## CI expectations (example) - Use Python 3.13 runner (project supports Python 3.11+). - Steps: - 1. `pip install -e .[dev]` - 2. `flake8 .` - 3. `mypy .` - 4. `pytest --cov=postalign --cov-report=xml` (record artifact, enforce threshold) + 1. `pipenv run pip install -e .[dev]` + 2. `pipenv run flake8 .` + 3. `pipenv run mypy .` + 4. `pipenv run pytest --cov=postalign --cov-report=xml` (record artifact, enforce threshold) ## Sphinx docstring style (minimal rules) - Use Sphinx fields: `:param name:`, `:type name:`, `:returns:`, `:rtype:`, `:raises:`. @@ -69,24 +69,10 @@ make requirements.txt # With pipenv pipenv update # safe minor/patch upgrades per constraints pipenv update - -# If using pip/requirements: -pip list --outdated -pip install -U -# If using pip-tools: -pip-compile --upgrade -pip-sync ``` - Pin in requirements/lockfile as appropriate. - Run tests and type checks after any upgrade. -## Pre-commit (recommended) -```bash -pip install pre-commit -pre-commit install -# Example hooks: flake8, mypy (via local hook), trailing-whitespace, end-of-file-fixer -``` - ## Pull request checklist - [ ] Code runs on Python 3.11+ (tests executed on Python 3.13). - [ ] Added/updated tests for all touched code. diff --git a/Makefile b/Makefile index 9665706..95377e9 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,5 @@ requirements.txt: Pipfile.lock - @pipenv requirements --from-pipfile > requirements.txt -@sed -i.bak -- '/^-e \.$/d' requirements.txt && rm -f requirements.txt.bak + @pipenv requirements --from-pipfile | grep -v '^-i' > requirements.txt test-unit: @pytest tests/unit diff --git a/requirements.txt b/requirements.txt index 4534c4a..8c1d0ec 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,3 @@ --i https://pypi.org/simple cython==3.1.2; python_version >= '3.8' more-itertools==10.7.0; python_version >= '3.9' orjson==3.11.1; python_version >= '3.9'