11from __future__ import annotations
22
33import gzip
4+ import shutil
5+ import subprocess
46from concurrent .futures import ProcessPoolExecutor
7+ from importlib .util import find_spec
58from pathlib import Path
6- from typing import Dict , Iterable , Tuple
9+ from typing import Dict , Iterable , Tuple , TYPE_CHECKING
710
811import numpy as np
912from Bio import SeqIO
1013from Bio .Seq import Seq
1114from Bio .SeqRecord import SeqRecord
1215
1316from smftools .logging_utils import get_logger
17+ from smftools .optional_imports import require
1418
1519from ..readwrite import time_string
1620
1721logger = get_logger (__name__ )
1822
19- try :
20- import pysam
21- except Exception :
22- pysam = None # type: ignore
23+ if TYPE_CHECKING :
24+ import pysam as pysam_module
2325
24- try :
25- import shutil
26- import subprocess
27- except Exception : # pragma: no cover - stdlib
28- shutil = None # type: ignore
29- subprocess = None # type: ignore
26+
27+ def _require_pysam () -> "pysam_module" :
28+ if pysam_types is not None :
29+ return pysam_types
30+ return require ("pysam" , extra = "pysam" , purpose = "FASTA access" )
31+
32+ pysam_types = None
33+ if find_spec ("pysam" ) is not None :
34+ pysam_types = require ("pysam" , extra = "pysam" , purpose = "FASTA access" )
3035
3136
3237def _resolve_fasta_backend () -> str :
3338 """Resolve the backend to use for FASTA access."""
3439 if shutil is not None and shutil .which ("samtools" ):
3540 return "cli"
36- if pysam is not None :
41+ if pysam_types is not None :
3742 return "python"
3843 raise RuntimeError ("FASTA access requires pysam or samtools in PATH." )
3944
@@ -43,10 +48,9 @@ def _ensure_fasta_index(fasta: Path) -> None:
4348 if fai .exists ():
4449 return
4550 if subprocess is None or shutil is None or not shutil .which ("samtools" ):
46- if pysam is not None :
47- pysam .faidx (str (fasta ))
48- return
49- raise RuntimeError ("FASTA indexing requires pysam or samtools in PATH." )
51+ pysam_mod = _require_pysam ()
52+ pysam_mod .faidx (str (fasta ))
53+ return
5054 cp = subprocess .run (
5155 ["samtools" , "faidx" , str (fasta )],
5256 stdout = subprocess .DEVNULL ,
@@ -225,7 +229,7 @@ def index_fasta(fasta: str | Path, write_chrom_sizes: bool = True) -> Path:
225229 Path: Path to the index file or chromosome sizes file.
226230 """
227231 fasta = Path (fasta )
228- pysam .faidx (str (fasta )) # creates <fasta>.fai
232+ _require_pysam () .faidx (str (fasta )) # creates <fasta>.fai
229233
230234 fai = fasta .with_suffix (fasta .suffix + ".fai" )
231235 if write_chrom_sizes :
@@ -377,8 +381,8 @@ def subsample_fasta_from_bed(
377381
378382 fasta_handle = None
379383 if backend == "python" :
380- assert pysam is not None
381- fasta_handle = pysam .FastaFile (str (input_FASTA ))
384+ pysam_mod = _require_pysam ()
385+ fasta_handle = pysam_mod .FastaFile (str (input_FASTA ))
382386
383387 # Open BED + output FASTA
384388 with input_bed .open ("r" ) as bed , output_FASTA .open ("w" ) as out_fasta :
0 commit comments