varcode 9.2.7__tar.gz → 9.3.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {varcode-9.2.7/varcode.egg-info → varcode-9.3.0}/PKG-INFO +1 -1
- {varcode-9.2.7 → varcode-9.3.0}/tests/test_documentation_examples.py +21 -0
- varcode-9.3.0/tests/test_fusion_partner_candidates.py +184 -0
- {varcode-9.2.7 → varcode-9.3.0}/tests/test_nonlocal_sv_transcripts.py +1 -1
- varcode-9.3.0/tests/test_sv_sequence_changes.py +296 -0
- {varcode-9.2.7 → varcode-9.3.0}/varcode/effects/effect_classes.py +30 -0
- {varcode-9.2.7 → varcode-9.3.0}/varcode/effects/effect_collection.py +17 -3
- varcode-9.3.0/varcode/effects/sequence_change.py +142 -0
- {varcode-9.2.7 → varcode-9.3.0}/varcode/effects/structural.py +44 -28
- varcode-9.3.0/varcode/version.py +1 -0
- {varcode-9.2.7 → varcode-9.3.0/varcode.egg-info}/PKG-INFO +1 -1
- {varcode-9.2.7 → varcode-9.3.0}/varcode.egg-info/SOURCES.txt +3 -0
- varcode-9.2.7/varcode/version.py +0 -1
- {varcode-9.2.7 → varcode-9.3.0}/LICENSE +0 -0
- {varcode-9.2.7 → varcode-9.3.0}/MANIFEST.in +0 -0
- {varcode-9.2.7 → varcode-9.3.0}/README.md +0 -0
- {varcode-9.2.7 → varcode-9.3.0}/pyproject.toml +0 -0
- {varcode-9.2.7 → varcode-9.3.0}/requirements.txt +0 -0
- {varcode-9.2.7 → varcode-9.3.0}/setup.cfg +0 -0
- {varcode-9.2.7 → varcode-9.3.0}/tests/test_annotator_divergence_scenarios.py +0 -0
- {varcode-9.2.7 → varcode-9.3.0}/tests/test_annotator_parity_adversarial.py +0 -0
- {varcode-9.2.7 → varcode-9.3.0}/tests/test_annotators.py +0 -0
- {varcode-9.2.7 → varcode-9.3.0}/tests/test_bnd_retained_segments.py +0 -0
- {varcode-9.2.7 → varcode-9.3.0}/tests/test_cli_effects.py +0 -0
- {varcode-9.2.7 → varcode-9.3.0}/tests/test_cli_genes.py +0 -0
- {varcode-9.2.7 → varcode-9.3.0}/tests/test_cli_without_pkg_resources.py +0 -0
- {varcode-9.2.7 → varcode-9.3.0}/tests/test_collection_filtering.py +0 -0
- {varcode-9.2.7 → varcode-9.3.0}/tests/test_collection_variants_attr_consistency.py +0 -0
- {varcode-9.2.7 → varcode-9.3.0}/tests/test_common.py +0 -0
- {varcode-9.2.7 → varcode-9.3.0}/tests/test_cosmic_mutations.py +0 -0
- {varcode-9.2.7 → varcode-9.3.0}/tests/test_cryptic_exons.py +0 -0
- {varcode-9.2.7 → varcode-9.3.0}/tests/test_csv_roundtrip.py +0 -0
- {varcode-9.2.7 → varcode-9.3.0}/tests/test_dbnsfp_validation.py +0 -0
- {varcode-9.2.7 → varcode-9.3.0}/tests/test_default_structural_helpers.py +0 -0
- {varcode-9.2.7 → varcode-9.3.0}/tests/test_effect_annotation_errors.py +0 -0
- {varcode-9.2.7 → varcode-9.3.0}/tests/test_effect_candidates.py +0 -0
- {varcode-9.2.7 → varcode-9.3.0}/tests/test_effect_classes.py +0 -0
- {varcode-9.2.7 → varcode-9.3.0}/tests/test_effect_collection.py +0 -0
- {varcode-9.2.7 → varcode-9.3.0}/tests/test_effect_collection_serialization.py +0 -0
- {varcode-9.2.7 → varcode-9.3.0}/tests/test_effect_collection_sort_order.py +0 -0
- {varcode-9.2.7 → varcode-9.3.0}/tests/test_effect_hypotheses.py +0 -0
- {varcode-9.2.7 → varcode-9.3.0}/tests/test_effects_from_mutagenix_variants.py +0 -0
- {varcode-9.2.7 → varcode-9.3.0}/tests/test_exacto_fusions.py +0 -0
- {varcode-9.2.7 → varcode-9.3.0}/tests/test_exonic_splice_site.py +0 -0
- {varcode-9.2.7 → varcode-9.3.0}/tests/test_fast_path.py +0 -0
- {varcode-9.2.7 → varcode-9.3.0}/tests/test_frameshift_cterminus_regression.py +0 -0
- {varcode-9.2.7 → varcode-9.3.0}/tests/test_frameshift_helpers.py +0 -0
- {varcode-9.2.7 → varcode-9.3.0}/tests/test_genome_contig_validation.py +0 -0
- {varcode-9.2.7 → varcode-9.3.0}/tests/test_genome_sequence.py +0 -0
- {varcode-9.2.7 → varcode-9.3.0}/tests/test_genomic_layout.py +0 -0
- {varcode-9.2.7 → varcode-9.3.0}/tests/test_genotype.py +0 -0
- {varcode-9.2.7 → varcode-9.3.0}/tests/test_genotype_from_vcf.py +0 -0
- {varcode-9.2.7 → varcode-9.3.0}/tests/test_germline_context.py +0 -0
- {varcode-9.2.7 → varcode-9.3.0}/tests/test_germline_effects.py +0 -0
- {varcode-9.2.7 → varcode-9.3.0}/tests/test_haplotype_effect.py +0 -0
- {varcode-9.2.7 → varcode-9.3.0}/tests/test_maf.py +0 -0
- {varcode-9.2.7 → varcode-9.3.0}/tests/test_mitochondrial.py +0 -0
- {varcode-9.2.7 → varcode-9.3.0}/tests/test_mm10_klf6_frameshift.py +0 -0
- {varcode-9.2.7 → varcode-9.3.0}/tests/test_mouse.py +0 -0
- {varcode-9.2.7 → varcode-9.3.0}/tests/test_mutant_transcript.py +0 -0
- {varcode-9.2.7 → varcode-9.3.0}/tests/test_mutate.py +0 -0
- {varcode-9.2.7 → varcode-9.3.0}/tests/test_no_duplicate_variants.py +0 -0
- {varcode-9.2.7 → varcode-9.3.0}/tests/test_osteosarc_fusions.py +0 -0
- {varcode-9.2.7 → varcode-9.3.0}/tests/test_partial_annotators.py +0 -0
- {varcode-9.2.7 → varcode-9.3.0}/tests/test_premature_stop_short_description.py +0 -0
- {varcode-9.2.7 → varcode-9.3.0}/tests/test_problematic_variants.py +0 -0
- {varcode-9.2.7 → varcode-9.3.0}/tests/test_protein_diff_parity.py +0 -0
- {varcode-9.2.7 → varcode-9.3.0}/tests/test_read_phase_resolver.py +0 -0
- {varcode-9.2.7 → varcode-9.3.0}/tests/test_real_world_callers.py +0 -0
- {varcode-9.2.7 → varcode-9.3.0}/tests/test_reference.py +0 -0
- {varcode-9.2.7 → varcode-9.3.0}/tests/test_reference_mismatch_error.py +0 -0
- {varcode-9.2.7 → varcode-9.3.0}/tests/test_reference_segments.py +0 -0
- {varcode-9.2.7 → varcode-9.3.0}/tests/test_rna_evidence.py +0 -0
- {varcode-9.2.7 → varcode-9.3.0}/tests/test_rna_haplotype_sequences.py +0 -0
- {varcode-9.2.7 → varcode-9.3.0}/tests/test_rna_read_phasing_source.py +0 -0
- {varcode-9.2.7 → varcode-9.3.0}/tests/test_selenocysteine.py +0 -0
- {varcode-9.2.7 → varcode-9.3.0}/tests/test_silent_aa_pos.py +0 -0
- {varcode-9.2.7 → varcode-9.3.0}/tests/test_silent_hgvs_description.py +0 -0
- {varcode-9.2.7 → varcode-9.3.0}/tests/test_splice_graph.py +0 -0
- {varcode-9.2.7 → varcode-9.3.0}/tests/test_splice_outcomes.py +0 -0
- {varcode-9.2.7 → varcode-9.3.0}/tests/test_splice_site_effects.py +0 -0
- {varcode-9.2.7 → varcode-9.3.0}/tests/test_stop_codon_classification_bugs.py +0 -0
- {varcode-9.2.7 → varcode-9.3.0}/tests/test_string_helpers.py +0 -0
- {varcode-9.2.7 → varcode-9.3.0}/tests/test_structural_variant_annotator.py +0 -0
- {varcode-9.2.7 → varcode-9.3.0}/tests/test_structural_variants.py +0 -0
- {varcode-9.2.7 → varcode-9.3.0}/tests/test_sv_tabular_metadata.py +0 -0
- {varcode-9.2.7 → varcode-9.3.0}/tests/test_symbolic_alleles.py +0 -0
- {varcode-9.2.7 → varcode-9.3.0}/tests/test_symbolic_sv_padding.py +0 -0
- {varcode-9.2.7 → varcode-9.3.0}/tests/test_timings.py +0 -0
- {varcode-9.2.7 → varcode-9.3.0}/tests/test_transcript_layout.py +0 -0
- {varcode-9.2.7 → varcode-9.3.0}/tests/test_transcript_model.py +0 -0
- {varcode-9.2.7 → varcode-9.3.0}/tests/test_transforms_left_align.py +0 -0
- {varcode-9.2.7 → varcode-9.3.0}/tests/test_transforms_pair_breakends.py +0 -0
- {varcode-9.2.7 → varcode-9.3.0}/tests/test_variant.py +0 -0
- {varcode-9.2.7 → varcode-9.3.0}/tests/test_variant_collection.py +0 -0
- {varcode-9.2.7 → varcode-9.3.0}/tests/test_vcf.py +0 -0
- {varcode-9.2.7 → varcode-9.3.0}/tests/test_vcf_output.py +0 -0
- {varcode-9.2.7 → varcode-9.3.0}/tests/test_vcf_parsing.py +0 -0
- {varcode-9.2.7 → varcode-9.3.0}/tests/test_vcf_phase_resolver.py +0 -0
- {varcode-9.2.7 → varcode-9.3.0}/tests/test_vcf_spec_examples.py +0 -0
- {varcode-9.2.7 → varcode-9.3.0}/varcode/__init__.py +0 -0
- {varcode-9.2.7 → varcode-9.3.0}/varcode/annotators/__init__.py +0 -0
- {varcode-9.2.7 → varcode-9.3.0}/varcode/annotators/fast.py +0 -0
- {varcode-9.2.7 → varcode-9.3.0}/varcode/annotators/protein_diff.py +0 -0
- {varcode-9.2.7 → varcode-9.3.0}/varcode/annotators/registry.py +0 -0
- {varcode-9.2.7 → varcode-9.3.0}/varcode/cli/__init__.py +0 -0
- {varcode-9.2.7 → varcode-9.3.0}/varcode/cli/effects_script.py +0 -0
- {varcode-9.2.7 → varcode-9.3.0}/varcode/cli/genes_script.py +0 -0
- {varcode-9.2.7 → varcode-9.3.0}/varcode/cli/logging.conf +0 -0
- {varcode-9.2.7 → varcode-9.3.0}/varcode/cli/variant_args.py +0 -0
- {varcode-9.2.7 → varcode-9.3.0}/varcode/cli/version_info.py +0 -0
- {varcode-9.2.7 → varcode-9.3.0}/varcode/common.py +0 -0
- {varcode-9.2.7 → varcode-9.3.0}/varcode/cryptic_exons.py +0 -0
- {varcode-9.2.7 → varcode-9.3.0}/varcode/csv_helpers.py +0 -0
- {varcode-9.2.7 → varcode-9.3.0}/varcode/effect_candidates.py +0 -0
- {varcode-9.2.7 → varcode-9.3.0}/varcode/effect_hypotheses.py +0 -0
- {varcode-9.2.7 → varcode-9.3.0}/varcode/effects/__init__.py +0 -0
- {varcode-9.2.7 → varcode-9.3.0}/varcode/effects/classify.py +0 -0
- {varcode-9.2.7 → varcode-9.3.0}/varcode/effects/codon_tables.py +0 -0
- {varcode-9.2.7 → varcode-9.3.0}/varcode/effects/common.py +0 -0
- {varcode-9.2.7 → varcode-9.3.0}/varcode/effects/effect_helpers.py +0 -0
- {varcode-9.2.7 → varcode-9.3.0}/varcode/effects/effect_ordering.py +0 -0
- {varcode-9.2.7 → varcode-9.3.0}/varcode/effects/effect_prediction.py +0 -0
- {varcode-9.2.7 → varcode-9.3.0}/varcode/effects/effect_prediction_coding.py +0 -0
- {varcode-9.2.7 → varcode-9.3.0}/varcode/effects/effect_prediction_coding_frameshift.py +0 -0
- {varcode-9.2.7 → varcode-9.3.0}/varcode/effects/effect_prediction_coding_in_frame.py +0 -0
- {varcode-9.2.7 → varcode-9.3.0}/varcode/effects/fast_path.py +0 -0
- {varcode-9.2.7 → varcode-9.3.0}/varcode/effects/mutate.py +0 -0
- {varcode-9.2.7 → varcode-9.3.0}/varcode/effects/transcript_helpers.py +0 -0
- {varcode-9.2.7 → varcode-9.3.0}/varcode/effects/translate.py +0 -0
- {varcode-9.2.7 → varcode-9.3.0}/varcode/errors.py +0 -0
- {varcode-9.2.7 → varcode-9.3.0}/varcode/exacto.py +0 -0
- {varcode-9.2.7 → varcode-9.3.0}/varcode/genome.py +0 -0
- {varcode-9.2.7 → varcode-9.3.0}/varcode/genome_sequence.py +0 -0
- {varcode-9.2.7 → varcode-9.3.0}/varcode/genomic_layout.py +0 -0
- {varcode-9.2.7 → varcode-9.3.0}/varcode/genotype.py +0 -0
- {varcode-9.2.7 → varcode-9.3.0}/varcode/germline.py +0 -0
- {varcode-9.2.7 → varcode-9.3.0}/varcode/maf.py +0 -0
- {varcode-9.2.7 → varcode-9.3.0}/varcode/mutant_transcript.py +0 -0
- {varcode-9.2.7 → varcode-9.3.0}/varcode/nucleotides.py +0 -0
- {varcode-9.2.7 → varcode-9.3.0}/varcode/phasing.py +0 -0
- {varcode-9.2.7 → varcode-9.3.0}/varcode/realized_effects.py +0 -0
- {varcode-9.2.7 → varcode-9.3.0}/varcode/reference.py +0 -0
- {varcode-9.2.7 → varcode-9.3.0}/varcode/rna_evidence.py +0 -0
- {varcode-9.2.7 → varcode-9.3.0}/varcode/rna_read_phasing.py +0 -0
- {varcode-9.2.7 → varcode-9.3.0}/varcode/splice_graph.py +0 -0
- {varcode-9.2.7 → varcode-9.3.0}/varcode/splice_outcomes.py +0 -0
- {varcode-9.2.7 → varcode-9.3.0}/varcode/string_helpers.py +0 -0
- {varcode-9.2.7 → varcode-9.3.0}/varcode/structural_variant.py +0 -0
- {varcode-9.2.7 → varcode-9.3.0}/varcode/sv_allele_parser.py +0 -0
- {varcode-9.2.7 → varcode-9.3.0}/varcode/transcript_layout.py +0 -0
- {varcode-9.2.7 → varcode-9.3.0}/varcode/transcript_model.py +0 -0
- {varcode-9.2.7 → varcode-9.3.0}/varcode/transforms/__init__.py +0 -0
- {varcode-9.2.7 → varcode-9.3.0}/varcode/ucsc_reference_names.py +0 -0
- {varcode-9.2.7 → varcode-9.3.0}/varcode/util.py +0 -0
- {varcode-9.2.7 → varcode-9.3.0}/varcode/variant.py +0 -0
- {varcode-9.2.7 → varcode-9.3.0}/varcode/variant_collection.py +0 -0
- {varcode-9.2.7 → varcode-9.3.0}/varcode/vcf.py +0 -0
- {varcode-9.2.7 → varcode-9.3.0}/varcode/vcf_output.py +0 -0
- {varcode-9.2.7 → varcode-9.3.0}/varcode/vcf_parsing.py +0 -0
- {varcode-9.2.7 → varcode-9.3.0}/varcode.egg-info/dependency_links.txt +0 -0
- {varcode-9.2.7 → varcode-9.3.0}/varcode.egg-info/entry_points.txt +0 -0
- {varcode-9.2.7 → varcode-9.3.0}/varcode.egg-info/requires.txt +0 -0
- {varcode-9.2.7 → varcode-9.3.0}/varcode.egg-info/top_level.txt +0 -0
|
@@ -69,6 +69,14 @@ def test_documentation_first_steps(path, count, input_directory):
|
|
|
69
69
|
for effect in namespace["effects"])
|
|
70
70
|
|
|
71
71
|
|
|
72
|
+
def test_documentation_sv_filtering(input_directory):
|
|
73
|
+
namespace = _run(_blocks("docs/structural_variants.md")[:2])
|
|
74
|
+
assert len(namespace["retained"]) > 0
|
|
75
|
+
assert len(namespace["resolved_changes"]) > 0
|
|
76
|
+
assert all(effect.modifies_protein_sequence is True
|
|
77
|
+
for effect in namespace["resolved_changes"])
|
|
78
|
+
|
|
79
|
+
|
|
72
80
|
def test_documentation_germline_scenarios():
|
|
73
81
|
namespace = _run(_blocks(
|
|
74
82
|
"docs/phasing.md", "## Two variants in one codon")[:4])
|
|
@@ -103,6 +111,19 @@ def test_documentation_germline_with_vcf_phase(input_directory):
|
|
|
103
111
|
assert cftr[0].short_description == "p.S159T"
|
|
104
112
|
|
|
105
113
|
|
|
114
|
+
def test_documentation_fusion_protein_candidates(capsys):
|
|
115
|
+
genome = cached_release(95)
|
|
116
|
+
transcript = genome.transcript_by_id("ENST00000538197")
|
|
117
|
+
variant = StructuralVariant(
|
|
118
|
+
"4", 15012987, "BND", alt="N[4:2664478[",
|
|
119
|
+
mate_contig="4", mate_start=2664478, genome=genome)
|
|
120
|
+
namespace = {"effect": variant.effect_on_transcript(transcript)}
|
|
121
|
+
_run(_blocks("docs/structural_variants.md", "## Fusion protein candidates")[:1], namespace)
|
|
122
|
+
output = capsys.readouterr().out
|
|
123
|
+
assert "ENST00000324666" in output
|
|
124
|
+
assert "ENST00000637812" in output
|
|
125
|
+
|
|
126
|
+
|
|
106
127
|
def test_documentation_effect_families_keep_all_classes():
|
|
107
128
|
source = ast.parse((ROOT / "varcode/effects/effect_classes.py").read_text())
|
|
108
129
|
classes = {node.name for node in source.body if isinstance(node, ast.ClassDef)}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
"""Fusion proteins must not depend on discarding all but the first isoform."""
|
|
2
|
+
|
|
3
|
+
from types import SimpleNamespace
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
from pyensembl import cached_release
|
|
7
|
+
|
|
8
|
+
from varcode import EffectCandidate, StructuralVariant
|
|
9
|
+
from varcode.effects import GeneFusion, LargeDeletion, TranslocationToIntergenic
|
|
10
|
+
from varcode.effects import structural
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@pytest.fixture
|
|
14
|
+
def fusion_case(monkeypatch):
|
|
15
|
+
genome = cached_release(95)
|
|
16
|
+
transcript = genome.transcript_by_id("ENST00000538197") # CPEB2, forward
|
|
17
|
+
variant = StructuralVariant(
|
|
18
|
+
"4", 15012987, "BND", alt="N[4:2664478[",
|
|
19
|
+
mate_contig="4", mate_start=2664478, genome=genome)
|
|
20
|
+
# These tests isolate junction/isoform enumeration. Dedicated existing
|
|
21
|
+
# tests cover the splice and cryptic layers added after it.
|
|
22
|
+
monkeypatch.setattr(structural, "_enumerate_and_attach_cryptics", lambda *args: None)
|
|
23
|
+
monkeypatch.setattr(structural, "_enumerate_and_attach_splice_outcomes", lambda *args: None)
|
|
24
|
+
return genome, transcript, variant
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _fusions(effect):
|
|
28
|
+
return [candidate.effect for candidate in effect.candidates
|
|
29
|
+
if isinstance(candidate.effect, GeneFusion)]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def test_audited_cpeb2_fam193a_proteins_are_both_candidates(fusion_case):
|
|
33
|
+
genome, transcript, variant = fusion_case
|
|
34
|
+
effect = variant.effect_on_transcript(transcript)
|
|
35
|
+
candidates = _fusions(effect)
|
|
36
|
+
expected = [t.id for t in genome.transcripts_at_locus("4", 2664478, 2664478)
|
|
37
|
+
if t.is_protein_coding and t.gene_name == "FAM193A" and t.strand == "+"]
|
|
38
|
+
assert [c.partner_transcript.id for c in candidates] == expected
|
|
39
|
+
assert len(candidates) == 7
|
|
40
|
+
assert candidates[0] is effect # existing primary remains compatible
|
|
41
|
+
proteins = {c.partner_transcript.id: c.mutant_protein_sequence for c in candidates}
|
|
42
|
+
assert len(proteins["ENST00000324666"]) == 1541
|
|
43
|
+
assert len(proteins["ENST00000637812"]) == 1500
|
|
44
|
+
assert proteins["ENST00000324666"] != proteins["ENST00000637812"]
|
|
45
|
+
partner = genome.transcript_by_id("ENST00000637812")
|
|
46
|
+
assert proteins[partner.id] == transcript.protein_sequence[:678] + partner.protein_sequence[693:]
|
|
47
|
+
for candidate in candidates:
|
|
48
|
+
assert candidate.transcript is transcript
|
|
49
|
+
assert candidate.five_prime_transcript is transcript
|
|
50
|
+
assert candidate.three_prime_transcript is candidate.partner_transcript
|
|
51
|
+
model = candidate.mutant_transcript
|
|
52
|
+
assert model.cdna_sequence == "".join(
|
|
53
|
+
segment.source.sequence[segment.start:segment.end]
|
|
54
|
+
for segment in model.reference_segments)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def test_same_transcript_pair_has_same_protein_from_either_end(fusion_case):
|
|
58
|
+
genome, transcript, variant = fusion_case
|
|
59
|
+
partner = genome.transcript_by_id("ENST00000637812")
|
|
60
|
+
mate = StructuralVariant(
|
|
61
|
+
"4", 2664478, "BND", alt="]4:15012987]N",
|
|
62
|
+
mate_contig="4", mate_start=15012987, genome=genome)
|
|
63
|
+
from_five = next(c for c in _fusions(variant.effect_on_transcript(transcript))
|
|
64
|
+
if c.partner_transcript.id == partner.id)
|
|
65
|
+
from_three = next(c for c in _fusions(mate.effect_on_transcript(partner))
|
|
66
|
+
if c.partner_transcript.id == transcript.id)
|
|
67
|
+
assert from_five.mutant_protein_sequence == from_three.mutant_protein_sequence
|
|
68
|
+
assert from_three.five_prime_transcript.id == transcript.id
|
|
69
|
+
assert from_three.three_prime_transcript.id == partner.id
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class _Isoform:
|
|
73
|
+
"""Reuse a reference transcript while varying only ID or available sequence."""
|
|
74
|
+
def __init__(self, transcript, name, sequence):
|
|
75
|
+
self._transcript = transcript
|
|
76
|
+
self.id = name
|
|
77
|
+
self.sequence = sequence
|
|
78
|
+
|
|
79
|
+
def __getattr__(self, name):
|
|
80
|
+
return getattr(self._transcript, name)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def test_large_isoform_set_is_not_capped_or_merged_by_protein(fusion_case, monkeypatch):
|
|
84
|
+
genome, transcript, variant = fusion_case
|
|
85
|
+
partner = genome.transcript_by_id("ENST00000637812")
|
|
86
|
+
partners = [_Isoform(partner, "isoform_%03d" % i, partner.sequence) for i in range(257)]
|
|
87
|
+
monkeypatch.setattr(structural, "_coding_transcripts_at", lambda *args: partners)
|
|
88
|
+
candidates = _fusions(variant.effect_on_transcript(transcript))
|
|
89
|
+
assert [c.partner_transcript.id for c in candidates] == [p.id for p in partners]
|
|
90
|
+
assert len({c.mutant_protein_sequence for c in candidates}) == 1
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def test_duplicate_junction_and_partner_records_are_not_repeated(fusion_case, monkeypatch):
|
|
94
|
+
genome, transcript, variant = fusion_case
|
|
95
|
+
partner = genome.transcript_by_id("ENST00000637812")
|
|
96
|
+
monkeypatch.setattr(structural, "_coding_transcripts_at", lambda *args: [partner, partner])
|
|
97
|
+
duplicate = SimpleNamespace(junctions=variant.junctions * 2)
|
|
98
|
+
effect = structural._fusion_across_junction(duplicate, transcript, None)
|
|
99
|
+
assert len(_fusions(effect)) == 1
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
@pytest.mark.parametrize("sequence", [None, ""])
|
|
103
|
+
def test_missing_partner_sequence_keeps_an_unresolved_candidate(fusion_case, monkeypatch, sequence):
|
|
104
|
+
genome, transcript, variant = fusion_case
|
|
105
|
+
partner = genome.transcript_by_id("ENST00000637812")
|
|
106
|
+
missing = _Isoform(partner, "missing_sequence", sequence)
|
|
107
|
+
monkeypatch.setattr(structural, "_coding_transcripts_at", lambda *args: [partner, missing])
|
|
108
|
+
candidates = _fusions(variant.effect_on_transcript(transcript))
|
|
109
|
+
assert len(candidates) == 2
|
|
110
|
+
assert candidates[0].mutant_protein_sequence is not None
|
|
111
|
+
assert candidates[1].partner_transcript is missing
|
|
112
|
+
assert candidates[1].mutant_transcript is None
|
|
113
|
+
assert candidates[1].mutant_protein_sequence is None
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def test_supplied_assembly_and_external_candidates_are_preserved(fusion_case):
|
|
117
|
+
genome, transcript, variant = fusion_case
|
|
118
|
+
variant.alt_assembly = "ATG" + "GCC" * 20 + "TAA"
|
|
119
|
+
effect = variant.effect_on_transcript(transcript)
|
|
120
|
+
candidates = _fusions(effect)
|
|
121
|
+
assert len(candidates) == 7
|
|
122
|
+
assert all(c.mutant_transcript.cdna_sequence == variant.alt_assembly for c in candidates)
|
|
123
|
+
assert all(c.mutant_transcript.evidence == {"source": "alt_assembly"} for c in candidates)
|
|
124
|
+
observed = EffectCandidate(candidates[-1], source="rna", evidence={"read_count": 4})
|
|
125
|
+
effect._extra_candidates = (observed,)
|
|
126
|
+
assert effect.candidates[-1] is observed
|
|
127
|
+
assert len(effect.candidates) == 8
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def test_opposite_strand_partner_isoforms_are_all_kept(monkeypatch):
|
|
131
|
+
genome = cached_release(81)
|
|
132
|
+
transcript = genome.transcript_by_id("ENST00000003084") # CFTR, +
|
|
133
|
+
variant = StructuralVariant(
|
|
134
|
+
"7", 117485000, "BND", alt="N]17:43120000]",
|
|
135
|
+
mate_contig="17", mate_start=43120000, genome=genome)
|
|
136
|
+
monkeypatch.setattr(structural, "_enumerate_and_attach_cryptics", lambda *args: None)
|
|
137
|
+
effect = variant.effect_on_transcript(transcript)
|
|
138
|
+
expected = {t.id for t in genome.transcripts_at_locus("17", 43120000, 43120000)
|
|
139
|
+
if t.is_protein_coding and t.strand == "-"}
|
|
140
|
+
assert {c.partner_transcript.id for c in _fusions(effect)} == expected
|
|
141
|
+
assert len(expected) > 1
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def test_same_role_join_still_has_no_fusion_candidates(fusion_case):
|
|
145
|
+
genome, transcript, _ = fusion_case
|
|
146
|
+
variant = StructuralVariant(
|
|
147
|
+
"4", 15012987, "BND", alt="N]4:2664478]",
|
|
148
|
+
mate_contig="4", mate_start=2664478, genome=genome)
|
|
149
|
+
effect = variant.effect_on_transcript(transcript)
|
|
150
|
+
assert isinstance(effect, TranslocationToIntergenic)
|
|
151
|
+
assert not _fusions(effect)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def test_span_effect_is_retained_after_all_fusion_partners(fusion_case):
|
|
155
|
+
genome, _, _ = fusion_case
|
|
156
|
+
tmprss2 = max((t for t in genome.genes_by_name("TMPRSS2")[0].transcripts if t.is_protein_coding),
|
|
157
|
+
key=lambda t: len(t.sequence or ""))
|
|
158
|
+
erg = max((t for t in genome.genes_by_name("ERG")[0].transcripts if t.is_protein_coding),
|
|
159
|
+
key=lambda t: len(t.sequence or ""))
|
|
160
|
+
def midpoint(transcript, exon):
|
|
161
|
+
left, right = transcript.exons[exon - 1:exon + 1]
|
|
162
|
+
return (min(left.end, right.end) + max(left.start, right.start)) // 2
|
|
163
|
+
variant = StructuralVariant("21", midpoint(erg, 3), "DEL",
|
|
164
|
+
end=midpoint(tmprss2, 2) - 1, genome=genome)
|
|
165
|
+
effect = variant.effect_on_transcript(tmprss2)
|
|
166
|
+
assert len(_fusions(effect)) > 1
|
|
167
|
+
assert isinstance(effect.candidates[-1].effect, LargeDeletion)
|
|
168
|
+
assert effect.candidates[-1].effect.affected_exons
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def test_inversion_keeps_both_junction_directions_with_local_five_prime_first(fusion_case):
|
|
172
|
+
genome, _, _ = fusion_case
|
|
173
|
+
transcript = genome.transcript_by_id("ENST00000003084") # CFTR, +
|
|
174
|
+
partner_gene = genome.genes_by_name("CTTNBP2")[0] # downstream, -
|
|
175
|
+
far = (max(partner_gene.start, transcript.gene.end + 1) + partner_gene.end) // 2
|
|
176
|
+
variant = StructuralVariant("7", 117485000, "INV", end=far, genome=genome)
|
|
177
|
+
effect = variant.effect_on_transcript(transcript)
|
|
178
|
+
candidates = _fusions(effect)
|
|
179
|
+
assert effect.five_prime_transcript.id == transcript.id
|
|
180
|
+
directions = {(c.five_prime_transcript.gene_name, c.three_prime_transcript.gene_name)
|
|
181
|
+
for c in candidates}
|
|
182
|
+
assert directions == {("CFTR", "CTTNBP2"), ("CTTNBP2", "CFTR")}
|
|
183
|
+
keys = [(c.five_prime_transcript.id, c.three_prime_transcript.id) for c in candidates]
|
|
184
|
+
assert len(keys) == len(set(keys))
|
|
@@ -16,7 +16,7 @@ def transcript(request):
|
|
|
16
16
|
@pytest.fixture(autouse=True)
|
|
17
17
|
def no_optional_candidates(monkeypatch):
|
|
18
18
|
# Isolate the span fallback; fusion preservation also has real esvee tests.
|
|
19
|
-
monkeypatch.setattr(structural, "
|
|
19
|
+
monkeypatch.setattr(structural, "_fusion_partners", lambda *args: ())
|
|
20
20
|
monkeypatch.setattr(structural, "_enumerate_and_attach_cryptics", lambda *args: None)
|
|
21
21
|
monkeypatch.setattr(structural, "_enumerate_and_attach_splice_outcomes", lambda *args: None)
|
|
22
22
|
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
"""SV predictions must survive filtering without calling unknowns unchanged."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import replace
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
from pyensembl import cached_release
|
|
7
|
+
|
|
8
|
+
from varcode import (
|
|
9
|
+
EffectCandidate, MutantTranscript, ReferenceSegment, StructuralVariant,
|
|
10
|
+
make_fusion_outcome,
|
|
11
|
+
)
|
|
12
|
+
from varcode.effects import (
|
|
13
|
+
CrypticExonCandidate, EffectCollection, GeneFusion, LargeDeletion,
|
|
14
|
+
StructuralVariantEffect, TranslocationToIntergenic, Unresolved,
|
|
15
|
+
)
|
|
16
|
+
from varcode.effects import structural
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@pytest.fixture
|
|
20
|
+
def cftr():
|
|
21
|
+
return cached_release(81).transcript_by_id("ENST00000003084")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@pytest.fixture
|
|
25
|
+
def isolate_span(monkeypatch):
|
|
26
|
+
monkeypatch.setattr(structural, "_fusion_partners", lambda *args: ())
|
|
27
|
+
monkeypatch.setattr(structural, "_enumerate_and_attach_cryptics", lambda *args: None)
|
|
28
|
+
monkeypatch.setattr(structural, "_enumerate_and_attach_splice_outcomes", lambda *args: None)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _effect(transcript, cdna=None, protein=None, segments=None, evidence=None):
|
|
32
|
+
variant = StructuralVariant(transcript.contig, transcript.start, "BND",
|
|
33
|
+
genome=transcript.genome)
|
|
34
|
+
model = MutantTranscript(
|
|
35
|
+
reference_transcript=transcript, reference_segments=segments,
|
|
36
|
+
cdna_sequence=cdna, mutant_protein_sequence=protein, evidence=evidence)
|
|
37
|
+
return StructuralVariantEffect(variant, transcript, mutant_transcript=model)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _identity_segments(transcript):
|
|
41
|
+
return (ReferenceSegment(transcript, 0, len(transcript.sequence)),)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def test_seven_audited_fusion_proteins_survive_filter():
|
|
45
|
+
genome = cached_release(95)
|
|
46
|
+
transcript = genome.transcript_by_id("ENST00000538197")
|
|
47
|
+
variant = StructuralVariant("4", 15012987, "BND", alt="N[4:2664478[",
|
|
48
|
+
mate_contig="4", mate_start=2664478, genome=genome)
|
|
49
|
+
effect = variant.effect_on_transcript(transcript)
|
|
50
|
+
fusions = [c.effect for c in effect.candidates if isinstance(c.effect, GeneFusion)]
|
|
51
|
+
assert len(fusions) == 7
|
|
52
|
+
assert all(f.mutant_protein_sequence != transcript.protein_sequence for f in fusions)
|
|
53
|
+
assert all(f.modifies_protein_sequence is True for f in fusions)
|
|
54
|
+
assert all(f.modifies_coding_sequence is True for f in fusions)
|
|
55
|
+
assert len(EffectCollection(fusions).drop_silent_and_noncoding()) == 7
|
|
56
|
+
assert list(EffectCollection([effect]).drop_silent_and_noncoding()) == [effect]
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@pytest.mark.parametrize("transcript_id", ["ENST00000003084", "ENST00000357654"])
|
|
60
|
+
@pytest.mark.parametrize("region", ["coding", "start", "whole", "5utr", "3utr"])
|
|
61
|
+
def test_deletion_flags_on_both_strands(transcript_id, region, isolate_span):
|
|
62
|
+
transcript = cached_release(81).transcript_by_id(transcript_id)
|
|
63
|
+
exons = transcript.exons
|
|
64
|
+
if region == "whole":
|
|
65
|
+
start, end = transcript.start, transcript.end
|
|
66
|
+
elif region == "coding":
|
|
67
|
+
start, end = exons[4].start, exons[4].end
|
|
68
|
+
elif region == "start":
|
|
69
|
+
start, end = min(transcript.start_codon_positions), max(transcript.start_codon_positions)
|
|
70
|
+
else:
|
|
71
|
+
exon = exons[0] if region == "5utr" else exons[-1]
|
|
72
|
+
at_low_end = (region == "5utr") == (transcript.strand == "+")
|
|
73
|
+
start, end = ((exon.start, exon.start + 9) if at_low_end
|
|
74
|
+
else (exon.end - 9, exon.end))
|
|
75
|
+
variant = StructuralVariant(transcript.contig, start, "DEL", end=end,
|
|
76
|
+
genome=transcript.genome)
|
|
77
|
+
effect = variant.effect_on_transcript(transcript)
|
|
78
|
+
assert isinstance(effect, LargeDeletion)
|
|
79
|
+
expected = region in ("coding", "start", "whole")
|
|
80
|
+
assert effect.modifies_coding_sequence is expected
|
|
81
|
+
assert effect.modifies_protein_sequence is expected
|
|
82
|
+
assert len(EffectCollection([effect]).drop_silent_and_noncoding()) == int(expected)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
@pytest.mark.parametrize("change", ["none", "utr", "synonymous", "missense", "stop"])
|
|
86
|
+
def test_cds_and_protein_are_distinct_comparisons(cftr, change):
|
|
87
|
+
cdna = cftr.sequence
|
|
88
|
+
start = min(cftr.start_codon_spliced_offsets)
|
|
89
|
+
if change == "utr":
|
|
90
|
+
cdna = ("A" if cdna[0] != "A" else "C") + cdna[1:]
|
|
91
|
+
elif change == "synonymous":
|
|
92
|
+
# A GCT alanine codon changed to GCC.
|
|
93
|
+
pos = next(i for i in range(start + 3, start + 300, 3) if cdna[i:i+3] == "GCT")
|
|
94
|
+
cdna = cdna[:pos] + "GCC" + cdna[pos+3:]
|
|
95
|
+
elif change == "missense":
|
|
96
|
+
cdna = cdna[:start+3] + "TGG" + cdna[start+6:]
|
|
97
|
+
elif change == "stop":
|
|
98
|
+
pos = min(cftr.stop_codon_spliced_offsets)
|
|
99
|
+
cdna = cdna[:pos] + "TAA" + cdna[pos+3:]
|
|
100
|
+
effect = _effect(cftr, cdna=cdna, segments=_identity_segments(cftr))
|
|
101
|
+
assert effect.modifies_coding_sequence is (change in ("synonymous", "missense", "stop"))
|
|
102
|
+
assert effect.modifies_protein_sequence is (change == "missense")
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
@pytest.mark.parametrize("kind", ["DUP", "INV", "BND"])
|
|
106
|
+
def test_unresolved_models_are_not_silent(cftr, kind, isolate_span):
|
|
107
|
+
variant = StructuralVariant(cftr.contig, cftr.start - 100, kind,
|
|
108
|
+
end=cftr.end - 50, genome=cftr.genome)
|
|
109
|
+
if kind == "BND":
|
|
110
|
+
effect = TranslocationToIntergenic(variant, cftr)
|
|
111
|
+
else:
|
|
112
|
+
effect = variant.effect_on_transcript(cftr)
|
|
113
|
+
assert effect.modifies_coding_sequence is None
|
|
114
|
+
assert effect.modifies_protein_sequence is None
|
|
115
|
+
collection = EffectCollection([effect], annotator="fast", annotator_version="test")
|
|
116
|
+
retained = collection.drop_silent_and_noncoding()
|
|
117
|
+
assert list(retained) == [effect]
|
|
118
|
+
assert retained.annotator == "fast"
|
|
119
|
+
assert retained.annotator_version == "test"
|
|
120
|
+
assert len(collection.drop_silent_and_noncoding(keep_unresolved=False)) == 0
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def test_partial_fragment_and_unmapped_assembly_are_unknown(cftr):
|
|
124
|
+
partial = _effect(cftr, segments=(ReferenceSegment(cftr, 0, 500),))
|
|
125
|
+
assembly = _effect(cftr, cdna=cftr.sequence)
|
|
126
|
+
for effect in (partial, assembly):
|
|
127
|
+
assert effect.modifies_protein_sequence is None
|
|
128
|
+
assert effect.modifies_coding_sequence is None
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
@pytest.mark.parametrize("primary_status", [False, None])
|
|
132
|
+
def test_later_changed_candidate_controls_flags_and_filter(cftr, primary_status):
|
|
133
|
+
primary = _effect(cftr, protein=cftr.protein_sequence if primary_status is False else None)
|
|
134
|
+
changed = _effect(cftr, protein=cftr.protein_sequence + "A")
|
|
135
|
+
primary._extra_candidates = (EffectCandidate(changed, source="rna", evidence={"reads": 3}),)
|
|
136
|
+
before = primary.candidates
|
|
137
|
+
assert primary.modifies_coding_sequence is True
|
|
138
|
+
assert primary.modifies_protein_sequence is True
|
|
139
|
+
assert list(EffectCollection([primary]).drop_silent_and_noncoding(False)) == [primary]
|
|
140
|
+
assert primary.candidates == before
|
|
141
|
+
assert primary.candidates[1].evidence == {"reads": 3}
|
|
142
|
+
# Self references and back edges must not recurse indefinitely.
|
|
143
|
+
changed._attach_primary_effects([primary])
|
|
144
|
+
assert primary.modifies_protein_sequence is True
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def test_unchanged_plus_unknown_is_unknown(cftr):
|
|
148
|
+
unchanged = _effect(cftr, protein=cftr.protein_sequence)
|
|
149
|
+
unknown = _effect(cftr)
|
|
150
|
+
unchanged._attach_primary_effects([unknown])
|
|
151
|
+
assert unchanged.modifies_protein_sequence is None
|
|
152
|
+
assert len(EffectCollection([unchanged]).drop_silent_and_noncoding()) == 1
|
|
153
|
+
assert len(EffectCollection([unchanged]).drop_silent_and_noncoding(False)) == 0
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def test_explicit_protein_without_cds_does_not_prove_cds_unchanged(cftr):
|
|
157
|
+
effect = _effect(cftr, protein=cftr.protein_sequence)
|
|
158
|
+
assert effect.modifies_protein_sequence is False
|
|
159
|
+
assert effect.modifies_coding_sequence is None
|
|
160
|
+
assert len(EffectCollection([effect]).drop_silent_and_noncoding()) == 0
|
|
161
|
+
effect.mutant_transcript = replace(effect.mutant_transcript, mutant_protein_sequence="")
|
|
162
|
+
assert effect.modifies_protein_sequence is True
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def test_local_duplication_is_changed(cftr, isolate_span):
|
|
166
|
+
exon = cftr.exons[4]
|
|
167
|
+
variant = StructuralVariant(cftr.contig, exon.start, "DUP", end=exon.end,
|
|
168
|
+
genome=cftr.genome)
|
|
169
|
+
effect = variant.effect_on_transcript(cftr)
|
|
170
|
+
assert effect.modifies_coding_sequence is True
|
|
171
|
+
assert effect.modifies_protein_sequence is True
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def test_replacement_primary_effects_do_not_include_local_unknown(cftr):
|
|
175
|
+
wrapper = _effect(cftr)
|
|
176
|
+
unchanged = _effect(cftr, protein=cftr.protein_sequence)
|
|
177
|
+
wrapper._primary_effects = (unchanged,)
|
|
178
|
+
assert wrapper.modifies_protein_sequence is False
|
|
179
|
+
assert len(EffectCollection([wrapper]).drop_silent_and_noncoding()) == 0
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
@pytest.mark.parametrize("synonymous", [False, True])
|
|
183
|
+
def test_imported_rna_has_explicit_cds_boundaries(cftr, synonymous):
|
|
184
|
+
variant = StructuralVariant(cftr.contig, cftr.start, "BND", genome=cftr.genome)
|
|
185
|
+
start = min(cftr.start_codon_spliced_offsets)
|
|
186
|
+
cdna = cftr.sequence
|
|
187
|
+
if synonymous:
|
|
188
|
+
pos = next(i for i in range(start + 3, start + 300, 3) if cdna[i:i+3] == "GCT")
|
|
189
|
+
cdna = cdna[:pos] + "GCC" + cdna[pos+3:]
|
|
190
|
+
candidate = make_fusion_outcome(
|
|
191
|
+
variant, cftr, sequence=cdna, cds_start=start,
|
|
192
|
+
transcript_model_id="observed-model")
|
|
193
|
+
assert candidate.effect.modifies_coding_sequence is synonymous
|
|
194
|
+
assert candidate.effect.modifies_protein_sequence is False
|
|
195
|
+
assert candidate.evidence["protein_status"] == "predicted_from_observed_rna"
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def test_fusion_after_stop_is_unchanged_for_five_prime_partner(cftr):
|
|
199
|
+
partner = cached_release(81).transcript_by_id("ENST00000357654")
|
|
200
|
+
model = structural._build_fusion_mutant_transcript(
|
|
201
|
+
cftr, cftr, cftr.end - 10, partner, partner.end - 10)
|
|
202
|
+
assert model.mutant_protein_sequence == cftr.protein_sequence
|
|
203
|
+
variant = StructuralVariant(cftr.contig, cftr.end - 10, "BND", genome=cftr.genome)
|
|
204
|
+
effect = GeneFusion(variant, cftr, partner, mutant_transcript=model)
|
|
205
|
+
assert effect.modifies_coding_sequence is False
|
|
206
|
+
assert effect.modifies_protein_sequence is False
|
|
207
|
+
assert len(EffectCollection([effect]).drop_silent_and_noncoding()) == 0
|
|
208
|
+
# On the opposite end, compare the same allele with that transcript's
|
|
209
|
+
# reference, but still use the 5' partner's annotated start for the CDS.
|
|
210
|
+
other = GeneFusion(variant, partner, cftr, mutant_transcript=model,
|
|
211
|
+
five_prime_transcript=cftr, three_prime_transcript=partner)
|
|
212
|
+
assert other.modifies_coding_sequence is True
|
|
213
|
+
assert other.modifies_protein_sequence is True
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
@pytest.mark.parametrize("region", ["5utr", "3utr"])
|
|
217
|
+
def test_local_utr_duplication_does_not_change_cds(cftr, region, isolate_span):
|
|
218
|
+
start = cftr.start + 10 if region == "5utr" else cftr.end - 20
|
|
219
|
+
variant = StructuralVariant(cftr.contig, start, "DUP", end=start + 5,
|
|
220
|
+
genome=cftr.genome)
|
|
221
|
+
effect = variant.effect_on_transcript(cftr)
|
|
222
|
+
assert effect.modifies_coding_sequence is False
|
|
223
|
+
assert effect.modifies_protein_sequence is False
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def test_mitochondrial_translation_uses_correct_table():
|
|
227
|
+
transcript = cached_release(81).transcript_by_id("ENST00000361624")
|
|
228
|
+
effect = _effect(transcript, cdna=transcript.sequence,
|
|
229
|
+
segments=_identity_segments(transcript))
|
|
230
|
+
assert effect.modifies_coding_sequence is False
|
|
231
|
+
assert effect.modifies_protein_sequence is False
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def test_ambiguous_coding_sequence_remains_unknown(cftr):
|
|
235
|
+
start = min(cftr.start_codon_spliced_offsets)
|
|
236
|
+
cdna = cftr.sequence[:start+6] + "N" + cftr.sequence[start+7:]
|
|
237
|
+
effect = _effect(cftr, cdna=cdna, segments=_identity_segments(cftr))
|
|
238
|
+
assert effect.modifies_coding_sequence is None
|
|
239
|
+
assert effect.modifies_protein_sequence is None
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def test_split_start_codon_and_deleted_start_base(cftr):
|
|
243
|
+
start = min(cftr.start_codon_spliced_offsets)
|
|
244
|
+
segments = (ReferenceSegment(cftr, 0, start + 1),
|
|
245
|
+
ReferenceSegment(cftr, start + 1, len(cftr.sequence)))
|
|
246
|
+
effect = _effect(cftr, cdna=cftr.sequence, segments=segments)
|
|
247
|
+
assert effect.modifies_protein_sequence is False
|
|
248
|
+
cut_segments = (segments[0], replace(segments[1], start=start + 2))
|
|
249
|
+
cut_cdna = cftr.sequence[:start+1] + cftr.sequence[start+2:]
|
|
250
|
+
cut = _effect(cftr, cdna=cut_cdna, segments=cut_segments)
|
|
251
|
+
# Do not translate from one retained base of a destroyed initiator.
|
|
252
|
+
assert cut.modifies_protein_sequence is None
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def test_unresolved_and_cryptic_alternatives_are_not_known_unchanged(cftr):
|
|
256
|
+
primary = _effect(cftr, protein=cftr.protein_sequence)
|
|
257
|
+
unknown = Unresolved(primary.variant, cftr, mechanism="missing_sequence")
|
|
258
|
+
cryptic = CrypticExonCandidate(primary.variant, cftr.contig, cftr.start, cftr.start + 20)
|
|
259
|
+
for alternative in (unknown, cryptic):
|
|
260
|
+
assert alternative.modifies_coding_sequence is None
|
|
261
|
+
assert alternative.modifies_protein_sequence is None
|
|
262
|
+
primary._extra_candidates = (EffectCandidate(alternative, source="test"),)
|
|
263
|
+
assert primary.modifies_protein_sequence is None
|
|
264
|
+
assert len(EffectCollection([primary]).drop_silent_and_noncoding()) == 1
|
|
265
|
+
assert len(EffectCollection([primary]).drop_silent_and_noncoding(False)) == 0
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def test_annotation_without_start_codon_remains_unresolved(cftr, monkeypatch):
|
|
269
|
+
exon = cftr.exons[4]
|
|
270
|
+
variant = StructuralVariant(cftr.contig, exon.start, "DEL", end=exon.end,
|
|
271
|
+
genome=cftr.genome)
|
|
272
|
+
monkeypatch.setattr(type(cftr), "contains_start_codon", property(lambda self: False))
|
|
273
|
+
effect = LargeDeletion(variant, cftr, affected_exons=[exon])
|
|
274
|
+
assert effect.modifies_coding_sequence is True
|
|
275
|
+
assert effect.modifies_protein_sequence is None
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def test_annotation_without_cds_features_is_unknown(cftr, monkeypatch):
|
|
279
|
+
def missing_cds(self):
|
|
280
|
+
raise ValueError("No CDS features")
|
|
281
|
+
|
|
282
|
+
monkeypatch.setattr(type(cftr), "coding_sequence_position_ranges", property(missing_cds))
|
|
283
|
+
variant = StructuralVariant(cftr.contig, cftr.start, "DEL", end=cftr.end,
|
|
284
|
+
genome=cftr.genome)
|
|
285
|
+
effect = LargeDeletion(variant, cftr, affected_exons=cftr.exons)
|
|
286
|
+
assert effect.modifies_coding_sequence is None
|
|
287
|
+
assert effect.modifies_protein_sequence is None
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def test_empty_and_cyclic_candidate_sets_are_unknown(cftr):
|
|
291
|
+
first, second = _effect(cftr), _effect(cftr)
|
|
292
|
+
first._primary_effects = ()
|
|
293
|
+
assert first.modifies_protein_sequence is None
|
|
294
|
+
first._primary_effects = (second,)
|
|
295
|
+
second._primary_effects = (first,)
|
|
296
|
+
assert first.modifies_protein_sequence is None
|
|
@@ -306,6 +306,9 @@ class Unresolved(TranscriptMutationEffect):
|
|
|
306
306
|
unresolved does not mean the variant is harmless.
|
|
307
307
|
"""
|
|
308
308
|
|
|
309
|
+
modifies_coding_sequence = None
|
|
310
|
+
modifies_protein_sequence = None
|
|
311
|
+
|
|
309
312
|
def __init__(self, variant, transcript, mechanism, reason=None):
|
|
310
313
|
TranscriptMutationEffect.__init__(self, variant, transcript)
|
|
311
314
|
self.mechanism = mechanism
|
|
@@ -1424,6 +1427,25 @@ class StructuralVariantEffect(TranscriptMutationEffect, MultiOutcomeEffect):
|
|
|
1424
1427
|
return getattr(
|
|
1425
1428
|
self.mutant_transcript, "mutant_protein_sequence", None)
|
|
1426
1429
|
|
|
1430
|
+
@property
|
|
1431
|
+
def modifies_coding_sequence(self):
|
|
1432
|
+
"""True if any candidate changes CDS, None if unresolved, else False.
|
|
1433
|
+
|
|
1434
|
+
Protein equality alone does not establish coding-sequence equality.
|
|
1435
|
+
"""
|
|
1436
|
+
from .sequence_change import modification_status
|
|
1437
|
+
return modification_status(self, "modifies_coding_sequence")
|
|
1438
|
+
|
|
1439
|
+
@property
|
|
1440
|
+
def modifies_protein_sequence(self):
|
|
1441
|
+
"""True if any candidate changes protein, None if unresolved, else False.
|
|
1442
|
+
|
|
1443
|
+
A set with an unchanged primary and a changed alternative returns True.
|
|
1444
|
+
False requires every candidate to be known unchanged; None is not silent.
|
|
1445
|
+
"""
|
|
1446
|
+
from .sequence_change import modification_status
|
|
1447
|
+
return modification_status(self, "modifies_protein_sequence")
|
|
1448
|
+
|
|
1427
1449
|
@property
|
|
1428
1450
|
def candidates(self):
|
|
1429
1451
|
"""Unified :class:`~varcode.EffectCandidate` view over primary
|
|
@@ -1549,6 +1571,12 @@ class GeneFusion(StructuralVariantEffect):
|
|
|
1549
1571
|
produce it, and so do deletions, duplications and inversions with
|
|
1550
1572
|
one end in each gene.
|
|
1551
1573
|
|
|
1574
|
+
The default annotator retains every compatible annotated partner isoform
|
|
1575
|
+
and junction in :attr:`candidates`, with no count cap. The first is the
|
|
1576
|
+
historical primary; producer order is not a probability ranking. Distinct
|
|
1577
|
+
transcript pairs remain separate even when their proteins match. Missing
|
|
1578
|
+
sequence leaves that candidate's transcript/protein unresolved.
|
|
1579
|
+
|
|
1552
1580
|
``transcript`` is the transcript being annotated and
|
|
1553
1581
|
``partner_transcript`` the other one; ``five_prime_transcript``
|
|
1554
1582
|
and ``three_prime_transcript`` say which is which (by default
|
|
@@ -1611,6 +1639,8 @@ class CrypticExonCandidate(MutationEffect):
|
|
|
1611
1639
|
"""
|
|
1612
1640
|
|
|
1613
1641
|
short_description = "sv-cryptic-exon-candidate"
|
|
1642
|
+
modifies_coding_sequence = None
|
|
1643
|
+
modifies_protein_sequence = None
|
|
1614
1644
|
|
|
1615
1645
|
def __init__(
|
|
1616
1646
|
self, variant, contig, interval_start, interval_end,
|
|
@@ -220,11 +220,25 @@ class EffectCollection(Collection):
|
|
|
220
220
|
return self.filter(
|
|
221
221
|
lambda effect: effect_priority(effect) >= min_priority)
|
|
222
222
|
|
|
223
|
-
def drop_silent_and_noncoding(self):
|
|
223
|
+
def drop_silent_and_noncoding(self, keep_unresolved=True):
|
|
224
224
|
"""
|
|
225
|
-
|
|
225
|
+
Keep effects with a protein-changing candidate or unresolved SV outcome.
|
|
226
|
+
|
|
227
|
+
Parameters
|
|
228
|
+
----------
|
|
229
|
+
keep_unresolved : bool
|
|
230
|
+
Keep effects whose protein-change status is None (default True).
|
|
231
|
+
False requires a positive prediction of protein change. Candidate
|
|
232
|
+
sets are retained intact if any alternative qualifies; their order
|
|
233
|
+
and provenance are unchanged.
|
|
226
234
|
"""
|
|
227
|
-
|
|
235
|
+
from .sequence_change import modification_status
|
|
236
|
+
|
|
237
|
+
def retain(effect):
|
|
238
|
+
status = modification_status(effect, "modifies_protein_sequence")
|
|
239
|
+
return status is True or (keep_unresolved and status is None)
|
|
240
|
+
|
|
241
|
+
return self.filter(retain)
|
|
228
242
|
|
|
229
243
|
def detailed_string(self):
|
|
230
244
|
"""
|