Skip to content

Commit bbf3644

Browse files
committed
revamp logging for python3.14
1 parent c95867f commit bbf3644

File tree

5 files changed

+77
-34
lines changed

5 files changed

+77
-34
lines changed

isoquant.py

Lines changed: 11 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747
from src.multimap_resolver import MultimapResolvingStrategy
4848
from src.stats import combine_counts
4949
from src.barcode_calling import process_single_thread, process_in_parallel, get_umi_length
50+
from src.common import setup_worker_logging, _get_log_params
5051

5152

5253
logger = logging.getLogger('IsoQuant')
@@ -676,38 +677,17 @@ def create_output_dirs(args):
676677
os.makedirs(sample_aux_dir)
677678

678679

679-
def set_logger(args, logger_instance):
680-
if "debug" not in args.__dict__ or not args.debug:
681-
output_level = logging.INFO
682-
else:
683-
output_level = logging.DEBUG
684-
685-
logger_instance.setLevel(output_level)
680+
def set_logger(args):
681+
output_level = logging.DEBUG if args.__dict__.get('debug') else logging.INFO
686682
log_file = os.path.join(args.output, "isoquant.log")
687683
if os.path.exists(log_file):
688684
old_log_file = os.path.join(args.output, "isoquant.log.old")
689685
with open(old_log_file, "a") as olf:
690686
olf.write("\n")
691687
shutil.copyfileobj(open(log_file, "r"), olf)
692-
693-
f = open(log_file, "w")
694-
f.write("Command line: " + args._cmd_line + '\n')
695-
f.close()
696-
fh = logging.FileHandler(log_file)
697-
fh.set_name("isoquant_file_log")
698-
fh.setLevel(output_level)
699-
ch = logging.StreamHandler(sys.stdout)
700-
ch.set_name("isoquant_screen_log")
701-
ch.setLevel(logging.INFO)
702-
703-
formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
704-
fh.setFormatter(formatter)
705-
ch.setFormatter(formatter)
706-
if all(fh.get_name() != h.get_name() for h in logger_instance.handlers):
707-
logger_instance.addHandler(fh)
708-
if all(ch.get_name() != h.get_name() for h in logger_instance.handlers):
709-
logger_instance.addHandler(ch)
710-
688+
with open(log_file, "w") as f:
689+
f.write("Command line: " + args._cmd_line + '\n')
690+
setup_worker_logging(log_file, output_level)
711691
logger.info("Running IsoQuant version " + args._version)
712692

713693

@@ -1013,7 +993,10 @@ def call_barcodes(args):
1013993
# Read chunks are not cleared by the GC in the end of barcode calling, leaving the main
1014994
# IsoQuant process to consume ~2,5 GB even when barcode calling is done.
1015995
# Once 16 child processes are created later, IsoQuant instantly takes threads x 2,5 GB for nothing.
1016-
with ProcessPoolExecutor(max_workers=1) as proc:
996+
log_file, log_level = _get_log_params()
997+
with ProcessPoolExecutor(max_workers=1,
998+
initializer=setup_worker_logging,
999+
initargs=(log_file, log_level)) as proc:
10171000
logger.info("Detecting barcodes for %d file(s)" % len(input_files))
10181001
if bc_threads == 1:
10191002
future_res = proc.submit(process_single_thread, bc_args)
@@ -1117,7 +1100,7 @@ def main(cmd_args):
11171100
if not cmd_args:
11181101
parser.print_usage()
11191102
sys.exit(IsoQuantExitCode.SUCCESS)
1120-
set_logger(args, logger)
1103+
set_logger(args)
11211104
args = check_and_load_args(args, parser)
11221105
create_output_dirs(args)
11231106
set_additional_params(args)

src/barcode_calling/detect_barcodes.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727

2828
from ..modes import IsoQuantMode
2929
from ..error_codes import IsoQuantExitCode
30+
from ..common import setup_worker_logging, _get_log_params
3031
from .common import reverese_complement, load_barcodes
3132
from . import (
3233
TenXBarcodeDetector,
@@ -369,8 +370,14 @@ def _process_single_file_in_parallel(input_file, output_tsv, out_fasta, args, ba
369370
# Clean up parent memory before spawning workers
370371
gc.collect()
371372
mp_context = multiprocessing.get_context('spawn')
373+
log_file, log_level = _get_log_params()
372374
# max_tasks_per_child requires Python 3.11+
373-
executor_kwargs = {'max_workers': args.threads, 'mp_context': mp_context}
375+
executor_kwargs = {
376+
'max_workers': args.threads,
377+
'mp_context': mp_context,
378+
'initializer': setup_worker_logging,
379+
'initargs': (log_file, log_level),
380+
}
374381
if sys.version_info >= (3, 11):
375382
executor_kwargs['max_tasks_per_child'] = 20
376383
with concurrent.futures.ProcessPoolExecutor(**executor_kwargs) as proc:

src/common.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,53 @@
99
import os
1010
import re
1111
import subprocess
12+
import sys
1213
import math
1314
from collections import defaultdict
1415
from enum import Enum
1516

1617
logger = logging.getLogger('IsoQuant')
1718

1819

20+
def _get_log_params():
21+
"""Return (log_file, log_level) from the current IsoQuant logger configuration."""
22+
w = logging.getLogger('IsoQuant')
23+
log_level = w.level or logging.INFO
24+
log_file = None
25+
for h in w.handlers:
26+
if hasattr(h, 'baseFilename'):
27+
log_file = h.baseFilename
28+
break
29+
return log_file, log_level
30+
31+
32+
def setup_worker_logging(log_file, log_level):
33+
"""Initialize IsoQuant logging in a worker process.
34+
35+
Must be passed as the 'initializer' argument to every ProcessPoolExecutor.
36+
Works for both 'fork' (clears broken inherited handlers) and 'spawn'
37+
(creates handlers from scratch). Python 3.14 changed the default start
38+
method from 'fork' to 'spawn' on Linux, and also changed _at_fork_reinit
39+
behaviour so inherited handlers can break even with explicit fork.
40+
"""
41+
w = logging.getLogger('IsoQuant')
42+
for h in w.handlers[:]:
43+
w.removeHandler(h)
44+
w.setLevel(log_level)
45+
formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
46+
if log_file:
47+
fh = logging.FileHandler(log_file, mode='a')
48+
fh.set_name("isoquant_file_log")
49+
fh.setLevel(log_level)
50+
fh.setFormatter(formatter)
51+
w.addHandler(fh)
52+
ch = logging.StreamHandler(sys.stdout)
53+
ch.set_name("isoquant_screen_log")
54+
ch.setLevel(logging.INFO)
55+
ch.setFormatter(formatter)
56+
w.addHandler(ch)
57+
58+
1959
class CigarEvent(Enum):
2060
match = 0
2161
insertion = 1

src/dataset_processor.py

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
from pyfaidx import Fasta
2323

2424
from .modes import IsoQuantMode
25-
from .common import proper_plural_form, large_output_enabled
25+
from .common import proper_plural_form, large_output_enabled, setup_worker_logging, _get_log_params
2626
from .error_codes import IsoQuantExitCode
2727
from .serialization import *
2828
from .stats import EnumStats
@@ -319,7 +319,10 @@ def collect_reads(self, sample):
319319
# Clean up parent memory before spawning workers
320320
gc.collect()
321321
mp_context = multiprocessing.get_context('fork')
322-
with ProcessPoolExecutor(max_workers=self.args.threads, mp_context=mp_context) as proc:
322+
log_file, log_level = _get_log_params()
323+
with ProcessPoolExecutor(max_workers=self.args.threads, mp_context=mp_context,
324+
initializer=setup_worker_logging,
325+
initargs=(log_file, log_level)) as proc:
323326
results = proc.map(*read_gen, chunksize=1)
324327
else:
325328
results = map(*read_gen)
@@ -427,7 +430,10 @@ def process_assigned_reads(self, sample, dump_filename):
427430
# Clean up parent memory before spawning workers
428431
gc.collect()
429432
mp_context = multiprocessing.get_context('fork')
430-
with ProcessPoolExecutor(max_workers=self.args.threads, mp_context=mp_context) as proc:
433+
log_file, log_level = _get_log_params()
434+
with ProcessPoolExecutor(max_workers=self.args.threads, mp_context=mp_context,
435+
initializer=setup_worker_logging,
436+
initargs=(log_file, log_level)) as proc:
431437
results = proc.map(*model_gen, chunksize=1)
432438
else:
433439
results = map(*model_gen)
@@ -527,7 +533,10 @@ def filter_umis(self, sample):
527533
# Clean up parent memory before spawning workers
528534
gc.collect()
529535
mp_context = multiprocessing.get_context('fork')
530-
with ProcessPoolExecutor(max_workers=self.args.threads, mp_context=mp_context) as proc:
536+
log_file, log_level = _get_log_params()
537+
with ProcessPoolExecutor(max_workers=self.args.threads, mp_context=mp_context,
538+
initializer=setup_worker_logging,
539+
initargs=(log_file, log_level)) as proc:
531540
results = proc.map(*umi_gen, chunksize=1)
532541
else:
533542
results = map(*umi_gen)

src/table_splitter.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515

1616
import pysam
1717

18+
from .common import setup_worker_logging, _get_log_params
1819

1920
logger = logging.getLogger('IsoQuant')
2021

@@ -268,7 +269,10 @@ def split_read_table_parallel(sample, input_tsvs, split_reads_file_names, num_th
268269
# Clean up parent memory before spawning workers
269270
gc.collect()
270271
mp_context = multiprocessing.get_context('spawn')
271-
with ProcessPoolExecutor(max_workers=num_workers, mp_context=mp_context) as executor:
272+
log_file, log_level = _get_log_params()
273+
with ProcessPoolExecutor(max_workers=num_workers, mp_context=mp_context,
274+
initializer=setup_worker_logging,
275+
initargs=(log_file, log_level)) as executor:
272276
futures = []
273277
for worker_id, my_chrs in enumerate(chr_assignments):
274278
future = executor.submit(

0 commit comments

Comments
 (0)