diff --git a/.gitignore b/.gitignore index eeb5ed3..308676a 100644 --- a/.gitignore +++ b/.gitignore @@ -68,3 +68,8 @@ ENV/ # mypy .mypy_cache/ + +# Test output files +*.png +*.pdf +*.svg diff --git a/src/flexidot/utils/alignments.py b/src/flexidot/utils/alignments.py index c81f624..8454e63 100644 --- a/src/flexidot/utils/alignments.py +++ b/src/flexidot/utils/alignments.py @@ -133,7 +133,15 @@ def parse_blast6( } ) - logging.info(f'Parsed {len(alignments)} alignments from BLAST6 file: {filepath}') + if len(alignments) == 0: + logging.warning( + f'No alignments found in BLAST6 file: {filepath}. ' + 'Plot will be generated without alignment overlays.' + ) + else: + logging.info( + f'Parsed {len(alignments)} alignments from BLAST6 file: {filepath}' + ) return alignments @@ -260,7 +268,13 @@ def parse_paf( } ) - logging.info(f'Parsed {len(alignments)} alignments from PAF file: {filepath}') + if len(alignments) == 0: + logging.warning( + f'No alignments found in PAF file: {filepath}. ' + 'Plot will be generated without alignment overlays.' + ) + else: + logging.info(f'Parsed {len(alignments)} alignments from PAF file: {filepath}') return alignments diff --git a/src/flexidot/utils/file_handling.py b/src/flexidot/utils/file_handling.py index db0d373..04f1b2b 100644 --- a/src/flexidot/utils/file_handling.py +++ b/src/flexidot/utils/file_handling.py @@ -342,28 +342,33 @@ def read_gffs( ) logging.info(text) - # create color legend - colors, alphas = [], [] - for item in sorted(used_feats): - colors.append(color_dict[item][0]) - alphas.append(color_dict[item][1]) - legend_figure( - colors=colors, - lcs_shading_num=len(used_feats), - type_nuc=type_nuc, - bins=sorted(used_feats), - alphas=alphas, - gff_legend=True, - prefix=prefix, - filetype=filetype, - ) + # check if any annotations were found + if len(used_feats) == 0: + text = 'Warning: No annotation records found in GFF file(s). Plot will be generated without annotations.\n' + logging.warning(text) + else: + # create color legend + colors, alphas = [], [] + for item in sorted(used_feats): + colors.append(color_dict[item][0]) + alphas.append(color_dict[item][1]) + legend_figure( + colors=colors, + lcs_shading_num=len(used_feats), + type_nuc=type_nuc, + bins=sorted(used_feats), + alphas=alphas, + gff_legend=True, + prefix=prefix, + filetype=filetype, + ) - # print settings - text = 'GFF Feature Types: %s\nGFF Colors: %s' % ( - ', '.join(sorted(used_feats)), - ', '.join(sorted(colors)), - ) - logging.info(text) + # print settings + text = 'GFF Feature Types: %s\nGFF Colors: %s' % ( + ', '.join(sorted(used_feats)), + ', '.join(sorted(colors)), + ) + logging.info(text) return feat_dict @@ -548,6 +553,12 @@ def legend_figure( alphas = [] alphas = [1] * len(colors) + # handle empty colors list + if len(colors) == 0: + text = 'Warning: No colors provided for legend. Skipping legend creation.\n' + logging.warning(text) + return None + # legend data points data_points = list(range(len(colors))) if not gff_legend: diff --git a/tests/test-data/empty.gff3 b/tests/test-data/empty.gff3 new file mode 100644 index 0000000..bbfaa17 --- /dev/null +++ b/tests/test-data/empty.gff3 @@ -0,0 +1,2 @@ +##gff-version 3 +# This is an empty GFF3 file with only header and comments diff --git a/tests/test_gff_handling.py b/tests/test_gff_handling.py new file mode 100644 index 0000000..0275f8e --- /dev/null +++ b/tests/test_gff_handling.py @@ -0,0 +1,104 @@ +"""Tests for GFF file handling functions.""" + +from pathlib import Path + +from flexidot.utils.alignments import load_alignments +from flexidot.utils.file_handling import read_gffs + +# Test data paths +TEST_DATA_DIR = Path(__file__).parent / 'test-data' +EMPTY_GFF_FILE = TEST_DATA_DIR / 'empty.gff3' +EXAMPLE_GFF_FILE = TEST_DATA_DIR / 'example.gff3' + + +class TestReadGffs: + """Tests for read_gffs function.""" + + def test_read_empty_gff(self): + """Test reading an empty GFF file with no annotation records.""" + # Should not raise an error even with empty GFF + feat_dict = read_gffs( + str(EMPTY_GFF_FILE), + color_dict={'others': ('grey', 1, 0)}, + type_nuc=True, + prefix='test', + filetype='png', + ) + # Should return empty dictionary + assert feat_dict == {} + + def test_read_valid_gff(self): + """Test reading a valid GFF file with annotations.""" + feat_dict = read_gffs( + str(EXAMPLE_GFF_FILE), + color_dict={ + 'spacer1': ('blue', 1, 0), + 'repeat_region': ('red', 1, 0), + 'spacerzoom': ('green', 1, 0), + 'spacer2': ('yellow', 1, 0), + 'spacer3': ('purple', 1, 0), + 'others': ('grey', 1, 0), + }, + type_nuc=True, + prefix='test', + filetype='png', + ) + # Should return non-empty dictionary + assert len(feat_dict) > 0 + assert 'Seq2' in feat_dict + + def test_read_multiple_gffs_with_empty(self): + """Test reading multiple GFF files where one is empty.""" + feat_dict = read_gffs( + [str(EXAMPLE_GFF_FILE), str(EMPTY_GFF_FILE)], + color_dict={ + 'spacer1': ('blue', 1, 0), + 'repeat_region': ('red', 1, 0), + 'spacerzoom': ('green', 1, 0), + 'spacer2': ('yellow', 1, 0), + 'spacer3': ('purple', 1, 0), + 'others': ('grey', 1, 0), + }, + type_nuc=True, + prefix='test', + filetype='png', + ) + # Should still work and return data from the non-empty file + assert len(feat_dict) > 0 + assert 'Seq2' in feat_dict + + +class TestEmptyAlignments: + """Tests for empty alignment file handling.""" + + def test_empty_blast6_file(self, tmp_path): + """Test reading an empty BLAST6 file.""" + empty_file = tmp_path / 'empty.blast6' + empty_file.write_text('') + + alignments = load_alignments(str(empty_file), file_format='blast6') + assert alignments == [] + + def test_empty_paf_file(self, tmp_path): + """Test reading an empty PAF file.""" + empty_file = tmp_path / 'empty.paf' + empty_file.write_text('') + + alignments = load_alignments(str(empty_file), file_format='paf') + assert alignments == [] + + def test_blast6_file_with_only_comments(self, tmp_path): + """Test reading a BLAST6 file with only comments.""" + comment_file = tmp_path / 'comments.blast6' + comment_file.write_text('# This is a comment\n# Another comment\n') + + alignments = load_alignments(str(comment_file), file_format='blast6') + assert alignments == [] + + def test_paf_file_with_only_comments(self, tmp_path): + """Test reading a PAF file with only comments.""" + comment_file = tmp_path / 'comments.paf' + comment_file.write_text('# This is a comment\n# Another comment\n') + + alignments = load_alignments(str(comment_file), file_format='paf') + assert alignments == []