Skip to content

Commit 0c20348

Browse files
committed
Emulate samtools et al if these tools are not available
Add devtools/emulate-tools.py script that, when invoked as samtools, bcftools, bgzip, or tabix, emulates that tool's behaviour by using pysam's facilities. The script implements all functionality used in the test suite (and in particular when creating test data in tests/*_data/Makefile). devtools/install-prerequisites.sh installs symlinks to this script on platforms where samtools/bcftools/tabix packages are not available. Add a new "don't disturb stdout" redirection mode to _pysam_dispatch(), for use by the script. For now this is invoked via catch_stdout=None, but we won't document this facility until it has bedded in.
1 parent 9c4c693 commit 0c20348

File tree

4 files changed

+76
-3
lines changed

4 files changed

+76
-3
lines changed

devtools/emulate-tools.py

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
#!/usr/bin/env python3
2+
"""
3+
This script can be symlinked to samtools, bcftools, bgzip, or tabix.
4+
When invoked under one of those names, it will emulate that tool's
5+
behaviour by using pysam's facilities.
6+
"""
7+
8+
import argparse
9+
import gzip
10+
import os
11+
import sys
12+
import tempfile
13+
14+
import pysam
15+
16+
command = os.path.basename(sys.argv[0])
17+
18+
if command in ("samtools", "bcftools"):
19+
if len(sys.argv) > 1:
20+
try:
21+
tool = pysam.utils.PysamDispatcher(command, sys.argv[1])
22+
tool(*sys.argv[2:], catch_stdout=None)
23+
print(tool.stderr, end="", file=sys.stderr)
24+
except pysam.utils.SamtoolsError as e:
25+
sys.exit(f"emulate-tools.py: {e}")
26+
27+
else:
28+
version = getattr(pysam.version, f"__{command}_version__")
29+
print(f"Program: {command}\nVersion: {version}", file=sys.stderr)
30+
31+
else:
32+
parser = argparse.ArgumentParser()
33+
parser.add_argument("-c", "--stdout", action="store_true")
34+
parser.add_argument("-d", "--decompress", action="store_true")
35+
parser.add_argument("-f", "--force", action="store_true")
36+
parser.add_argument("-p", "--preset")
37+
parser.add_argument("input_file", nargs="?")
38+
opt = parser.parse_args()
39+
40+
if command == "bgzip":
41+
if opt.decompress:
42+
with gzip.open(sys.stdin.buffer, "rb") as f:
43+
sys.stdout.buffer.write(f.read())
44+
45+
elif opt.input_file and opt.stdout:
46+
pysam.tabix_compress(opt.input_file, "-", force=True)
47+
48+
else:
49+
f = tempfile.NamedTemporaryFile(delete=False)
50+
f.write(sys.stdin.buffer.read())
51+
f.close()
52+
pysam.tabix_compress(f.name, "-", force=True)
53+
os.remove(f.name)
54+
55+
elif command == "tabix":
56+
pysam.tabix_index(opt.input_file, preset=opt.preset, force=opt.force)
57+
58+
else:
59+
sys.exit(f"emulate-tools.py: unknown command {command!r}")

devtools/install-prerequisites.sh

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ elif test -x /usr/bin/yum; then
1212
else
1313
echo Installing non-test prerequisites via yum...
1414
yum -y install zlib-devel bzip2-devel xz-devel curl-devel openssl-devel
15+
emulate=yes
1516
fi
1617

1718
elif test -d /etc/dpkg; then
@@ -23,6 +24,7 @@ elif test -x /sbin/apk; then
2324
echo Installing non-test prerequisites via apk...
2425
apk update
2526
apk add zlib-dev bzip2-dev xz-dev curl-dev openssl-dev
27+
emulate=yes
2628

2729
elif test -x ${HOMEBREW_PREFIX-/usr/local}/bin/brew; then
2830
echo Installing prerequisites via brew...
@@ -32,3 +34,14 @@ elif test -x ${HOMEBREW_PREFIX-/usr/local}/bin/brew; then
3234
else
3335
echo No package manager detected
3436
fi
37+
38+
if test -n "$emulate" && test $# -ge 2; then
39+
emulator=$1
40+
bindir=$2
41+
echo Creating symlinks to $emulator in $bindir...
42+
mkdir -p $bindir
43+
ln -s $emulator $bindir/samtools
44+
ln -s $emulator $bindir/bcftools
45+
ln -s $emulator $bindir/bgzip
46+
ln -s $emulator $bindir/tabix
47+
fi

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ requires = ["setuptools>=59.0", "Cython>=0.29.12,<4"]
2222
build-backend = "setuptools.build_meta:__legacy__"
2323

2424
[tool.cibuildwheel]
25-
before-all = "{project}/devtools/install-prerequisites.sh"
25+
before-all = "{project}/devtools/install-prerequisites.sh {project}/devtools/emulate-tools.py /usr/local/bin"
2626
# Necessary until we build libhts.a out-of-tree from within build_temp
2727
before-build = "make -C {project}/htslib distclean"
2828

pysam/libcutils.pyx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ from libc.stdint cimport INT32_MAX, int32_t
1919
from libc.stdio cimport fprintf, stderr, fflush
2020
from libc.stdio cimport stdout as c_stdout
2121
from posix.fcntl cimport open as c_open, O_WRONLY, O_CREAT, O_TRUNC
22-
from posix.unistd cimport SEEK_SET, SEEK_CUR, SEEK_END
22+
from posix.unistd cimport dup as c_dup, SEEK_SET, SEEK_CUR, SEEK_END, STDOUT_FILENO
2323

2424
from pysam.libcsamtools cimport samtools_dispatch, samtools_set_stdout, samtools_set_stderr, \
2525
samtools_close_stdout, samtools_close_stderr, samtools_set_stdout_fn
@@ -361,7 +361,8 @@ def _pysam_dispatch(collection,
361361
else:
362362
samtools_set_stdout_fn("-")
363363
bcftools_set_stdout_fn("-")
364-
stdout_h = c_open(b"/dev/null", O_WRONLY)
364+
if catch_stdout is None: stdout_h = c_dup(STDOUT_FILENO)
365+
else: stdout_h = c_open(b"/dev/null", O_WRONLY)
365366

366367
# setup the function call to samtools/bcftools main
367368
cdef char ** cargs

0 commit comments

Comments
 (0)