Skip to content

Commit dc35d9d

Browse files
committed
FIX: remove multiprocessing for gbk file filtering
1 parent 0c262a6 commit dc35d9d

1 file changed

Lines changed: 47 additions & 55 deletions

File tree

scripts/filter_gbk_by_product.py

Lines changed: 47 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1,55 +1,65 @@
1-
"""Filter GenBank files by antiSMASH cand_cluster/protocluster product and copy matches."""
1+
#!/usr/bin/env python3
2+
"""Filter GenBank files by antiSMASH cand_cluster/protocluster product, EXACT product match, and copy matches."""
23

34
from __future__ import annotations
45

56
import argparse
67
import os
78
import re
89
import shutil
9-
import sys
10-
from concurrent.futures import ProcessPoolExecutor
1110
from pathlib import Path
1211
from typing import Iterable, Sequence
1312

1413
from tqdm import tqdm
1514

1615

16+
# Regex to capture all product qualifiers
17+
PRODUCT_RE = re.compile(r'/product="([^"]*)"', re.IGNORECASE)
18+
19+
1720
def _iter_gbk_files(root: Path, exts: Sequence[str]) -> Iterable[Path]:
18-
for ext in exts:
19-
yield from root.rglob(f"*{ext}")
21+
"""Yield all files with specified extensions."""
22+
exts = {e.lower() for e in exts}
23+
for p in root.rglob("*"):
24+
if p.is_file() and p.suffix.lower() in exts:
25+
yield p
2026

2127

2228
def _file_matches(
2329
gbk_path: str,
2430
products: Sequence[str],
2531
) -> dict | None:
2632
"""
27-
Return match info if the GBK contains cand_cluster/protocluster with desired product.
28-
Uses lightweight text search to avoid full parsing overhead.
33+
Return match info if the GBK contains cand_cluster/protocluster and has a product EXACTLY equal
34+
to one of the given search terms (case-insensitive).
2935
"""
3036
path = Path(gbk_path)
37+
3138
try:
3239
text = path.read_text(errors="ignore")
33-
except Exception as exc: # noqa: BLE001
40+
except Exception as exc:
3441
return {"error": f"read failed: {exc}", "file": gbk_path}
3542

3643
lower = text.lower()
44+
45+
# quick skip
3746
if "cand_cluster" not in lower and "protocluster" not in lower:
3847
return None
3948

40-
matched: set[str] = set()
41-
for prod in products:
42-
pat = re.compile(r'/product="[^"]*' + re.escape(prod.lower()) + r'[^"]*"', re.IGNORECASE)
43-
if pat.search(text):
44-
matched.add(prod)
49+
# extract all /product="..."`
50+
found_products = PRODUCT_RE.findall(text)
51+
found_products_norm = {fp.strip().lower() for fp in found_products}
52+
53+
# find exact matches
54+
matched = sorted(fp for fp in found_products_norm if fp in products)
4555

4656
if not matched:
4757
return None
4858

4959
return {
5060
"file": path.name,
5161
"path": str(path),
52-
"matched_products": sorted(matched),
62+
"matched_products": matched,
5363
}
5464

5565

@@ -62,34 +72,23 @@ def cli() -> argparse.Namespace:
6272
"--products",
6373
nargs="+",
6474
required=True,
65-
help='Product substrings to match (e.g., "NRPS", "T1PKS")',
66-
)
67-
parser.add_argument(
68-
"-w",
69-
"--workers",
70-
type=int,
71-
default=os.cpu_count() or 1,
72-
help="Number of worker processes",
75+
help='Exact product terms to match (case-insensitive)',
7376
)
7477
parser.add_argument(
7578
"--exts",
7679
nargs="+",
7780
default=[".gbk", ".gbff", ".gb"],
7881
help="File extensions to include",
7982
)
80-
parser.add_argument(
81-
"--chunksize",
82-
type=int,
83-
default=16,
84-
help="Chunksize for process pool mapping",
85-
)
8683
return parser.parse_args()
8784

8885

8986
def main() -> None:
9087
args = cli()
9188
input_dir: Path = args.input_dir
9289
output_dir: Path = args.output_dir
90+
91+
# normalize to lowercase for exact matching
9392
products = [p.lower() for p in args.products]
9493

9594
if not input_dir.exists():
@@ -101,38 +100,31 @@ def main() -> None:
101100

102101
output_dir.mkdir(parents=True, exist_ok=True)
103102

104-
total = len(files)
105103
copied = 0
106104
errors = 0
107105

108-
with ProcessPoolExecutor(max_workers=args.workers) as executor:
109-
with tqdm(total=total, desc="Scanning GBKs", unit="file") as pbar:
110-
for result in executor.map(
111-
_file_matches,
112-
(str(p) for p in files),
113-
[products] * len(files),
114-
chunksize=args.chunksize,
115-
):
116-
pbar.update(1)
117-
118-
if not result:
119-
continue
120-
121-
if isinstance(result, dict) and "error" in result:
122-
errors += 1
123-
pbar.set_postfix(copied=copied, errors=errors)
124-
continue
125-
126-
src_path = Path(result["path"])
127-
dst_path = output_dir / src_path.name
128-
try:
129-
shutil.copy2(src_path, dst_path)
130-
copied += 1
131-
except Exception as exc: # noqa: BLE001
132-
errors += 1
133-
result = {"error": f"copy failed: {exc}", "file": str(src_path)}
106+
with tqdm(total=len(files), desc="Scanning GBKs", unit="file") as pbar:
107+
for path in files:
108+
result = _file_matches(str(path), products)
109+
pbar.update(1)
110+
111+
if not result:
112+
continue
134113

114+
if "error" in result:
115+
errors += 1
135116
pbar.set_postfix(copied=copied, errors=errors)
117+
continue
118+
119+
try:
120+
shutil.copy2(result["path"], output_dir / result["file"])
121+
copied += 1
122+
except Exception as exc:
123+
errors += 1
124+
125+
pbar.set_postfix(copied=copied, errors=errors)
126+
127+
print(f"\nDone. Copied: {copied}, errors: {errors}, total scanned: {len(files)}")
136128

137129

138130
if __name__ == "__main__":

0 commit comments

Comments
 (0)