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.
Files changed (164) hide show
  1. {varcode-9.2.7/varcode.egg-info → varcode-9.3.0}/PKG-INFO +1 -1
  2. {varcode-9.2.7 → varcode-9.3.0}/tests/test_documentation_examples.py +21 -0
  3. varcode-9.3.0/tests/test_fusion_partner_candidates.py +184 -0
  4. {varcode-9.2.7 → varcode-9.3.0}/tests/test_nonlocal_sv_transcripts.py +1 -1
  5. varcode-9.3.0/tests/test_sv_sequence_changes.py +296 -0
  6. {varcode-9.2.7 → varcode-9.3.0}/varcode/effects/effect_classes.py +30 -0
  7. {varcode-9.2.7 → varcode-9.3.0}/varcode/effects/effect_collection.py +17 -3
  8. varcode-9.3.0/varcode/effects/sequence_change.py +142 -0
  9. {varcode-9.2.7 → varcode-9.3.0}/varcode/effects/structural.py +44 -28
  10. varcode-9.3.0/varcode/version.py +1 -0
  11. {varcode-9.2.7 → varcode-9.3.0/varcode.egg-info}/PKG-INFO +1 -1
  12. {varcode-9.2.7 → varcode-9.3.0}/varcode.egg-info/SOURCES.txt +3 -0
  13. varcode-9.2.7/varcode/version.py +0 -1
  14. {varcode-9.2.7 → varcode-9.3.0}/LICENSE +0 -0
  15. {varcode-9.2.7 → varcode-9.3.0}/MANIFEST.in +0 -0
  16. {varcode-9.2.7 → varcode-9.3.0}/README.md +0 -0
  17. {varcode-9.2.7 → varcode-9.3.0}/pyproject.toml +0 -0
  18. {varcode-9.2.7 → varcode-9.3.0}/requirements.txt +0 -0
  19. {varcode-9.2.7 → varcode-9.3.0}/setup.cfg +0 -0
  20. {varcode-9.2.7 → varcode-9.3.0}/tests/test_annotator_divergence_scenarios.py +0 -0
  21. {varcode-9.2.7 → varcode-9.3.0}/tests/test_annotator_parity_adversarial.py +0 -0
  22. {varcode-9.2.7 → varcode-9.3.0}/tests/test_annotators.py +0 -0
  23. {varcode-9.2.7 → varcode-9.3.0}/tests/test_bnd_retained_segments.py +0 -0
  24. {varcode-9.2.7 → varcode-9.3.0}/tests/test_cli_effects.py +0 -0
  25. {varcode-9.2.7 → varcode-9.3.0}/tests/test_cli_genes.py +0 -0
  26. {varcode-9.2.7 → varcode-9.3.0}/tests/test_cli_without_pkg_resources.py +0 -0
  27. {varcode-9.2.7 → varcode-9.3.0}/tests/test_collection_filtering.py +0 -0
  28. {varcode-9.2.7 → varcode-9.3.0}/tests/test_collection_variants_attr_consistency.py +0 -0
  29. {varcode-9.2.7 → varcode-9.3.0}/tests/test_common.py +0 -0
  30. {varcode-9.2.7 → varcode-9.3.0}/tests/test_cosmic_mutations.py +0 -0
  31. {varcode-9.2.7 → varcode-9.3.0}/tests/test_cryptic_exons.py +0 -0
  32. {varcode-9.2.7 → varcode-9.3.0}/tests/test_csv_roundtrip.py +0 -0
  33. {varcode-9.2.7 → varcode-9.3.0}/tests/test_dbnsfp_validation.py +0 -0
  34. {varcode-9.2.7 → varcode-9.3.0}/tests/test_default_structural_helpers.py +0 -0
  35. {varcode-9.2.7 → varcode-9.3.0}/tests/test_effect_annotation_errors.py +0 -0
  36. {varcode-9.2.7 → varcode-9.3.0}/tests/test_effect_candidates.py +0 -0
  37. {varcode-9.2.7 → varcode-9.3.0}/tests/test_effect_classes.py +0 -0
  38. {varcode-9.2.7 → varcode-9.3.0}/tests/test_effect_collection.py +0 -0
  39. {varcode-9.2.7 → varcode-9.3.0}/tests/test_effect_collection_serialization.py +0 -0
  40. {varcode-9.2.7 → varcode-9.3.0}/tests/test_effect_collection_sort_order.py +0 -0
  41. {varcode-9.2.7 → varcode-9.3.0}/tests/test_effect_hypotheses.py +0 -0
  42. {varcode-9.2.7 → varcode-9.3.0}/tests/test_effects_from_mutagenix_variants.py +0 -0
  43. {varcode-9.2.7 → varcode-9.3.0}/tests/test_exacto_fusions.py +0 -0
  44. {varcode-9.2.7 → varcode-9.3.0}/tests/test_exonic_splice_site.py +0 -0
  45. {varcode-9.2.7 → varcode-9.3.0}/tests/test_fast_path.py +0 -0
  46. {varcode-9.2.7 → varcode-9.3.0}/tests/test_frameshift_cterminus_regression.py +0 -0
  47. {varcode-9.2.7 → varcode-9.3.0}/tests/test_frameshift_helpers.py +0 -0
  48. {varcode-9.2.7 → varcode-9.3.0}/tests/test_genome_contig_validation.py +0 -0
  49. {varcode-9.2.7 → varcode-9.3.0}/tests/test_genome_sequence.py +0 -0
  50. {varcode-9.2.7 → varcode-9.3.0}/tests/test_genomic_layout.py +0 -0
  51. {varcode-9.2.7 → varcode-9.3.0}/tests/test_genotype.py +0 -0
  52. {varcode-9.2.7 → varcode-9.3.0}/tests/test_genotype_from_vcf.py +0 -0
  53. {varcode-9.2.7 → varcode-9.3.0}/tests/test_germline_context.py +0 -0
  54. {varcode-9.2.7 → varcode-9.3.0}/tests/test_germline_effects.py +0 -0
  55. {varcode-9.2.7 → varcode-9.3.0}/tests/test_haplotype_effect.py +0 -0
  56. {varcode-9.2.7 → varcode-9.3.0}/tests/test_maf.py +0 -0
  57. {varcode-9.2.7 → varcode-9.3.0}/tests/test_mitochondrial.py +0 -0
  58. {varcode-9.2.7 → varcode-9.3.0}/tests/test_mm10_klf6_frameshift.py +0 -0
  59. {varcode-9.2.7 → varcode-9.3.0}/tests/test_mouse.py +0 -0
  60. {varcode-9.2.7 → varcode-9.3.0}/tests/test_mutant_transcript.py +0 -0
  61. {varcode-9.2.7 → varcode-9.3.0}/tests/test_mutate.py +0 -0
  62. {varcode-9.2.7 → varcode-9.3.0}/tests/test_no_duplicate_variants.py +0 -0
  63. {varcode-9.2.7 → varcode-9.3.0}/tests/test_osteosarc_fusions.py +0 -0
  64. {varcode-9.2.7 → varcode-9.3.0}/tests/test_partial_annotators.py +0 -0
  65. {varcode-9.2.7 → varcode-9.3.0}/tests/test_premature_stop_short_description.py +0 -0
  66. {varcode-9.2.7 → varcode-9.3.0}/tests/test_problematic_variants.py +0 -0
  67. {varcode-9.2.7 → varcode-9.3.0}/tests/test_protein_diff_parity.py +0 -0
  68. {varcode-9.2.7 → varcode-9.3.0}/tests/test_read_phase_resolver.py +0 -0
  69. {varcode-9.2.7 → varcode-9.3.0}/tests/test_real_world_callers.py +0 -0
  70. {varcode-9.2.7 → varcode-9.3.0}/tests/test_reference.py +0 -0
  71. {varcode-9.2.7 → varcode-9.3.0}/tests/test_reference_mismatch_error.py +0 -0
  72. {varcode-9.2.7 → varcode-9.3.0}/tests/test_reference_segments.py +0 -0
  73. {varcode-9.2.7 → varcode-9.3.0}/tests/test_rna_evidence.py +0 -0
  74. {varcode-9.2.7 → varcode-9.3.0}/tests/test_rna_haplotype_sequences.py +0 -0
  75. {varcode-9.2.7 → varcode-9.3.0}/tests/test_rna_read_phasing_source.py +0 -0
  76. {varcode-9.2.7 → varcode-9.3.0}/tests/test_selenocysteine.py +0 -0
  77. {varcode-9.2.7 → varcode-9.3.0}/tests/test_silent_aa_pos.py +0 -0
  78. {varcode-9.2.7 → varcode-9.3.0}/tests/test_silent_hgvs_description.py +0 -0
  79. {varcode-9.2.7 → varcode-9.3.0}/tests/test_splice_graph.py +0 -0
  80. {varcode-9.2.7 → varcode-9.3.0}/tests/test_splice_outcomes.py +0 -0
  81. {varcode-9.2.7 → varcode-9.3.0}/tests/test_splice_site_effects.py +0 -0
  82. {varcode-9.2.7 → varcode-9.3.0}/tests/test_stop_codon_classification_bugs.py +0 -0
  83. {varcode-9.2.7 → varcode-9.3.0}/tests/test_string_helpers.py +0 -0
  84. {varcode-9.2.7 → varcode-9.3.0}/tests/test_structural_variant_annotator.py +0 -0
  85. {varcode-9.2.7 → varcode-9.3.0}/tests/test_structural_variants.py +0 -0
  86. {varcode-9.2.7 → varcode-9.3.0}/tests/test_sv_tabular_metadata.py +0 -0
  87. {varcode-9.2.7 → varcode-9.3.0}/tests/test_symbolic_alleles.py +0 -0
  88. {varcode-9.2.7 → varcode-9.3.0}/tests/test_symbolic_sv_padding.py +0 -0
  89. {varcode-9.2.7 → varcode-9.3.0}/tests/test_timings.py +0 -0
  90. {varcode-9.2.7 → varcode-9.3.0}/tests/test_transcript_layout.py +0 -0
  91. {varcode-9.2.7 → varcode-9.3.0}/tests/test_transcript_model.py +0 -0
  92. {varcode-9.2.7 → varcode-9.3.0}/tests/test_transforms_left_align.py +0 -0
  93. {varcode-9.2.7 → varcode-9.3.0}/tests/test_transforms_pair_breakends.py +0 -0
  94. {varcode-9.2.7 → varcode-9.3.0}/tests/test_variant.py +0 -0
  95. {varcode-9.2.7 → varcode-9.3.0}/tests/test_variant_collection.py +0 -0
  96. {varcode-9.2.7 → varcode-9.3.0}/tests/test_vcf.py +0 -0
  97. {varcode-9.2.7 → varcode-9.3.0}/tests/test_vcf_output.py +0 -0
  98. {varcode-9.2.7 → varcode-9.3.0}/tests/test_vcf_parsing.py +0 -0
  99. {varcode-9.2.7 → varcode-9.3.0}/tests/test_vcf_phase_resolver.py +0 -0
  100. {varcode-9.2.7 → varcode-9.3.0}/tests/test_vcf_spec_examples.py +0 -0
  101. {varcode-9.2.7 → varcode-9.3.0}/varcode/__init__.py +0 -0
  102. {varcode-9.2.7 → varcode-9.3.0}/varcode/annotators/__init__.py +0 -0
  103. {varcode-9.2.7 → varcode-9.3.0}/varcode/annotators/fast.py +0 -0
  104. {varcode-9.2.7 → varcode-9.3.0}/varcode/annotators/protein_diff.py +0 -0
  105. {varcode-9.2.7 → varcode-9.3.0}/varcode/annotators/registry.py +0 -0
  106. {varcode-9.2.7 → varcode-9.3.0}/varcode/cli/__init__.py +0 -0
  107. {varcode-9.2.7 → varcode-9.3.0}/varcode/cli/effects_script.py +0 -0
  108. {varcode-9.2.7 → varcode-9.3.0}/varcode/cli/genes_script.py +0 -0
  109. {varcode-9.2.7 → varcode-9.3.0}/varcode/cli/logging.conf +0 -0
  110. {varcode-9.2.7 → varcode-9.3.0}/varcode/cli/variant_args.py +0 -0
  111. {varcode-9.2.7 → varcode-9.3.0}/varcode/cli/version_info.py +0 -0
  112. {varcode-9.2.7 → varcode-9.3.0}/varcode/common.py +0 -0
  113. {varcode-9.2.7 → varcode-9.3.0}/varcode/cryptic_exons.py +0 -0
  114. {varcode-9.2.7 → varcode-9.3.0}/varcode/csv_helpers.py +0 -0
  115. {varcode-9.2.7 → varcode-9.3.0}/varcode/effect_candidates.py +0 -0
  116. {varcode-9.2.7 → varcode-9.3.0}/varcode/effect_hypotheses.py +0 -0
  117. {varcode-9.2.7 → varcode-9.3.0}/varcode/effects/__init__.py +0 -0
  118. {varcode-9.2.7 → varcode-9.3.0}/varcode/effects/classify.py +0 -0
  119. {varcode-9.2.7 → varcode-9.3.0}/varcode/effects/codon_tables.py +0 -0
  120. {varcode-9.2.7 → varcode-9.3.0}/varcode/effects/common.py +0 -0
  121. {varcode-9.2.7 → varcode-9.3.0}/varcode/effects/effect_helpers.py +0 -0
  122. {varcode-9.2.7 → varcode-9.3.0}/varcode/effects/effect_ordering.py +0 -0
  123. {varcode-9.2.7 → varcode-9.3.0}/varcode/effects/effect_prediction.py +0 -0
  124. {varcode-9.2.7 → varcode-9.3.0}/varcode/effects/effect_prediction_coding.py +0 -0
  125. {varcode-9.2.7 → varcode-9.3.0}/varcode/effects/effect_prediction_coding_frameshift.py +0 -0
  126. {varcode-9.2.7 → varcode-9.3.0}/varcode/effects/effect_prediction_coding_in_frame.py +0 -0
  127. {varcode-9.2.7 → varcode-9.3.0}/varcode/effects/fast_path.py +0 -0
  128. {varcode-9.2.7 → varcode-9.3.0}/varcode/effects/mutate.py +0 -0
  129. {varcode-9.2.7 → varcode-9.3.0}/varcode/effects/transcript_helpers.py +0 -0
  130. {varcode-9.2.7 → varcode-9.3.0}/varcode/effects/translate.py +0 -0
  131. {varcode-9.2.7 → varcode-9.3.0}/varcode/errors.py +0 -0
  132. {varcode-9.2.7 → varcode-9.3.0}/varcode/exacto.py +0 -0
  133. {varcode-9.2.7 → varcode-9.3.0}/varcode/genome.py +0 -0
  134. {varcode-9.2.7 → varcode-9.3.0}/varcode/genome_sequence.py +0 -0
  135. {varcode-9.2.7 → varcode-9.3.0}/varcode/genomic_layout.py +0 -0
  136. {varcode-9.2.7 → varcode-9.3.0}/varcode/genotype.py +0 -0
  137. {varcode-9.2.7 → varcode-9.3.0}/varcode/germline.py +0 -0
  138. {varcode-9.2.7 → varcode-9.3.0}/varcode/maf.py +0 -0
  139. {varcode-9.2.7 → varcode-9.3.0}/varcode/mutant_transcript.py +0 -0
  140. {varcode-9.2.7 → varcode-9.3.0}/varcode/nucleotides.py +0 -0
  141. {varcode-9.2.7 → varcode-9.3.0}/varcode/phasing.py +0 -0
  142. {varcode-9.2.7 → varcode-9.3.0}/varcode/realized_effects.py +0 -0
  143. {varcode-9.2.7 → varcode-9.3.0}/varcode/reference.py +0 -0
  144. {varcode-9.2.7 → varcode-9.3.0}/varcode/rna_evidence.py +0 -0
  145. {varcode-9.2.7 → varcode-9.3.0}/varcode/rna_read_phasing.py +0 -0
  146. {varcode-9.2.7 → varcode-9.3.0}/varcode/splice_graph.py +0 -0
  147. {varcode-9.2.7 → varcode-9.3.0}/varcode/splice_outcomes.py +0 -0
  148. {varcode-9.2.7 → varcode-9.3.0}/varcode/string_helpers.py +0 -0
  149. {varcode-9.2.7 → varcode-9.3.0}/varcode/structural_variant.py +0 -0
  150. {varcode-9.2.7 → varcode-9.3.0}/varcode/sv_allele_parser.py +0 -0
  151. {varcode-9.2.7 → varcode-9.3.0}/varcode/transcript_layout.py +0 -0
  152. {varcode-9.2.7 → varcode-9.3.0}/varcode/transcript_model.py +0 -0
  153. {varcode-9.2.7 → varcode-9.3.0}/varcode/transforms/__init__.py +0 -0
  154. {varcode-9.2.7 → varcode-9.3.0}/varcode/ucsc_reference_names.py +0 -0
  155. {varcode-9.2.7 → varcode-9.3.0}/varcode/util.py +0 -0
  156. {varcode-9.2.7 → varcode-9.3.0}/varcode/variant.py +0 -0
  157. {varcode-9.2.7 → varcode-9.3.0}/varcode/variant_collection.py +0 -0
  158. {varcode-9.2.7 → varcode-9.3.0}/varcode/vcf.py +0 -0
  159. {varcode-9.2.7 → varcode-9.3.0}/varcode/vcf_output.py +0 -0
  160. {varcode-9.2.7 → varcode-9.3.0}/varcode/vcf_parsing.py +0 -0
  161. {varcode-9.2.7 → varcode-9.3.0}/varcode.egg-info/dependency_links.txt +0 -0
  162. {varcode-9.2.7 → varcode-9.3.0}/varcode.egg-info/entry_points.txt +0 -0
  163. {varcode-9.2.7 → varcode-9.3.0}/varcode.egg-info/requires.txt +0 -0
  164. {varcode-9.2.7 → varcode-9.3.0}/varcode.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: varcode
3
- Version: 9.2.7
3
+ Version: 9.3.0
4
4
  Summary: Variant annotation in Python
5
5
  Author-email: Alex Rubinsteyn <alex.rubinsteyn@unc.edu>
6
6
  Project-URL: Homepage, https://github.com/openvax/varcode
@@ -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, "_fusion_partner", lambda *args: None)
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
- Create a new EffectCollection containing only non-silent coding effects
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
- return self.filter(lambda effect: effect.modifies_protein_sequence)
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
  """