Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -68,3 +68,8 @@ ENV/

# mypy
.mypy_cache/

# Test output files
*.png
*.pdf
*.svg
18 changes: 16 additions & 2 deletions src/flexidot/utils/alignments.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down Expand Up @@ -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


Expand Down
53 changes: 32 additions & 21 deletions src/flexidot/utils/file_handling.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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:
Expand Down
2 changes: 2 additions & 0 deletions tests/test-data/empty.gff3
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
##gff-version 3
# This is an empty GFF3 file with only header and comments
104 changes: 104 additions & 0 deletions tests/test_gff_handling.py
Original file line number Diff line number Diff line change
@@ -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 == []