varcode 9.2.6__tar.gz → 9.2.8__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 (162) hide show
  1. {varcode-9.2.6/varcode.egg-info → varcode-9.2.8}/PKG-INFO +1 -1
  2. {varcode-9.2.6 → varcode-9.2.8}/tests/test_documentation_examples.py +75 -6
  3. varcode-9.2.8/tests/test_fusion_partner_candidates.py +184 -0
  4. {varcode-9.2.6 → varcode-9.2.8}/tests/test_nonlocal_sv_transcripts.py +1 -1
  5. {varcode-9.2.6 → varcode-9.2.8}/varcode/effects/effect_classes.py +6 -0
  6. {varcode-9.2.6 → varcode-9.2.8}/varcode/effects/structural.py +44 -28
  7. varcode-9.2.8/varcode/version.py +1 -0
  8. {varcode-9.2.6 → varcode-9.2.8/varcode.egg-info}/PKG-INFO +1 -1
  9. {varcode-9.2.6 → varcode-9.2.8}/varcode.egg-info/SOURCES.txt +1 -0
  10. varcode-9.2.6/varcode/version.py +0 -1
  11. {varcode-9.2.6 → varcode-9.2.8}/LICENSE +0 -0
  12. {varcode-9.2.6 → varcode-9.2.8}/MANIFEST.in +0 -0
  13. {varcode-9.2.6 → varcode-9.2.8}/README.md +0 -0
  14. {varcode-9.2.6 → varcode-9.2.8}/pyproject.toml +0 -0
  15. {varcode-9.2.6 → varcode-9.2.8}/requirements.txt +0 -0
  16. {varcode-9.2.6 → varcode-9.2.8}/setup.cfg +0 -0
  17. {varcode-9.2.6 → varcode-9.2.8}/tests/test_annotator_divergence_scenarios.py +0 -0
  18. {varcode-9.2.6 → varcode-9.2.8}/tests/test_annotator_parity_adversarial.py +0 -0
  19. {varcode-9.2.6 → varcode-9.2.8}/tests/test_annotators.py +0 -0
  20. {varcode-9.2.6 → varcode-9.2.8}/tests/test_bnd_retained_segments.py +0 -0
  21. {varcode-9.2.6 → varcode-9.2.8}/tests/test_cli_effects.py +0 -0
  22. {varcode-9.2.6 → varcode-9.2.8}/tests/test_cli_genes.py +0 -0
  23. {varcode-9.2.6 → varcode-9.2.8}/tests/test_cli_without_pkg_resources.py +0 -0
  24. {varcode-9.2.6 → varcode-9.2.8}/tests/test_collection_filtering.py +0 -0
  25. {varcode-9.2.6 → varcode-9.2.8}/tests/test_collection_variants_attr_consistency.py +0 -0
  26. {varcode-9.2.6 → varcode-9.2.8}/tests/test_common.py +0 -0
  27. {varcode-9.2.6 → varcode-9.2.8}/tests/test_cosmic_mutations.py +0 -0
  28. {varcode-9.2.6 → varcode-9.2.8}/tests/test_cryptic_exons.py +0 -0
  29. {varcode-9.2.6 → varcode-9.2.8}/tests/test_csv_roundtrip.py +0 -0
  30. {varcode-9.2.6 → varcode-9.2.8}/tests/test_dbnsfp_validation.py +0 -0
  31. {varcode-9.2.6 → varcode-9.2.8}/tests/test_default_structural_helpers.py +0 -0
  32. {varcode-9.2.6 → varcode-9.2.8}/tests/test_effect_annotation_errors.py +0 -0
  33. {varcode-9.2.6 → varcode-9.2.8}/tests/test_effect_candidates.py +0 -0
  34. {varcode-9.2.6 → varcode-9.2.8}/tests/test_effect_classes.py +0 -0
  35. {varcode-9.2.6 → varcode-9.2.8}/tests/test_effect_collection.py +0 -0
  36. {varcode-9.2.6 → varcode-9.2.8}/tests/test_effect_collection_serialization.py +0 -0
  37. {varcode-9.2.6 → varcode-9.2.8}/tests/test_effect_collection_sort_order.py +0 -0
  38. {varcode-9.2.6 → varcode-9.2.8}/tests/test_effect_hypotheses.py +0 -0
  39. {varcode-9.2.6 → varcode-9.2.8}/tests/test_effects_from_mutagenix_variants.py +0 -0
  40. {varcode-9.2.6 → varcode-9.2.8}/tests/test_exacto_fusions.py +0 -0
  41. {varcode-9.2.6 → varcode-9.2.8}/tests/test_exonic_splice_site.py +0 -0
  42. {varcode-9.2.6 → varcode-9.2.8}/tests/test_fast_path.py +0 -0
  43. {varcode-9.2.6 → varcode-9.2.8}/tests/test_frameshift_cterminus_regression.py +0 -0
  44. {varcode-9.2.6 → varcode-9.2.8}/tests/test_frameshift_helpers.py +0 -0
  45. {varcode-9.2.6 → varcode-9.2.8}/tests/test_genome_contig_validation.py +0 -0
  46. {varcode-9.2.6 → varcode-9.2.8}/tests/test_genome_sequence.py +0 -0
  47. {varcode-9.2.6 → varcode-9.2.8}/tests/test_genomic_layout.py +0 -0
  48. {varcode-9.2.6 → varcode-9.2.8}/tests/test_genotype.py +0 -0
  49. {varcode-9.2.6 → varcode-9.2.8}/tests/test_genotype_from_vcf.py +0 -0
  50. {varcode-9.2.6 → varcode-9.2.8}/tests/test_germline_context.py +0 -0
  51. {varcode-9.2.6 → varcode-9.2.8}/tests/test_germline_effects.py +0 -0
  52. {varcode-9.2.6 → varcode-9.2.8}/tests/test_haplotype_effect.py +0 -0
  53. {varcode-9.2.6 → varcode-9.2.8}/tests/test_maf.py +0 -0
  54. {varcode-9.2.6 → varcode-9.2.8}/tests/test_mitochondrial.py +0 -0
  55. {varcode-9.2.6 → varcode-9.2.8}/tests/test_mm10_klf6_frameshift.py +0 -0
  56. {varcode-9.2.6 → varcode-9.2.8}/tests/test_mouse.py +0 -0
  57. {varcode-9.2.6 → varcode-9.2.8}/tests/test_mutant_transcript.py +0 -0
  58. {varcode-9.2.6 → varcode-9.2.8}/tests/test_mutate.py +0 -0
  59. {varcode-9.2.6 → varcode-9.2.8}/tests/test_no_duplicate_variants.py +0 -0
  60. {varcode-9.2.6 → varcode-9.2.8}/tests/test_osteosarc_fusions.py +0 -0
  61. {varcode-9.2.6 → varcode-9.2.8}/tests/test_partial_annotators.py +0 -0
  62. {varcode-9.2.6 → varcode-9.2.8}/tests/test_premature_stop_short_description.py +0 -0
  63. {varcode-9.2.6 → varcode-9.2.8}/tests/test_problematic_variants.py +0 -0
  64. {varcode-9.2.6 → varcode-9.2.8}/tests/test_protein_diff_parity.py +0 -0
  65. {varcode-9.2.6 → varcode-9.2.8}/tests/test_read_phase_resolver.py +0 -0
  66. {varcode-9.2.6 → varcode-9.2.8}/tests/test_real_world_callers.py +0 -0
  67. {varcode-9.2.6 → varcode-9.2.8}/tests/test_reference.py +0 -0
  68. {varcode-9.2.6 → varcode-9.2.8}/tests/test_reference_mismatch_error.py +0 -0
  69. {varcode-9.2.6 → varcode-9.2.8}/tests/test_reference_segments.py +0 -0
  70. {varcode-9.2.6 → varcode-9.2.8}/tests/test_rna_evidence.py +0 -0
  71. {varcode-9.2.6 → varcode-9.2.8}/tests/test_rna_haplotype_sequences.py +0 -0
  72. {varcode-9.2.6 → varcode-9.2.8}/tests/test_rna_read_phasing_source.py +0 -0
  73. {varcode-9.2.6 → varcode-9.2.8}/tests/test_selenocysteine.py +0 -0
  74. {varcode-9.2.6 → varcode-9.2.8}/tests/test_silent_aa_pos.py +0 -0
  75. {varcode-9.2.6 → varcode-9.2.8}/tests/test_silent_hgvs_description.py +0 -0
  76. {varcode-9.2.6 → varcode-9.2.8}/tests/test_splice_graph.py +0 -0
  77. {varcode-9.2.6 → varcode-9.2.8}/tests/test_splice_outcomes.py +0 -0
  78. {varcode-9.2.6 → varcode-9.2.8}/tests/test_splice_site_effects.py +0 -0
  79. {varcode-9.2.6 → varcode-9.2.8}/tests/test_stop_codon_classification_bugs.py +0 -0
  80. {varcode-9.2.6 → varcode-9.2.8}/tests/test_string_helpers.py +0 -0
  81. {varcode-9.2.6 → varcode-9.2.8}/tests/test_structural_variant_annotator.py +0 -0
  82. {varcode-9.2.6 → varcode-9.2.8}/tests/test_structural_variants.py +0 -0
  83. {varcode-9.2.6 → varcode-9.2.8}/tests/test_sv_tabular_metadata.py +0 -0
  84. {varcode-9.2.6 → varcode-9.2.8}/tests/test_symbolic_alleles.py +0 -0
  85. {varcode-9.2.6 → varcode-9.2.8}/tests/test_symbolic_sv_padding.py +0 -0
  86. {varcode-9.2.6 → varcode-9.2.8}/tests/test_timings.py +0 -0
  87. {varcode-9.2.6 → varcode-9.2.8}/tests/test_transcript_layout.py +0 -0
  88. {varcode-9.2.6 → varcode-9.2.8}/tests/test_transcript_model.py +0 -0
  89. {varcode-9.2.6 → varcode-9.2.8}/tests/test_transforms_left_align.py +0 -0
  90. {varcode-9.2.6 → varcode-9.2.8}/tests/test_transforms_pair_breakends.py +0 -0
  91. {varcode-9.2.6 → varcode-9.2.8}/tests/test_variant.py +0 -0
  92. {varcode-9.2.6 → varcode-9.2.8}/tests/test_variant_collection.py +0 -0
  93. {varcode-9.2.6 → varcode-9.2.8}/tests/test_vcf.py +0 -0
  94. {varcode-9.2.6 → varcode-9.2.8}/tests/test_vcf_output.py +0 -0
  95. {varcode-9.2.6 → varcode-9.2.8}/tests/test_vcf_parsing.py +0 -0
  96. {varcode-9.2.6 → varcode-9.2.8}/tests/test_vcf_phase_resolver.py +0 -0
  97. {varcode-9.2.6 → varcode-9.2.8}/tests/test_vcf_spec_examples.py +0 -0
  98. {varcode-9.2.6 → varcode-9.2.8}/varcode/__init__.py +0 -0
  99. {varcode-9.2.6 → varcode-9.2.8}/varcode/annotators/__init__.py +0 -0
  100. {varcode-9.2.6 → varcode-9.2.8}/varcode/annotators/fast.py +0 -0
  101. {varcode-9.2.6 → varcode-9.2.8}/varcode/annotators/protein_diff.py +0 -0
  102. {varcode-9.2.6 → varcode-9.2.8}/varcode/annotators/registry.py +0 -0
  103. {varcode-9.2.6 → varcode-9.2.8}/varcode/cli/__init__.py +0 -0
  104. {varcode-9.2.6 → varcode-9.2.8}/varcode/cli/effects_script.py +0 -0
  105. {varcode-9.2.6 → varcode-9.2.8}/varcode/cli/genes_script.py +0 -0
  106. {varcode-9.2.6 → varcode-9.2.8}/varcode/cli/logging.conf +0 -0
  107. {varcode-9.2.6 → varcode-9.2.8}/varcode/cli/variant_args.py +0 -0
  108. {varcode-9.2.6 → varcode-9.2.8}/varcode/cli/version_info.py +0 -0
  109. {varcode-9.2.6 → varcode-9.2.8}/varcode/common.py +0 -0
  110. {varcode-9.2.6 → varcode-9.2.8}/varcode/cryptic_exons.py +0 -0
  111. {varcode-9.2.6 → varcode-9.2.8}/varcode/csv_helpers.py +0 -0
  112. {varcode-9.2.6 → varcode-9.2.8}/varcode/effect_candidates.py +0 -0
  113. {varcode-9.2.6 → varcode-9.2.8}/varcode/effect_hypotheses.py +0 -0
  114. {varcode-9.2.6 → varcode-9.2.8}/varcode/effects/__init__.py +0 -0
  115. {varcode-9.2.6 → varcode-9.2.8}/varcode/effects/classify.py +0 -0
  116. {varcode-9.2.6 → varcode-9.2.8}/varcode/effects/codon_tables.py +0 -0
  117. {varcode-9.2.6 → varcode-9.2.8}/varcode/effects/common.py +0 -0
  118. {varcode-9.2.6 → varcode-9.2.8}/varcode/effects/effect_collection.py +0 -0
  119. {varcode-9.2.6 → varcode-9.2.8}/varcode/effects/effect_helpers.py +0 -0
  120. {varcode-9.2.6 → varcode-9.2.8}/varcode/effects/effect_ordering.py +0 -0
  121. {varcode-9.2.6 → varcode-9.2.8}/varcode/effects/effect_prediction.py +0 -0
  122. {varcode-9.2.6 → varcode-9.2.8}/varcode/effects/effect_prediction_coding.py +0 -0
  123. {varcode-9.2.6 → varcode-9.2.8}/varcode/effects/effect_prediction_coding_frameshift.py +0 -0
  124. {varcode-9.2.6 → varcode-9.2.8}/varcode/effects/effect_prediction_coding_in_frame.py +0 -0
  125. {varcode-9.2.6 → varcode-9.2.8}/varcode/effects/fast_path.py +0 -0
  126. {varcode-9.2.6 → varcode-9.2.8}/varcode/effects/mutate.py +0 -0
  127. {varcode-9.2.6 → varcode-9.2.8}/varcode/effects/transcript_helpers.py +0 -0
  128. {varcode-9.2.6 → varcode-9.2.8}/varcode/effects/translate.py +0 -0
  129. {varcode-9.2.6 → varcode-9.2.8}/varcode/errors.py +0 -0
  130. {varcode-9.2.6 → varcode-9.2.8}/varcode/exacto.py +0 -0
  131. {varcode-9.2.6 → varcode-9.2.8}/varcode/genome.py +0 -0
  132. {varcode-9.2.6 → varcode-9.2.8}/varcode/genome_sequence.py +0 -0
  133. {varcode-9.2.6 → varcode-9.2.8}/varcode/genomic_layout.py +0 -0
  134. {varcode-9.2.6 → varcode-9.2.8}/varcode/genotype.py +0 -0
  135. {varcode-9.2.6 → varcode-9.2.8}/varcode/germline.py +0 -0
  136. {varcode-9.2.6 → varcode-9.2.8}/varcode/maf.py +0 -0
  137. {varcode-9.2.6 → varcode-9.2.8}/varcode/mutant_transcript.py +0 -0
  138. {varcode-9.2.6 → varcode-9.2.8}/varcode/nucleotides.py +0 -0
  139. {varcode-9.2.6 → varcode-9.2.8}/varcode/phasing.py +0 -0
  140. {varcode-9.2.6 → varcode-9.2.8}/varcode/realized_effects.py +0 -0
  141. {varcode-9.2.6 → varcode-9.2.8}/varcode/reference.py +0 -0
  142. {varcode-9.2.6 → varcode-9.2.8}/varcode/rna_evidence.py +0 -0
  143. {varcode-9.2.6 → varcode-9.2.8}/varcode/rna_read_phasing.py +0 -0
  144. {varcode-9.2.6 → varcode-9.2.8}/varcode/splice_graph.py +0 -0
  145. {varcode-9.2.6 → varcode-9.2.8}/varcode/splice_outcomes.py +0 -0
  146. {varcode-9.2.6 → varcode-9.2.8}/varcode/string_helpers.py +0 -0
  147. {varcode-9.2.6 → varcode-9.2.8}/varcode/structural_variant.py +0 -0
  148. {varcode-9.2.6 → varcode-9.2.8}/varcode/sv_allele_parser.py +0 -0
  149. {varcode-9.2.6 → varcode-9.2.8}/varcode/transcript_layout.py +0 -0
  150. {varcode-9.2.6 → varcode-9.2.8}/varcode/transcript_model.py +0 -0
  151. {varcode-9.2.6 → varcode-9.2.8}/varcode/transforms/__init__.py +0 -0
  152. {varcode-9.2.6 → varcode-9.2.8}/varcode/ucsc_reference_names.py +0 -0
  153. {varcode-9.2.6 → varcode-9.2.8}/varcode/util.py +0 -0
  154. {varcode-9.2.6 → varcode-9.2.8}/varcode/variant.py +0 -0
  155. {varcode-9.2.6 → varcode-9.2.8}/varcode/variant_collection.py +0 -0
  156. {varcode-9.2.6 → varcode-9.2.8}/varcode/vcf.py +0 -0
  157. {varcode-9.2.6 → varcode-9.2.8}/varcode/vcf_output.py +0 -0
  158. {varcode-9.2.6 → varcode-9.2.8}/varcode/vcf_parsing.py +0 -0
  159. {varcode-9.2.6 → varcode-9.2.8}/varcode.egg-info/dependency_links.txt +0 -0
  160. {varcode-9.2.6 → varcode-9.2.8}/varcode.egg-info/entry_points.txt +0 -0
  161. {varcode-9.2.6 → varcode-9.2.8}/varcode.egg-info/requires.txt +0 -0
  162. {varcode-9.2.6 → varcode-9.2.8}/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.6
3
+ Version: 9.2.8
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
@@ -1,6 +1,7 @@
1
1
  """Keep the reader-facing examples executable, using their actual Markdown."""
2
2
 
3
3
  from pathlib import Path
4
+ import ast
4
5
  import re
5
6
  import shutil
6
7
 
@@ -9,6 +10,7 @@ from pyensembl import cached_release
9
10
 
10
11
  import varcode
11
12
  from varcode import EffectCandidate, StructuralVariant, Variant
13
+ from varcode.effects.effect_classes import HaplotypeEffect
12
14
  from varcode.effect_hypotheses import RealizedEffectCandidate
13
15
 
14
16
 
@@ -46,6 +48,7 @@ def input_directory(tmp_path, monkeypatch):
46
48
  ("docs/structural_variants.md", 1),
47
49
  ("docs/transforms.md", 1),
48
50
  ("docs/germline.md", 1),
51
+ ("docs/phasing.md", 1),
49
52
  ])
50
53
  def test_documentation_first_steps(path, count, input_directory):
51
54
  namespace = _run(_blocks(path)[:count])
@@ -59,29 +62,95 @@ def test_documentation_first_steps(path, count, input_directory):
59
62
  assert (input_directory / "effects.csv").is_file()
60
63
  if path == "docs/structural_variants.md":
61
64
  assert namespace["variants"][0].is_structural
65
+ if path == "docs/phasing.md":
66
+ variants = namespace["phased_variants"]
67
+ assert namespace["phaser"].in_cis(*variants) is True
68
+ assert any(isinstance(effect, HaplotypeEffect)
69
+ for effect in namespace["effects"])
62
70
 
63
71
 
64
72
  def test_documentation_germline_scenarios():
65
73
  namespace = _run(_blocks(
66
- "docs/germline.md", "## Concrete example: same codon, three scenarios")[:4])
74
+ "docs/phasing.md", "## Two variants in one codon")[:4])
67
75
  assert namespace["eff_cis"].short_description == "p.S159T"
68
76
  assert namespace["eff_trans"].short_description == "p.L159M"
69
77
  assert {c.effect.short_description for c in namespace["eff"].candidates} == {
70
78
  "p.S159T", "p.L159M"}
71
79
 
72
- _run(_blocks("docs/germline.md", "## Loss of heterozygosity (LOH)")[:1],
73
- namespace)
74
- assert any(getattr(effect, "is_loh", False)
75
- for effect in namespace["overlap"].effects(germline=namespace["ctx"]))
80
+ namespace["effects"] = varcode.VariantCollection(
81
+ [namespace["germline"]]).effects(germline=namespace["ctx"])
82
+ _run(_blocks("docs/germline.md", "## Loss of heterozygosity (LOH)")[:1], namespace)
83
+ assert any(getattr(effect, "is_loh", False) for effect in namespace["effects"])
76
84
 
77
85
  validation = _blocks("docs/germline.md", "## Cross-VCF build mismatch")[:1]
86
+ namespace["somatic_variants"] = varcode.VariantCollection([namespace["somatic"]])
87
+ namespace["germline_ctx"] = namespace["ctx"]
78
88
  _run(validation, namespace)
79
- namespace["ctx"] = varcode.GermlineContext.from_variants(
89
+ namespace["germline_ctx"] = varcode.GermlineContext.from_variants(
80
90
  [namespace["germline"]], reference_name="GRCh37")
81
91
  with pytest.raises(varcode.GenomeBuildMismatchError):
82
92
  _run(validation, namespace)
83
93
 
84
94
 
95
+ def test_documentation_germline_with_vcf_phase(input_directory):
96
+ namespace = _run(_blocks("docs/germline.md")[:1])
97
+ _run(_blocks("docs/phasing.md", "## Phased VCF")[:1], namespace)
98
+ namespace["rna"] = varcode.NullRNAEvidenceResolver()
99
+ _run(_blocks("docs/phasing.md", "## Combining evidence")[:1], namespace)
100
+ cftr = [effect for effect in namespace["effects"]
101
+ if effect.transcript_id == "ENST00000003084"]
102
+ assert len(cftr) == 1
103
+ assert cftr[0].short_description == "p.S159T"
104
+
105
+
106
+ def test_documentation_fusion_protein_candidates(capsys):
107
+ genome = cached_release(95)
108
+ transcript = genome.transcript_by_id("ENST00000538197")
109
+ variant = StructuralVariant(
110
+ "4", 15012987, "BND", alt="N[4:2664478[",
111
+ mate_contig="4", mate_start=2664478, genome=genome)
112
+ namespace = {"effect": variant.effect_on_transcript(transcript)}
113
+ _run(_blocks("docs/structural_variants.md", "## Fusion protein candidates")[:1], namespace)
114
+ output = capsys.readouterr().out
115
+ assert "ENST00000324666" in output
116
+ assert "ENST00000637812" in output
117
+
118
+
119
+ def test_documentation_effect_families_keep_all_classes():
120
+ source = ast.parse((ROOT / "varcode/effects/effect_classes.py").read_text())
121
+ classes = {node.name for node in source.body if isinstance(node, ast.ClassDef)}
122
+ guide = (ROOT / "docs/effect_types.md").read_text()
123
+ documented = re.findall(r"^::: varcode.effects.effect_classes\.(\w+)$", guide, re.M)
124
+ assert set(documented) == classes
125
+ assert len(documented) == len(classes)
126
+ assert "::: varcode.SpliceOutcomeSet" in guide
127
+
128
+
129
+ @pytest.mark.parametrize("path,anchor,destination", [
130
+ ("structural_variants.md", "alleles-coordinates-and-exports", "sv_reference.md"),
131
+ ("structural_variants.md", "importing-observed-rna-structures", "rna_structures.md"),
132
+ ("germline.md", "concrete-example-same-codon-three-scenarios", "phasing.md"),
133
+ ("germline.md", "known-deletion-haplotypes-in-rna-alignments", "phasing.md"),
134
+ ("api.md", "varcode.Variant.effects", "api_variants.md"),
135
+ ("api.md", "varcode.MutantTranscript", "api_rna.md"),
136
+ ("api.md", "varcode.VCFPhaseResolver", "api_phasing.md"),
137
+ ])
138
+ def test_documentation_topic_links_preserve_old_fragments(path, anchor, destination):
139
+ guide = (ROOT / "docs" / path).read_text()
140
+ row = next(line for line in guide.splitlines() if f'id="{anchor}"' in line)
141
+ assert f"]({destination}" in row
142
+ assert (ROOT / "docs" / destination).is_file()
143
+
144
+
145
+ def test_documentation_api_objects_have_one_heading():
146
+ topics = list((ROOT / "docs").glob("api_*.md"))
147
+ assert len(topics) == 5
148
+ for topic in topics:
149
+ text = topic.read_text()
150
+ assert "::: varcode." in text
151
+ assert not re.search(r"^### `varcode\.", text, re.M)
152
+
153
+
85
154
  def test_documentation_splice_accessors():
86
155
  namespace = _run(_blocks("docs/splice_variants.md"))
87
156
  assert isinstance(namespace["splice_set"], varcode.SpliceOutcomeSet)
@@ -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
 
@@ -1549,6 +1549,12 @@ class GeneFusion(StructuralVariantEffect):
1549
1549
  produce it, and so do deletions, duplications and inversions with
1550
1550
  one end in each gene.
1551
1551
 
1552
+ The default annotator retains every compatible annotated partner isoform
1553
+ and junction in :attr:`candidates`, with no count cap. The first is the
1554
+ historical primary; producer order is not a probability ranking. Distinct
1555
+ transcript pairs remain separate even when their proteins match. Missing
1556
+ sequence leaves that candidate's transcript/protein unresolved.
1557
+
1552
1558
  ``transcript`` is the transcript being annotated and
1553
1559
  ``partner_transcript`` the other one; ``five_prime_transcript``
1554
1560
  and ``three_prime_transcript`` say which is which (by default
@@ -473,8 +473,14 @@ def _build_fusion_mutant_transcript(
473
473
  segments. The annotator assigns the 5' / 3' roles from breakend
474
474
  orientation and strand before calling this.
475
475
  """
476
- five_prime_cdna = str(five_prime_transcript.sequence)
477
- three_prime_cdna = str(three_prime_transcript.sequence)
476
+ five_prime_cdna = five_prime_transcript.sequence
477
+ three_prime_cdna = three_prime_transcript.sequence
478
+ if not five_prime_cdna or not three_prime_cdna:
479
+ # The partner is still a candidate when sequence is unavailable.
480
+ # In particular, str(None) is not an observed or predicted cDNA.
481
+ return None
482
+ five_prime_cdna = str(five_prime_cdna)
483
+ three_prime_cdna = str(three_prime_cdna)
478
484
  five_prime_end = _cdna_cut(
479
485
  five_prime_transcript, five_prime_position, True)
480
486
  three_prime_start = _cdna_cut(
@@ -798,8 +804,11 @@ def _intronic_or_intergenic(variant, transcript):
798
804
 
799
805
 
800
806
  def _fusion_across_junction(variant, transcript, assembly):
801
- """The :class:`GeneFusion` ``transcript`` forms at one of
802
- ``variant``'s junctions, or ``None``.
807
+ """Fusion candidates across all compatible junctions and partner isoforms.
808
+
809
+ Return the first :class:`GeneFusion`, carrying all other fusions in its
810
+ primary candidates, or ``None``. Annotation order is not a likelihood
811
+ ranking; no compatible partner is discarded by a candidate-count limit.
803
812
 
804
813
  A junction end belongs to this transcript when the transcript
805
814
  spans it and the transcript's *gene* doesn't span the other
@@ -827,34 +836,42 @@ def _fusion_across_junction(variant, transcript, assembly):
827
836
  continue
828
837
  ends.append((near, far))
829
838
 
839
+ fusions = []
840
+ seen = set()
830
841
  for near, far in sorted(
831
842
  ends, key=lambda pair: not keeps_five_prime(pair[0])):
832
843
  five_prime = keeps_five_prime(near)
833
- partner = _fusion_partner(
834
- variant, transcript, near, far, five_prime)
835
- if partner is None:
836
- continue
837
- if five_prime:
838
- five, five_position = transcript, near.position
839
- three, three_position = partner, far.position
840
- else:
841
- five, five_position = partner, far.position
842
- three, three_position = transcript, near.position
843
- return GeneFusion(
844
- variant=variant,
845
- transcript=transcript,
846
- partner_transcript=partner,
847
- five_prime_transcript=five,
848
- three_prime_transcript=three,
849
- mutant_transcript=assembly or _build_fusion_mutant_transcript(
850
- transcript, five, five_position, three, three_position))
851
- return None
844
+ for partner in _fusion_partners(
845
+ variant, transcript, near, far, five_prime):
846
+ key = (near, far, partner.id)
847
+ if key in seen:
848
+ continue
849
+ seen.add(key)
850
+ if five_prime:
851
+ five, five_position = transcript, near.position
852
+ three, three_position = partner, far.position
853
+ else:
854
+ five, five_position = partner, far.position
855
+ three, three_position = transcript, near.position
856
+ fusions.append(GeneFusion(
857
+ variant=variant,
858
+ transcript=transcript,
859
+ partner_transcript=partner,
860
+ five_prime_transcript=five,
861
+ three_prime_transcript=three,
862
+ mutant_transcript=assembly or _build_fusion_mutant_transcript(
863
+ transcript, five, five_position, three, three_position)))
864
+ if not fusions:
865
+ return None
866
+ primary = fusions[0]
867
+ primary._attach_primary_effects(fusions[1:])
868
+ return primary
852
869
 
853
870
 
854
- def _fusion_partner(
871
+ def _fusion_partners(
855
872
  variant, transcript, near, far, transcript_is_five_prime):
856
- """The protein-coding transcript at ``far`` that fuses with
857
- ``transcript``, or ``None``.
873
+ """Yield every protein-coding transcript at ``far`` that fuses with
874
+ ``transcript``, in annotation order.
858
875
 
859
876
  A partner is in another gene, keeps the end opposite to
860
877
  ``transcript``'s, and — like ``transcript`` — belongs to a gene
@@ -870,8 +887,7 @@ def _fusion_partner(
870
887
  if far.keeps is None or (
871
888
  _retains_five_prime_end(candidate, far.keeps)
872
889
  != transcript_is_five_prime):
873
- return candidate
874
- return None
890
+ yield candidate
875
891
 
876
892
 
877
893
  def _coding_transcripts_at(variant, contig, position):
@@ -0,0 +1 @@
1
+ __version__ = "9.2.8"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: varcode
3
- Version: 9.2.6
3
+ Version: 9.2.8
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
@@ -32,6 +32,7 @@ tests/test_exonic_splice_site.py
32
32
  tests/test_fast_path.py
33
33
  tests/test_frameshift_cterminus_regression.py
34
34
  tests/test_frameshift_helpers.py
35
+ tests/test_fusion_partner_candidates.py
35
36
  tests/test_genome_contig_validation.py
36
37
  tests/test_genome_sequence.py
37
38
  tests/test_genomic_layout.py
@@ -1 +0,0 @@
1
- __version__ = "9.2.6"
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