Skip to content

Commit 624f050

Browse files
authored
Merge pull request #136 from cokelaer/main
Enhance shell modules, tests, and CI configurations
2 parents 2c4dd03 + 053e4b1 commit 624f050

353 files changed

Lines changed: 44023 additions & 49 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/main.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,4 +76,4 @@ jobs:
7676
else
7777
echo "No parent commit. Skipping git diff."
7878
fi
79-
pytest test.py -vv
79+
pytest wrappers/test.py -vv

.github/workflows/pypi.yml

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ jobs:
1010
build-n-publish:
1111
name: Build and publish to PyPI
1212
runs-on: ubuntu-latest
13+
environment: pypi
14+
permissions:
15+
id-token: write
1316

1417
steps:
1518
- uses: actions/checkout@v4
@@ -27,16 +30,6 @@ jobs:
2730
rm -rf dist
2831
python -m build
2932
30-
- name: Publish to Test PyPI
31-
uses: pypa/gh-action-pypi-publish@release/v1
32-
with:
33-
user: __token__
34-
password: ${{ secrets.TEST_PYPI_API_TOKEN }}
35-
repository-url: https://test.pypi.org/legacy/
36-
3733
- name: Publish to PyPI
3834
if: startsWith(github.ref, 'refs/tags')
3935
uses: pypa/gh-action-pypi-publish@release/v1
40-
with:
41-
user: __token__
42-
password: ${{ secrets.PYPI_API_TOKEN }}

.github/workflows/shells.yml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,8 +61,15 @@ jobs:
6161
fi
6262
6363
# Keep only shells that actually have a test/ directory
64+
# Exclude shells whose container download is too slow for CI
65+
EXCLUDED="medaka"
6466
TESTABLE=""
6567
for shell in $CHANGED; do
68+
tool=$(echo "$shell" | cut -d/ -f1)
69+
if echo "$EXCLUDED" | grep -qw "$tool"; then
70+
echo "Skipping excluded shell: $shell"
71+
continue
72+
fi
6673
if [ -d "${SHELLS_ROOT}/${shell}/test" ]; then
6774
TESTABLE="${TESTABLE} ${shell}"
6875
fi

.github/workflows/snippets.yml

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
name: Tests snippets
2+
3+
on:
4+
push:
5+
branches:
6+
- main
7+
- dev
8+
paths:
9+
- "sequana_wrappers/snippets/**"
10+
pull_request:
11+
branches-ignore: []
12+
paths:
13+
- "sequana_wrappers/snippets/**"
14+
workflow_dispatch:
15+
schedule:
16+
# Monthly full run — same cadence as other test suites
17+
- cron: "0 0 4 * *"
18+
19+
20+
jobs:
21+
test:
22+
runs-on: ubuntu-latest
23+
24+
steps:
25+
- uses: actions/checkout@v4
26+
27+
- uses: actions/setup-python@v5
28+
with:
29+
python-version: "3.10"
30+
31+
- name: Install dependencies
32+
run: |
33+
pip install sequana_pipetools snakemake pytest
34+
pip install -e .
35+
36+
- name: Run snippet tests
37+
run: pytest -v .

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,4 @@ __pycache__
44
.cache
55
.idea
66
.vscode
7+
CLAUDE.md

MANIFEST.in

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
recursive-exclude * test *
2+
recursive-exclude * tests *
3+
exclude Makefile
4+
exclude Snakefile
5+
global-exclude Snakefile
6+
global-exclude Makefile

README.md

Lines changed: 96 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,13 @@
22

33
[![Tests wrappers](https://github.com/sequana/sequana-wrappers/actions/workflows/main.yml/badge.svg)](https://github.com/sequana/sequana-wrappers/actions/workflows/main.yml)
44
[![Tests shells](https://github.com/sequana/sequana-wrappers/actions/workflows/shells.yml/badge.svg)](https://github.com/sequana/sequana-wrappers/actions/workflows/shells.yml)
5+
[![Tests snippets](https://github.com/sequana/sequana-wrappers/actions/workflows/snippets.yml/badge.svg)](https://github.com/sequana/sequana-wrappers/actions/workflows/snippets.yml)
56
[![Tests](http://joss.theoj.org/papers/10.21105/joss.00352/status.svg)](http://joss.theoj.org/papers/10.21105/joss.00352)
67

78
|||
89
| --- | --- |
910
| Overview | Shell command library and Snakemake wrappers for Sequana pipelines |
10-
| Status | Production (`wrappers/` — maintenance only) / Active (`shells/`) |
11+
| Status | Production (`wrappers/` — maintenance only) / Active (`shells/`, `snippets/`) |
1112
| Issues | Please fill a report on [github/sequana/sequana-wrappers](https://github.com/sequana/sequana/issues) |
1213
| Python version | Python 3.8+ |
1314
| Citation | Cokelaer et al, (2017), ‘Sequana’: a Set of Snakemake NGS pipelines, Journal of Open Source Software, 2(16), 352, [doi:10.21105/joss.00352](http://www.doi2bib.org/bib/10.21105%2Fjoss.00352) |
@@ -28,6 +29,21 @@ tool commands to Sequana pipelines:
2829
inside the container. This is the active development track and the
2930
recommended approach for all new Sequana pipelines.
3031

32+
- **`sequana_wrappers/snippets/`** — versioned Python callables for pipeline
33+
steps that require Python logic but still benefit from shared, versioned
34+
definitions. Used via `run:` blocks (not `shell:`). See
35+
[the snippets section below](#the-snippets-directory--rationale) for details.
36+
37+
All wrappers available in shells except 3 (require Python imports from the sequana library —
38+
not expressible as pure bash):
39+
- fastq_stats — uses sequana.FastQC + matplotlib
40+
- freebayes_vcf_filter — uses sequana.VCF_freebayes Python class
41+
- snpeff_add_locus_in_fasta — uses sequana.SnpEff.add_locus_in_fasta()
42+
43+
The `rulegraph` rule formerly in wrappers has been migrated to
44+
`sequana_wrappers/snippets/rulegraph/` because it requires Python imports from
45+
`sequana_pipetools` — it cannot run as a pure bash command inside a container.
46+
3147
## Quick start — shells (recommended)
3248

3349
Install the package:
@@ -47,6 +63,25 @@ rule minimap2:
4763
shell: manager.get_shell("minimap2/align", "v1")
4864
```
4965

66+
## Quick start — snippets (Python run blocks)
67+
68+
When a pipeline step requires Python logic (host-side imports, file path
69+
resolution, etc.) but you still want the code to be shared and versioned, use
70+
`get_run` with a `run:` block:
71+
72+
```python
73+
rule rulegraph:
74+
input: "Snakefile"
75+
output: "rulegraph/rulegraph.svg"
76+
params: configname="config.yaml"
77+
run:
78+
manager.get_run("rulegraph/run", "v1")(snakemake)
79+
```
80+
81+
The snippet's `execute(input, output, params)` function runs on the **host**
82+
(where `sequana_pipetools` and other Python dependencies are available) — no
83+
container is involved.
84+
5085
## Quick start — wrappers (legacy)
5186

5287
```bash
@@ -151,23 +186,19 @@ sequana-wrappers/
151186
├── wrappers/ # existing — kept for backward compat
152187
│ ├── bwa/align/wrapper.py
153188
│ └── ...
154-
└── sequana_wrappers/shells/ # Python package — container-first shell library
155-
├── __init__.py
156-
├── bwa/
157-
│ ├── __init__.py
158-
│ ├── align/
159-
│ │ ├── __init__.py
160-
│ │ └── v1/
161-
│ │ ├── __init__.py
162-
│ │ └── cmd.py # frozen at release v1
163-
│ └── build/
164-
│ ├── __init__.py
165-
│ └── v1/cmd.py
166-
├── bamtools/
167-
│ └── stats/
168-
│ ├── __init__.py
169-
│ └── v1/cmd.py
170-
└── ...
189+
└── sequana_wrappers/
190+
├── __init__.py # get_shell() and get_run()
191+
├── shells/ # container-first shell strings
192+
│ ├── bwa/
193+
│ │ ├── align/
194+
│ │ │ └── v1/cmd.py # frozen at release v1
195+
│ │ └── build/
196+
│ │ └── v1/cmd.py
197+
│ ├── bamtools/stats/v1/cmd.py
198+
│ └── ...
199+
└── snippets/ # host-side Python callables
200+
├── rulegraph/run/v1/code.py
201+
└── ...
171202
```
172203

173204
### Versioning convention
@@ -278,6 +309,53 @@ and bump `version` in `pyproject.toml`. No git tag required — the directory
278309

279310
---
280311

312+
# The `snippets/` directory — rationale
313+
314+
Some pipeline steps require Python logic that cannot be expressed as a pure
315+
bash command string. Examples: generating a rule graph (needs
316+
`sequana_pipetools.DOTParser`), post-processing VCF files with a custom Python
317+
class, or running tools that depend on host-side Python libraries.
318+
319+
These steps **cannot use `shell:` + `container:`** (the container has no Python
320+
runtime; and even if it did, the ABI mismatch problem described above applies).
321+
They also cannot use `wrapper:` for the same ABI reason.
322+
323+
The solution is a `snippets/` library of versioned Python callables that are
324+
invoked inside Snakemake `run:` blocks, running entirely on the host where all
325+
Python dependencies are available:
326+
327+
```
328+
sequana_wrappers/snippets/<tool>/<command>/<version>/code.py
329+
```
330+
331+
Each `code.py` exports an `execute(input, output, params)` function. Versioning
332+
follows the same convention as shells (`v1`, `v2`, …, `dev`). `get_run` loads
333+
the callable by path and version — no git-time fetching, no ABI concerns.
334+
335+
| Property | Wrappers | Shell library | Snippet library |
336+
|---|---|---|---|
337+
| Python in container | Required | Not needed | N/A (host-side) |
338+
| Container needed | Optional | Yes | No |
339+
| Snakemake directive | `wrapper:` | `shell:` | `run:` |
340+
| Reusable & versioned | Yes | Yes | **Yes** |
341+
| Python imports on host | Yes | No | **Yes** |
342+
343+
The repository layout extended with snippets:
344+
345+
```
346+
sequana-wrappers/
347+
├── wrappers/ # legacy — maintenance only
348+
├── sequana_wrappers/
349+
│ ├── shells/ # container-first shell strings
350+
│ │ ├── bwa/align/v1/cmd.py
351+
│ │ └── ...
352+
│ └── snippets/ # host-side Python callables
353+
│ ├── rulegraph/run/v1/code.py
354+
│ └── ...
355+
```
356+
357+
---
358+
281359
# Notes for developers
282360

283361
## Overview

pyproject.toml

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[build-system]
2-
requires = ["setuptools>=61"]
3-
build-backend = "setuptools.build_meta"
2+
requires = ["poetry-core>=2.0.0"]
3+
build-backend = "poetry.core.masonry.api"
44

55
[project]
66
name = "sequana_wrappers"
@@ -10,6 +10,14 @@ readme = "README.md"
1010
license = { text = "BSD-3-Clause" }
1111
requires-python = ">=3.8"
1212

13-
[tool.setuptools.packages.find]
14-
where = ["."]
15-
include = ["sequana_wrappers*"]
13+
[tool.poetry]
14+
packages = [{ include = "sequana_wrappers" }]
15+
exclude = [
16+
"sequana_wrappers/shells/**/test",
17+
"sequana_wrappers/shells/**/test/**",
18+
"sequana_wrappers/shells/**/README.md",
19+
]
20+
21+
[tool.pytest.ini_options]
22+
norecursedirs = ["sequana_wrappers/shells", "dist", "build", ".git"]
23+

sequana_wrappers/__init__.py

Lines changed: 29 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,21 +4,13 @@
44
def get_shell(tool_path: str, version: str) -> str:
55
"""Return a shell command string from the sequana_wrappers shell library.
66
7-
``tool_path`` is a slash-separated string encoding the tool and command
8-
(and any future sub-commands), e.g. ``"bwa/align"`` or ``"bamtools/stats"``.
9-
10-
Every version — including ``"dev"`` — maps to a subdirectory of the same
11-
name under ``shells/<tool>/<command>/``. No silent fallback: an explicit
12-
error is raised if the requested version does not exist.
13-
147
:param tool_path: slash-separated tool/command path, e.g. ``"bwa/align"``
158
:param version: shell command version, e.g. ``"v1"``, or ``"dev"`` for the
169
development (unreleased) version.
1710
1811
Example usage in a pipeline rules file::
1912
20-
shell: manager.get_shell("bwa/align", "v1") # pinned — recommended
21-
shell: manager.get_shell("bwa/align", "dev") # development version
13+
shell: manager.get_shell("bwa/align", "v1")
2214
"""
2315
parts = tool_path.split("/")
2416
module_path = ".".join(["sequana_wrappers", "shells"] + parts + [version, "cmd"])
@@ -31,3 +23,31 @@ def get_shell(tool_path: str, version: str) -> str:
3123
f"sequana_wrappers/shells/{'/'.join(parts)}/.\n"
3224
f"Use version='dev' for the development version."
3325
)
26+
27+
28+
def get_run(tool_path: str, version: str):
29+
"""Return a Python callable from the sequana_wrappers snippets library.
30+
31+
The returned callable has the signature ``execute(snakemake)`` and is
32+
intended for use inside Snakemake ``run:`` blocks.
33+
34+
:param tool_path: slash-separated tool/command path, e.g. ``"rulegraph/run"``
35+
:param version: snippet version, e.g. ``"v1"``, or ``"dev"`` for the
36+
development (unreleased) version.
37+
38+
Example usage in a pipeline rules file::
39+
40+
run:
41+
manager.get_run("rulegraph/run", "v1")(snakemake)
42+
"""
43+
parts = tool_path.split("/")
44+
module_path = ".".join(["sequana_wrappers", "snippets"] + parts + [version, "code"])
45+
try:
46+
return importlib.import_module(module_path).execute
47+
except ModuleNotFoundError:
48+
raise ModuleNotFoundError(
49+
f"Snippet version '{version}' not found: '{module_path}'.\n"
50+
f"Available versions are the subdirectories under "
51+
f"sequana_wrappers/snippets/{'/'.join(parts)}/.\n"
52+
f"Use version='dev' for the development version."
53+
)

sequana_wrappers/shells/Makefile

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
MAKEFILES := $(shell find . -mindepth 3 -name Makefile)
2+
3+
clean:
4+
@for mk in $(MAKEFILES); do \
5+
$(MAKE) -C $$(dirname $$mk) clean; \
6+
done

0 commit comments

Comments
 (0)