varcode 9.2.8__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.8/varcode.egg-info → varcode-9.3.0}/PKG-INFO +1 -1
  2. {varcode-9.2.8 → varcode-9.3.0}/tests/test_documentation_examples.py +8 -0
  3. varcode-9.3.0/tests/test_sv_sequence_changes.py +296 -0
  4. {varcode-9.2.8 → varcode-9.3.0}/varcode/effects/effect_classes.py +24 -0
  5. {varcode-9.2.8 → varcode-9.3.0}/varcode/effects/effect_collection.py +17 -3
  6. varcode-9.3.0/varcode/effects/sequence_change.py +142 -0
  7. varcode-9.3.0/varcode/version.py +1 -0
  8. {varcode-9.2.8 → varcode-9.3.0/varcode.egg-info}/PKG-INFO +1 -1
  9. {varcode-9.2.8 → varcode-9.3.0}/varcode.egg-info/SOURCES.txt +2 -0
  10. varcode-9.2.8/varcode/version.py +0 -1
  11. {varcode-9.2.8 → varcode-9.3.0}/LICENSE +0 -0
  12. {varcode-9.2.8 → varcode-9.3.0}/MANIFEST.in +0 -0
  13. {varcode-9.2.8 → varcode-9.3.0}/README.md +0 -0
  14. {varcode-9.2.8 → varcode-9.3.0}/pyproject.toml +0 -0
  15. {varcode-9.2.8 → varcode-9.3.0}/requirements.txt +0 -0
  16. {varcode-9.2.8 → varcode-9.3.0}/setup.cfg +0 -0
  17. {varcode-9.2.8 → varcode-9.3.0}/tests/test_annotator_divergence_scenarios.py +0 -0
  18. {varcode-9.2.8 → varcode-9.3.0}/tests/test_annotator_parity_adversarial.py +0 -0
  19. {varcode-9.2.8 → varcode-9.3.0}/tests/test_annotators.py +0 -0
  20. {varcode-9.2.8 → varcode-9.3.0}/tests/test_bnd_retained_segments.py +0 -0
  21. {varcode-9.2.8 → varcode-9.3.0}/tests/test_cli_effects.py +0 -0
  22. {varcode-9.2.8 → varcode-9.3.0}/tests/test_cli_genes.py +0 -0
  23. {varcode-9.2.8 → varcode-9.3.0}/tests/test_cli_without_pkg_resources.py +0 -0
  24. {varcode-9.2.8 → varcode-9.3.0}/tests/test_collection_filtering.py +0 -0
  25. {varcode-9.2.8 → varcode-9.3.0}/tests/test_collection_variants_attr_consistency.py +0 -0
  26. {varcode-9.2.8 → varcode-9.3.0}/tests/test_common.py +0 -0
  27. {varcode-9.2.8 → varcode-9.3.0}/tests/test_cosmic_mutations.py +0 -0
  28. {varcode-9.2.8 → varcode-9.3.0}/tests/test_cryptic_exons.py +0 -0
  29. {varcode-9.2.8 → varcode-9.3.0}/tests/test_csv_roundtrip.py +0 -0
  30. {varcode-9.2.8 → varcode-9.3.0}/tests/test_dbnsfp_validation.py +0 -0
  31. {varcode-9.2.8 → varcode-9.3.0}/tests/test_default_structural_helpers.py +0 -0
  32. {varcode-9.2.8 → varcode-9.3.0}/tests/test_effect_annotation_errors.py +0 -0
  33. {varcode-9.2.8 → varcode-9.3.0}/tests/test_effect_candidates.py +0 -0
  34. {varcode-9.2.8 → varcode-9.3.0}/tests/test_effect_classes.py +0 -0
  35. {varcode-9.2.8 → varcode-9.3.0}/tests/test_effect_collection.py +0 -0
  36. {varcode-9.2.8 → varcode-9.3.0}/tests/test_effect_collection_serialization.py +0 -0
  37. {varcode-9.2.8 → varcode-9.3.0}/tests/test_effect_collection_sort_order.py +0 -0
  38. {varcode-9.2.8 → varcode-9.3.0}/tests/test_effect_hypotheses.py +0 -0
  39. {varcode-9.2.8 → varcode-9.3.0}/tests/test_effects_from_mutagenix_variants.py +0 -0
  40. {varcode-9.2.8 → varcode-9.3.0}/tests/test_exacto_fusions.py +0 -0
  41. {varcode-9.2.8 → varcode-9.3.0}/tests/test_exonic_splice_site.py +0 -0
  42. {varcode-9.2.8 → varcode-9.3.0}/tests/test_fast_path.py +0 -0
  43. {varcode-9.2.8 → varcode-9.3.0}/tests/test_frameshift_cterminus_regression.py +0 -0
  44. {varcode-9.2.8 → varcode-9.3.0}/tests/test_frameshift_helpers.py +0 -0
  45. {varcode-9.2.8 → varcode-9.3.0}/tests/test_fusion_partner_candidates.py +0 -0
  46. {varcode-9.2.8 → varcode-9.3.0}/tests/test_genome_contig_validation.py +0 -0
  47. {varcode-9.2.8 → varcode-9.3.0}/tests/test_genome_sequence.py +0 -0
  48. {varcode-9.2.8 → varcode-9.3.0}/tests/test_genomic_layout.py +0 -0
  49. {varcode-9.2.8 → varcode-9.3.0}/tests/test_genotype.py +0 -0
  50. {varcode-9.2.8 → varcode-9.3.0}/tests/test_genotype_from_vcf.py +0 -0
  51. {varcode-9.2.8 → varcode-9.3.0}/tests/test_germline_context.py +0 -0
  52. {varcode-9.2.8 → varcode-9.3.0}/tests/test_germline_effects.py +0 -0
  53. {varcode-9.2.8 → varcode-9.3.0}/tests/test_haplotype_effect.py +0 -0
  54. {varcode-9.2.8 → varcode-9.3.0}/tests/test_maf.py +0 -0
  55. {varcode-9.2.8 → varcode-9.3.0}/tests/test_mitochondrial.py +0 -0
  56. {varcode-9.2.8 → varcode-9.3.0}/tests/test_mm10_klf6_frameshift.py +0 -0
  57. {varcode-9.2.8 → varcode-9.3.0}/tests/test_mouse.py +0 -0
  58. {varcode-9.2.8 → varcode-9.3.0}/tests/test_mutant_transcript.py +0 -0
  59. {varcode-9.2.8 → varcode-9.3.0}/tests/test_mutate.py +0 -0
  60. {varcode-9.2.8 → varcode-9.3.0}/tests/test_no_duplicate_variants.py +0 -0
  61. {varcode-9.2.8 → varcode-9.3.0}/tests/test_nonlocal_sv_transcripts.py +0 -0
  62. {varcode-9.2.8 → varcode-9.3.0}/tests/test_osteosarc_fusions.py +0 -0
  63. {varcode-9.2.8 → varcode-9.3.0}/tests/test_partial_annotators.py +0 -0
  64. {varcode-9.2.8 → varcode-9.3.0}/tests/test_premature_stop_short_description.py +0 -0
  65. {varcode-9.2.8 → varcode-9.3.0}/tests/test_problematic_variants.py +0 -0
  66. {varcode-9.2.8 → varcode-9.3.0}/tests/test_protein_diff_parity.py +0 -0
  67. {varcode-9.2.8 → varcode-9.3.0}/tests/test_read_phase_resolver.py +0 -0
  68. {varcode-9.2.8 → varcode-9.3.0}/tests/test_real_world_callers.py +0 -0
  69. {varcode-9.2.8 → varcode-9.3.0}/tests/test_reference.py +0 -0
  70. {varcode-9.2.8 → varcode-9.3.0}/tests/test_reference_mismatch_error.py +0 -0
  71. {varcode-9.2.8 → varcode-9.3.0}/tests/test_reference_segments.py +0 -0
  72. {varcode-9.2.8 → varcode-9.3.0}/tests/test_rna_evidence.py +0 -0
  73. {varcode-9.2.8 → varcode-9.3.0}/tests/test_rna_haplotype_sequences.py +0 -0
  74. {varcode-9.2.8 → varcode-9.3.0}/tests/test_rna_read_phasing_source.py +0 -0
  75. {varcode-9.2.8 → varcode-9.3.0}/tests/test_selenocysteine.py +0 -0
  76. {varcode-9.2.8 → varcode-9.3.0}/tests/test_silent_aa_pos.py +0 -0
  77. {varcode-9.2.8 → varcode-9.3.0}/tests/test_silent_hgvs_description.py +0 -0
  78. {varcode-9.2.8 → varcode-9.3.0}/tests/test_splice_graph.py +0 -0
  79. {varcode-9.2.8 → varcode-9.3.0}/tests/test_splice_outcomes.py +0 -0
  80. {varcode-9.2.8 → varcode-9.3.0}/tests/test_splice_site_effects.py +0 -0
  81. {varcode-9.2.8 → varcode-9.3.0}/tests/test_stop_codon_classification_bugs.py +0 -0
  82. {varcode-9.2.8 → varcode-9.3.0}/tests/test_string_helpers.py +0 -0
  83. {varcode-9.2.8 → varcode-9.3.0}/tests/test_structural_variant_annotator.py +0 -0
  84. {varcode-9.2.8 → varcode-9.3.0}/tests/test_structural_variants.py +0 -0
  85. {varcode-9.2.8 → varcode-9.3.0}/tests/test_sv_tabular_metadata.py +0 -0
  86. {varcode-9.2.8 → varcode-9.3.0}/tests/test_symbolic_alleles.py +0 -0
  87. {varcode-9.2.8 → varcode-9.3.0}/tests/test_symbolic_sv_padding.py +0 -0
  88. {varcode-9.2.8 → varcode-9.3.0}/tests/test_timings.py +0 -0
  89. {varcode-9.2.8 → varcode-9.3.0}/tests/test_transcript_layout.py +0 -0
  90. {varcode-9.2.8 → varcode-9.3.0}/tests/test_transcript_model.py +0 -0
  91. {varcode-9.2.8 → varcode-9.3.0}/tests/test_transforms_left_align.py +0 -0
  92. {varcode-9.2.8 → varcode-9.3.0}/tests/test_transforms_pair_breakends.py +0 -0
  93. {varcode-9.2.8 → varcode-9.3.0}/tests/test_variant.py +0 -0
  94. {varcode-9.2.8 → varcode-9.3.0}/tests/test_variant_collection.py +0 -0
  95. {varcode-9.2.8 → varcode-9.3.0}/tests/test_vcf.py +0 -0
  96. {varcode-9.2.8 → varcode-9.3.0}/tests/test_vcf_output.py +0 -0
  97. {varcode-9.2.8 → varcode-9.3.0}/tests/test_vcf_parsing.py +0 -0
  98. {varcode-9.2.8 → varcode-9.3.0}/tests/test_vcf_phase_resolver.py +0 -0
  99. {varcode-9.2.8 → varcode-9.3.0}/tests/test_vcf_spec_examples.py +0 -0
  100. {varcode-9.2.8 → varcode-9.3.0}/varcode/__init__.py +0 -0
  101. {varcode-9.2.8 → varcode-9.3.0}/varcode/annotators/__init__.py +0 -0
  102. {varcode-9.2.8 → varcode-9.3.0}/varcode/annotators/fast.py +0 -0
  103. {varcode-9.2.8 → varcode-9.3.0}/varcode/annotators/protein_diff.py +0 -0
  104. {varcode-9.2.8 → varcode-9.3.0}/varcode/annotators/registry.py +0 -0
  105. {varcode-9.2.8 → varcode-9.3.0}/varcode/cli/__init__.py +0 -0
  106. {varcode-9.2.8 → varcode-9.3.0}/varcode/cli/effects_script.py +0 -0
  107. {varcode-9.2.8 → varcode-9.3.0}/varcode/cli/genes_script.py +0 -0
  108. {varcode-9.2.8 → varcode-9.3.0}/varcode/cli/logging.conf +0 -0
  109. {varcode-9.2.8 → varcode-9.3.0}/varcode/cli/variant_args.py +0 -0
  110. {varcode-9.2.8 → varcode-9.3.0}/varcode/cli/version_info.py +0 -0
  111. {varcode-9.2.8 → varcode-9.3.0}/varcode/common.py +0 -0
  112. {varcode-9.2.8 → varcode-9.3.0}/varcode/cryptic_exons.py +0 -0
  113. {varcode-9.2.8 → varcode-9.3.0}/varcode/csv_helpers.py +0 -0
  114. {varcode-9.2.8 → varcode-9.3.0}/varcode/effect_candidates.py +0 -0
  115. {varcode-9.2.8 → varcode-9.3.0}/varcode/effect_hypotheses.py +0 -0
  116. {varcode-9.2.8 → varcode-9.3.0}/varcode/effects/__init__.py +0 -0
  117. {varcode-9.2.8 → varcode-9.3.0}/varcode/effects/classify.py +0 -0
  118. {varcode-9.2.8 → varcode-9.3.0}/varcode/effects/codon_tables.py +0 -0
  119. {varcode-9.2.8 → varcode-9.3.0}/varcode/effects/common.py +0 -0
  120. {varcode-9.2.8 → varcode-9.3.0}/varcode/effects/effect_helpers.py +0 -0
  121. {varcode-9.2.8 → varcode-9.3.0}/varcode/effects/effect_ordering.py +0 -0
  122. {varcode-9.2.8 → varcode-9.3.0}/varcode/effects/effect_prediction.py +0 -0
  123. {varcode-9.2.8 → varcode-9.3.0}/varcode/effects/effect_prediction_coding.py +0 -0
  124. {varcode-9.2.8 → varcode-9.3.0}/varcode/effects/effect_prediction_coding_frameshift.py +0 -0
  125. {varcode-9.2.8 → varcode-9.3.0}/varcode/effects/effect_prediction_coding_in_frame.py +0 -0
  126. {varcode-9.2.8 → varcode-9.3.0}/varcode/effects/fast_path.py +0 -0
  127. {varcode-9.2.8 → varcode-9.3.0}/varcode/effects/mutate.py +0 -0
  128. {varcode-9.2.8 → varcode-9.3.0}/varcode/effects/structural.py +0 -0
  129. {varcode-9.2.8 → varcode-9.3.0}/varcode/effects/transcript_helpers.py +0 -0
  130. {varcode-9.2.8 → varcode-9.3.0}/varcode/effects/translate.py +0 -0
  131. {varcode-9.2.8 → varcode-9.3.0}/varcode/errors.py +0 -0
  132. {varcode-9.2.8 → varcode-9.3.0}/varcode/exacto.py +0 -0
  133. {varcode-9.2.8 → varcode-9.3.0}/varcode/genome.py +0 -0
  134. {varcode-9.2.8 → varcode-9.3.0}/varcode/genome_sequence.py +0 -0
  135. {varcode-9.2.8 → varcode-9.3.0}/varcode/genomic_layout.py +0 -0
  136. {varcode-9.2.8 → varcode-9.3.0}/varcode/genotype.py +0 -0
  137. {varcode-9.2.8 → varcode-9.3.0}/varcode/germline.py +0 -0
  138. {varcode-9.2.8 → varcode-9.3.0}/varcode/maf.py +0 -0
  139. {varcode-9.2.8 → varcode-9.3.0}/varcode/mutant_transcript.py +0 -0
  140. {varcode-9.2.8 → varcode-9.3.0}/varcode/nucleotides.py +0 -0
  141. {varcode-9.2.8 → varcode-9.3.0}/varcode/phasing.py +0 -0
  142. {varcode-9.2.8 → varcode-9.3.0}/varcode/realized_effects.py +0 -0
  143. {varcode-9.2.8 → varcode-9.3.0}/varcode/reference.py +0 -0
  144. {varcode-9.2.8 → varcode-9.3.0}/varcode/rna_evidence.py +0 -0
  145. {varcode-9.2.8 → varcode-9.3.0}/varcode/rna_read_phasing.py +0 -0
  146. {varcode-9.2.8 → varcode-9.3.0}/varcode/splice_graph.py +0 -0
  147. {varcode-9.2.8 → varcode-9.3.0}/varcode/splice_outcomes.py +0 -0
  148. {varcode-9.2.8 → varcode-9.3.0}/varcode/string_helpers.py +0 -0
  149. {varcode-9.2.8 → varcode-9.3.0}/varcode/structural_variant.py +0 -0
  150. {varcode-9.2.8 → varcode-9.3.0}/varcode/sv_allele_parser.py +0 -0
  151. {varcode-9.2.8 → varcode-9.3.0}/varcode/transcript_layout.py +0 -0
  152. {varcode-9.2.8 → varcode-9.3.0}/varcode/transcript_model.py +0 -0
  153. {varcode-9.2.8 → varcode-9.3.0}/varcode/transforms/__init__.py +0 -0
  154. {varcode-9.2.8 → varcode-9.3.0}/varcode/ucsc_reference_names.py +0 -0
  155. {varcode-9.2.8 → varcode-9.3.0}/varcode/util.py +0 -0
  156. {varcode-9.2.8 → varcode-9.3.0}/varcode/variant.py +0 -0
  157. {varcode-9.2.8 → varcode-9.3.0}/varcode/variant_collection.py +0 -0
  158. {varcode-9.2.8 → varcode-9.3.0}/varcode/vcf.py +0 -0
  159. {varcode-9.2.8 → varcode-9.3.0}/varcode/vcf_output.py +0 -0
  160. {varcode-9.2.8 → varcode-9.3.0}/varcode/vcf_parsing.py +0 -0
  161. {varcode-9.2.8 → varcode-9.3.0}/varcode.egg-info/dependency_links.txt +0 -0
  162. {varcode-9.2.8 → varcode-9.3.0}/varcode.egg-info/entry_points.txt +0 -0
  163. {varcode-9.2.8 → varcode-9.3.0}/varcode.egg-info/requires.txt +0 -0
  164. {varcode-9.2.8 → 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.8
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])
@@ -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
@@ -1617,6 +1639,8 @@ class CrypticExonCandidate(MutationEffect):
1617
1639
  """
1618
1640
 
1619
1641
  short_description = "sv-cryptic-exon-candidate"
1642
+ modifies_coding_sequence = None
1643
+ modifies_protein_sequence = None
1620
1644
 
1621
1645
  def __init__(
1622
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
  """
@@ -0,0 +1,142 @@
1
+ """Sequence-change flags for existing SV predictions, not a new SV model."""
2
+
3
+ from .codon_tables import codon_table_for_transcript, translate_sequence
4
+
5
+
6
+ def modification_status(effect, attribute):
7
+ """Any changed candidate wins; otherwise unknown wins over unchanged.
8
+
9
+ Walk by identity because SVs include themselves among their candidates.
10
+ Read their local status directly, avoiding recursive property calls.
11
+ """
12
+ from .effect_classes import MultiOutcomeEffect, StructuralVariantEffect
13
+
14
+ pending, seen = [effect], set()
15
+ unknown = False
16
+ evaluated = False
17
+ while pending:
18
+ current = pending.pop()
19
+ if id(current) in seen:
20
+ continue
21
+ seen.add(id(current))
22
+ candidates = (current.candidates
23
+ if isinstance(current, MultiOutcomeEffect) else ())
24
+ if isinstance(current, StructuralVariantEffect):
25
+ if any(candidate.effect is current for candidate in candidates):
26
+ evaluated = True
27
+ coding, protein = structural_sequence_changes(current)
28
+ status = protein if attribute == "modifies_protein_sequence" else coding
29
+ else:
30
+ # An explicit primary_effects tuple replaces the local result.
31
+ status = False if candidates else None
32
+ else:
33
+ evaluated = True
34
+ status = getattr(current, attribute)
35
+ if status is True:
36
+ return True
37
+ unknown |= status is None
38
+ pending.extend(candidate.effect for candidate in candidates)
39
+ return None if unknown or not evaluated else False
40
+
41
+
42
+ def _retained_start(model, transcript):
43
+ """Map all three annotated start bases, including split segment boundaries."""
44
+ positions = [set(), set(), set()]
45
+ start = min(transcript.start_codon_spliced_offsets)
46
+ offset = 0
47
+ for segment in model.reference_segments or ():
48
+ if segment.source == transcript and segment.strand == "+":
49
+ for i in range(3):
50
+ if segment.start <= start + i < segment.end:
51
+ positions[i].add(offset + start + i - segment.start)
52
+ offset += segment.length
53
+ return next((p for p in sorted(positions[0])
54
+ if p + 1 in positions[1] and p + 2 in positions[2]), None)
55
+
56
+
57
+ def _coding_sequence(model, initiator):
58
+ """Read a known ORF, or the retained reference ORF; never search for one.
59
+
60
+ Return None for unmapped assemblies, partial fragments, missing sequence,
61
+ overlaid edits whose coordinates haven't been mapped, or an incomplete ORF.
62
+ """
63
+ if model.cdna_sequence is None:
64
+ return None
65
+ evidence = model.evidence or {}
66
+ start, end = evidence.get("cds_start"), evidence.get("cds_end")
67
+ if start is None or end is None:
68
+ if not initiator.complete or model.edits:
69
+ return None
70
+ start = _retained_start(model, initiator)
71
+ end = None
72
+ if start is None:
73
+ return None
74
+ sequence = model.cdna_sequence[start:end].upper()
75
+ table = codon_table_for_transcript(initiator)
76
+ if sequence[:3] not in table.start_codons:
77
+ return None
78
+ for i in range(3, len(sequence) - 2, 3):
79
+ if sequence[i:i + 3] in table.stop_codons:
80
+ coding = sequence[:i + 3]
81
+ return coding if not set(coding) - set("ACGT") else None
82
+ return None
83
+
84
+
85
+ def structural_sequence_changes(effect):
86
+ """Return local (coding, protein) changes as True / False / None.
87
+
88
+ Compare against the transcript being annotated, including on a fusion's
89
+ 3' side. The 5' partner supplies the initiation site and translation table.
90
+ These are sequence predictions, not evidence of expression or translation.
91
+ """
92
+ from .effect_classes import LargeDeletion
93
+
94
+ transcript = effect.transcript
95
+ model = effect.mutant_transcript
96
+ coding_status = protein_status = None
97
+ if model is not None:
98
+ protein = model.mutant_protein_sequence
99
+ reference_protein = transcript.protein_sequence
100
+ if protein is not None and reference_protein is not None:
101
+ # An empty protein is a known absence, not missing data.
102
+ if "X" not in protein and "X" not in reference_protein:
103
+ protein_status = protein != reference_protein
104
+ initiator = getattr(effect, "five_prime_transcript", transcript)
105
+ coding = _coding_sequence(model, initiator)
106
+ if coding is not None:
107
+ reference_coding = transcript.coding_sequence
108
+ if reference_coding is not None:
109
+ coding_status = coding != reference_coding.upper()
110
+ if protein_status is None and reference_protein is not None:
111
+ # Initiator methionine also applies to alternative start codons.
112
+ protein = "M" + translate_sequence(
113
+ coding[3:], codon_table=codon_table_for_transcript(initiator))
114
+ if "X" not in reference_protein:
115
+ protein_status = protein != reference_protein
116
+
117
+ # A pure deletion can establish CDS/start loss without a translated model.
118
+ # Do not apply DNA-span inference to a supplied assembly or an RNA outcome.
119
+ if (isinstance(effect, LargeDeletion)
120
+ and not getattr(effect.variant, "alt_assembly", None)
121
+ and (model is None or model.annotator_name == "structural_variant")):
122
+ from .structural import _affected_span
123
+ start, end = _affected_span(effect.variant)
124
+ try:
125
+ coding_ranges = transcript.coding_sequence_position_ranges
126
+ except ValueError:
127
+ # A protein-coding biotype need not have annotated CDS features.
128
+ return coding_status, protein_status
129
+ coding_overlap = any(start <= hi and lo <= end
130
+ for lo, hi in coding_ranges)
131
+ if coding_status is None:
132
+ coding_status = coding_overlap
133
+ if protein_status is None:
134
+ if not coding_overlap:
135
+ protein_status = False
136
+ elif (transcript.contains_start_codon
137
+ and any(start <= pos <= end for pos in transcript.start_codon_positions)):
138
+ protein_status = True
139
+
140
+ if protein_status is True and coding_status is None:
141
+ coding_status = True
142
+ return coding_status, protein_status
@@ -0,0 +1 @@
1
+ __version__ = "9.3.0"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: varcode
3
- Version: 9.2.8
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
@@ -72,6 +72,7 @@ tests/test_stop_codon_classification_bugs.py
72
72
  tests/test_string_helpers.py
73
73
  tests/test_structural_variant_annotator.py
74
74
  tests/test_structural_variants.py
75
+ tests/test_sv_sequence_changes.py
75
76
  tests/test_sv_tabular_metadata.py
76
77
  tests/test_symbolic_alleles.py
77
78
  tests/test_symbolic_sv_padding.py
@@ -153,6 +154,7 @@ varcode/effects/effect_prediction_coding_frameshift.py
153
154
  varcode/effects/effect_prediction_coding_in_frame.py
154
155
  varcode/effects/fast_path.py
155
156
  varcode/effects/mutate.py
157
+ varcode/effects/sequence_change.py
156
158
  varcode/effects/structural.py
157
159
  varcode/effects/transcript_helpers.py
158
160
  varcode/effects/translate.py
@@ -1 +0,0 @@
1
- __version__ = "9.2.8"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes