From 3fa608f54850d9a93e910cdc55fa1f87c4c87c7c Mon Sep 17 00:00:00 2001 From: mvdbeek Date: Mon, 13 Nov 2017 14:58:19 +0100 Subject: [PATCH 01/22] Use pysam.merge instead of samtools merge --- lib/galaxy/datatypes/binary.py | 53 +++++++++------------------------- 1 file changed, 14 insertions(+), 39 deletions(-) diff --git a/lib/galaxy/datatypes/binary.py b/lib/galaxy/datatypes/binary.py index 8148133decf..4fa9bd6b069 100644 --- a/lib/galaxy/datatypes/binary.py +++ b/lib/galaxy/datatypes/binary.py @@ -206,48 +206,23 @@ class Bam(Binary): MetadataElement(name="column_types", default=['str', 'int', 'str', 'int', 'int', 'str', 'str', 'int', 'int', 'str', 'str', 'str'], desc="Column types", param=metadata.ColumnTypesParameter, readonly=True, visible=False, no_value=[]) MetadataElement(name="column_names", default=['QNAME', 'FLAG', 'RNAME', 'POS', 'MAPQ', 'CIGAR', 'MRNM', 'MPOS', 'ISIZE', 'SEQ', 'QUAL', 'OPT'], desc="Column names", readonly=True, visible=False, optional=True, no_value=[]) - def _get_samtools_version(self): - version = '0.0.0' - samtools_exec = which('samtools') - if not samtools_exec: - message = 'Attempting to use functionality requiring samtools, but it cannot be located on Galaxy\'s PATH.' - raise Exception(message) - - p = subprocess.Popen(['samtools', '--version-only'], stdout=subprocess.PIPE, stderr=subprocess.PIPE) - output, error = p.communicate() - # --version-only is available - # Format is +htslib- - if p.returncode == 0: - version = output.split('+')[0] - return version - - output = subprocess.Popen(['samtools'], stderr=subprocess.PIPE, stdout=subprocess.PIPE).communicate()[1] - lines = output.split('\n') - for line in lines: - if line.lower().startswith('version'): - # Assuming line looks something like: version: 0.1.12a (r862) - version = line.split()[1] - break - return version - @staticmethod def merge(split_files, output_file): + """ + Merges Bam files - tmp_dir = tempfile.mkdtemp() - stderr_name = tempfile.NamedTemporaryFile(dir=tmp_dir, prefix="bam_merge_stderr").name - command = ["samtools", "merge", "-f", output_file] + split_files - proc = subprocess.Popen(args=command, stderr=open(stderr_name, 'wb')) - exit_code = proc.wait() - # Did merge succeed? - stderr = open(stderr_name).read().strip() - if stderr: - if exit_code != 0: - shutil.rmtree(tmp_dir) # clean up - raise Exception("Error merging BAM files: %s" % stderr) - else: - print(stderr) - os.unlink(stderr_name) - os.rmdir(tmp_dir) + :param split_files: list of files to merge + :param output_file: Write merge bam file to this location + + >>> from galaxy.datatypes.sniff import get_test_fname + >>> bamfile = get_test_fname('1.bam') + >>> out_dir = tempfile.mkdtemp() + >>> outpath = os.path.join(out_dir, 'out.bam') + >>> merge([bamfile, bamfile], outpath) + >>> assert int(pysam.view('-c', outpath).strip()) == 2 * int(pysam.view('-c', bamfile).strip()) + >>> shutil.rmtree(outpath, ignore_errors=True) + """ + pysam.merge('-O', 'BAM', output_file, *split_files) def _is_coordinate_sorted(self, file_name): """See if the input BAM file is sorted from the header information.""" From 16d0d3ecafb47ca8f112e8fa339f492446f9e80c Mon Sep 17 00:00:00 2001 From: mvdbeek Date: Mon, 13 Nov 2017 20:45:34 +0100 Subject: [PATCH 02/22] Add unsorted test bam --- lib/galaxy/datatypes/test/1.unsorted.bam | Bin 0 -> 4913 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 lib/galaxy/datatypes/test/1.unsorted.bam diff --git a/lib/galaxy/datatypes/test/1.unsorted.bam b/lib/galaxy/datatypes/test/1.unsorted.bam new file mode 100644 index 0000000000000000000000000000000000000000..354af1405689d3a3ad05be3b6d64bfeaa673870b GIT binary patch literal 4913 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 literal 0 HcmV?d00001 From 3c9999d89acfdd7aa1f56969ca7e03da3d83e655 Mon Sep 17 00:00:00 2001 From: mvdbeek Date: Tue, 14 Nov 2017 21:03:26 +0100 Subject: [PATCH 03/22] Use pysam for metadata setting and grooming There are also some noteworthy changes here: - We do always respect the sort-order specified in the header - If the sort order is not mentioned in the header or no header exists we coordinate-sort the file. - We do not use indexing to determine if a file is coordinate sorted, because this does not work reliably with samtools/pysam > 1.X, since arbitrarily sorted files can be indexed now. This also fixes advanced metadata setting (sort_order, bam_version and more), which appears to have been broken. This probably went by unnoticed because of the catch-all try-except-pass. The downside to fixing this is that I had to (temporarilly, hopefully) comment out the reference_names, reference_lengths, bam_header and readgroups attributes, because they led to the following error: ``` galaxy.model.metadata DEBUG 2017-11-19 14:47:30,540 loading metadata from file for: HistoryDatasetAssociation 582 galaxy.jobs.runners.local ERROR 2017-11-19 14:47:30,636 Job wrapper finish method failed Traceback (most recent call last): File "/Users/mvandenb/src/galaxy/lib/galaxy/jobs/runners/local.py", line 130, in queue_job job_wrapper.finish(stdout, stderr, exit_code) File "/Users/mvandenb/src/galaxy/lib/galaxy/jobs/__init__.py", line 1357, in finish self.sa_session.flush() File "/Users/mvandenb/src/galaxy/.venv/lib/python2.7/site-packages/sqlalchemy/orm/scoping.py", line 157, in do return getattr(self.registry(), name)(*args, **kwargs) File "/Users/mvandenb/src/galaxy/.venv/lib/python2.7/site-packages/sqlalchemy/orm/session.py", line 2019, in flush self._flush(objects) File "/Users/mvandenb/src/galaxy/.venv/lib/python2.7/site-packages/sqlalchemy/orm/session.py", line 2137, in _flush transaction.rollback(_capture_exception=True) File "/Users/mvandenb/src/galaxy/.venv/lib/python2.7/site-packages/sqlalchemy/util/langhelpers.py", line 60, in __exit__ compat.reraise(exc_type, exc_value, exc_tb) File "/Users/mvandenb/src/galaxy/.venv/lib/python2.7/site-packages/sqlalchemy/orm/session.py", line 2101, in _flush flush_context.execute() File "/Users/mvandenb/src/galaxy/.venv/lib/python2.7/site-packages/sqlalchemy/orm/unitofwork.py", line 373, in execute rec.execute(self) File "/Users/mvandenb/src/galaxy/.venv/lib/python2.7/site-packages/sqlalchemy/orm/unitofwork.py", line 532, in execute uow File "/Users/mvandenb/src/galaxy/.venv/lib/python2.7/site-packages/sqlalchemy/orm/persistence.py", line 170, in save_obj mapper, table, update) File "/Users/mvandenb/src/galaxy/.venv/lib/python2.7/site-packages/sqlalchemy/orm/persistence.py", line 706, in _emit_update_statements execute(statement, multiparams) File "/Users/mvandenb/src/galaxy/.venv/lib/python2.7/site-packages/sqlalchemy/engine/base.py", line 914, in execute return meth(self, multiparams, params) File "/Users/mvandenb/src/galaxy/.venv/lib/python2.7/site-packages/sqlalchemy/sql/elements.py", line 323, in _execute_on_connection return connection._execute_clauseelement(self, multiparams, params) File "/Users/mvandenb/src/galaxy/.venv/lib/python2.7/site-packages/sqlalchemy/engine/base.py", line 1010, in _execute_clauseelement compiled_sql, distilled_params File "/Users/mvandenb/src/galaxy/.venv/lib/python2.7/site-packages/sqlalchemy/engine/base.py", line 1146, in _execute_context context) File "/Users/mvandenb/src/galaxy/.venv/lib/python2.7/site-packages/sqlalchemy/engine/base.py", line 1341, in _handle_dbapi_exception exc_info File "/Users/mvandenb/src/galaxy/.venv/lib/python2.7/site-packages/sqlalchemy/util/compat.py", line 202, in raise_from_cause reraise(type(exception), exception, tb=exc_tb, cause=cause) File "/Users/mvandenb/src/galaxy/.venv/lib/python2.7/site-packages/sqlalchemy/engine/base.py", line 1139, in _execute_context context) File "/Users/mvandenb/src/galaxy/.venv/lib/python2.7/site-packages/sqlalchemy/engine/default.py", line 450, in do_execute cursor.execute(statement, parameters) OperationalError: (psycopg2.OperationalError) index row size 7208 exceeds maximum 2712 for index "ix_history_dataset_association_metadata" HINT: Values larger than 1/3 of a buffer page cannot be indexed. Consider a function index of an MD5 hash of the value, or use full text indexing. [SQL: 'UPDATE history_dataset_association SET update_time=%(update_time)s, blurb=%(blurb)s, peek=%(peek)s, metadata=%(_metadata)s WHERE history_dataset_association.id = %(history_dataset_association_id)s'] [parameters: {'_metadata': , 'update_time': datetime.datetime(2017, 11, 19, 13, 47, 30, 598058), 'history_dataset_association_id': 582, 'blurb': '3.5 KB', 'peek': 'Binary bam alignments file'}] ``` --- lib/galaxy/datatypes/binary.py | 203 ++++++++++------------- lib/galaxy/datatypes/test/1.bam | Bin 3592 -> 3537 bytes lib/galaxy/datatypes/test/1.unsorted.bam | Bin 4913 -> 4914 bytes 3 files changed, 92 insertions(+), 111 deletions(-) diff --git a/lib/galaxy/datatypes/binary.py b/lib/galaxy/datatypes/binary.py index 4fa9bd6b069..ce32ed28648 100644 --- a/lib/galaxy/datatypes/binary.py +++ b/lib/galaxy/datatypes/binary.py @@ -21,7 +21,7 @@ from bx.seq.twobit import TWOBIT_MAGIC_NUMBER, TWOBIT_MAGIC_NUMBER_SWAP, TWOBIT_ from galaxy import util from galaxy.datatypes import metadata from galaxy.datatypes.metadata import DictParameter, ListParameter, MetadataElement, MetadataParameter -from galaxy.util import FILENAME_VALID_CHARS, nice_size, sqlite, which +from galaxy.util import FILENAME_VALID_CHARS, nice_size, sqlite from galaxy.util.checkers import is_bz2, is_gzip from . import data, dataproviders @@ -211,156 +211,137 @@ class Bam(Binary): """ Merges Bam files - :param split_files: list of files to merge - :param output_file: Write merge bam file to this location + :param split_files: List of bam file paths to merge + :param output_file: Write merged bam file to this location >>> from galaxy.datatypes.sniff import get_test_fname >>> bamfile = get_test_fname('1.bam') >>> out_dir = tempfile.mkdtemp() >>> outpath = os.path.join(out_dir, 'out.bam') - >>> merge([bamfile, bamfile], outpath) + >>> Bam.merge([bamfile, bamfile], outpath) >>> assert int(pysam.view('-c', outpath).strip()) == 2 * int(pysam.view('-c', bamfile).strip()) >>> shutil.rmtree(outpath, ignore_errors=True) """ pysam.merge('-O', 'BAM', output_file, *split_files) - def _is_coordinate_sorted(self, file_name): - """See if the input BAM file is sorted from the header information.""" - output = subprocess.check_output(["samtools", "view", "-H", file_name]) - return 'SO:coordinate' in output or 'SO:sorted' in output + @staticmethod + def _is_coordinate_sorted(file_name): + """ + See if the input BAM file is sorted from the header information. + + >>> from galaxy.datatypes.sniff import get_test_fname + >>> bamfile = get_test_fname('1.bam') + >>> Bam._is_coordinate_sorted(bamfile) + True + >>> bamfile = get_test_fname('1.unsorted.bam') + >>> Bam._is_coordinate_sorted(bamfile) + False + """ + f = pysam.AlignmentFile(file_name) + coordinate_sorted = False + try: + coordinate_sorted = f.header['HD']['SO'] == 'coordinate' + except Exception: + pass + return coordinate_sorted def dataset_content_needs_grooming(self, file_name): - """See if file_name is a sorted BAM file""" - version = self._get_samtools_version() - if version < '0.1.13': - return not self._is_coordinate_sorted(file_name) - else: - # Samtools version 0.1.13 or newer produces an error condition when attempting to index an - # unsorted bam file - see http://biostar.stackexchange.com/questions/5273/is-my-bam-file-sorted. - # So when using a newer version of samtools, we'll first check if the input BAM file is sorted - # from the header information. If the header is present and sorted, we do nothing by returning False. - # If it's present and unsorted or if it's missing, we'll index the bam file to see if it produces the - # error. If it does, sorting is needed so we return True (otherwise False). - # - # TODO: we're creating an index file here and throwing it away. We then create it again when - # the set_meta() method below is called later in the job process. We need to enhance this overall - # process so we don't create an index twice. In order to make it worth the time to implement the - # upload tool / framework to allow setting metadata from directly within the tool itself, it should be - # done generically so that all tools will have the ability. In testing, a 6.6 gb BAM file took 128 - # seconds to index with samtools, and 45 minutes to sort, so indexing is relatively inexpensive. - if self._is_coordinate_sorted(file_name): - return False - index_name = tempfile.NamedTemporaryFile(prefix="bam_index").name - stderr_name = tempfile.NamedTemporaryFile(prefix="bam_index_stderr").name - proc = subprocess.Popen(['samtools', 'index', file_name, index_name], stderr=open(stderr_name, 'wb')) - proc.wait() - stderr = open(stderr_name).read().strip() - if stderr: - try: - os.unlink(index_name) - except OSError: - pass - try: - os.unlink(stderr_name) - except OSError: - pass - # Return True if unsorted error condition is found (find returns -1 if string is not found). - return stderr.find("[bam_index_core] the alignment is not sorted") != -1 - try: - os.unlink(index_name) - except OSError: - pass - try: - os.unlink(stderr_name) - except OSError: - pass - return False + """ + See if file_name is a coordinate-sorted BAM file + + >>> from galaxy.datatypes.sniff import get_test_fname + >>> bamfile = get_test_fname('1.bam') + >>> b = Bam() + >>> b.dataset_content_needs_grooming(bamfile) + False + >>> out_dir = tempfile.mkdtemp() + >>> qname_sorted = os.path.join(out_dir, 'qname_sorted.bam') + >>> _ = pysam.sort('-n', bamfile, '-o', qname_sorted ) + >>> assert b.dataset_content_needs_grooming(qname_sorted) == True + >>> shutil.rmtree(out_dir, ignore_errors=True) + >>> unsorted_bam = get_test_fname('1.unsorted.bam') + >>> assert b.dataset_content_needs_grooming(unsorted_bam) == True + """ + # We check if the input BAM file is coordinate-sorted from the header information. + return not self._is_coordinate_sorted(file_name) def groom_dataset_content(self, file_name): """ Ensures that the Bam file contents are sorted. This function is called on an output dataset after the content is initially generated. + + >>> from galaxy.datatypes.sniff import get_test_fname + >>> bamfile = get_test_fname('1.unsorted.bam') + >>> # Grooming happens in-place, so we copy the test dataset to a new location + >>> _, temp_path = tempfile.mkstemp() + >>> shutil.copy(bamfile, temp_path) + >>> b = Bam() + >>> b.groom_dataset_content(temp_path) + >>> b.dataset_content_needs_grooming(temp_path) + False + >>> os.remove(temp_path) """ - # Use samtools to sort the Bam file - # $ samtools sort - # Usage: samtools sort [-on] [-m ] - # Sort alignments by leftmost coordinates. File .bam will be created. - # This command may also create temporary files .%d.bam when the - # whole alignment cannot be fitted into memory ( controlled by option -m ). + # Use pysam to sort the Bam file + # This command may also creates temporary files .%d.bam when the + # whole alignment cannot fit into memory. # do this in a unique temp directory, because of possible .%d.bam temp files if not self.dataset_content_needs_grooming(file_name): # Don't re-sort if already sorted return tmp_dir = tempfile.mkdtemp() tmp_sorted_dataset_file_name_prefix = os.path.join(tmp_dir, 'sorted') - stderr_name = tempfile.NamedTemporaryFile(dir=tmp_dir, prefix="bam_sort_stderr").name - samtools_created_sorted_file_name = "%s.bam" % tmp_sorted_dataset_file_name_prefix # samtools accepts a prefix, not a filename, it always adds .bam to the prefix - proc = subprocess.Popen(['samtools', 'sort', file_name, tmp_sorted_dataset_file_name_prefix], - cwd=tmp_dir, stderr=open(stderr_name, 'wb')) - exit_code = proc.wait() - # Did sort succeed? - stderr = open(stderr_name).read().strip() - if stderr: - if exit_code != 0: - shutil.rmtree(tmp_dir) # clean up - raise Exception("Error Grooming BAM file contents: %s" % stderr) - else: - print(stderr) + sorted_file_name = "%s.bam" % tmp_sorted_dataset_file_name_prefix # samtools accepts a prefix, not a filename, it always adds .bam to the prefix + try: + pysam.sort(file_name, '-T', tmp_sorted_dataset_file_name_prefix, '-O', 'BAM', '-o', sorted_file_name) + except Exception: + shutil.rmtree(tmp_dir, ignore_errors=True) + raise # Move samtools_created_sorted_file_name to our output dataset location - shutil.move(samtools_created_sorted_file_name, file_name) + shutil.move(sorted_file_name, file_name) # Remove temp file and empty temporary directory - os.unlink(stderr_name) os.rmdir(tmp_dir) def init_meta(self, dataset, copy_from=None): Binary.init_meta(self, dataset, copy_from=copy_from) def set_meta(self, dataset, overwrite=True, **kwd): - """ Creates the index for the BAM file. """ + """ + Creates the index for the BAM file. + + >>> from galaxy.datatypes.sniff import get_test_fname + >>> from galaxy.util.bunch import Bunch + >>> dataset = Bunch() + >>> dataset.file_name = get_test_fname('1.bam') + >>> dataset.metadata = Bunch() + >>> dataset.metadata.bam_index = Bunch() + >>> _, dataset.metadata.bam_index.file_name = tempfile.mkstemp() + >>> b = Bam() + >>> b.set_meta(dataset=dataset) + >>> dataset.metadata.sort_order + 'coordinate' + >>> bam_file = pysam.AlignmentFile(dataset.file_name, mode='rb', index_filename=dataset.metadata.bam_index.file_name) + >>> bam_file.has_index() + True + >>> os.remove(dataset.metadata.bam_index.file_name) + """ # These metadata values are not accessible by users, always overwrite index_file = dataset.metadata.bam_index if not index_file: index_file = dataset.metadata.spec['bam_index'].param.new_file(dataset=dataset) - # Create the Bam index - # $ samtools index - # Usage: samtools index [] - stderr_name = tempfile.NamedTemporaryFile(prefix="bam_index_stderr").name - command = ['samtools', 'index', dataset.file_name, index_file.file_name] - exit_code = subprocess.call(args=command, stderr=open(stderr_name, 'wb')) - # Did index succeed? - if exit_code == -6: - # SIGABRT, most likely samtools 1.0+ which does not accept the index name parameter. - dataset_symlink = os.path.join(os.path.dirname(index_file.file_name), - '__dataset_%d_%s' % (dataset.id, os.path.basename(index_file.file_name))) - os.symlink(dataset.file_name, dataset_symlink) - try: - command = ['samtools', 'index', dataset_symlink] - exit_code = subprocess.call(args=command, stderr=open(stderr_name, 'wb')) - shutil.move(dataset_symlink + '.bai', index_file.file_name) - except Exception as e: - open(stderr_name, 'ab+').write('Galaxy attempted to build the BAM index with samtools 1.0+ but failed: %s\n' % e) - exit_code = 1 # Make sure an exception raised by shutil.move() is re-raised below - finally: - os.unlink(dataset_symlink) - stderr = open(stderr_name).read().strip() - if stderr: - if exit_code != 0: - os.unlink(stderr_name) # clean up - raise Exception("Error Setting BAM Metadata: %s" % stderr) - else: - print(stderr) + pysam.index(dataset.file_name, index_file.file_name) dataset.metadata.bam_index = index_file - # Remove temp file - os.unlink(stderr_name) # Now use pysam with BAI index to determine additional metadata try: bam_file = pysam.AlignmentFile(dataset.file_name, mode='rb', index_filename=index_file.file_name) - dataset.metadata.reference_names = list(bam_file.references) - dataset.metadata.reference_lengths = list(bam_file.lengths) - dataset.metadata.bam_header = bam_file.header - dataset.metadata.read_groups = [read_group['ID'] for read_group in dataset.metadata.bam_header.get('RG', []) if 'ID' in read_group] - dataset.metadata.sort_order = dataset.metadata.bam_header.get('HD', {}).get('SO', None) - dataset.metadata.bam_version = dataset.metadata.bam_header.get('HD', {}).get('VN', None) + # Reference names, lengths, read_groups and headers can become very large, + # but even small files error out with + # OperationalError: (psycopg2.OperationalError) index row size 3616 exceeds maximum 2712 for index "ix_history_dataset_association_metadata" + # dataset.metadata.reference_names = list(bam_file.references) + # dataset.metadata.reference_lengths = list(bam_file.lengths) + # dataset.metadata.bam_header = bam_file.header + # dataset.metadata.read_groups = [read_group['ID'] for read_group in dataset.metadata.bam_header.get('RG', []) if 'ID' in read_group] + dataset.metadata.sort_order = bam_file.header.get('HD', {}).get('SO', None) + dataset.metadata.bam_version = bam_file.header.get('HD', {}).get('VN', None) except Exception: # Per Dan, don't log here because doing so will cause datasets that # fail metadata to end in the error state diff --git a/lib/galaxy/datatypes/test/1.bam b/lib/galaxy/datatypes/test/1.bam index 95c65de857205099e47815d35fa2ae4861d9fba4..00ad79a950ebdf69c73d792b05612e1935bc1412 100644 GIT binary patch literal 3537 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 O00RI3000000001Z^!VBU diff --git a/lib/galaxy/datatypes/test/1.unsorted.bam b/lib/galaxy/datatypes/test/1.unsorted.bam index 354af1405689d3a3ad05be3b6d64bfeaa673870b..a24874147b5e651e2c4c221c5ba06f873df53753 100644 GIT binary patch delta 460 zcmV;-0W*;)D&WF|Ymb28Hk?V+pS z!KIgi|A1Go2_xjxJ@&uwKl8A~!`?JAyMRPG!T#9~`O)U#ik~jAVv6_D&Q%9D8K}=SZ0Y=zY zh<{cPY!0*2!(c8BgVUq4YL?EX6Adp_#DT6vqTU4W=W*7 zY1Szr*B#d?R1wvQ*yhc6ViS*^A7pW3LPV;6U0i#Soi*AQ)vI5KhF+y9A zKELuJ<&BVKRt==iCB!+kZR`epv1)ZYW;g$~$(E#xP5VGkDIwhBUCL5ftV1#p80ARZ z0)yl#M(zv;h)+F3`d)g4G!?Z=YCTFoDZ1y8(ugulmQg?5ebYxfl70cCkzAUS5fdN~ CIp5s? delta 487 zcmVvZRT_rAXQ$6;-=zrG1p@X+ZK0DBsnAm#9fza|ky!S;jLdYLi-`)^199%#^ zLK3a-mgSD*{PV@T_xe*UujO@`lvSnmbgJa?E2UK9tR^Q<2myaixrPSaA2#s Date: Sun, 3 Dec 2017 22:11:52 +0100 Subject: [PATCH 04/22] Move Bam doctests to unittests --- lib/galaxy/datatypes/binary.py | 56 ++++---------------------- test/unit/datatypes/test_bam.py | 71 +++++++++++++++++++++++++++++++++ 2 files changed, 78 insertions(+), 49 deletions(-) create mode 100644 test/unit/datatypes/test_bam.py diff --git a/lib/galaxy/datatypes/binary.py b/lib/galaxy/datatypes/binary.py index ce32ed28648..64e61e16368 100644 --- a/lib/galaxy/datatypes/binary.py +++ b/lib/galaxy/datatypes/binary.py @@ -213,21 +213,13 @@ class Bam(Binary): :param split_files: List of bam file paths to merge :param output_file: Write merged bam file to this location - - >>> from galaxy.datatypes.sniff import get_test_fname - >>> bamfile = get_test_fname('1.bam') - >>> out_dir = tempfile.mkdtemp() - >>> outpath = os.path.join(out_dir, 'out.bam') - >>> Bam.merge([bamfile, bamfile], outpath) - >>> assert int(pysam.view('-c', outpath).strip()) == 2 * int(pysam.view('-c', bamfile).strip()) - >>> shutil.rmtree(outpath, ignore_errors=True) """ pysam.merge('-O', 'BAM', output_file, *split_files) @staticmethod def _is_coordinate_sorted(file_name): """ - See if the input BAM file is sorted from the header information. + Check if the input BAM file is sorted from the header information. >>> from galaxy.datatypes.sniff import get_test_fname >>> bamfile = get_test_fname('1.bam') @@ -247,20 +239,7 @@ class Bam(Binary): def dataset_content_needs_grooming(self, file_name): """ - See if file_name is a coordinate-sorted BAM file - - >>> from galaxy.datatypes.sniff import get_test_fname - >>> bamfile = get_test_fname('1.bam') - >>> b = Bam() - >>> b.dataset_content_needs_grooming(bamfile) - False - >>> out_dir = tempfile.mkdtemp() - >>> qname_sorted = os.path.join(out_dir, 'qname_sorted.bam') - >>> _ = pysam.sort('-n', bamfile, '-o', qname_sorted ) - >>> assert b.dataset_content_needs_grooming(qname_sorted) == True - >>> shutil.rmtree(out_dir, ignore_errors=True) - >>> unsorted_bam = get_test_fname('1.unsorted.bam') - >>> assert b.dataset_content_needs_grooming(unsorted_bam) == True + Check if file_name is a coordinate-sorted BAM file """ # We check if the input BAM file is coordinate-sorted from the header information. return not self._is_coordinate_sorted(file_name) @@ -305,25 +284,6 @@ class Bam(Binary): Binary.init_meta(self, dataset, copy_from=copy_from) def set_meta(self, dataset, overwrite=True, **kwd): - """ - Creates the index for the BAM file. - - >>> from galaxy.datatypes.sniff import get_test_fname - >>> from galaxy.util.bunch import Bunch - >>> dataset = Bunch() - >>> dataset.file_name = get_test_fname('1.bam') - >>> dataset.metadata = Bunch() - >>> dataset.metadata.bam_index = Bunch() - >>> _, dataset.metadata.bam_index.file_name = tempfile.mkstemp() - >>> b = Bam() - >>> b.set_meta(dataset=dataset) - >>> dataset.metadata.sort_order - 'coordinate' - >>> bam_file = pysam.AlignmentFile(dataset.file_name, mode='rb', index_filename=dataset.metadata.bam_index.file_name) - >>> bam_file.has_index() - True - >>> os.remove(dataset.metadata.bam_index.file_name) - """ # These metadata values are not accessible by users, always overwrite index_file = dataset.metadata.bam_index if not index_file: @@ -333,13 +293,11 @@ class Bam(Binary): # Now use pysam with BAI index to determine additional metadata try: bam_file = pysam.AlignmentFile(dataset.file_name, mode='rb', index_filename=index_file.file_name) - # Reference names, lengths, read_groups and headers can become very large, - # but even small files error out with - # OperationalError: (psycopg2.OperationalError) index row size 3616 exceeds maximum 2712 for index "ix_history_dataset_association_metadata" - # dataset.metadata.reference_names = list(bam_file.references) - # dataset.metadata.reference_lengths = list(bam_file.lengths) - # dataset.metadata.bam_header = bam_file.header - # dataset.metadata.read_groups = [read_group['ID'] for read_group in dataset.metadata.bam_header.get('RG', []) if 'ID' in read_group] + # TODO: Reference names, lengths, read_groups and headers can become very large, truncate when necessary + dataset.metadata.reference_names = list(bam_file.references) + dataset.metadata.reference_lengths = list(bam_file.lengths) + dataset.metadata.bam_header = bam_file.header + dataset.metadata.read_groups = [read_group['ID'] for read_group in dataset.metadata.bam_header.get('RG', []) if 'ID' in read_group] dataset.metadata.sort_order = bam_file.header.get('HD', {}).get('SO', None) dataset.metadata.bam_version = bam_file.header.get('HD', {}).get('VN', None) except Exception: diff --git a/test/unit/datatypes/test_bam.py b/test/unit/datatypes/test_bam.py new file mode 100644 index 00000000000..1c7d5198f63 --- /dev/null +++ b/test/unit/datatypes/test_bam.py @@ -0,0 +1,71 @@ +import os +import shutil +import tempfile +from contextlib import contextmanager + +import pysam + +from galaxy.datatypes.binary import Bam +from galaxy.util.bunch import Bunch + + +def test_merge_bam(): + with get_input_files('1.bam', '1.bam') as input_files, get_tmp_path() as outpath: + Bam.merge(input_files, outpath) + alignment_count_output = int(pysam.view('-c', outpath).strip()) + alignment_count_input = int(pysam.view('-c', input_files[0]).strip()) * 2 + assert alignment_count_input == alignment_count_output + + +def test_dataset_content_needs_grooming(): + b = Bam() + with get_input_files('1.bam') as input_files: + assert b.dataset_content_needs_grooming(input_files[0]) is False + with get_tmp_path() as qname_sorted: + pysam.sort('-n', input_files[0], '-o', qname_sorted) + assert b.dataset_content_needs_grooming(qname_sorted) is True + with get_input_files('1.unsorted.bam') as input_files: + assert b.dataset_content_needs_grooming(input_files[0]) is True + + +def test_groom_dataset_content(): + b = Bam() + with get_input_files('1.unsorted.bam') as input_files: + b.groom_dataset_content(input_files[0]) + assert b.dataset_content_needs_grooming(input_files[0]) is False + + +def test_set_meta(): + b = Bam() + dataset = Bunch() + with get_input_files('1.bam') as input_files, get_tmp_path() as index_path: + dataset.file_name = input_files[0] + dataset.metadata = Bunch() + dataset.metadata.bam_index = Bunch() + dataset.metadata.bam_index.file_name = index_path + b.set_meta(dataset=dataset) + assert dataset.metadata.sort_order == 'coordinate' + bam_file = pysam.AlignmentFile(dataset.file_name, mode='rb', + index_filename=dataset.metadata.bam_index.file_name) + assert bam_file.has_index() is True + + +@contextmanager +def get_tmp_path(): + _, path = tempfile.mkstemp() + os.remove(path) + yield path + os.remove(path) + + +@contextmanager +def get_input_files(*args): + # need to import here, otherwise get_test_fname is treated as a test + from galaxy.datatypes.sniff import get_test_fname + temp_dir = tempfile.mkdtemp() + test_files = [] + for file in args: + shutil.copy(get_test_fname(file), temp_dir) + test_files.append(os.path.join(temp_dir, file)) + yield test_files + shutil.rmtree(temp_dir, ignore_errors=True) From ae72e5668364f6f72861c21a62a641f9f65636e1 Mon Sep 17 00:00:00 2001 From: mvdbeek Date: Tue, 5 Dec 2017 10:50:52 +0100 Subject: [PATCH 05/22] Use subprocess to check if pysam.index succeeds Checking if pysam.index succeeds tests whether a file is coordinate sorted. If pysam.index fails to index htslib writes to stderr and this fails set meta tool. To prevent this we run this in a subprocess and discard stderr. --- lib/galaxy/datatypes/binary.py | 71 +++++++++------------------------ test/unit/datatypes/test_bam.py | 33 ++++++++------- 2 files changed, 37 insertions(+), 67 deletions(-) diff --git a/lib/galaxy/datatypes/binary.py b/lib/galaxy/datatypes/binary.py index 64e61e16368..1a1126bf8a0 100644 --- a/lib/galaxy/datatypes/binary.py +++ b/lib/galaxy/datatypes/binary.py @@ -216,49 +216,33 @@ class Bam(Binary): """ pysam.merge('-O', 'BAM', output_file, *split_files) - @staticmethod - def _is_coordinate_sorted(file_name): - """ - Check if the input BAM file is sorted from the header information. - - >>> from galaxy.datatypes.sniff import get_test_fname - >>> bamfile = get_test_fname('1.bam') - >>> Bam._is_coordinate_sorted(bamfile) - True - >>> bamfile = get_test_fname('1.unsorted.bam') - >>> Bam._is_coordinate_sorted(bamfile) - False - """ - f = pysam.AlignmentFile(file_name) - coordinate_sorted = False - try: - coordinate_sorted = f.header['HD']['SO'] == 'coordinate' - except Exception: - pass - return coordinate_sorted - def dataset_content_needs_grooming(self, file_name): """ Check if file_name is a coordinate-sorted BAM file """ - # We check if the input BAM file is coordinate-sorted from the header information. - return not self._is_coordinate_sorted(file_name) + # The best way to ensure that BAM files are coordinate-sorted and indexable + # is to actually index them. + index_name = tempfile.NamedTemporaryFile(prefix="bam_index").name + try: + # If pysam fails to index a file it will write to stderr, + # and this causes the set_meta script to fail. So instead + # we start another process and discard stderr. + cmd = ['python', '-c', "import pysam; pysam.index('%s', '%s')" % (file_name, index_name)] + with open(os.devnull, 'w') as devnull: + subprocess.check_call(cmd, stderr=devnull, shell=False) + needs_sorting = False + except Exception: + needs_sorting = True + try: + os.unlink(index_name) + except Exception: + pass + return needs_sorting def groom_dataset_content(self, file_name): """ Ensures that the Bam file contents are sorted. This function is called on an output dataset after the content is initially generated. - - >>> from galaxy.datatypes.sniff import get_test_fname - >>> bamfile = get_test_fname('1.unsorted.bam') - >>> # Grooming happens in-place, so we copy the test dataset to a new location - >>> _, temp_path = tempfile.mkstemp() - >>> shutil.copy(bamfile, temp_path) - >>> b = Bam() - >>> b.groom_dataset_content(temp_path) - >>> b.dataset_content_needs_grooming(temp_path) - False - >>> os.remove(temp_path) """ # Use pysam to sort the Bam file # This command may also creates temporary files .%d.bam when the @@ -500,24 +484,7 @@ class CRAM(Binary): def set_index_file(self, dataset, index_file): try: - # @todo when pysam 1.2.1 or pysam 1.3.0 gets released and becomes - # a dependency of galaxy, use pysam.index(alignment, target_idx) - # This currently gives coredump in the current release but is - # fixed in the dev branch: - # xref: https://github.com/samtools/samtools/issues/199 - - dataset_symlink = os.path.join(os.path.dirname(index_file.file_name), '__dataset_%d_%s' % (dataset.id, os.path.basename(index_file.file_name))) - os.symlink(dataset.file_name, dataset_symlink) - pysam.index(dataset_symlink) - - tmp_index = dataset_symlink + ".crai" - if os.path.isfile(tmp_index): - shutil.move(tmp_index, index_file.file_name) - return index_file.file_name - else: - os.unlink(dataset_symlink) - log.warning('%s, expected crai index not created for: %s', self, dataset.file_name) - return False + pysam.index(dataset.file_name, index_file.file_name) except Exception as exc: log.warning('%s, set_index_file Exception: %s', self, exc) return False diff --git a/test/unit/datatypes/test_bam.py b/test/unit/datatypes/test_bam.py index 1c7d5198f63..b65df03302c 100644 --- a/test/unit/datatypes/test_bam.py +++ b/test/unit/datatypes/test_bam.py @@ -19,30 +19,22 @@ def test_merge_bam(): def test_dataset_content_needs_grooming(): b = Bam() - with get_input_files('1.bam') as input_files: - assert b.dataset_content_needs_grooming(input_files[0]) is False - with get_tmp_path() as qname_sorted: - pysam.sort('-n', input_files[0], '-o', qname_sorted) - assert b.dataset_content_needs_grooming(qname_sorted) is True - with get_input_files('1.unsorted.bam') as input_files: - assert b.dataset_content_needs_grooming(input_files[0]) is True + with get_input_files('1.bam', '2.shuffled.bam') as input_files: + sorted_bam, shuffled_bam = input_files + assert b.dataset_content_needs_grooming(sorted_bam) is False + assert b.dataset_content_needs_grooming(shuffled_bam) is True def test_groom_dataset_content(): b = Bam() - with get_input_files('1.unsorted.bam') as input_files: + with get_input_files('2.shuffled.bam') as input_files: b.groom_dataset_content(input_files[0]) assert b.dataset_content_needs_grooming(input_files[0]) is False -def test_set_meta(): +def test_set_meta_presorted(): b = Bam() - dataset = Bunch() - with get_input_files('1.bam') as input_files, get_tmp_path() as index_path: - dataset.file_name = input_files[0] - dataset.metadata = Bunch() - dataset.metadata.bam_index = Bunch() - dataset.metadata.bam_index.file_name = index_path + with get_dataset('1.bam') as dataset: b.set_meta(dataset=dataset) assert dataset.metadata.sort_order == 'coordinate' bam_file = pysam.AlignmentFile(dataset.file_name, mode='rb', @@ -50,6 +42,17 @@ def test_set_meta(): assert bam_file.has_index() is True +@contextmanager +def get_dataset(file): + dataset = Bunch() + dataset.metadata = Bunch() + dataset.metadata.bam_index = Bunch() + with get_input_files(file) as input_files, get_tmp_path() as index_path: + dataset.file_name = input_files[0] + dataset.metadata.bam_index.file_name = index_path + yield dataset + + @contextmanager def get_tmp_path(): _, path = tempfile.mkstemp() From 836aea072e84411fb4a24e37fb306b5ec72044c6 Mon Sep 17 00:00:00 2001 From: mvdbeek Date: Tue, 5 Dec 2017 12:50:33 +0100 Subject: [PATCH 06/22] Add test file with random order --- lib/galaxy/datatypes/test/2.shuffled.bam | Bin 0 -> 59858 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 lib/galaxy/datatypes/test/2.shuffled.bam diff --git a/lib/galaxy/datatypes/test/2.shuffled.bam b/lib/galaxy/datatypes/test/2.shuffled.bam new file mode 100644 index 0000000000000000000000000000000000000000..72aafe572c1c75d242d673da0fc738000e4f616d GIT binary patch literal 59858 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
    BNA&s>aZLJ(@gQJK#1W17l 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

    B#=)A;mzHt8<=k7$Bjtxtbw}qHRBrn+ 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 Date: Tue, 5 Dec 2017 12:53:03 +0100 Subject: [PATCH 07/22] Drop samtools from metadata and upload tools We only need samtools for the dataproviders, which shouldn't be used by these tools. --- lib/galaxy/datatypes/set_metadata_tool.xml | 1 - tools/data_source/upload.xml | 3 --- 2 files changed, 4 deletions(-) diff --git a/lib/galaxy/datatypes/set_metadata_tool.xml b/lib/galaxy/datatypes/set_metadata_tool.xml index 4daaed1d23f..69c84dd9c5c 100644 --- a/lib/galaxy/datatypes/set_metadata_tool.xml +++ b/lib/galaxy/datatypes/set_metadata_tool.xml @@ -1,7 +1,6 @@ - samtools bcftools diff --git a/tools/data_source/upload.xml b/tools/data_source/upload.xml index 8d3fe1959c5..6f4233dc66d 100644 --- a/tools/data_source/upload.xml +++ b/tools/data_source/upload.xml @@ -5,9 +5,6 @@ from your computer - - samtools - upload.py $GALAXY_ROOT_DIR $GALAXY_DATATYPES_CONF_FILE $paramfile #set $outnum = 0 From df35d089e3ea8c15330afaeedfa24e9c2f0fa7b2 Mon Sep 17 00:00:00 2001 From: mvdbeek Date: Tue, 5 Dec 2017 16:51:29 +0100 Subject: [PATCH 08/22] Add more unittests for CRAM, bcf and vcf --- lib/galaxy/datatypes/test/1.vcf | 34 ++++++++++++++++++++++ lib/galaxy/datatypes/test/1.vcf.gz | Bin 0 -> 422 bytes lib/galaxy/datatypes/test/2.cram | Bin 0 -> 54312 bytes test/unit/datatypes/test_bam.py | 43 ++++------------------------ test/unit/datatypes/test_bcf.py | 26 +++++++++++++++++ test/unit/datatypes/test_cram.py | 13 +++++++++ test/unit/datatypes/test_vcf.py | 25 ++++++++++++++++ test/unit/datatypes/util.py | 44 +++++++++++++++++++++++++++++ 8 files changed, 147 insertions(+), 38 deletions(-) create mode 100644 lib/galaxy/datatypes/test/1.vcf create mode 100644 lib/galaxy/datatypes/test/1.vcf.gz create mode 100644 lib/galaxy/datatypes/test/2.cram create mode 100644 test/unit/datatypes/test_bcf.py create mode 100644 test/unit/datatypes/test_cram.py create mode 100644 test/unit/datatypes/test_vcf.py create mode 100644 test/unit/datatypes/util.py diff --git a/lib/galaxy/datatypes/test/1.vcf b/lib/galaxy/datatypes/test/1.vcf new file mode 100644 index 00000000000..f75da66f171 --- /dev/null +++ b/lib/galaxy/datatypes/test/1.vcf @@ -0,0 +1,34 @@ +##fileformat=VCFv4.1 +##FORMAT= +##INFO= +#CHROM POS ID REF ALT QUAL FILTER INFO FORMAT wgs2 +chr1 500 . C T . . DP=10 GT 1/1 +chr1 1000 . A G . . DP=10 GT 0/1 +chr1 1500 . G A . . DP=10 GT 1/0 +chr1 2000 . G A . . DP=10 GT 1/1 +chr1 2500 . A G . . DP=10 GT 0/1 +chr1 3000 . G C . . DP=10 GT 1/0 +chr1 3500 . G T . . DP=10 GT 1/1 +chr1 4000 . A G . . DP=10 GT 0/1 +chr1 4500 . A C . . DP=10 GT 1/0 +chr1 5000 . A G . . DP=10 GT 1/1 +chr1 5500 . C T . . DP=10 GT 0/1 +chr1 6000 . C A . . DP=10 GT 1/0 +chr1 6500 . C T . . DP=10 GT 1/1 +chr1 7000 . G T . . DP=10 GT 0/1 +chr1 7500 . G C . . DP=10 GT 1/0 +chr1 8000 . T G . . DP=10 GT 1/1 +chr1 8500 . T A . . DP=10 GT 0/1 +chr1 9000 . C T . . DP=10 GT 1/0 +chr1 9500 . G A . . DP=10 GT 1/1 +chr1 10000 . A G . . DP=10 GT 0/1 +chr1 10500 . T C . . DP=10 GT 1/0 +chr1 11000 . A G . . DP=10 GT 1/1 +chr1 11500 . C T . . DP=10 GT 0/1 +chr1 12000 . A G . . DP=10 GT 1/0 +chr1 12500 . C G . . DP=10 GT 1/1 +chr1 13000 . T C . . DP=10 GT 0/1 +chr1 13500 . G T . . DP=10 GT 1/0 +chr1 14000 . A C . . DP=10 GT 1/1 +chr1 14500 . A G . . DP=10 GT 0/1 +chr1 15000 . T G . . DP=10 GT 1/0 diff --git a/lib/galaxy/datatypes/test/1.vcf.gz b/lib/galaxy/datatypes/test/1.vcf.gz new file mode 100644 index 0000000000000000000000000000000000000000..1b5b3146fd81bacf222d5eab141148ab4dc009a4 GIT binary patch literal 422 zcmb2|=3rp}f&Xj_PR>jWos6xMz4MzKL|U(3k9SPTe#6t$zr{pRVT#L9rMGgWA(K9x zaLE1p-8C^|>%9$$jL+x2pEI*t=lvr7ZQhkKV!w80%3rm6dt%eupT)`-=Er!KH{E+= z9A~`la$Lcw8T+&ZJ8f=rs!PN7%euppwrJRw%|D%3o>)CSFuT(Dl^W}9<2u{y z6iNU6i;Wm2TX1i`G%fY$!@yIQcT`REF07cu_i&4@69y7VxXFJl)KHEoeE*9EKOnYxe~g z2}(SO;r38oAu}ha=&^%VqmzN-+XWfA9gPJBf=3)u4n(cU)eV`*=$z5v+?Ec(bF>R`n66>|*r0k#G z3v<6aie7OJ5P8@b#P+1_Qasnb*-e*pp6}mw$T|17^NO}OotJ+Y^cuRlZvo>#9xW!M K8JNKl10nz)zqUgF literal 0 HcmV?d00001 diff --git a/lib/galaxy/datatypes/test/2.cram b/lib/galaxy/datatypes/test/2.cram new file mode 100644 index 0000000000000000000000000000000000000000..dacd2d459e3b0bc66494189063aaf74249fe00b5 GIT binary patch literal 54312 zcmc$_1yo$yvM$^Wjk~+MI|K;s?(XgooZ#;65ZpDmd$15ZI0O$8JP;uGYtFg*?%aFM z``^1`yfOY}(5q{%T2(b`&bfM4ukRC66;Xf$FaiGc6%PEX4FUoX_-uxMmH}=Fyb2N| ziGls+DM*P)5yuc#Owf#*jp8-}?#{~qYF#3FWvzLKjdGDCR>v|eV|}HFhU)|KYU29g zq%=joX7#;=l%nn>rpy)f8fwvLPnLly&w0r#I;M&)1U8`@atv|gS9JJ_A?RU)pH6zt zz0kBc0;r`_pE|#hUUuz^+l;Z*{T>v2u5$@=8Q@USEkdQjPMm+RYFu(T^b^WN`+R(s zq@*jF-J}g`+?sF|XTR`O^+pVaqjAgX3!;V97fapQlQ$i2EC$}8}928yE zj-QY+3+mU{4Kke~I@FmE<|^=WOd5@`$c9_53WB@+1*7a4+~Q9;bXRZT=q&CyZJ?VYZfgP1Fu znk&1Sh`6qZs;-#07(i7Opr%0Z`chQ{iYNmEE3ic+AwfV?AVBQ(2nhtJ%0of{BTaz- z5oK@#*u1I~BrNa+{6YZ&6ke6UZHj7tw<-RIp1RsU&#BA*^C+nZHWZiz1W1YjfdrDu zKp>%vA`}DwNn9QXBoY+?0*OUMLC`=TJrGExpaQ;#9qjB?13^U%2t-ts0XIPa0cv94 zCgi|Ige`C_`kM#fd$@if6$pioeZe@Xa{0Rgh z@KXJ~MCIf-W#tv**cZ82mDuOF*m$@W738_NXIePfl;k)Sn_0PJI86Wmw>!uTDzJso zo-9Nl2rCShg)}`~8zKf4tftg{+k#C$Dt1f9_ z=?YpHLi9rUfK5#&fUq%7sA!eZ;|-UY9M?q7O(h8k(#DZM*F_z>86Odl6dL7>Y43z- zA7XtAwy*qCw=Ik4lrgJ4(mnp%Og3VJ#5+;$!#iZ;9ALe0UbhlDn{p`Cr_5SdZ^=== z9HqS>Ya(5b?3*6~@CB}mxOH`bF78!XI; zVy0GE@2)o(io{?vxyJ;hR7k`!tI1b}6Olv`R0vtymG!DQPC~6~t_Zbg7HOOl-!ghM zgwrJuN1;mhfb>6i&Kp2rljsuEE*hFCajiEIZa`es!WHvfwJ?SX^VZWA4nt+ZhJ;F= zs6hzF1SXR1=M}kzZkdy@5u*~y8ZxN4la2UP6X#kO)(j7ues9qr>43V4VKc5FYhyf= zu#zbSDNVf(3F__Y z(&7LP5h_WY!e>fFSW_WZ1dB2?N6dm1Ik&p%3f7*MxB0M5kqBuSs((Pk^g~$kk|`3O zUFf9;4VnUe6Gi&$|O7ksa+k;X!dvOl++F8{mjTX zyXbgkM7nrYWU@AQ&f=QT^%g3X*xd)m?rn*p$_cUQaM)&{z;u@5SUpvpwn$?lGMtP< zX9KhOt@Po~SDsxT;i_5;vC3iW5SxVbSS1+~mIRt&VPIu4XnTC0i3p(D_Ro)7P9-9U z*E%z}4~o&26GtB(9PVuS9vD{xjFuj1xGX;`6icwkUh}e^pv7VEE?(WE~aWIB^|7O`RV_bxUN!<_;wq+2{lZz>RPQa zRydQgT>WDO@x%P{xs%=~t>463{v#iw?6QhYA(|SEI?u|Ur;kg=D|!yMl(A#wx2_)# z_&h@0u#H@fF>k^j&dq!Xv-9m92wT#%o_ry?ct4_2Sxt(5??2GO{K4oW+$@gX`;a$s zaBH=>xSFE>E&tpuQlY%2#r8-d#oO3TaH2Xq^_)UwPDS*{Zp`QL;o-do+ZPFq{DA2z z9R~WIRkQf_OPQ)zJ>LF)&%|6+>8VS}F}H$&kxQAbvD3B3i;J`I2lrR!cW*68bUMA8xPQjo2iwk2~rd()mjtx&$(xNa>PPXUmxGRU%d6gUQ6255;8-tTt}wKAm<{ z?m&p+$ppXqRT+WG>*v35(2K>?Yw4FOnD8A1)-KTAJh#^7f(@Fa!m)gu#r zw~rrR-FC-w!@nVigk>9kS)rX3$@a>n^8RY7bLO{3lEJF_*c&`ghJFfR@ zO7U~yV-$FE6~Fby5ePzJB>ejx3^gEv1^^(2u+})gwKaZjVU)@Gru;TFrs!w%PdNiZ z@U43%QfmMNLJmR>82~_m_bdaD;6{*0NTpbCs#Hj#dN?zn`LKx&iXH}IgH9O|XtIV< zY7xP$)vO*{YzaK1TLH3kyp#FaNZDDJ!bhoY==C6lS=lnBLI^j0k{>2LdAax9C}ZGW z*b_^o&mFHOrGpfGMqSIQzCMKykM^suR8@CD2oKb*xaRbM-|=cpMN=f^rVOi`T-&vJ z)hzRk66`6X|LExfM-*aQ?XsE`Y5{{eq__j(SJ8Qwp{r}W%ra>?F>`Vpxnh|w_21g( z&v^SxHlUsYqs!y4qPv|cni6Q{?}^a z)n$%eK82hYLp^3K;gANWYBr$I;tnA1SP}yS5vT@c{h3ZSGyp5=Agha^|F5*5whJ2N z9}?o?oW(0A^NJKjE3t|nW>1>u4mruQCoMU$(IzdMR%|%=<02z1IrtnTNvIzK(6hk# zC560W0fGqK1GE3MEdn1b3=p1u;gSkh2_M6h2#t=kUifI?*2=s%>pQ&}uq(SYH4&|i z_A?aHR#M8*_eaCs=KasII|BqX z-Kewp9Qjg_+km$#X7$e@h{( z#6bYQ#x9pl9S9<(#r*pkS5HqwXJltYIA=s>PfrgwHxy?N7yxXMG!rKH0-9eqZ$Tic zAkI)900Vep`G5cbk?;_)5DlqdZS|08K;xGgD@$xF($g&^V9!<`L{Ig%h(wBocnclr zr;j;Hm1##eX%E2_--)Uj+ufhfuTp7!!pI1!oH6>|Q+h2(a#yr+3(Y^qz7s@^&~Xho zqX!7p3%egXr6Ajfp2mj`Ifd zUF0Vl6=H!kw{zhl8bqZxm2#Hz11ESvR37K>pM);SZghMZMI1YW>o~*+AV3@_c zwu!Kk8R73loQ9`orFSogAbQYE;O~mom?b$u9D*CJGJ&0oM%%Ky{XSmO@Q3l|R-R*% z8G(xhdLW2if%orxr)Zd}r6gLK?_i?s<7ha4x4=nF-NP}TGv(UaH@C9D#B4e++0iuK z-PMj1wXH6fHqt^n2mZ+ZWA0{Smb+(mF1z#8D)HxA_AaUvLlI}5EucJz9; z(~r#xcr_xf8~7Ad{g)B|k|~kRaZnf)a&Di6Ui@q$S1}h2dH`GAe*A?M_4|u7*)5?< z{{#37^jV-mKOmmSp=SKlOaCmv??&G-e{(hJWL(P6;Oyb#{X6_%GDWcuABylyU8A=b z7h;PFCv}!%KXK0Ia7~gUO7P5oAPZI5MGV8WWJ5#}vktH4<0&oNu8?oa7VF3gV|7m# zo211JGr_dMj8M^5>OwWn%M~f^RDmr{lvHTCY|+WI3@cRoGV@N2rz@Nezo_un45uwe zs-nA?n_6!HC#d)A)0@iZg|{f7D%O@^@2l#d83?&1@Go3WGR%HDoFA_}2$=v9$Ev|8 zUzPh%0Zs+DZ1{6XTwwsc{GEKSLt=+h$_rKi*A#y$tbqVb0uHyY3g)rgSIjH`0Mypr z6;`<^5LoW>T5Bq?VBr1NT2l=?Gr;5aSFNe`k6Kgge_m^r`}{wuHN%Gf2eszgf2}pa z-=6c|11SP%ed=*mdA|^+2=RDb+sA|5q4bTVJ6Qm5(tNCO8Ks4H@)9A!>vZV&_+pX& zmSQl`$@yZEvbn2lShqcho8rl2-PDT!h*BwJKDA<-tMr``0bQ%$$f8cz*S4jGzl%A8 z3ut*nxjqw1zuFg}3*m+JJPpjpt$vstp1pIByEQ-F{nYf3LdXZ~ADV2WROZ_?__s;#aj@{`T^_XJvYg2P~6MotuahKlWWNN{)P~5>yVs1sQQWi zVc}<~F&~0k4GmSrc?G(eE(%G4Q%Bg`2saF8+omUH(+7y_E^R&B(TXZI=C&k8FZg6Y zPw4W_Oov2EZa^YNG}+>(-;3A-kXmM$_!}dc+d3iFE73pr?gka1Fq@jRWe$kkS_V#U z1B9}QsjjwVC1rWeqhjchY3y3jIjNCvzlSmnj!>|8lx7+SD3_q{MC8&9Yr3rClXLT= zSDf1alyC6anJ@d^wJj4;GEW?w(QI|~%RQM<>Z==Mat$ydJ2!s8ZbsqoDixEel<-B! z*lPPLQ8ytIdiDELMrFjj_h|oiD=yb5;WV1|cR~&dgV6UapR>4j-B})( z*oU&)x3kus6sOhNwi2&m4%8Nq>n_TxPGx+>)_05gtAamJ>fxBC7^{2sJ(up^{U94d zE#ZwxU_Rw{GPaQq0i|SV#FxVV5 zx>#g2mK2vVLAsW80w_hxvdSF+_SZEr%g3;gj)BMF)>2uWiEPi|0EAYmDZNCL%P^-H zjRO)>JYDXx54Lfu`Oma*#*oW8S|7Ah1lpF(YDKEiWwb1>hf}n)f5QnJ#(NtSCHvU! zVQ#tz+uQx-mF-q=+=;Lr@hn+-W-tBL))kbxT-m85Yl4BASF;q%L236vF1S{)|N3cV z^F6^gdgU9}tSAO`q>^9~Tm!_1H=St?mn?zc+b9RJS;Zq$Px((;&@a1^`O(4%qYtIZ z+}9PT-f&FBHG+f@cP84j6~vcT`BXbvH0?n#Ov4PItJ=G<2k+SGyvsUPF@xq!sFs3j z*GU6!Auj9QC|lmnm=SX;guNa}ktSCPe|vLh1a3iQRifS$UJWSag$#;^m7b_^AK^q) zHqLJ1iZ?z+`OnE4=`C*4UCX&jIq?aG$;!-GPHpC}=SUh*p4Ds3KA%tX;O>6za$a41 z(tKtXDjYp%n8gxjGE+?#ft5@PxCrZiL18HxWrmIN$yulC8+Z5r$t(jubD=%#*(AM$ zc_pI-Apx|bw@kl*6spTD#E4Rvfs9YB6zG>iTsXPn13JaY0ldP8x14`^#4y>@(5xZs z%X$>0Gg(|X!y_9k77A6hFUKgXm{*fa*LFd&V8*VYz8}qyD#Y0dY_p=7 zcI43btOtjWZ3p9}$rvcC2i#|#pS~LM4MXM)SUKI9p6q+yZvcmem^yT|w1!n7-Ouyg z6O9j}ikbHY#|$@yqWY3uE+A7MTL{`x$)KKyCV+&-!HW;1qcKCys$J z4mrMX$K2{RmST8AVBGEgtLicHvqQnj-rS&BTK*pEx=>@}QPO~X@U>lq$(0CWG1hk5 z4Jm2b_u;{{3TMCVy`SS&t5r!9YyzasL{H%(WQ6F-)zgk2-(I`vNkiS)r}QV^aC_JD zo=9|56WEL?5uv#EoX6`s!Mqb{{vE=>UWFMFW&*rfo|aYrn0oG!-Tnp9Eh!%h-9hXZ zmi778Xi)MV8EbJ>m(^mDw`5+j&xWUjbSg)v7Sk}o2^Cm?Ud0gy~;Q7qNE$sF8 zZYQ6#z~4;hf_L=8$GHzYQj9h|PV=nx1&p4*buTiRR^R1oQl@X)&GYq^nd4?z=Fl~l zA&tj~koq2vd@e+0;zjTEl=~K|iy6-<K*Ag!;)*1 zCHaONF9GIHs>$p=_D3PX(G}pcjTf@-ouaRE@C=Dt`EORMqE;^&q%_l;^iK3sIR(}I zQ-kEQWqM%fV)r~&kt(NAaUQW(2ODyt?2i^kj)P(u*!(aO(^Ewl;y-PyUVyS5)vI^5 zjY|g&=TB84oy}cBa74c`>9=XeJA5dSFl7iY{-o3l-Ip;6w6?lg{JIB$+uopMO^}D- zyGL1rnAshzJmherwTzYD9);kQx`&dwmQ3k~JB=_#z^A*H9_Qydat|NKy5G|G20jGj zKgsXzmfqFDpBLHMG#kSyhIqyhV*=&JKR$~4)7a-uVezp!l~f|{J6z7eLGw+wK-FEv z3N6MXD!q@B@ugh8Jd%2#pKqZ?LK&I=h%!cZ&D$Rtl3VRs%6f|%9%XYJcEqthdKoA! zrR0opml8U(Od9OBNfha@j{s)LNhavqZ`*HAFx_7I^a($b7`wI~Z!{lIPkegxVmA6+ zsp;XapOAt&|Blrpb%uuT(}yGa9?(kNns9J>>8cQN*C~B&Tu7)CtKhdUm(Pth&8ac0 z>zx}Bl|qfmexLT#m^LC=Dq&Izm$nVGbk3_zo+!xBs~X(kL;?x_i0#=EAJFmV)`tXT!z1&jqts9 zUw7;;VscMoY&FTUGp|F^H+{ zzZ?jCEO}Ngd0rDWETEfV8tzF8^1&mDA}Pm1;=YHv-M7B=I)?E{J)R0O&+hqf|2bSs7A+Shvp_EkuoT^reno#x1F?YBixwmG z4LA8SdqM^d7$^{+6FKc%%MKF0n9~h2q0IdN(|`4?o`ctGBQqB7c@~J73T?@+wh%$?vsL zbnd>o$5gT1Uh{>PNZf*x)^*u+m|*0XMT(fr2Q`>d={$M#VX&d!Awc!4>+=*DCHbZ0 zi2?a&}Ss)lT;f$t>683{N>)jWNxnx*$QI zrAN&Q(feFP*I~DF_&53GvqC}Cg_XFvKKfWXSaldR2 zZrQ3NasnZZLrWmCPR_Vq=97jJ#YAF+&8WSMZyU*PHm|u`Xy6!)KgW}q77W^)5d)n= z2`nPuZnj7rI@mly?!}`bh)OVXH6o|cO}y`ppu+5X@NuReP&?weQ75Oa(OQ!d*`)kF zu_9_6c%Zz7KuRC$`E>3^CmTN@FkFzWy5%tUp;F8+ihQ9FCfLQ6J8B;~FYX;HH9=py zIV|v~DiADtveKA7VZUsVDZA0svMOSRB~xT5#F64cjuO%fTdx=$ZlMNHTp0ENr-ocjg91N-^Y9WdNB|A z;maZmI_lNuOYK=4e`5J5dZnk36^IQrqo$3e_xCHW-A{Q}_fQg4q!_=UpDIc|<2x!j z^B&8KSnV0RgLicAx%&-I7@^#bf`Pd!B#~(_sL}a2_m$C2G3*TWSW$s(p?J$atpp7Bo5&L#w!lDGV>VsKkt_?YOr za&m<6yUy2q@vvp8) zI#&2h_hqAST<13=ZC$%Zf>tP&v03D^zHV5VEMuzrBhQCkP)b~?tej)D{jSf*ZE;8D zwyOmdI?Q+UjE8vyWdcD2k3tl69+|L5(07_PcpJ3WmL_Sp*mG+rMMA4 zAB`y7#9{P@Fz-|mz4g_$n7dCPA%S(HJ8dVvblG|+`);GsM<|!M32`Ao^3R2vN?Xln z!Dt1{{(h><{oC72Alo>S$BZ8R>cNk>F4uQi)!f+w+22|m`p{Y|J_8<`**`uOms?Pr z`^D@Mx@jzXHGZ?#q_!KKcWA>wA^+el(IoD(>9zb=v0S|kJ+9_ljijQS9a4YG=bP8v z#9Dn7m_|}U4#UPSwsw*x);6XcDAX`i7k`Domp#@vfXK9HYvYVM5bEFFofQ#AIV-hz zjvg(Cdtu$RM2-tbe>b&d#1)ag32b8N0i$;V*1T5!3UMA(qYd2 zx5T*0LRQ&j>9P?%LDiR;5v-MR#-C3!CEYY4wkXB5ut`)kDUJg^GbK`dn=tHDLL%Jc zE0;M9YjvG!qXM4a`s6tyMB~LFVAl7oQPv@Tnw9F>-9*dDOktPx{Xol=-T#e0J5+Ow zcM9p&sBYSqUgax`*~HHS?xyhgG^&oG`=Hst57f1k(py(?7+!3RYU1S`2e)UtlulFn zk1|h=@6g9@H0DeaunN5%cD~uh)GuXJ6bX&WU+MNx!s*OPX3x}b03>^ z#|5|PW9MYD^LC)04GNvC09^|GbXzGTl^+TJBV)!uUi7DxrfvHoDi}ApalfQ_BJM;~ zeh6lzEB)9<$&@6+c#DmXx)}3RT_^rEg&gz29Ki(r4b4mN)A1ft&0m2dg zHt3Mh{%AAM*eLCqTt}<@SnIEZt8)s-$@Y8%nA=%kw<7VN4~I9!UZfdryTxyaly67a z&6gS~)->$zS4D83_x5{`m9Kjr@yR9$lOVUH!T1=?Bm?aNp8PXnQ~bvsWW%XNY)n1R zP;TTA)`h966$g7AJ^L$-)mXTLLQ&0k52ZG*k!VEuOd|xn(wVMW!ru|T&|I*ScQF33e^Ox z4rM_gBe3Nt9ZQ8;?>@Yoxur{lpM}!8U8#|CtIU!0&SXkeGDwBtcWJMhTx}T-whH`$ zdq9#v#?^wknOclC`taU+g2#|P&M6g(#^EW4;iYET>$iZ_?wq6d_Uk zTXGJhU@}5>nVy$M=yN{M@mW_6^sm~U32zcfIDQU3qNENVyhrF zQpE2Z6&^05cK@-t2(4@Lq%iBq&=lG!f0?-`UZmMvxadJw+Lfv1#^b{Sl)0UKv({Tg)OG>Hz~3x%6gp7gZ)dBnm_O`wb$N-U9S-h z-1uk{x6j-unJNn28{+wvk5KGKx^i?hvNAq7O7SUa2?tRHm<_F{=lez=1TIG6NG=&U zv%Y_|lN&%p7x~$nm}t zLI5kp6y~mruBateXUQwA<{{i@^YIuFkFCsl!RJuml70E^Q6Xg}*Tbe%uvOVva+pnTmP-bmKI2acJs~w(u zYC9RUEyi$7l>J7-`A6fQDUB+2eiXlnshuY79iL4>8Gm=3di?1*+>9Q2XP2Ik3zX#{ z7~S1z<-DZt1wu2$hCh^M(uS!OXF*5$+=p~nkHxCyaz%MYKkae&ICJG=0=K;kQ;o|e zW`4rw<(Ha<{IM!W2|b#2csGJsT0lk;{9EKH2wy(xLq4S=S^%uiL8$folDC!*H+j#j z@d-ZR`uqHjADuY@hiQG62S@{A!>zgs&ig@S#WI2@n^6=#BI7o_2XHDet|rzRt}^?5 zI6nHB9~9Pp2rs7#zHM0!gq}`#(jThEpY=6%(=%SL>oKaRtCO`R=6>hde(j~UBR(F7 zw!Zo@U*o2HOo1CK)r!m|C}d6M_BuCD# zTqk5Kg<<5U5vV*jqzx;XZekKD;m6{z?nFB*-+RqO8A?R>A%Y8hukH<&Of zcZJcMcL_i09Y@;*2{h?ji3brJl~A+3wB7DDXvqHRjYxsW^>FRAUJDgMw?iRwwwc*g z$0erw+hJ~`7_5c_kyYZNBM$@1{Sd@(MOFb22q`cw$N>-venRE&`W!S=P9(TZEEr!Z z7)3pF9w3k_3|y#t1EJk{h?sFHNF7oC!+<^_gg*Tep01vW1HuijCX_b}xzs!M0Q!hU zcKOP|To5XWcstq;gPg)rC4Zr;PDOK5RovUj;&bjeRjW5i)DpZJQxb;rvGNzrtKojw zG)4shhq|zovaqG!yD=Nx?D~S|R?t8Y@}JMWT|F!T0EL?|&DZDN^Gp7IAP_;|H}I3s zIj}tR3IY`dKqdrW6Z!(Mc|8fpHKhi! z2`=*C#%x1RAxZ&dEbDQ#7Jpp7pIV@^dfrVS{>F{nPP~zd*P?MIj$~R+ay)o6_U^?Y zBfe80G+YEKxe!QHu*`KzbzGQ$T+rWpX2ua9gLp(tf{tc@x?@1YSd}aSK}+d^oBCF9 zxkdG0{|nW?m!5a=PIK&P+P&(ST;K(Ey$P za4vB)x!yn)i)=$w{=r@<SR%v$bq&QY#K~ zbbse?CdrTzcEg;CuMV?b*`w=S)-*OTwKx_6Bezhp@xkt^QIDo=Wm;}a6SD4yN}j5; zu~~iveEaok1gly%&AOhJp9Oj$oijr7Enzp@_nNLP+_ybaO!G_Gl?+mTE^ao(jbAZz zY%BS%CqySXE8}}-(lj;GiDM;GM2N1{+<%qk@_1WR^ZkzNxzWVGJ}gcDs6cICV|Mm^ zQNH#%SBj`x^T)}-(f3l~g*1ee)oQoJFP`2mjUMc6?5@>ur5)1?6PhV@OI0EWagGkZ zgj|{4dfOH9*;IFU>{m5y@bQoLAQbOYBk-qqc-P6)YAEvA)frhIh`K(>7DMN9Szq$E zG;?W%tX7A}g=EcIJGeR3bI(cgw{T<~cTlHza94$-QR~r2`LWMQ%H^KAmwWK(`IMHn zq%GQ3#;K%mmvLp2$L;st!k0yt}3QxI$7`F)`D@E z9}#sWU?TcO;#1pvqbCJ}!t7Q!rsBhOrK0q_zoh#x5SN9bY3aS4*6P3iRmr!Ernm=R zpWyUYK;4E`vRJK6_lz34@naJ&V?kUy6k2-QGwp81}ijvAR~{@8u#?YV|ICvHGb0V`}C68s}P(Nj|TVz`(Yg`UTpH zAtG!Tyk6l7=a4}lBJCh3fD(B1z9RU5(RmP{5XT_;z&I2XSQK~^Kmt=FKyV5JbgJ3- z<{Rk_QU=7CE3joPtK_QUWU!hfAygw}aq`|fdx8on#>

      &bmASGb~zqwlxSV)#^4 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

        4o!w85 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 literal 0 HcmV?d00001 diff --git a/test/unit/datatypes/test_bam.py b/test/unit/datatypes/test_bam.py index b65df03302c..d9483e5f524 100644 --- a/test/unit/datatypes/test_bam.py +++ b/test/unit/datatypes/test_bam.py @@ -1,12 +1,11 @@ -import os -import shutil -import tempfile -from contextlib import contextmanager - import pysam from galaxy.datatypes.binary import Bam -from galaxy.util.bunch import Bunch +from .util import ( + get_dataset, + get_input_files, + get_tmp_path +) def test_merge_bam(): @@ -40,35 +39,3 @@ def test_set_meta_presorted(): bam_file = pysam.AlignmentFile(dataset.file_name, mode='rb', index_filename=dataset.metadata.bam_index.file_name) assert bam_file.has_index() is True - - -@contextmanager -def get_dataset(file): - dataset = Bunch() - dataset.metadata = Bunch() - dataset.metadata.bam_index = Bunch() - with get_input_files(file) as input_files, get_tmp_path() as index_path: - dataset.file_name = input_files[0] - dataset.metadata.bam_index.file_name = index_path - yield dataset - - -@contextmanager -def get_tmp_path(): - _, path = tempfile.mkstemp() - os.remove(path) - yield path - os.remove(path) - - -@contextmanager -def get_input_files(*args): - # need to import here, otherwise get_test_fname is treated as a test - from galaxy.datatypes.sniff import get_test_fname - temp_dir = tempfile.mkdtemp() - test_files = [] - for file in args: - shutil.copy(get_test_fname(file), temp_dir) - test_files.append(os.path.join(temp_dir, file)) - yield test_files - shutil.rmtree(temp_dir, ignore_errors=True) diff --git a/test/unit/datatypes/test_bcf.py b/test/unit/datatypes/test_bcf.py new file mode 100644 index 00000000000..c9577ae78f9 --- /dev/null +++ b/test/unit/datatypes/test_bcf.py @@ -0,0 +1,26 @@ +from galaxy.datatypes.binary import ( + Bcf, + BcfUncompressed +) + +from .util import ( + get_dataset, + get_input_files, +) + + +def test_bcf_sniff(): + bcf = Bcf() + bcfu = BcfUncompressed() + with get_input_files('1.bcf', '1.bcf_uncompressed') as input_files: + compressed, uncompressed = input_files + assert bcf.sniff(compressed) is True + assert bcf.sniff(uncompressed) is False + assert bcfu.sniff(compressed) is False + assert bcfu.sniff(uncompressed) is True + + +def test_bcf_set_meta(): + bcf = Bcf() + with get_input_files('1.bcf') as input_files, get_dataset(input_files[0], index_attr='bcf_index') as dataset: + bcf.set_meta(dataset) diff --git a/test/unit/datatypes/test_cram.py b/test/unit/datatypes/test_cram.py new file mode 100644 index 00000000000..57ae8aef1b5 --- /dev/null +++ b/test/unit/datatypes/test_cram.py @@ -0,0 +1,13 @@ +from galaxy.datatypes.binary import CRAM + +from .util import ( + get_dataset, + get_input_files, +) + + +def test_cram(): + c = CRAM() + with get_input_files('2.cram') as input_files, get_dataset(input_files[0], index_attr='cram_index') as dataset: + c.set_meta(dataset) + assert dataset.metadata.cram_version == '3.0' diff --git a/test/unit/datatypes/test_vcf.py b/test/unit/datatypes/test_vcf.py new file mode 100644 index 00000000000..cb40e6f708b --- /dev/null +++ b/test/unit/datatypes/test_vcf.py @@ -0,0 +1,25 @@ +from galaxy.datatypes.tabular import ( + Vcf, + VcfGz, +) +from .util import ( + get_dataset, + get_input_files, +) + + +def test_vcf_sniff(): + vcf = Vcf() + vcf_gz = VcfGz() + with get_input_files('1.vcf.gz', '1.vcf') as input_files: + compressed, uncompressed = input_files + assert vcf_gz.sniff(compressed) is True + assert vcf_gz.sniff(uncompressed) is False + assert vcf.sniff(compressed) is False + assert vcf.sniff(uncompressed) is True + + +def test_vcf_gz_set_meta(): + vcf_gz = Vcf() + with get_input_files('1.vcf.gz') as input_files, get_dataset(input_files[0], index_attr='tabix_index') as dataset: + vcf_gz.set_meta(dataset) diff --git a/test/unit/datatypes/util.py b/test/unit/datatypes/util.py new file mode 100644 index 00000000000..0456e40cea3 --- /dev/null +++ b/test/unit/datatypes/util.py @@ -0,0 +1,44 @@ +import os +import shutil +import tempfile +from contextlib import contextmanager + +from galaxy.datatypes.sniff import get_test_fname +from galaxy.util.bunch import Bunch + + +@contextmanager +def get_dataset(file, index_attr='bam_index', dataset_id=1, has_data=True): + dataset = Bunch() + dataset.has_data = lambda: True + dataset.id = dataset_id + dataset.metadata = Bunch() + with get_input_files(file) as input_files, get_tmp_path() as index_path: + dataset.file_name = input_files[0] + index = Bunch() + index.file_name = index_path + setattr(dataset.metadata, index_attr, index) + yield dataset + + +@contextmanager +def get_tmp_path(): + _, path = tempfile.mkstemp() + os.remove(path) + yield path + try: + os.remove(path) + except Exception: + pass + + +@contextmanager +def get_input_files(*args): + # need to import here, otherwise get_test_fname is treated as a test + temp_dir = tempfile.mkdtemp() + test_files = [] + for file in args: + shutil.copy(get_test_fname(file), temp_dir) + test_files.append(os.path.join(temp_dir, file)) + yield test_files + shutil.rmtree(temp_dir, ignore_errors=True) From 3f6a59e85ff8acf2fb0b39670f450a523ea11f38 Mon Sep 17 00:00:00 2001 From: mvdbeek Date: Tue, 5 Dec 2017 17:10:59 +0100 Subject: [PATCH 09/22] Replace bcftools index with pysam.bcftools.index or pysam.tabix_index --- lib/galaxy/datatypes/binary.py | 22 ++++----------------- lib/galaxy/datatypes/tabular.py | 34 ++++++++++++++++----------------- 2 files changed, 21 insertions(+), 35 deletions(-) diff --git a/lib/galaxy/datatypes/binary.py b/lib/galaxy/datatypes/binary.py index 1a1126bf8a0..18b0b3a760a 100644 --- a/lib/galaxy/datatypes/binary.py +++ b/lib/galaxy/datatypes/binary.py @@ -16,6 +16,7 @@ from json import dumps import h5py import pysam +import pysam.bcftools from bx.seq.twobit import TWOBIT_MAGIC_NUMBER, TWOBIT_MAGIC_NUMBER_SWAP, TWOBIT_MAGIC_SIZE from galaxy import util @@ -516,13 +517,6 @@ class Bcf(BaseBcf): """ Class describing a (BGZF-compressed) BCF file - >>> from galaxy.datatypes.sniff import get_test_fname - >>> fname = get_test_fname('1.bcf') - >>> Bcf().sniff(fname) - True - >>> fname = get_test_fname('1.bcf_uncompressed') - >>> Bcf().sniff(fname) - False """ file_ext = "bcf" @@ -546,24 +540,16 @@ class Bcf(BaseBcf): if not index_file: index_file = dataset.metadata.spec['bcf_index'].param.new_file(dataset=dataset) # Create the bcf index - # $ bcftools index - # Usage: bcftools index - dataset_symlink = os.path.join(os.path.dirname(index_file.file_name), '__dataset_%d_%s' % (dataset.id, os.path.basename(index_file.file_name))) os.symlink(dataset.file_name, dataset_symlink) - - stderr_name = tempfile.NamedTemporaryFile(prefix="bcf_index_stderr").name - command = ['bcftools', 'index', dataset_symlink] try: - subprocess.check_call(args=command, stderr=open(stderr_name, 'wb')) - shutil.move(dataset_symlink + '.csi', index_file.file_name) # this will fail if bcftools < 1.0 is used, because it creates a .bci index file instead of .csi + pysam.bcftools.index(dataset_symlink) + shutil.move(dataset_symlink + '.csi', index_file.file_name) except Exception as e: - stderr = open(stderr_name).read().strip() - raise Exception('Error setting BCF metadata: %s' % (stderr or str(e))) + raise Exception('Error setting BCF metadata: %s' % (str(e))) finally: # Remove temp file and symlink - os.remove(stderr_name) os.remove(dataset_symlink) dataset.metadata.bcf_index = index_file diff --git a/lib/galaxy/datatypes/tabular.py b/lib/galaxy/datatypes/tabular.py index 7f94565fb89..d1e0454fe4b 100644 --- a/lib/galaxy/datatypes/tabular.py +++ b/lib/galaxy/datatypes/tabular.py @@ -15,6 +15,9 @@ import tempfile from cgi import escape from json import dumps +import pysam +import pysam.bcftools + from galaxy import util from galaxy.datatypes import binary, data, metadata from galaxy.datatypes.metadata import MetadataElement @@ -23,6 +26,8 @@ from galaxy.datatypes.sniff import ( iter_headers ) from galaxy.util import compression_utils +from galaxy.util.checkers import is_gzip + from . import dataproviders if sys.version_info > (3,): @@ -733,6 +738,11 @@ class BaseVcf(Tabular): class Vcf(BaseVcf): file_ext = 'vcf' + def sniff(self, filename): + if is_gzip(filename): + return False + return BaseVcf.sniff(self, filename) + class VcfGz(BaseVcf, binary.Binary): file_ext = 'vcf_bgzip' @@ -740,6 +750,11 @@ class VcfGz(BaseVcf, binary.Binary): MetadataElement(name="tabix_index", desc="Vcf Index File", param=metadata.FileParameter, file_ext="tbi", readonly=True, no_value=None, visible=False, optional=True) + def sniff(self, filename): + if not is_gzip(filename): + return False + return BaseVcf.sniff(self, filename) + def set_meta(self, dataset, **kwd): super(BaseVcf, self).set_meta(dataset, **kwd) """ Creates the index for the VCF file. """ @@ -747,26 +762,11 @@ class VcfGz(BaseVcf, binary.Binary): index_file = dataset.metadata.bcf_index if not index_file: index_file = dataset.metadata.spec['tabix_index'].param.new_file(dataset=dataset) - # Create the bcf index - # $ bcftools index - # Usage: bcftools index - dataset_symlink = os.path.join(os.path.dirname(index_file.file_name), - '__dataset_%d_%s' % (dataset.id, os.path.basename(index_file.file_name))) + ".vcf.gz" - os.symlink(dataset.file_name, dataset_symlink) - - stderr_name = tempfile.NamedTemporaryFile(prefix="bcf_index_stderr").name - command = ['bcftools', 'index', '-t', dataset_symlink] try: - subprocess.check_call(args=command, stderr=open(stderr_name, 'wb')) - shutil.move(dataset_symlink + '.tbi', index_file.file_name) # this will fail if bcftools < 1.0 is used, because it creates a .bci index file instead of .csi + pysam.tabix_index(dataset.file_name, index_file) except Exception as e: - stderr = open(stderr_name).read().strip() - raise Exception('Error setting BCF metadata: %s' % (stderr or str(e))) - finally: - # Remove temp file and symlink - os.remove(stderr_name) - os.remove(dataset_symlink) + raise Exception('Error setting VCF.gz metadata: %s' % (str(e))) dataset.metadata.tabix_index = index_file From 8e580ec7ac599993ae082732b5c80f2b9bcce493 Mon Sep 17 00:00:00 2001 From: mvdbeek Date: Tue, 5 Dec 2017 18:33:43 +0100 Subject: [PATCH 10/22] Metadata fixes for VcfGz --- lib/galaxy/datatypes/tabular.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/galaxy/datatypes/tabular.py b/lib/galaxy/datatypes/tabular.py index d1e0454fe4b..8a92dd2d082 100644 --- a/lib/galaxy/datatypes/tabular.py +++ b/lib/galaxy/datatypes/tabular.py @@ -759,12 +759,12 @@ class VcfGz(BaseVcf, binary.Binary): super(BaseVcf, self).set_meta(dataset, **kwd) """ Creates the index for the VCF file. """ # These metadata values are not accessible by users, always overwrite - index_file = dataset.metadata.bcf_index + index_file = dataset.metadata.tabix_index if not index_file: index_file = dataset.metadata.spec['tabix_index'].param.new_file(dataset=dataset) try: - pysam.tabix_index(dataset.file_name, index_file) + pysam.tabix_index(dataset.file_name, index_file, preset='vcf') except Exception as e: raise Exception('Error setting VCF.gz metadata: %s' % (str(e))) dataset.metadata.tabix_index = index_file From 0ac77ad7fd5c6d6660521d69f6010cb61d4947ac Mon Sep 17 00:00:00 2001 From: mvdbeek Date: Tue, 5 Dec 2017 18:56:27 +0100 Subject: [PATCH 11/22] Fix Vcf index generation and test --- lib/galaxy/datatypes/tabular.py | 2 +- test/unit/datatypes/test_vcf.py | 6 +++++- test/unit/datatypes/util.py | 4 +--- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/lib/galaxy/datatypes/tabular.py b/lib/galaxy/datatypes/tabular.py index 8a92dd2d082..35d028c0f03 100644 --- a/lib/galaxy/datatypes/tabular.py +++ b/lib/galaxy/datatypes/tabular.py @@ -764,7 +764,7 @@ class VcfGz(BaseVcf, binary.Binary): index_file = dataset.metadata.spec['tabix_index'].param.new_file(dataset=dataset) try: - pysam.tabix_index(dataset.file_name, index_file, preset='vcf') + pysam.tabix_index(dataset.file_name, index=index_file, preset='vcf') except Exception as e: raise Exception('Error setting VCF.gz metadata: %s' % (str(e))) dataset.metadata.tabix_index = index_file diff --git a/test/unit/datatypes/test_vcf.py b/test/unit/datatypes/test_vcf.py index cb40e6f708b..06c9292bb96 100644 --- a/test/unit/datatypes/test_vcf.py +++ b/test/unit/datatypes/test_vcf.py @@ -1,3 +1,5 @@ +import pysam + from galaxy.datatypes.tabular import ( Vcf, VcfGz, @@ -20,6 +22,8 @@ def test_vcf_sniff(): def test_vcf_gz_set_meta(): - vcf_gz = Vcf() + vcf_gz = VcfGz() with get_input_files('1.vcf.gz') as input_files, get_dataset(input_files[0], index_attr='tabix_index') as dataset: vcf_gz.set_meta(dataset) + f = pysam.VariantFile(dataset.file_name, index_filename=dataset.metadata.tabix_index) + assert isinstance(f.index, pysam.libcbcf.TabixIndex) is True diff --git a/test/unit/datatypes/util.py b/test/unit/datatypes/util.py index 0456e40cea3..b14096ee4f4 100644 --- a/test/unit/datatypes/util.py +++ b/test/unit/datatypes/util.py @@ -15,9 +15,7 @@ def get_dataset(file, index_attr='bam_index', dataset_id=1, has_data=True): dataset.metadata = Bunch() with get_input_files(file) as input_files, get_tmp_path() as index_path: dataset.file_name = input_files[0] - index = Bunch() - index.file_name = index_path - setattr(dataset.metadata, index_attr, index) + setattr(dataset.metadata, index_attr, index_path) yield dataset From 6ca896ff9b199ac097f408e35e37a3365ff2ec46 Mon Sep 17 00:00:00 2001 From: mvdbeek Date: Tue, 5 Dec 2017 19:26:20 +0100 Subject: [PATCH 12/22] Fix index file syntax and fix linting problems --- lib/galaxy/datatypes/tabular.py | 3 +-- test/unit/datatypes/test_bcf.py | 1 - test/unit/datatypes/test_cram.py | 7 ++++++- test/unit/datatypes/test_vcf.py | 2 +- test/unit/datatypes/util.py | 4 +++- 5 files changed, 11 insertions(+), 6 deletions(-) diff --git a/lib/galaxy/datatypes/tabular.py b/lib/galaxy/datatypes/tabular.py index 35d028c0f03..bb06bff86ce 100644 --- a/lib/galaxy/datatypes/tabular.py +++ b/lib/galaxy/datatypes/tabular.py @@ -27,7 +27,6 @@ from galaxy.datatypes.sniff import ( ) from galaxy.util import compression_utils from galaxy.util.checkers import is_gzip - from . import dataproviders if sys.version_info > (3,): @@ -764,7 +763,7 @@ class VcfGz(BaseVcf, binary.Binary): index_file = dataset.metadata.spec['tabix_index'].param.new_file(dataset=dataset) try: - pysam.tabix_index(dataset.file_name, index=index_file, preset='vcf') + pysam.tabix_index(dataset.file_name, index=index_file.file_name, preset='vcf') except Exception as e: raise Exception('Error setting VCF.gz metadata: %s' % (str(e))) dataset.metadata.tabix_index = index_file diff --git a/test/unit/datatypes/test_bcf.py b/test/unit/datatypes/test_bcf.py index c9577ae78f9..f455a50f670 100644 --- a/test/unit/datatypes/test_bcf.py +++ b/test/unit/datatypes/test_bcf.py @@ -2,7 +2,6 @@ from galaxy.datatypes.binary import ( Bcf, BcfUncompressed ) - from .util import ( get_dataset, get_input_files, diff --git a/test/unit/datatypes/test_cram.py b/test/unit/datatypes/test_cram.py index 57ae8aef1b5..4ea890d8e17 100644 --- a/test/unit/datatypes/test_cram.py +++ b/test/unit/datatypes/test_cram.py @@ -1,5 +1,6 @@ +import os +import pysam from galaxy.datatypes.binary import CRAM - from .util import ( get_dataset, get_input_files, @@ -9,5 +10,9 @@ from .util import ( def test_cram(): c = CRAM() with get_input_files('2.cram') as input_files, get_dataset(input_files[0], index_attr='cram_index') as dataset: + assert os.path.exists(dataset.metadata.cram_index.file_name) is False + c.set_index_file(dataset=dataset, index_file=dataset.metadata.cram_index) + assert os.path.exists(dataset.metadata.cram_index.file_name) is True c.set_meta(dataset) + pysam.AlignmentFile(dataset.file_name, index_filename=dataset.metadata.cram_index.file_name) assert dataset.metadata.cram_version == '3.0' diff --git a/test/unit/datatypes/test_vcf.py b/test/unit/datatypes/test_vcf.py index 06c9292bb96..e1219c3211f 100644 --- a/test/unit/datatypes/test_vcf.py +++ b/test/unit/datatypes/test_vcf.py @@ -25,5 +25,5 @@ def test_vcf_gz_set_meta(): vcf_gz = VcfGz() with get_input_files('1.vcf.gz') as input_files, get_dataset(input_files[0], index_attr='tabix_index') as dataset: vcf_gz.set_meta(dataset) - f = pysam.VariantFile(dataset.file_name, index_filename=dataset.metadata.tabix_index) + f = pysam.VariantFile(dataset.file_name, index_filename=dataset.metadata.tabix_index.file_name) assert isinstance(f.index, pysam.libcbcf.TabixIndex) is True diff --git a/test/unit/datatypes/util.py b/test/unit/datatypes/util.py index b14096ee4f4..0456e40cea3 100644 --- a/test/unit/datatypes/util.py +++ b/test/unit/datatypes/util.py @@ -15,7 +15,9 @@ def get_dataset(file, index_attr='bam_index', dataset_id=1, has_data=True): dataset.metadata = Bunch() with get_input_files(file) as input_files, get_tmp_path() as index_path: dataset.file_name = input_files[0] - setattr(dataset.metadata, index_attr, index_path) + index = Bunch() + index.file_name = index_path + setattr(dataset.metadata, index_attr, index) yield dataset From 9c4a2f70ea6a62f35b3c736730fcc71bddf6dce9 Mon Sep 17 00:00:00 2001 From: Dannon Baker Date: Tue, 5 Dec 2017 12:05:06 -0500 Subject: [PATCH 13/22] Tabix indexing fixes. Upstream used 'index' instead of 'index_filename', and 'force' is now required since we precreate the destination location. --- .../datatypes/converters/interval_to_tabix_converter.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/galaxy/datatypes/converters/interval_to_tabix_converter.py b/lib/galaxy/datatypes/converters/interval_to_tabix_converter.py index a8bee5eafc4..0b39f54dc7b 100644 --- a/lib/galaxy/datatypes/converters/interval_to_tabix_converter.py +++ b/lib/galaxy/datatypes/converters/interval_to_tabix_converter.py @@ -27,12 +27,12 @@ def main(): if options.preset: # Preset type. pysam.tabix_index(filename=index_fname, preset=options.preset, keep_original=True, - index_filename=out_fname) + index=out_fname, force=True) else: # For interval files; column indices are 0-based. pysam.tabix_index(filename=index_fname, seq_col=(options.chrom_col - 1), start_col=(options.start_col - 1), end_col=(options.end_col - 1), - keep_original=True, index_filename=out_fname) + keep_original=True, index=out_fname, force=True) if os.path.getsize(index_fname) == 0: sys.stderr.write("The converted tabix index file is empty, meaning the input data is invalid.") From 2a6e00dd001ea4f27cd7d9a12cddfcf9649da7ef Mon Sep 17 00:00:00 2001 From: mvdbeek Date: Tue, 5 Dec 2017 19:31:12 +0100 Subject: [PATCH 14/22] one more linting fix --- test/unit/datatypes/test_cram.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/unit/datatypes/test_cram.py b/test/unit/datatypes/test_cram.py index 4ea890d8e17..c117ff785d2 100644 --- a/test/unit/datatypes/test_cram.py +++ b/test/unit/datatypes/test_cram.py @@ -1,5 +1,7 @@ import os + import pysam + from galaxy.datatypes.binary import CRAM from .util import ( get_dataset, From 07158e03308e026ee013bd6a50708167918c2d27 Mon Sep 17 00:00:00 2001 From: mvdbeek Date: Wed, 6 Dec 2017 09:31:22 +0100 Subject: [PATCH 15/22] Need force=True in pysam.tabix_index because the index file path exists in the object store --- lib/galaxy/datatypes/tabular.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/galaxy/datatypes/tabular.py b/lib/galaxy/datatypes/tabular.py index bb06bff86ce..bf4b264fadf 100644 --- a/lib/galaxy/datatypes/tabular.py +++ b/lib/galaxy/datatypes/tabular.py @@ -763,7 +763,7 @@ class VcfGz(BaseVcf, binary.Binary): index_file = dataset.metadata.spec['tabix_index'].param.new_file(dataset=dataset) try: - pysam.tabix_index(dataset.file_name, index=index_file.file_name, preset='vcf') + pysam.tabix_index(dataset.file_name, index=index_file.file_name, preset='vcf', force=True) except Exception as e: raise Exception('Error setting VCF.gz metadata: %s' % (str(e))) dataset.metadata.tabix_index = index_file From 64deb4f935ac7969a0e81024297d84d9831a8cfd Mon Sep 17 00:00:00 2001 From: mvdbeek Date: Wed, 6 Dec 2017 09:57:13 +0100 Subject: [PATCH 16/22] We can use multiple threads when converting cram to bam files --- lib/galaxy/datatypes/converters/cram_to_bam.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/galaxy/datatypes/converters/cram_to_bam.py b/lib/galaxy/datatypes/converters/cram_to_bam.py index 6f2c6142601..c0634d77e3b 100644 --- a/lib/galaxy/datatypes/converters/cram_to_bam.py +++ b/lib/galaxy/datatypes/converters/cram_to_bam.py @@ -5,6 +5,7 @@ Uses pysam to convert a CRAM file to a sorted bam file. usage: %prog in_file out_file """ import optparse +import os import pysam @@ -14,7 +15,8 @@ def main(): parser = optparse.OptionParser() (options, args) = parser.parse_args() input_fname, output_fname = args - pysam.sort('-o', output_fname, '-O', 'bam', '-T', '.', input_fname) + slots = os.getenv('GALAXY_SLOTS', 1) + pysam.sort("-@%s" % slots, '-o', output_fname, '-O', 'bam', '-T', '.', input_fname) if __name__ == "__main__": From 90a56aea3f19df6943dfc648474d161e5c8f6d66 Mon Sep 17 00:00:00 2001 From: mvdbeek Date: Wed, 6 Dec 2017 10:03:16 +0100 Subject: [PATCH 17/22] Use more threads when sorting BAM files and GALAXY_SLOTS is set --- lib/galaxy/datatypes/binary.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/galaxy/datatypes/binary.py b/lib/galaxy/datatypes/binary.py index 18b0b3a760a..acbb0474177 100644 --- a/lib/galaxy/datatypes/binary.py +++ b/lib/galaxy/datatypes/binary.py @@ -255,8 +255,9 @@ class Bam(Binary): tmp_dir = tempfile.mkdtemp() tmp_sorted_dataset_file_name_prefix = os.path.join(tmp_dir, 'sorted') sorted_file_name = "%s.bam" % tmp_sorted_dataset_file_name_prefix # samtools accepts a prefix, not a filename, it always adds .bam to the prefix + slots = os.environ.get('GALAXY_SLOTS', 1) try: - pysam.sort(file_name, '-T', tmp_sorted_dataset_file_name_prefix, '-O', 'BAM', '-o', sorted_file_name) + pysam.sort("-@%s" % slots, file_name, '-T', tmp_sorted_dataset_file_name_prefix, '-O', 'BAM', '-o', sorted_file_name) except Exception: shutil.rmtree(tmp_dir, ignore_errors=True) raise From eb636c11361ef8f6cf85c8750f0a37608dcbee1a Mon Sep 17 00:00:00 2001 From: mvdbeek Date: Wed, 6 Dec 2017 14:18:57 +0100 Subject: [PATCH 18/22] Make sure files don't change in-place --- test/unit/datatypes/converters/__init__.py | 0 .../converters/test_interval_to_tabix.py | 0 test/unit/datatypes/test_bam.py | 13 ++++++++--- test/unit/datatypes/util.py | 23 ++++++++++++------- 4 files changed, 25 insertions(+), 11 deletions(-) create mode 100644 test/unit/datatypes/converters/__init__.py create mode 100644 test/unit/datatypes/converters/test_interval_to_tabix.py diff --git a/test/unit/datatypes/converters/__init__.py b/test/unit/datatypes/converters/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/test/unit/datatypes/converters/test_interval_to_tabix.py b/test/unit/datatypes/converters/test_interval_to_tabix.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/test/unit/datatypes/test_bam.py b/test/unit/datatypes/test_bam.py index d9483e5f524..c06efc18962 100644 --- a/test/unit/datatypes/test_bam.py +++ b/test/unit/datatypes/test_bam.py @@ -26,9 +26,16 @@ def test_dataset_content_needs_grooming(): def test_groom_dataset_content(): b = Bam() - with get_input_files('2.shuffled.bam') as input_files: - b.groom_dataset_content(input_files[0]) - assert b.dataset_content_needs_grooming(input_files[0]) is False + try: + with get_input_files('2.shuffled.bam') as input_files: + b.groom_dataset_content(input_files[0]) + assert b.dataset_content_needs_grooming(input_files[0]) is False + except AssertionError as e: + # Grooming modifies files in-place, so the md5 hash comparison has to fail + assert 'Unexpected change' in e.message + return + # should not reach this part of the test + raise Exception('Bam grooming did not occur in-place') def test_set_meta_presorted(): diff --git a/test/unit/datatypes/util.py b/test/unit/datatypes/util.py index 0456e40cea3..f1cce511941 100644 --- a/test/unit/datatypes/util.py +++ b/test/unit/datatypes/util.py @@ -5,6 +5,7 @@ from contextlib import contextmanager from galaxy.datatypes.sniff import get_test_fname from galaxy.util.bunch import Bunch +from galaxy.util.hash_util import md5_hash_file @contextmanager @@ -22,9 +23,10 @@ def get_dataset(file, index_attr='bam_index', dataset_id=1, has_data=True): @contextmanager -def get_tmp_path(): +def get_tmp_path(should_exist=False): _, path = tempfile.mkstemp() - os.remove(path) + if not should_exist: + os.remove(path) yield path try: os.remove(path) @@ -34,11 +36,16 @@ def get_tmp_path(): @contextmanager def get_input_files(*args): - # need to import here, otherwise get_test_fname is treated as a test temp_dir = tempfile.mkdtemp() test_files = [] - for file in args: - shutil.copy(get_test_fname(file), temp_dir) - test_files.append(os.path.join(temp_dir, file)) - yield test_files - shutil.rmtree(temp_dir, ignore_errors=True) + try: + for file in args: + shutil.copy(get_test_fname(file), temp_dir) + test_files.append(os.path.join(temp_dir, file)) + md5_sums = [md5_hash_file(f) for f in test_files] + yield test_files + new_md5_sums = [md5_hash_file(f) for f in test_files] + for old_hash, new_hash, f in zip(md5_sums, new_md5_sums, test_files): + assert old_hash == new_hash, 'Unexpected change of content for file %s' % f + finally: + shutil.rmtree(temp_dir, ignore_errors=True) From 1195183ea63c3916e1b8e163b2cc0839bd18d8c4 Mon Sep 17 00:00:00 2001 From: mvdbeek Date: Fri, 8 Dec 2017 10:24:03 +0100 Subject: [PATCH 19/22] Symlink tbi index to work around pysam limitation Before https://github.com/pysam-developers/pysam/pull/586 is merged and a new release is out we create a symlink to the tbi file, which is required for creating TabixFile instances. Since we want to cleanup the symlinks I turned `get_data_file` into a contextmanager. Along the way I also changed many open()/close() calls to `with` statements. --- .../visualization/data_providers/genome.py | 213 +++++++++--------- 1 file changed, 101 insertions(+), 112 deletions(-) diff --git a/lib/galaxy/visualization/data_providers/genome.py b/lib/galaxy/visualization/data_providers/genome.py index 1f7445104ea..ff4df6b942b 100644 --- a/lib/galaxy/visualization/data_providers/genome.py +++ b/lib/galaxy/visualization/data_providers/genome.py @@ -8,6 +8,8 @@ import os import random import re import sys +import tempfile +from contextlib import contextmanager from json import loads import pysam @@ -158,6 +160,7 @@ class GenomeDataProvider(BaseDataProvider): """ raise Exception("Unimplemented Function") + @contextmanager def open_data_file(self): """ Open data file for reading data. @@ -186,17 +189,9 @@ class GenomeDataProvider(BaseDataProvider): dataset_type, data """ start, end = int(low), int(high) - data_file = self.open_data_file() - iterator = self.get_iterator(data_file, chrom, start, end, **kwargs) - data = self.process_data(iterator, start_val, max_vals, start=start, end=end, **kwargs) - try: - data_file.close() - except AttributeError: - # FIXME: some data providers do not have a close function implemented. - # Providers without a close function include: - # bx IntervalIndex - pass - + with self.open_data_file() as data_file: + iterator = self.get_iterator(data_file, chrom, start, end, **kwargs) + data = self.process_data(iterator, start_val, max_vals, start=start, end=end, **kwargs) return data def get_genome_data(self, chroms_info, **kwargs): @@ -322,9 +317,17 @@ class TabixDataProvider(FilterableMixin, GenomeDataProvider): col_name_data_attr_mapping = {4: {'index': 4, 'name': 'Score'}} + @contextmanager def open_data_file(self): - return pysam.Tabixfile(self.dependencies['bgzip'].file_name, - index=self.converted_dataset.file_name) + # We create a symlnk to the index file. This is + # required until https://github.com/pysam-developers/pysam/pull/586 is merged. + fd, symlink_path = tempfile.mkstemp(suffix='.tbi') + os.close(fd) + os.unlink(symlink_path) + os.symlink(self.converted_dataset.file_name, symlink_path) + with pysam.TabixFile(self.dependencies['bgzip'].file_name, index=symlink_path) as f: + yield f + os.unlink(symlink_path) def get_iterator(self, data_file, chrom, start, end, **kwargs): # chrom must be a string, start/end integers. @@ -347,19 +350,12 @@ class TabixDataProvider(FilterableMixin, GenomeDataProvider): return iterator def write_data_to_file(self, regions, filename): - out = open(filename, "w") - - data_file = self.open_data_file() - for region in regions: - # Write data in region. - iterator = self.get_iterator(data_file, region.chrom, region.start, region.end) - for line in iterator: - out.write("%s\n" % line) - - # TODO: once Pysam is updated and Tabixfile has a close() method, - # data_file.close() - - out.close() + with self.open_data_file() as data_file, open(filename, 'w') as out: + for region in regions: + # Write data in region. + iterator = self.get_iterator(data_file, region.chrom, region.start, region.end) + for line in iterator: + out.write("%s\n" % line) # # -- Interval data providers -- @@ -528,20 +524,16 @@ class BedDataProvider(GenomeDataProvider): return {'data': rval, 'dataset_type': self.dataset_type, 'message': message} def write_data_to_file(self, regions, filename): - out = open(filename, "w") - - for region in regions: - # Write data in region. - chrom = region.chrom - start = region.start - end = region.end - data_file = self.open_data_file() - iterator = self.get_iterator(data_file, chrom, start, end) - for line in iterator: - out.write("%s\n" % line) - data_file.close() - - out.close() + with open(filename, "w") as out: + for region in regions: + # Write data in region. + chrom = region.chrom + start = region.start + end = region.end + with self.open_data_file() as data_file: + iterator = self.get_iterator(data_file, chrom, start, end) + for line in iterator: + out.write("%s\n" % line) class BedTabixDataProvider(TabixDataProvider, BedDataProvider): @@ -569,18 +561,19 @@ class RawBedDataProvider(BedDataProvider): data_file.seek(0) def line_filter_iter(): - for line in open(self.original_dataset.file_name): - if line.startswith("track") or line.startswith("browser"): - continue - feature = line.split() - feature_chrom = feature[0] - feature_start = int(feature[1]) - feature_end = int(feature[2]) - if (chrom is not None and feature_chrom != chrom) \ - or (start is not None and feature_start > end) \ - or (end is not None and feature_end < start): - continue - yield line + with open(self.original_dataset.file_name) as data_file: + for line in data_file: + if line.startswith("track") or line.startswith("browser"): + continue + feature = line.split() + feature_chrom = feature[0] + feature_start = int(feature[1]) + feature_end = int(feature[2]) + if (chrom is not None and feature_chrom != chrom) \ + or (start is not None and feature_start > end) \ + or (end is not None and feature_end < start): + continue + yield line return line_filter_iter() @@ -733,14 +726,12 @@ class VcfDataProvider(GenomeDataProvider): def write_data_to_file(self, regions, filename): out = open(filename, "w") - data_file = self.open_data_file() - - for region in regions: - # Write data in region. - iterator = self.get_iterator(data_file, region.chrom, region.start, region.end) - for line in iterator: - out.write("%s\n" % line) - out.close() + with self.open_data_file() as data_file: + for region in regions: + # Write data in region. + iterator = self.get_iterator(data_file, region.chrom, region.start, region.end) + for line in iterator: + out.write("%s\n" % line) class VcfTabixDataProvider(TabixDataProvider, VcfDataProvider): @@ -759,8 +750,10 @@ class RawVcfDataProvider(VcfDataProvider): for large datasets. """ + @contextmanager def open_data_file(self): - return open(self.original_dataset.file_name) + with open(self.original_dataset.file_name) as f: + yield f def get_iterator(self, data_file, chrom, start, end, **kwargs): # Skip comments. @@ -856,10 +849,12 @@ class BamDataProvider(GenomeDataProvider, FilterableMixin): new_bamfile.close() bamfile.close() + @contextmanager def open_data_file(self): # Attempt to open the BAM file with index - return pysam.AlignmentFile(self.original_dataset.file_name, mode='rb', - index_filename=self.converted_dataset.file_name) + with pysam.AlignmentFile(self.original_dataset.file_name, mode='rb', + index_filename=self.converted_dataset.file_name) as f: + yield f def get_iterator(self, data_file, chrom, start, end, **kwargs): """ @@ -1268,33 +1263,30 @@ class IntervalIndexDataProvider(FilterableMixin, GenomeDataProvider): dataset_type = 'interval_index' def write_data_to_file(self, regions, filename): - source = open(self.original_dataset.file_name) index = Indexes(self.converted_dataset.file_name) - out = open(filename, 'w') + with open(self.original_dataset.file_name) as source, open(filename, 'w') as out: + for region in regions: + # Write data from region. + chrom = region.chrom + start = region.start + end = region.end + for start, end, offset in index.find(chrom, start, end): + source.seek(offset) - for region in regions: - # Write data from region. - chrom = region.chrom - start = region.start - end = region.end - for start, end, offset in index.find(chrom, start, end): - source.seek(offset) - - # HACK: write differently depending on original dataset format. - if self.original_dataset.ext not in ['gff', 'gff3', 'gtf']: - line = source.readline() - out.write(line) - else: - reader = GFFReaderWrapper(source, fix_strand=True) - feature = reader.next() - for interval in feature.intervals: - out.write('\t'.join(interval.fields) + '\n') - - source.close() - out.close() + # HACK: write differently depending on original dataset format. + if self.original_dataset.ext not in ['gff', 'gff3', 'gtf']: + line = source.readline() + out.write(line) + else: + reader = GFFReaderWrapper(source, fix_strand=True) + feature = reader.next() + for interval in feature.intervals: + out.write('\t'.join(interval.fields) + '\n') + @contextmanager def open_data_file(self): - return Indexes(self.converted_dataset.file_name) + i = Indexes(self.converted_dataset.file_name) + yield i def get_iterator(self, data_file, chrom, start, end, **kwargs): """ @@ -1309,34 +1301,31 @@ class IntervalIndexDataProvider(FilterableMixin, GenomeDataProvider): def process_data(self, iterator, start_val=0, max_vals=None, **kwargs): results = [] message = None - source = open(self.original_dataset.file_name) + with open(self.original_dataset.file_name) as source: + # Build data to return. Payload format is: + # [ , , , , , , , + # , ] + # + # First three entries are mandatory, others are optional. + filter_cols = loads(kwargs.get("filter_cols", "[]")) + no_detail = ("no_detail" in kwargs) + for count, val in enumerate(iterator): + offset = val[2] + if count < start_val: + continue + if count - start_val >= max_vals: + message = self.error_max_vals % (max_vals, "features") + break + source.seek(offset) + # TODO: can we use column metadata to fill out payload? - # - # Build data to return. Payload format is: - # [ , , , , , , , - # , ] - # - # First three entries are mandatory, others are optional. - # - filter_cols = loads(kwargs.get("filter_cols", "[]")) - no_detail = ("no_detail" in kwargs) - for count, val in enumerate(iterator): - offset = val[2] - if count < start_val: - continue - if count - start_val >= max_vals: - message = self.error_max_vals % (max_vals, "features") - break - source.seek(offset) - # TODO: can we use column metadata to fill out payload? + # GFF dataset. + reader = GFFReaderWrapper(source, fix_strand=True) + feature = reader.next() + payload = package_gff_feature(feature, no_detail, filter_cols) + payload.insert(0, offset) - # GFF dataset. - reader = GFFReaderWrapper(source, fix_strand=True) - feature = reader.next() - payload = package_gff_feature(feature, no_detail, filter_cols) - payload.insert(0, offset) - - results.append(payload) + results.append(payload) return {'data': results, 'message': message} From 4795e490636e9a9234cb2d61c0a14508472a297d Mon Sep 17 00:00:00 2001 From: mvdbeek Date: Fri, 8 Dec 2017 13:33:18 +0100 Subject: [PATCH 20/22] Detect if index symlink hack is necessary --- .../visualization/data_providers/genome.py | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/lib/galaxy/visualization/data_providers/genome.py b/lib/galaxy/visualization/data_providers/genome.py index ff4df6b942b..8bf1cdafae6 100644 --- a/lib/galaxy/visualization/data_providers/genome.py +++ b/lib/galaxy/visualization/data_providers/genome.py @@ -10,6 +10,7 @@ import re import sys import tempfile from contextlib import contextmanager +from distutils.version import LooseVersion from json import loads import pysam @@ -26,6 +27,8 @@ from galaxy.visualization.data_providers.cigar import get_ref_based_read_seq_and # Utility functions. # +PYSAM_INDEX_SYMLINK_NECESSARY = LooseVersion(pysam.__version__) <= LooseVersion('0.13.0') + def float_nan(n): ''' @@ -321,13 +324,17 @@ class TabixDataProvider(FilterableMixin, GenomeDataProvider): def open_data_file(self): # We create a symlnk to the index file. This is # required until https://github.com/pysam-developers/pysam/pull/586 is merged. - fd, symlink_path = tempfile.mkstemp(suffix='.tbi') - os.close(fd) - os.unlink(symlink_path) - os.symlink(self.converted_dataset.file_name, symlink_path) - with pysam.TabixFile(self.dependencies['bgzip'].file_name, index=symlink_path) as f: + if PYSAM_INDEX_SYMLINK_NECESSARY: + fd, index_path = tempfile.mkstemp(suffix='.tbi') + os.close(fd) + os.unlink(index_path) + os.symlink(self.converted_dataset.file_name, index_path) + else: + index_path = self.converted_dataset.file_name + with pysam.TabixFile(self.dependencies['bgzip'].file_name, index=index_path) as f: yield f - os.unlink(symlink_path) + if PYSAM_INDEX_SYMLINK_NECESSARY: + os.unlink(index_path) def get_iterator(self, data_file, chrom, start, end, **kwargs): # chrom must be a string, start/end integers. From 46371d1251ed80b740956dc7111708aa93041092 Mon Sep 17 00:00:00 2001 From: mvdbeek Date: Fri, 8 Dec 2017 14:56:13 +0100 Subject: [PATCH 21/22] Decompose interval_to_tabix converter script This renames some variables to make it clearer what files they reflect. Also adds a very basic test that this works as intended. --- .../converters/interval_to_tabix_converter.py | 25 +++++++++++++------ .../converters/test_interval_to_tabix.py | 17 +++++++++++++ test/unit/datatypes/util.py | 4 +-- 3 files changed, 36 insertions(+), 10 deletions(-) diff --git a/lib/galaxy/datatypes/converters/interval_to_tabix_converter.py b/lib/galaxy/datatypes/converters/interval_to_tabix_converter.py index 0b39f54dc7b..faf988da48e 100644 --- a/lib/galaxy/datatypes/converters/interval_to_tabix_converter.py +++ b/lib/galaxy/datatypes/converters/interval_to_tabix_converter.py @@ -21,20 +21,29 @@ def main(): parser.add_option('-e', '--end-col', type='int', dest='end_col') parser.add_option('-P', '--preset', dest='preset') (options, args) = parser.parse_args() - input_fname, index_fname, out_fname = args + _, bgzip_fname, out_fname = args + to_tabix(bgzip_fname=bgzip_fname, + out_fname=out_fname, + preset=options.preset, + chrom_col=options.chrom_col, + start_col=options.start_col, + end_col=options.end_col) + +def to_tabix(bgzip_fname, out_fname, preset=None, chrom_col=None, start_col=None, end_col=None): # Create index. - if options.preset: + if preset: # Preset type. - pysam.tabix_index(filename=index_fname, preset=options.preset, keep_original=True, - index=out_fname, force=True) + bgzip_fname = pysam.tabix_index(filename=bgzip_fname, preset=preset, keep_original=True, + index=out_fname, force=True) else: # For interval files; column indices are 0-based. - pysam.tabix_index(filename=index_fname, seq_col=(options.chrom_col - 1), - start_col=(options.start_col - 1), end_col=(options.end_col - 1), - keep_original=True, index=out_fname, force=True) - if os.path.getsize(index_fname) == 0: + bgzip_fname = pysam.tabix_index(filename=bgzip_fname, seq_col=(chrom_col - 1), + start_col=(start_col - 1), end_col=(end_col - 1), + keep_original=True, index=out_fname, force=True) + if os.path.getsize(out_fname) == 0: sys.stderr.write("The converted tabix index file is empty, meaning the input data is invalid.") + return bgzip_fname if __name__ == "__main__": diff --git a/test/unit/datatypes/converters/test_interval_to_tabix.py b/test/unit/datatypes/converters/test_interval_to_tabix.py index e69de29bb2d..574de716ed5 100644 --- a/test/unit/datatypes/converters/test_interval_to_tabix.py +++ b/test/unit/datatypes/converters/test_interval_to_tabix.py @@ -0,0 +1,17 @@ +import pysam + +from galaxy.datatypes.converters.interval_to_tabix_converter import to_tabix +from ..util import ( + get_input_files, + get_tmp_path +) + + +def test_to_tabix(): + with get_input_files('1.vcf') as input_files: + with get_tmp_path(suffix='.tbi') as index: + bgzip_fname = to_tabix(input_files[0], + index, + preset='vcf') + f = pysam.TabixFile(bgzip_fname, index=index) + f.close() diff --git a/test/unit/datatypes/util.py b/test/unit/datatypes/util.py index f1cce511941..e49b75f12c5 100644 --- a/test/unit/datatypes/util.py +++ b/test/unit/datatypes/util.py @@ -23,8 +23,8 @@ def get_dataset(file, index_attr='bam_index', dataset_id=1, has_data=True): @contextmanager -def get_tmp_path(should_exist=False): - _, path = tempfile.mkstemp() +def get_tmp_path(should_exist=False, suffix=''): + _, path = tempfile.mkstemp(suffix=suffix) if not should_exist: os.remove(path) yield path From 309b71720e3b5615fac6abd028aa66735fa94eef Mon Sep 17 00:00:00 2001 From: mvdbeek Date: Sat, 9 Dec 2017 22:16:02 +0100 Subject: [PATCH 22/22] Improvements to datatypes suggested by @nsoranzo --- lib/galaxy/datatypes/binary.py | 10 +++++----- lib/galaxy/datatypes/tabular.py | 5 ++--- lib/galaxy/datatypes/test/1.unsorted.bam | Bin 4914 -> 0 bytes .../visualization/data_providers/genome.py | 2 +- test/unit/datatypes/util.py | 10 +++++----- 5 files changed, 13 insertions(+), 14 deletions(-) delete mode 100644 lib/galaxy/datatypes/test/1.unsorted.bam diff --git a/lib/galaxy/datatypes/binary.py b/lib/galaxy/datatypes/binary.py index acbb0474177..daefe7a7e97 100644 --- a/lib/galaxy/datatypes/binary.py +++ b/lib/galaxy/datatypes/binary.py @@ -210,7 +210,7 @@ class Bam(Binary): @staticmethod def merge(split_files, output_file): """ - Merges Bam files + Merges BAM files :param split_files: List of bam file paths to merge :param output_file: Write merged bam file to this location @@ -232,7 +232,7 @@ class Bam(Binary): with open(os.devnull, 'w') as devnull: subprocess.check_call(cmd, stderr=devnull, shell=False) needs_sorting = False - except Exception: + except subprocess.CalledProcessError: needs_sorting = True try: os.unlink(index_name) @@ -242,10 +242,10 @@ class Bam(Binary): def groom_dataset_content(self, file_name): """ - Ensures that the Bam file contents are sorted. This function is called + Ensures that the BAM file contents are sorted. This function is called on an output dataset after the content is initially generated. """ - # Use pysam to sort the Bam file + # Use pysam to sort the BAM file # This command may also creates temporary files .%d.bam when the # whole alignment cannot fit into memory. # do this in a unique temp directory, because of possible .%d.bam temp files @@ -254,7 +254,7 @@ class Bam(Binary): return tmp_dir = tempfile.mkdtemp() tmp_sorted_dataset_file_name_prefix = os.path.join(tmp_dir, 'sorted') - sorted_file_name = "%s.bam" % tmp_sorted_dataset_file_name_prefix # samtools accepts a prefix, not a filename, it always adds .bam to the prefix + sorted_file_name = "%s.bam" % tmp_sorted_dataset_file_name_prefix slots = os.environ.get('GALAXY_SLOTS', 1) try: pysam.sort("-@%s" % slots, file_name, '-T', tmp_sorted_dataset_file_name_prefix, '-O', 'BAM', '-o', sorted_file_name) diff --git a/lib/galaxy/datatypes/tabular.py b/lib/galaxy/datatypes/tabular.py index bf4b264fadf..51886d7b1e3 100644 --- a/lib/galaxy/datatypes/tabular.py +++ b/lib/galaxy/datatypes/tabular.py @@ -16,7 +16,6 @@ from cgi import escape from json import dumps import pysam -import pysam.bcftools from galaxy import util from galaxy.datatypes import binary, data, metadata @@ -740,7 +739,7 @@ class Vcf(BaseVcf): def sniff(self, filename): if is_gzip(filename): return False - return BaseVcf.sniff(self, filename) + return super(Vcf, self).sniff(filename) class VcfGz(BaseVcf, binary.Binary): @@ -752,7 +751,7 @@ class VcfGz(BaseVcf, binary.Binary): def sniff(self, filename): if not is_gzip(filename): return False - return BaseVcf.sniff(self, filename) + return super(VcfGz, self).sniff(filename) def set_meta(self, dataset, **kwd): super(BaseVcf, self).set_meta(dataset, **kwd) diff --git a/lib/galaxy/datatypes/test/1.unsorted.bam b/lib/galaxy/datatypes/test/1.unsorted.bam deleted file mode 100644 index a24874147b5e651e2c4c221c5ba06f873df53753..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4914 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 kn!N=803VA81ONa4009360763o02=@U00000000000HTq&Z~y=R diff --git a/lib/galaxy/visualization/data_providers/genome.py b/lib/galaxy/visualization/data_providers/genome.py index 8bf1cdafae6..d78fbccadc5 100644 --- a/lib/galaxy/visualization/data_providers/genome.py +++ b/lib/galaxy/visualization/data_providers/genome.py @@ -322,7 +322,7 @@ class TabixDataProvider(FilterableMixin, GenomeDataProvider): @contextmanager def open_data_file(self): - # We create a symlnk to the index file. This is + # We create a symlink to the index file. This is # required until https://github.com/pysam-developers/pysam/pull/586 is merged. if PYSAM_INDEX_SYMLINK_NECESSARY: fd, index_path = tempfile.mkstemp(suffix='.tbi') diff --git a/test/unit/datatypes/util.py b/test/unit/datatypes/util.py index e49b75f12c5..47d5be832b9 100644 --- a/test/unit/datatypes/util.py +++ b/test/unit/datatypes/util.py @@ -9,12 +9,12 @@ from galaxy.util.hash_util import md5_hash_file @contextmanager -def get_dataset(file, index_attr='bam_index', dataset_id=1, has_data=True): +def get_dataset(filename, index_attr='bam_index', dataset_id=1, has_data=True): dataset = Bunch() dataset.has_data = lambda: True dataset.id = dataset_id dataset.metadata = Bunch() - with get_input_files(file) as input_files, get_tmp_path() as index_path: + with get_input_files(filename) as input_files, get_tmp_path() as index_path: dataset.file_name = input_files[0] index = Bunch() index.file_name = index_path @@ -39,9 +39,9 @@ def get_input_files(*args): temp_dir = tempfile.mkdtemp() test_files = [] try: - for file in args: - shutil.copy(get_test_fname(file), temp_dir) - test_files.append(os.path.join(temp_dir, file)) + for filename in args: + shutil.copy(get_test_fname(filename), temp_dir) + test_files.append(os.path.join(temp_dir, filename)) md5_sums = [md5_hash_file(f) for f in test_files] yield test_files new_md5_sums = [md5_hash_file(f) for f in test_files]