[med-svn] [Git][med-team/khmer][master] 2 commits: Add support for Python 3.12
Andreas Tille (@tille)
gitlab at salsa.debian.org
Wed Nov 29 14:51:08 GMT 2023
Andreas Tille pushed to branch master at Debian Med / khmer
Commits:
30c5f78d by Andreas Tille at 2023-11-29T15:39:17+01:00
Add support for Python 3.12
- - - - -
c7d0662b by Andreas Tille at 2023-11-29T15:50:20+01:00
Apply second patch belonging to Python3.12 port
- - - - -
4 changed files:
- debian/changelog
- + debian/patches/close-opened-files.patch
- + debian/patches/python3.12-support.patch
- debian/patches/series
Changes:
=====================================
debian/changelog
=====================================
@@ -1,3 +1,13 @@
+khmer (3.0.0~a3+dfsg-6) UNRELEASED; urgency=medium
+
+ * Team upload.
+
+ [ Olivier Gayot ]
+ * Add support for Python 3.12
+ Closes: #1055687 (LP: #2044383)
+
+ -- Andreas Tille <tille at debian.org> Wed, 29 Nov 2023 15:38:38 +0100
+
khmer (3.0.0~a3+dfsg-5) unstable; urgency=medium
* Team Upload.
=====================================
debian/patches/close-opened-files.patch
=====================================
@@ -0,0 +1,1124 @@
+Description: ensure that Python scripts close files that they open for writing
+ Python scripts under scripts/ in the source tree do not consistently close
+ files that they open for writing. While some of the scripts use context
+ managers, most of them do not (or do so inconsistently).
+ In previous releases of Ubuntu, this apparently was not much of a concern.
+ However, Python 3.12 seems to be much less forgiving when files are not
+ properly closed. When running the test suite, many of the files that are not
+ explicitly closed appear truncated. This leads to various tests failing or
+ hanging and causing FTBFS when the test suite runs at build time.
+ .
+ Furthermore, khmer defines the get_file_writer() function, but it cannot be
+ consistently used as a context manager because it sometimes closes the
+ underlying file descriptor ; and sometimes does not depending on the
+ arguments.
+ .
+ Fixed by defining a new FileWriter context manager and ensuring that
+ each call to open() / get_file_writer() frees up resources properly.
+Author: Olivier Gayot <olivier.gayot at canonical.com>
+Bug-Ubuntu: https://launchpad.net/bugs/2044383
+Bug-Debian: https://bugs.debian.org/1055687
+Forwarded: https://github.com/dib-lab/khmer/pull/1922
+Last-Update: 2023-11-26
+---
+This patch header follows DEP-3: http://dep.debian.net/deps/dep3/
+Index: khmer-3.0.0~a3+dfsg/scripts/abundance-dist.py
+===================================================================
+--- khmer-3.0.0~a3+dfsg.orig/scripts/abundance-dist.py 2023-11-26 20:23:39.915485717 +0100
++++ khmer-3.0.0~a3+dfsg/scripts/abundance-dist.py 2023-11-26 20:23:39.911485747 +0100
+@@ -42,6 +42,7 @@
+ Use '-h' for parameter help.
+ """
+
++import contextlib
+ import sys
+ import csv
+ import khmer
+@@ -143,26 +144,28 @@
+ sys.exit(1)
+
+ if args.output_histogram_filename in ('-', '/dev/stdout'):
+- countgraph_fp = sys.stdout
++ countgraph_ctx = contextlib.nullcontext(enter_result=sys.stdout)
+ else:
+- countgraph_fp = open(args.output_histogram_filename, 'w')
+- countgraph_fp_csv = csv.writer(countgraph_fp)
+- # write headers:
+- countgraph_fp_csv.writerow(['abundance', 'count', 'cumulative',
+- 'cumulative_fraction'])
+-
+- sofar = 0
+- for _, i in enumerate(abundances):
+- if i == 0 and not args.output_zero:
+- continue
++ countgraph_ctx = open(args.output_histogram_filename, 'w')
+
+- sofar += i
+- frac = sofar / float(total)
++ with countgraph_ctx as countgraph_fp:
++ countgraph_fp_csv = csv.writer(countgraph_fp)
++ # write headers:
++ countgraph_fp_csv.writerow(['abundance', 'count', 'cumulative',
++ 'cumulative_fraction'])
++
++ sofar = 0
++ for _, i in enumerate(abundances):
++ if i == 0 and not args.output_zero:
++ continue
+
+- countgraph_fp_csv.writerow([_, i, sofar, round(frac, 3)])
++ sofar += i
++ frac = sofar / float(total)
+
+- if sofar == total:
+- break
++ countgraph_fp_csv.writerow([_, i, sofar, round(frac, 3)])
++
++ if sofar == total:
++ break
+
+
+ if __name__ == '__main__':
+Index: khmer-3.0.0~a3+dfsg/scripts/abundance-dist-single.py
+===================================================================
+--- khmer-3.0.0~a3+dfsg.orig/scripts/abundance-dist-single.py 2023-11-26 20:23:39.915485717 +0100
++++ khmer-3.0.0~a3+dfsg/scripts/abundance-dist-single.py 2023-11-26 20:23:39.911485747 +0100
+@@ -218,6 +218,10 @@
+
+ log_info('wrote to: {output}', output=args.output_histogram_filename)
+
++ # Ensure that the output files are properly written. Python 3.12 seems to
++ # be less forgiving here ..
++ hist_fp.close()
++
+
+ if __name__ == '__main__':
+ main()
+Index: khmer-3.0.0~a3+dfsg/scripts/do-partition.py
+===================================================================
+--- khmer-3.0.0~a3+dfsg.orig/scripts/do-partition.py 2023-11-26 20:23:39.915485717 +0100
++++ khmer-3.0.0~a3+dfsg/scripts/do-partition.py 2023-11-26 20:23:39.911485747 +0100
+@@ -168,8 +168,8 @@
+ worker_q.put((nodegraph, _, start, end))
+
+ print('enqueued %d subset tasks' % n_subsets, file=sys.stderr)
+- open('%s.info' % args.graphbase, 'w').write('%d subsets total\n'
+- % (n_subsets))
++ with open('%s.info' % args.graphbase, 'w') as info_fp:
++ info_fp.write('%d subsets total\n' % (n_subsets))
+
+ if n_subsets < args.threads:
+ args.threads = n_subsets
+Index: khmer-3.0.0~a3+dfsg/scripts/extract-long-sequences.py
+===================================================================
+--- khmer-3.0.0~a3+dfsg.orig/scripts/extract-long-sequences.py 2023-11-26 20:23:39.915485717 +0100
++++ khmer-3.0.0~a3+dfsg/scripts/extract-long-sequences.py 2023-11-26 20:23:39.911485747 +0100
+@@ -52,7 +52,7 @@
+ import sys
+ from khmer import __version__
+ from khmer.utils import write_record
+-from khmer.kfile import add_output_compression_type, get_file_writer
++from khmer.kfile import add_output_compression_type, FileWriter
+ from khmer.khmer_args import sanitize_help, KhmerArgumentParser
+
+
+@@ -81,12 +81,12 @@
+
+ def main():
+ args = sanitize_help(get_parser()).parse_args()
+- outfp = get_file_writer(args.output, args.gzip, args.bzip)
+- for filename in args.input_filenames:
+- for record in screed.open(filename):
+- if len(record['sequence']) >= args.length:
+- write_record(record, outfp)
+- print('wrote to: ' + args.output.name, file=sys.stderr)
++ with FileWriter(args.output, args.gzip, args.bzip) as outfp:
++ for filename in args.input_filenames:
++ for record in screed.open(filename):
++ if len(record['sequence']) >= args.length:
++ write_record(record, outfp)
++ print('wrote to: ' + args.output.name, file=sys.stderr)
+
+
+ if __name__ == '__main__':
+Index: khmer-3.0.0~a3+dfsg/khmer/kfile.py
+===================================================================
+--- khmer-3.0.0~a3+dfsg.orig/khmer/kfile.py 2023-11-26 20:23:39.915485717 +0100
++++ khmer-3.0.0~a3+dfsg/khmer/kfile.py 2023-11-26 20:23:39.911485747 +0100
+@@ -35,6 +35,7 @@
+ """File handling/checking utilities for command-line scripts."""
+
+
++import contextlib
+ import os
+ import sys
+ import errno
+@@ -252,3 +253,37 @@
+ ofile = file_handle
+
+ return ofile
++
++
++ at contextlib.contextmanager
++def FileWriter(file_handle, do_gzip, do_bzip, *, steal_ownership=False):
++ """Alternative to get_file_writer that requires the use of a with block.
++ The intent is to address an inherent problem with get_file_writer() that
++ makes it difficult to use as a context manager. When get_file_writer() is
++ called with both gzip=False and bzip=False, the underlying file handle is
++ returned. As a consequence, doing:
++ > with get_file_writer(sys.stdout, bzip=False, gzip=False) as fh:
++ > pass
++ ends up closing sys.stdout when the with block is exited. Using the
++ function without a context manager avoids the issue, but then it results in
++ leaked open files when either bzip=True or gzip=True.
++ FileWriter must be used as a context manager, but it ensures that resources
++ are closed upon exiting the with block. Furthermore, it can be explicitly
++ requested to close the underlying file_handle."""
++ ofile = None
++
++ if do_gzip and do_bzip:
++ raise ValueError("Cannot specify both bzip and gzip compression!")
++
++ if do_gzip:
++ ofile = gzip.GzipFile(fileobj=file_handle, mode='w')
++ elif do_bzip:
++ ofile = bz2.open(file_handle, mode='w')
++ else:
++ ofile = contextlib.nullcontext(enter_result=file_handle)
++
++ with ofile as x:
++ yield x
++
++ if steal_ownership:
++ file_handle.close()
+Index: khmer-3.0.0~a3+dfsg/scripts/extract-paired-reads.py
+===================================================================
+--- khmer-3.0.0~a3+dfsg.orig/scripts/extract-paired-reads.py 2023-11-26 20:23:39.915485717 +0100
++++ khmer-3.0.0~a3+dfsg/scripts/extract-paired-reads.py 2023-11-26 20:23:39.911485747 +0100
+@@ -44,6 +44,7 @@
+
+ Reads FASTQ and FASTA input, retains format for output.
+ """
++from contextlib import nullcontext
+ import sys
+ import os.path
+ import textwrap
+@@ -53,7 +54,7 @@
+ from khmer.khmer_args import sanitize_help, KhmerArgumentParser
+ from khmer.khmer_args import FileType as khFileType
+ from khmer.kfile import add_output_compression_type
+-from khmer.kfile import get_file_writer
++from khmer.kfile import FileWriter
+
+ from khmer.utils import broken_paired_reader, write_record, write_record_pair
+
+@@ -132,39 +133,40 @@
+
+ # OVERRIDE default output file locations with -p, -s
+ if args.output_paired:
+- paired_fp = get_file_writer(args.output_paired, args.gzip, args.bzip)
+- out2 = paired_fp.name
++ paired_ctx = FileWriter(args.output_paired, args.gzip, args.bzip)
++ out2 = args.output_paired.name
+ else:
+ # Don't override, just open the default filename from above
+- paired_fp = get_file_writer(open(out2, 'wb'), args.gzip, args.bzip)
++ paired_ctx = FileWriter(open(out2, 'wb'), args.gzip, args.bzip,
++ steal_ownership=True)
+ if args.output_single:
+- single_fp = get_file_writer(args.output_single, args.gzip, args.bzip)
++ single_ctx = FileWriter(args.output_single, args.gzip, args.bzip)
+ out1 = args.output_single.name
+ else:
+ # Don't override, just open the default filename from above
+- single_fp = get_file_writer(open(out1, 'wb'), args.gzip, args.bzip)
++ single_ctx = FileWriter(open(out1, 'wb'), args.gzip, args.bzip,
++ steal_ownership=True)
+
+- print('reading file "%s"' % infile, file=sys.stderr)
+- print('outputting interleaved pairs to "%s"' % out2, file=sys.stderr)
+- print('outputting orphans to "%s"' % out1, file=sys.stderr)
+-
+- n_pe = 0
+- n_se = 0
+-
+- reads = ReadParser(infile)
+- for index, is_pair, read1, read2 in broken_paired_reader(reads):
+- if index % 100000 == 0 and index > 0:
+- print('...', index, file=sys.stderr)
+-
+- if is_pair:
+- write_record_pair(read1, read2, paired_fp)
+- n_pe += 1
+- else:
+- write_record(read1, single_fp)
+- n_se += 1
+-
+- single_fp.close()
+- paired_fp.close()
++ with paired_ctx as paired_fp, single_ctx as single_fp:
++ print('reading file "%s"' % infile, file=sys.stderr)
++ print('outputting interleaved pairs to "%s"' % out2,
++ file=sys.stderr)
++ print('outputting orphans to "%s"' % out1, file=sys.stderr)
++
++ n_pe = 0
++ n_se = 0
++
++ reads = ReadParser(infile)
++ for index, is_pair, read1, read2 in broken_paired_reader(reads):
++ if index % 100000 == 0 and index > 0:
++ print('...', index, file=sys.stderr)
++
++ if is_pair:
++ write_record_pair(read1, read2, paired_fp)
++ n_pe += 1
++ else:
++ write_record(read1, single_fp)
++ n_se += 1
+
+ if n_pe == 0:
+ raise TypeError("no paired reads!? check file formats...")
+Index: khmer-3.0.0~a3+dfsg/scripts/extract-partitions.py
+===================================================================
+--- khmer-3.0.0~a3+dfsg.orig/scripts/extract-partitions.py 2023-11-26 20:23:39.915485717 +0100
++++ khmer-3.0.0~a3+dfsg/scripts/extract-partitions.py 2023-11-26 20:23:39.911485747 +0100
+@@ -301,10 +301,10 @@
+ args.max_size)
+
+ if args.output_unassigned:
+- ofile = open('%s.unassigned.%s' % (args.prefix, suffix), 'wb')
+- unassigned_fp = get_file_writer(ofile, args.gzip, args.bzip)
+- extractor.process_unassigned(unassigned_fp)
+- unassigned_fp.close()
++ with open('%s.unassigned.%s' % (args.prefix, suffix), 'wb') as ofile:
++ unassigned_fp = get_file_writer(ofile, args.gzip, args.bzip)
++ extractor.process_unassigned(unassigned_fp)
++ unassigned_fp.close()
+ else:
+ extractor.process_unassigned()
+
+@@ -320,13 +320,21 @@
+ print('nothing to output; exiting!', file=sys.stderr)
+ return
+
++ to_close = []
+ # open a bunch of output files for the different groups
+ group_fps = {}
+ for index in range(extractor.group_n):
+ fname = '%s.group%04d.%s' % (args.prefix, index, suffix)
+- group_fp = get_file_writer(open(fname, 'wb'), args.gzip,
++ back_fp = open(fname, 'wb')
++ group_fp = get_file_writer(back_fp, args.gzip,
+ args.bzip)
+ group_fps[index] = group_fp
++ # It feels more natural to close the writer before closing the
++ # underlying file. fp.close() is theoretically idempotent, so it should
++ # be fine even though sometimes get_file_writer "steals" ownership of
++ # the underlying stream.
++ to_close.append(group_fp)
++ to_close.append(back_fp)
+
+ # write 'em all out!
+ # refresh the generator
+@@ -351,6 +359,9 @@
+ args.prefix,
+ suffix), file=sys.stderr)
+
++ for fp in to_close:
++ fp.close()
++
+
+ if __name__ == '__main__':
+ main()
+Index: khmer-3.0.0~a3+dfsg/scripts/fastq-to-fasta.py
+===================================================================
+--- khmer-3.0.0~a3+dfsg.orig/scripts/fastq-to-fasta.py 2023-11-26 20:23:39.915485717 +0100
++++ khmer-3.0.0~a3+dfsg/scripts/fastq-to-fasta.py 2023-11-26 20:23:39.911485747 +0100
+@@ -45,7 +45,7 @@
+ import sys
+ import screed
+ from khmer import __version__
+-from khmer.kfile import (add_output_compression_type, get_file_writer,
++from khmer.kfile import (add_output_compression_type, FileWriter,
+ describe_file_handle)
+ from khmer.utils import write_record
+ from khmer.khmer_args import sanitize_help, KhmerArgumentParser
+@@ -74,21 +74,21 @@
+ args = sanitize_help(get_parser()).parse_args()
+
+ print('fastq from ', args.input_sequence, file=sys.stderr)
+- outfp = get_file_writer(args.output, args.gzip, args.bzip)
+- n_count = 0
+- for n, record in enumerate(screed.open(args.input_sequence)):
+- if n % 10000 == 0:
+- print('...', n, file=sys.stderr)
+-
+- sequence = record['sequence']
+-
+- if 'N' in sequence:
+- if not args.n_keep:
+- n_count += 1
+- continue
++ with FileWriter(args.output, args.gzip, args.bzip) as outfp:
++ n_count = 0
++ for n, record in enumerate(screed.open(args.input_sequence)):
++ if n % 10000 == 0:
++ print('...', n, file=sys.stderr)
++
++ sequence = record['sequence']
++
++ if 'N' in sequence:
++ if not args.n_keep:
++ n_count += 1
++ continue
+
+- del record['quality']
+- write_record(record, outfp)
++ del record['quality']
++ write_record(record, outfp)
+
+ print('\n' + 'lines from ' + args.input_sequence, file=sys.stderr)
+
+Index: khmer-3.0.0~a3+dfsg/scripts/filter-abund.py
+===================================================================
+--- khmer-3.0.0~a3+dfsg.orig/scripts/filter-abund.py 2023-11-26 20:23:39.915485717 +0100
++++ khmer-3.0.0~a3+dfsg/scripts/filter-abund.py 2023-11-26 20:23:39.911485747 +0100
+@@ -44,6 +44,7 @@
+
+ Use '-h' for parameter help.
+ """
++from contextlib import nullcontext
+ import sys
+ import os
+ import textwrap
+@@ -56,7 +57,7 @@
+ sanitize_help, check_argument_range)
+ from khmer.khmer_args import FileType as khFileType
+ from khmer.kfile import (check_input_files, check_space,
+- add_output_compression_type, get_file_writer)
++ add_output_compression_type, FileWriter)
+ from khmer.khmer_logger import (configure_logging, log_info, log_error,
+ log_warn)
+ from khmer.trimming import (trim_record)
+@@ -137,31 +138,38 @@
+
+ if args.single_output_file:
+ outfile = args.single_output_file.name
+- outfp = get_file_writer(args.single_output_file, args.gzip, args.bzip)
++ out_single_ctx = FileWriter(args.single_output_file, args.gzip,
++ args.bzip)
++ else:
++ out_single_ctx = nullcontext()
++
++ with out_single_ctx as out_single_fp:
++ # the filtering loop
++ for infile in infiles:
++ log_info('filtering {infile}', infile=infile)
++ if not args.single_output_file:
++ outfile = os.path.basename(infile) + '.abundfilt'
++ out_ctx = FileWriter(open(outfile, 'wb'), args.gzip,
++ args.bzip, steal_ownership=True)
++ else:
++ out_ctx = nullcontext(enter_result=out_single_fp)
++
++ paired_iter = broken_paired_reader(ReadParser(infile),
++ min_length=ksize,
++ force_single=True)
++
++ with out_ctx as outfp:
++ for n, is_pair, read1, read2 in paired_iter:
++ assert not is_pair
++ assert read2 is None
++
++ trimmed_record, _ = trim_record(countgraph, read1, args.cutoff,
++ args.variable_coverage,
++ args.normalize_to)
++ if trimmed_record:
++ write_record(trimmed_record, outfp)
+
+- # the filtering loop
+- for infile in infiles:
+- log_info('filtering {infile}', infile=infile)
+- if not args.single_output_file:
+- outfile = os.path.basename(infile) + '.abundfilt'
+- outfp = open(outfile, 'wb')
+- outfp = get_file_writer(outfp, args.gzip, args.bzip)
+-
+- paired_iter = broken_paired_reader(ReadParser(infile),
+- min_length=ksize,
+- force_single=True)
+-
+- for n, is_pair, read1, read2 in paired_iter:
+- assert not is_pair
+- assert read2 is None
+-
+- trimmed_record, _ = trim_record(countgraph, read1, args.cutoff,
+- args.variable_coverage,
+- args.normalize_to)
+- if trimmed_record:
+- write_record(trimmed_record, outfp)
+-
+- log_info('output in {outfile}', outfile=outfile)
++ log_info('output in {outfile}', outfile=outfile)
+
+
+ if __name__ == '__main__':
+Index: khmer-3.0.0~a3+dfsg/scripts/filter-abund-single.py
+===================================================================
+--- khmer-3.0.0~a3+dfsg.orig/scripts/filter-abund-single.py 2023-11-26 20:23:39.915485717 +0100
++++ khmer-3.0.0~a3+dfsg/scripts/filter-abund-single.py 2023-11-26 20:23:39.911485747 +0100
+@@ -60,7 +60,7 @@
+ from khmer.kfile import (check_input_files, check_space,
+ check_space_for_graph,
+ add_output_compression_type,
+- get_file_writer)
++ FileWriter)
+ from khmer.khmer_logger import (configure_logging, log_info, log_error,
+ log_warn)
+ from khmer.trimming import (trim_record)
+@@ -160,22 +160,23 @@
+ outfile = os.path.basename(args.datafile) + '.abundfilt'
+ else:
+ outfile = args.outfile
+- outfp = open(outfile, 'wb')
+- outfp = get_file_writer(outfp, args.gzip, args.bzip)
+
+- paired_iter = broken_paired_reader(ReadParser(args.datafile),
+- min_length=graph.ksize(),
+- force_single=True)
+-
+- for n, is_pair, read1, read2 in paired_iter:
+- assert not is_pair
+- assert read2 is None
+-
+- trimmed_record, _ = trim_record(graph, read1, args.cutoff,
+- args.variable_coverage,
+- args.normalize_to)
+- if trimmed_record:
+- write_record(trimmed_record, outfp)
++ with FileWriter(open(outfile, 'wb'), args.gzip, args.bzip,
++ steal_ownership=True) as outfp:
++
++ paired_iter = broken_paired_reader(ReadParser(args.datafile),
++ min_length=graph.ksize(),
++ force_single=True)
++
++ for n, is_pair, read1, read2 in paired_iter:
++ assert not is_pair
++ assert read2 is None
++
++ trimmed_record, _ = trim_record(graph, read1, args.cutoff,
++ args.variable_coverage,
++ args.normalize_to)
++ if trimmed_record:
++ write_record(trimmed_record, outfp)
+
+ log_info('output in {outfile}', outfile=outfile)
+
+Index: khmer-3.0.0~a3+dfsg/scripts/filter-stoptags.py
+===================================================================
+--- khmer-3.0.0~a3+dfsg.orig/scripts/filter-stoptags.py 2023-11-26 20:23:39.915485717 +0100
++++ khmer-3.0.0~a3+dfsg/scripts/filter-stoptags.py 2023-11-26 20:23:39.911485747 +0100
+@@ -108,10 +108,9 @@
+ print('filtering', infile, file=sys.stderr)
+ outfile = os.path.basename(infile) + '.stopfilt'
+
+- outfp = open(outfile, 'w')
+-
+- tsp = ThreadedSequenceProcessor(process_fn)
+- tsp.start(verbose_loader(infile), outfp)
++ with open(outfile, 'w') as outfp:
++ tsp = ThreadedSequenceProcessor(process_fn)
++ tsp.start(verbose_loader(infile), outfp)
+
+ print('output in', outfile, file=sys.stderr)
+
+Index: khmer-3.0.0~a3+dfsg/scripts/interleave-reads.py
+===================================================================
+--- khmer-3.0.0~a3+dfsg.orig/scripts/interleave-reads.py 2023-11-26 20:23:39.915485717 +0100
++++ khmer-3.0.0~a3+dfsg/scripts/interleave-reads.py 2023-11-26 20:23:39.911485747 +0100
+@@ -52,7 +52,7 @@
+ from khmer.kfile import check_input_files, check_space
+ from khmer.khmer_args import sanitize_help, KhmerArgumentParser
+ from khmer.khmer_args import FileType as khFileType
+-from khmer.kfile import (add_output_compression_type, get_file_writer,
++from khmer.kfile import (add_output_compression_type, FileWriter,
+ describe_file_handle)
+ from khmer.utils import (write_record_pair, check_is_left, check_is_right,
+ check_is_pair)
+@@ -109,42 +109,41 @@
+
+ print("Interleaving:\n\t%s\n\t%s" % (s1_file, s2_file), file=sys.stderr)
+
+- outfp = get_file_writer(args.output, args.gzip, args.bzip)
+-
+- counter = 0
+- screed_iter_1 = screed.open(s1_file)
+- screed_iter_2 = screed.open(s2_file)
+- for read1, read2 in zip_longest(screed_iter_1, screed_iter_2):
+- if read1 is None or read2 is None:
+- print(("ERROR: Input files contain different number"
+- " of records."), file=sys.stderr)
+- sys.exit(1)
+-
+- if counter % 100000 == 0:
+- print('...', counter, 'pairs', file=sys.stderr)
+- counter += 1
+-
+- name1 = read1.name
+- name2 = read2.name
+-
+- if not args.no_reformat:
+- if not check_is_left(name1):
+- name1 += '/1'
+- if not check_is_right(name2):
+- name2 += '/2'
+-
+- read1.name = name1
+- read2.name = name2
+-
+- if not check_is_pair(read1, read2):
+- print("ERROR: This doesn't look like paired data! "
+- "%s %s" % (read1.name, read2.name), file=sys.stderr)
++ with FileWriter(args.output, args.gzip, args.bzip) as outfp:
++ counter = 0
++ screed_iter_1 = screed.open(s1_file)
++ screed_iter_2 = screed.open(s2_file)
++ for read1, read2 in zip_longest(screed_iter_1, screed_iter_2):
++ if read1 is None or read2 is None:
++ print(("ERROR: Input files contain different number"
++ " of records."), file=sys.stderr)
+ sys.exit(1)
+
+- write_record_pair(read1, read2, outfp)
++ if counter % 100000 == 0:
++ print('...', counter, 'pairs', file=sys.stderr)
++ counter += 1
++
++ name1 = read1.name
++ name2 = read2.name
++
++ if not args.no_reformat:
++ if not check_is_left(name1):
++ name1 += '/1'
++ if not check_is_right(name2):
++ name2 += '/2'
++
++ read1.name = name1
++ read2.name = name2
++
++ if not check_is_pair(read1, read2):
++ print("ERROR: This doesn't look like paired data! "
++ "%s %s" % (read1.name, read2.name), file=sys.stderr)
++ sys.exit(1)
++
++ write_record_pair(read1, read2, outfp)
+
+- print('final: interleaved %d pairs' % counter, file=sys.stderr)
+- print('output written to', describe_file_handle(outfp), file=sys.stderr)
++ print('final: interleaved %d pairs' % counter, file=sys.stderr)
++ print('output written to', describe_file_handle(outfp), file=sys.stderr)
+
+
+ if __name__ == '__main__':
+Index: khmer-3.0.0~a3+dfsg/scripts/normalize-by-median.py
+===================================================================
+--- khmer-3.0.0~a3+dfsg.orig/scripts/normalize-by-median.py 2023-11-26 20:23:39.915485717 +0100
++++ khmer-3.0.0~a3+dfsg/scripts/normalize-by-median.py 2023-11-26 20:23:39.911485747 +0100
+@@ -46,6 +46,7 @@
+ Use '-h' for parameter help.
+ """
+
++from contextlib import nullcontext
+ import sys
+ import screed
+ import os
+@@ -60,7 +61,7 @@
+ import argparse
+ from khmer.kfile import (check_space, check_space_for_graph,
+ check_valid_file_exists, add_output_compression_type,
+- get_file_writer, describe_file_handle)
++ FileWriter, describe_file_handle)
+ from khmer.utils import (write_record, broken_paired_reader, ReadBundle,
+ clean_input_reads)
+ from khmer.khmer_logger import (configure_logging, log_info, log_error)
+@@ -360,39 +361,43 @@
+ output_name = None
+
+ if args.single_output_file:
+- outfp = get_file_writer(args.single_output_file, args.gzip, args.bzip)
++ out_single_ctx = FileWriter(args.single_output_file, args.gzip, args.bzip)
+ else:
++ out_single_ctx = nullcontext()
+ if '-' in filenames or '/dev/stdin' in filenames:
+ print("Accepting input from stdin; output filename must "
+ "be provided with '-o'.", file=sys.stderr)
+ sys.exit(1)
+
+- #
+- # main loop: iterate over all files given, do diginorm.
+- #
+-
+- for filename, require_paired in files:
+- if not args.single_output_file:
+- output_name = os.path.basename(filename) + '.keep'
+- outfp = open(output_name, 'wb')
+- outfp = get_file_writer(outfp, args.gzip, args.bzip)
+-
+- # failsafe context manager in case an input file breaks
+- with catch_io_errors(filename, outfp, args.single_output_file,
+- args.force, corrupt_files):
+- screed_iter = clean_input_reads(screed.open(filename))
+- reader = broken_paired_reader(screed_iter, min_length=args.ksize,
+- force_single=force_single,
+- require_paired=require_paired)
+-
+- # actually do diginorm
+- for record in with_diagnostics(reader, filename):
+- if record is not None:
+- write_record(record, outfp)
+-
+- log_info('output in {name}', name=describe_file_handle(outfp))
++ with out_single_ctx as out_single_fp:
++ #
++ # main loop: iterate over all files given, do diginorm.
++ #
++ for filename, require_paired in files:
+ if not args.single_output_file:
+- outfp.close()
++ output_name = os.path.basename(filename) + '.keep'
++ out_ctx = FileWriter(open(output_name, 'wb'), args.gzip,
++ args.bzip, steal_ownership=True)
++ else:
++ out_ctx = nullcontext(enter_result=out_single_fp)
++
++ with out_ctx as outfp:
++ # failsafe context manager in case an input file breaks
++ with catch_io_errors(filename, outfp, args.single_output_file,
++ args.force, corrupt_files):
++ screed_iter = clean_input_reads(screed.open(filename))
++ reader = broken_paired_reader(screed_iter,
++ min_length=args.ksize,
++ force_single=force_single,
++ require_paired=require_paired)
++
++ # actually do diginorm
++ for record in with_diagnostics(reader, filename):
++ if record is not None:
++ write_record(record, outfp)
++
++ log_info('output in {name}',
++ name=describe_file_handle(outfp))
+
+ # finished - print out some diagnostics.
+
+Index: khmer-3.0.0~a3+dfsg/scripts/partition-graph.py
+===================================================================
+--- khmer-3.0.0~a3+dfsg.orig/scripts/partition-graph.py 2023-11-26 20:23:39.915485717 +0100
++++ khmer-3.0.0~a3+dfsg/scripts/partition-graph.py 2023-11-26 20:23:39.911485747 +0100
+@@ -143,7 +143,8 @@
+ worker_q.put((nodegraph, _, start, end))
+
+ print('enqueued %d subset tasks' % n_subsets, file=sys.stderr)
+- open('%s.info' % basename, 'w').write('%d subsets total\n' % (n_subsets))
++ with open('%s.info' % basename, 'w') as info_fp:
++ info_fp.write('%d subsets total\n' % (n_subsets))
+
+ n_threads = args.threads
+ if n_subsets < n_threads:
+Index: khmer-3.0.0~a3+dfsg/scripts/sample-reads-randomly.py
+===================================================================
+--- khmer-3.0.0~a3+dfsg.orig/scripts/sample-reads-randomly.py 2023-11-26 20:23:39.915485717 +0100
++++ khmer-3.0.0~a3+dfsg/scripts/sample-reads-randomly.py 2023-11-26 20:23:39.911485747 +0100
+@@ -47,6 +47,7 @@
+ """
+
+ import argparse
++from contextlib import nullcontext
+ import os.path
+ import random
+ import textwrap
+@@ -55,7 +56,7 @@
+ from khmer import __version__
+ from khmer import ReadParser
+ from khmer.kfile import (check_input_files, add_output_compression_type,
+- get_file_writer)
++ FileWriter)
+ from khmer.khmer_args import sanitize_help, KhmerArgumentParser
+ from khmer.utils import write_record, broken_paired_reader
+
+@@ -201,27 +202,27 @@
+ print('Writing %d sequences to %s' %
+ (len(reads[0]), output_filename), file=sys.stderr)
+
+- output_file = args.output_file
+- if not output_file:
+- output_file = open(output_filename, 'wb')
+-
+- output_file = get_file_writer(output_file, args.gzip, args.bzip)
+-
+- for records in reads[0]:
+- write_record(records[0], output_file)
+- if records[1] is not None:
+- write_record(records[1], output_file)
++ output_back_ctx = nullcontext(args.output_file)
++ if not args.output_file:
++ output_back_ctx = open(output_filename, 'wb')
++
++ with output_back_ctx as output_back_fp:
++ with FileWriter(output_back_fp, args.gzip, args.bzip) as output_fp:
++ for records in reads[0]:
++ write_record(records[0], output_fp)
++ if records[1] is not None:
++ write_record(records[1], output_fp)
+ else:
+ for n in range(num_samples):
+ n_filename = output_filename + '.%d' % n
+ print('Writing %d sequences to %s' %
+ (len(reads[n]), n_filename), file=sys.stderr)
+- output_file = get_file_writer(open(n_filename, 'wb'), args.gzip,
+- args.bzip)
+- for records in reads[n]:
+- write_record(records[0], output_file)
+- if records[1] is not None:
+- write_record(records[1], output_file)
++ with FileWriter(open(n_filename, 'wb'), args.gzip, args.bzip,
++ steal_ownership=True) as output_fp:
++ for records in reads[n]:
++ write_record(records[0], output_fp)
++ if records[1] is not None:
++ write_record(records[1], output_fp)
+
+
+ if __name__ == '__main__':
+Index: khmer-3.0.0~a3+dfsg/scripts/split-paired-reads.py
+===================================================================
+--- khmer-3.0.0~a3+dfsg.orig/scripts/split-paired-reads.py 2023-11-26 20:23:39.915485717 +0100
++++ khmer-3.0.0~a3+dfsg/scripts/split-paired-reads.py 2023-11-26 20:23:39.911485747 +0100
+@@ -44,6 +44,7 @@
+
+ Reads FASTQ and FASTA input, retains format for output.
+ """
++from contextlib import nullcontext
+ import sys
+ import os
+ import textwrap
+@@ -56,7 +57,7 @@
+ UnpairedReadsError)
+ from khmer.kfile import (check_input_files, check_space,
+ add_output_compression_type,
+- get_file_writer, describe_file_handle)
++ FileWriter, describe_file_handle)
+
+
+ def get_parser():
+@@ -145,22 +146,26 @@
+
+ # OVERRIDE output file locations with -1, -2
+ if args.output_first:
+- fp_out1 = get_file_writer(args.output_first, args.gzip, args.bzip)
+- out1 = fp_out1.name
++ out1_ctx = FileWriter(args.output_first, args.gzip, args.bzip)
++ out1 = args.output_first.name
+ else:
+ # Use default filename created above
+- fp_out1 = get_file_writer(open(out1, 'wb'), args.gzip, args.bzip)
++ out1_ctx = FileWriter(open(out1, 'wb'), args.gzip, args.bzip,
++ steal_ownership=True)
+ if args.output_second:
+- fp_out2 = get_file_writer(args.output_second, args.gzip, args.bzip)
+- out2 = fp_out2.name
++ out2_ctx = FileWriter(args.output_second, args.gzip, args.bzip)
++ out2 = args.output_second.name
+ else:
+ # Use default filename created above
+- fp_out2 = get_file_writer(open(out2, 'wb'), args.gzip, args.bzip)
++ out2_ctx = FileWriter(open(out2, 'wb'), args.gzip, args.bzip,
++ steal_ownership=True)
+
+ # put orphaned reads here, if -0!
+ if args.output_orphaned:
+- fp_out0 = get_file_writer(args.output_orphaned, args.gzip, args.bzip)
++ out0_ctx = FileWriter(args.output_orphaned, args.gzip, args.bzip)
+ out0 = describe_file_handle(args.output_orphaned)
++ else:
++ out0_ctx = nullcontext()
+
+ counter1 = 0
+ counter2 = 0
+@@ -171,23 +176,24 @@
+ paired_iter = broken_paired_reader(ReadParser(infile),
+ require_paired=not args.output_orphaned)
+
+- try:
+- for index, is_pair, record1, record2 in paired_iter:
+- if index % 10000 == 0:
+- print('...', index, file=sys.stderr)
+-
+- if is_pair:
+- write_record(record1, fp_out1)
+- counter1 += 1
+- write_record(record2, fp_out2)
+- counter2 += 1
+- elif args.output_orphaned:
+- write_record(record1, fp_out0)
+- counter3 += 1
+- except UnpairedReadsError as e:
+- print("Unpaired reads found starting at {name}; exiting".format(
+- name=e.read1.name), file=sys.stderr)
+- sys.exit(1)
++ with out0_ctx as fp_out0, out1_ctx as fp_out1, out2_ctx as fp_out2:
++ try:
++ for index, is_pair, record1, record2 in paired_iter:
++ if index % 10000 == 0:
++ print('...', index, file=sys.stderr)
++
++ if is_pair:
++ write_record(record1, fp_out1)
++ counter1 += 1
++ write_record(record2, fp_out2)
++ counter2 += 1
++ elif args.output_orphaned:
++ write_record(record1, fp_out0)
++ counter3 += 1
++ except UnpairedReadsError as e:
++ print("Unpaired reads found starting at {name}; exiting".format(
++ name=e.read1.name), file=sys.stderr)
++ sys.exit(1)
+
+ print("DONE; split %d sequences (%d left, %d right, %d orphans)" %
+ (counter1 + counter2, counter1, counter2, counter3), file=sys.stderr)
+Index: khmer-3.0.0~a3+dfsg/scripts/trim-low-abund.py
+===================================================================
+--- khmer-3.0.0~a3+dfsg.orig/scripts/trim-low-abund.py 2023-11-26 20:23:39.915485717 +0100
++++ khmer-3.0.0~a3+dfsg/scripts/trim-low-abund.py 2023-11-26 20:28:16.389478687 +0100
+@@ -43,6 +43,7 @@
+
+ Use -h for parameter help.
+ """
++from contextlib import nullcontext
+ import csv
+ import sys
+ import os
+@@ -63,7 +64,7 @@
+ from khmer.utils import write_record, broken_paired_reader, ReadBundle
+ from khmer.kfile import (check_space, check_space_for_graph,
+ check_valid_file_exists, add_output_compression_type,
+- get_file_writer)
++ get_file_writer, FileWriter)
+ from khmer.khmer_logger import configure_logging, log_info, log_error
+ from khmer.trimming import trim_record
+
+@@ -374,108 +375,111 @@
+ # only create the file writer once if outfp is specified; otherwise,
+ # create it for each file.
+ if args.output:
+- trimfp = get_file_writer(args.output, args.gzip, args.bzip)
++ trim_ctx = FileWriter(args.output, args.gzip, args.bzip)
++ else:
++ trim_ctx = nullcontext()
+
+ pass2list = []
+- for filename in args.input_filenames:
+- # figure out temporary filename for 2nd pass
+- pass2filename = filename.replace(os.path.sep, '-') + '.pass2'
+- pass2filename = os.path.join(tempdir, pass2filename)
+- pass2fp = open(pass2filename, 'w')
+-
+- # construct output filenames
+- if args.output is None:
+- # note: this will be saved in trimfp.
+- outfp = open(os.path.basename(filename) + '.abundtrim', 'wb')
+-
+- # get file handle w/gzip, bzip
+- trimfp = get_file_writer(outfp, args.gzip, args.bzip)
+-
+- # record all this info
+- pass2list.append((filename, pass2filename, trimfp))
+-
+- # input file stuff: get a broken_paired reader.
+- paired_iter = broken_paired_reader(ReadParser(filename), min_length=K,
+- force_single=args.ignore_pairs)
+-
+- # main loop through the file.
+- n_start = trimmer.n_reads
+- save_start = trimmer.n_saved
+-
+- watermark = REPORT_EVERY_N_READS
+- for read in trimmer.pass1(paired_iter, pass2fp):
+- if (trimmer.n_reads - n_start) > watermark:
+- log_info("... {filename} {n_saved} {n_reads} {n_bp} "
+- "{w_reads} {w_bp}", filename=filename,
+- n_saved=trimmer.n_saved, n_reads=trimmer.n_reads,
+- n_bp=trimmer.n_bp, w_reads=written_reads,
+- w_bp=written_bp)
+- watermark += REPORT_EVERY_N_READS
+-
+- # write out the trimmed/etc sequences that AREN'T going to be
+- # revisited in a 2nd pass.
+- write_record(read, trimfp)
+- written_bp += len(read)
+- written_reads += 1
+- pass2fp.close()
+-
+- log_info("{filename}: kept aside {kept} of {total} from first pass",
+- filename=filename, kept=trimmer.n_saved - save_start,
+- total=trimmer.n_reads - n_start)
+-
+- # first pass goes across all the data, so record relevant stats...
+- n_reads = trimmer.n_reads
+- n_bp = trimmer.n_bp
+- n_skipped = trimmer.n_skipped
+- bp_skipped = trimmer.bp_skipped
+- save_pass2_total = trimmer.n_saved
+-
+- # ### SECOND PASS. ###
+-
+- # nothing should have been skipped yet!
+- assert trimmer.n_skipped == 0
+- assert trimmer.bp_skipped == 0
+-
+- if args.single_pass:
+- pass2list = []
+-
+- # go back through all the files again.
+- for _, pass2filename, trimfp in pass2list:
+- log_info('second pass: looking at sequences kept aside in {pass2}',
+- pass2=pass2filename)
+-
+- # note that for this second pass, we don't care about paired
+- # reads - they will be output in the same order they're read in,
+- # so pairs will stay together if not orphaned. This is in contrast
+- # to the first loop. Hence, force_single=True below.
+-
+- read_parser = ReadParser(pass2filename)
+- paired_iter = broken_paired_reader(read_parser,
+- min_length=K,
+- force_single=True)
+-
+- watermark = REPORT_EVERY_N_READS
+- for read in trimmer.pass2(paired_iter):
+- if (trimmer.n_reads - n_start) > watermark:
+- log_info('... x 2 {a} {b} {c} {d} {e} {f} {g}',
+- a=trimmer.n_reads - n_start,
+- b=pass2filename, c=trimmer.n_saved,
+- d=trimmer.n_reads, e=trimmer.n_bp,
+- f=written_reads, g=written_bp)
+- watermark += REPORT_EVERY_N_READS
+-
+- write_record(read, trimfp)
+- written_reads += 1
+- written_bp += len(read)
+-
+- read_parser.close()
+-
+- log_info('removing {pass2}', pass2=pass2filename)
+- os.unlink(pass2filename)
+-
+- # if we created our own trimfps, close 'em.
+- if not args.output:
+- trimfp.close()
++ with trim_ctx as trimfp:
++ for filename in args.input_filenames:
++ # figure out temporary filename for 2nd pass
++ pass2filename = filename.replace(os.path.sep, '-') + '.pass2'
++ pass2filename = os.path.join(tempdir, pass2filename)
++ pass2fp = open(pass2filename, 'w')
++
++ # construct output filenames
++ if args.output is None:
++ # note: this will be saved in trimfp.
++ outfp = open(os.path.basename(filename) + '.abundtrim', 'wb')
++
++ # get file handle w/gzip, bzip
++ trimfp = get_file_writer(outfp, args.gzip, args.bzip)
++
++ # record all this info
++ pass2list.append((filename, pass2filename, trimfp))
++
++ # input file stuff: get a broken_paired reader.
++ paired_iter = broken_paired_reader(ReadParser(filename), min_length=K,
++ force_single=args.ignore_pairs)
++
++ # main loop through the file.
++ n_start = trimmer.n_reads
++ save_start = trimmer.n_saved
++
++ watermark = REPORT_EVERY_N_READS
++ for read in trimmer.pass1(paired_iter, pass2fp):
++ if (trimmer.n_reads - n_start) > watermark:
++ log_info("... {filename} {n_saved} {n_reads} {n_bp} "
++ "{w_reads} {w_bp}", filename=filename,
++ n_saved=trimmer.n_saved, n_reads=trimmer.n_reads,
++ n_bp=trimmer.n_bp, w_reads=written_reads,
++ w_bp=written_bp)
++ watermark += REPORT_EVERY_N_READS
++
++ # write out the trimmed/etc sequences that AREN'T going to be
++ # revisited in a 2nd pass.
++ write_record(read, trimfp)
++ written_bp += len(read)
++ written_reads += 1
++ pass2fp.close()
++
++ log_info("{filename}: kept aside {kept} of {total} from first pass",
++ filename=filename, kept=trimmer.n_saved - save_start,
++ total=trimmer.n_reads - n_start)
++
++ # first pass goes across all the data, so record relevant stats...
++ n_reads = trimmer.n_reads
++ n_bp = trimmer.n_bp
++ n_skipped = trimmer.n_skipped
++ bp_skipped = trimmer.bp_skipped
++ save_pass2_total = trimmer.n_saved
++
++ # ### SECOND PASS. ###
++
++ # nothing should have been skipped yet!
++ assert trimmer.n_skipped == 0
++ assert trimmer.bp_skipped == 0
++
++ if args.single_pass:
++ pass2list = []
++
++ # go back through all the files again.
++ for _, pass2filename, trimfp in pass2list:
++ log_info('second pass: looking at sequences kept aside in {pass2}',
++ pass2=pass2filename)
++
++ # note that for this second pass, we don't care about paired
++ # reads - they will be output in the same order they're read in,
++ # so pairs will stay together if not orphaned. This is in contrast
++ # to the first loop. Hence, force_single=True below.
++
++ read_parser = ReadParser(pass2filename)
++ paired_iter = broken_paired_reader(read_parser,
++ min_length=K,
++ force_single=True)
++
++ watermark = REPORT_EVERY_N_READS
++ for read in trimmer.pass2(paired_iter):
++ if (trimmer.n_reads - n_start) > watermark:
++ log_info('... x 2 {a} {b} {c} {d} {e} {f} {g}',
++ a=trimmer.n_reads - n_start,
++ b=pass2filename, c=trimmer.n_saved,
++ d=trimmer.n_reads, e=trimmer.n_bp,
++ f=written_reads, g=written_bp)
++ watermark += REPORT_EVERY_N_READS
++
++ write_record(read, trimfp)
++ written_reads += 1
++ written_bp += len(read)
++
++ read_parser.close()
++
++ log_info('removing {pass2}', pass2=pass2filename)
++ os.unlink(pass2filename)
++
++ # if we created our own trimfps, close 'em.
++ if not args.output:
++ trimfp.close()
+
+ try:
+ log_info('removing temp directory & contents ({temp})', temp=tempdir)
=====================================
debian/patches/python3.12-support.patch
=====================================
@@ -0,0 +1,52 @@
+Description: Add support for Python 3.12
+ Ever since Python 3.2, configparser.SafeConfigParser has been deprecated in
+ favor of configparser.ConfigParser. An alias existed for backward
+ compatibility but the alias was dropped from Python 3.12. Let us now use
+ ConfigParser explicitly, and use .read_file() instead of .readfp() which was
+ dropped too.
+ .
+ The imp module has also been dropped, but a similar behavior can be achieved
+ using importlib.
+Author: Olivier Gayot <olivier.gayot at canonical.com>
+Bug-Ubuntu: https://launchpad.net/bugs/2044383
+Bug-Debian: https://bugs.debian.org/1055687
+Forwarded: https://github.com/dib-lab/khmer/pull/1922
+Last-Update: 2023-11-26
+---
+This patch header follows DEP-3: http://dep.debian.net/deps/dep3/
+Index: khmer-3.0.0~a3+dfsg/versioneer.py
+===================================================================
+--- khmer-3.0.0~a3+dfsg.orig/versioneer.py 2019-03-13 14:42:12.000000000 +0100
++++ khmer-3.0.0~a3+dfsg/versioneer.py 2023-11-23 15:19:50.025827413 +0100
+@@ -339,7 +339,7 @@
+ # configparser.NoOptionError (if it lacks "VCS="). See the docstring at
+ # the top of versioneer.py for instructions on writing your setup.cfg .
+ setup_cfg = os.path.join(root, "setup.cfg")
+- parser = configparser.SafeConfigParser()
++ parser = configparser.ConfigParser()
+ with open(setup_cfg, "r") as f:
+ parser.readfp(f)
+ VCS = parser.get("versioneer", "VCS") # mandatory
+Index: khmer-3.0.0~a3+dfsg/tests/test_sandbox_scripts.py
+===================================================================
+--- khmer-3.0.0~a3+dfsg.orig/tests/test_sandbox_scripts.py 2023-11-26 15:33:13.654983243 +0100
++++ khmer-3.0.0~a3+dfsg/tests/test_sandbox_scripts.py 2023-11-26 15:33:13.650983273 +0100
+@@ -42,7 +42,7 @@
+ from io import StringIO
+ import traceback
+ import glob
+-import imp
++import importlib
+
+ import pytest
+
+@@ -77,7 +77,8 @@
+ @pytest.mark.parametrize("filename", _sandbox_scripts())
+ def test_import_succeeds(filename, tmpdir, capsys):
+ try:
+- mod = imp.load_source('__zzz', filename)
++ loader = importlib.machinery.SourceFileLoader('__zzz', filename)
++ mod = loader.load_module()
+ except:
+ print(traceback.format_exc())
+ raise AssertionError("%s cannot be imported" % (filename,))
=====================================
debian/patches/series
=====================================
@@ -17,3 +17,5 @@ bz2.patch
stringio-buffer.patch
refresh_cython
find_object_files_at_right_loc.patch
+python3.12-support.patch
+close-opened-files.patch
View it on GitLab: https://salsa.debian.org/med-team/khmer/-/compare/a87e26dbb28e49f595447311e0194b82dc794c73...c7d0662b057724ef642049986859d5934d927959
--
View it on GitLab: https://salsa.debian.org/med-team/khmer/-/compare/a87e26dbb28e49f595447311e0194b82dc794c73...c7d0662b057724ef642049986859d5934d927959
You're receiving this email because of your account on salsa.debian.org.
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://alioth-lists.debian.net/pipermail/debian-med-commit/attachments/20231129/9cc16ca8/attachment-0001.htm>
More information about the debian-med-commit
mailing list