1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
"""``CoordinateMapper``'s numeric entry points must validate what they are handed.
Five methods take raw integers from Python and build position structs directly,
so none of the checks ``parse_hgvs`` applies to a written description reach
them. Three escapes follow, and all three are only observable from here — the
Rust API is reached through the parser, which refuses these values before a
position struct exists.
**Escape 1 — a base of zero.** ``parse_hgvs`` refuses ``c.0`` / ``n.0`` as
``E1003 InvalidPosition`` ("Position 0 is not valid in HGVS notation"), on the
spec's authority: ``background/numbering.md:31`` states there is no nucleotide
``c.0``. That is a claim about what the numbering axis contains, so it holds
however the coordinate arrives. Measured on the unfixed build, over the
two-exon transcript below::
c_to_g("NM_TEST.1", 0) -> ('chr1', 1011)
c_to_g("NM_TEST.1", 1) -> ('chr1', 1011)
— one entry point refusing and the other answering ``c.0`` *identically to*
``c.1``.
**Escape 2 — an unknown-offset sentinel used as a distance.** ``c.N+?`` / ``c.N-?``
parse to ``i64::MAX`` / ``i64::MIN``, which are markers for "unknown, unbounded
in this direction", not measured distances. Passed as ``offset`` they reach
``Transcript::intronic_to_genomic``'s unchecked arithmetic — the ``#1087`` /
PR ``#1088`` guard sits in ``src/data/mapping.rs`` and is not on this path.
Measured on the unfixed build::
c_to_g("NM_TEST.1", 90, 2**63 - 1) -> ('chr1', 9223372036854776907)
c_to_g("NM_TEST.1", 91, -2**63) !! pyo3_runtime.PanicException:
attempt to negate with overflow
The negative sentinel overflows in ``-offset`` *before* the cast, so it panics
under ``debug_assertions`` and wraps silently in the ``--release`` wheel CI
ships. The positive sentinel needs no overflow to do damage: it returns a
number that looks exactly like a coordinate.
**Escape 2b — the same class, one step away from the sentinel (#1765).** The
guard for escape 2 keys on sentinel *identity*, which is the right key for a
marker and the wrong one for the arithmetic. ``i64::MIN + 1`` and
``i64::MAX - 1`` are plain numbers, so no widening of the sentinel predicate
reaches them, and both reproduced escape 2 in full. The magnitude condition is
enforced where the magnitude can be judged — ``intronic_to_genomic`` is now
total (``unsigned_abs`` plus checked add/sub) and refuses a result outside the
intron the offset was measured into. See ``TestOversizedOffsetIsRefused``.
**Escape 3 — a value handed back that the caller cannot read.** ``c_to_n``
returns ``tx_pos.offset`` verbatim, so a sentinel comes back as
``9223372036854775807``, indistinguishable from a measured distance; and
``n_to_c`` forwards ``downstream``, which ``cds_to_tx``/``tx_to_cds`` discard,
so ``downstream=True`` and ``downstream=False`` returned the same triple.
The fix is at the boundary in every case: refuse the argument, or refuse to
return the value. Repairing the conversions themselves is separate work on
other branches, and a boundary that accepts a value it cannot mean is a defect
whichever way those land.
"""
# The parser's own sentinels, spelled as Python ints. ``c.N+?`` -> ``i64::MAX``,
# ``c.N-?`` -> ``i64::MIN`` (``src/hgvs/parser/position.rs``).
= 2**63 - 1
= -
# A two-exon plus-strand transcript with genomic coordinates, so ``c_to_g``
# resolves rather than declining for want of a genome. Built programmatically;
# nothing here is committed as data.
#
# exon 1: tx 1..100, genomic 1001..1100
# intron: genomic 1101..2000
# exon 2: tx 101..200, genomic 2001..2100
# CDS: tx 11..190 -> c.1 = tx 11 = g.1011, c.90 = tx 100 = g.1100
#
# So ``c.90`` is the last exonic base before the intron and ``c.91`` the first
# after it: ``c.90+n`` and ``c.91-n`` are the two intronic arms whose arithmetic
# escape 2 is about.
=
=
"""A mapper over the two-exon transcript above.
Construction may emit the reduced-capability ``UserWarning`` (the reference
carries transcript geometry but no genome FASTA), which is expected and
unrelated to anything under test. It is suppressed rather than asserted:
the warn-once flag is process-global and shared across every surface, so
only the first mapper built in a given interpreter emits it and a
``pytest.warns`` here would fail on every test after the first.
"""
= /
return
# ---------------------------------------------------------------------------
# Escape 1: a base of zero
# ---------------------------------------------------------------------------
"""``c.0`` / ``n.0`` get the parser's answer, not a coordinate."""
"""Every numeric entry point refuses a base of zero, with the parser's code.
The code is asserted, not just the exception type: ``c_to_p(0)`` already
failed before this change, but with "Cannot convert UTR position to
protein" — a wrong diagnosis that a bare ``pytest.raises`` would accept.
"""
assert ==
assert in
"""One coordinate, one answer, whichever door it arrives through.
This is the defect stated as a property: ``parse`` refuses ``c.0`` and
the numeric entry point answered it, so ferro had two entry points and
two answers. Only the *refusal* is compared, because the two doors
genuinely differ in how much they know: ``parse`` reports
``ParseError`` with ``code is None`` (the nom parser bails before the
preprocessor's position-zero phase, which is what attaches ``E1003``
and what ``ferro parse`` prints on the CLI), while the numeric entry
point can name the code exactly because it has the integer in hand.
Pinning ``parse``'s missing code here would be pinning an unrelated
inconsistency.
"""
"""The guard fires at zero and nowhere else.
Without this, refusing every base would satisfy the assertions above.
``c.-1`` is the 5'UTR base immediately before ``c.1`` and must survive:
it is a legal coordinate whose sign a naive "must be positive" check
would reject.
"""
assert ==
assert ==
assert == 1
assert ==
assert ==
# ---------------------------------------------------------------------------
# Escape 2: an unknown-offset sentinel used as a distance
# ---------------------------------------------------------------------------
"""A sentinel is not a distance, so no coordinate may be derived from it."""
"""Refused before the arithmetic, in both directions and both arms.
Reaching the assertion at all is half the test on the negative
sentinel: a ``PanicException`` subclasses ``BaseException``, so it
aborts the call rather than raising something ``pytest.raises`` sees.
"""
assert ==
assert in
"""The value must not come back either — see escape 3."""
assert ==
assert ==
"""A real intronic distance is untouched on both arms.
The intron is genomic 1101..2000, so ``c.90+5`` is 1105 and ``c.91-5``
is 1996. A guard that rejected every non-``None`` offset would pass
every assertion above and fail these.
"""
assert ==
assert ==
assert ==
assert ==
"""The hazard is the offset's magnitude, not its sentinel identity (#1765).
The guard above keys on the two exact sentinel values, which is the right
key for a *marker* and the wrong one for the arithmetic downstream. Stepping
one away from each sentinel reproduced the whole class, measured on this
fixture on the unfixed build::
c_to_g(90, 2**63 - 2) -> ('chr1', 9223372036854776906)
c_to_g(90, -(2**63) + 1) !! pyo3_runtime.PanicException:
attempt to subtract with overflow
(src/reference/transcript.rs:814)
Neither value is a sentinel, so no widening of the sentinel predicate could
reach them. ``Transcript::intronic_to_genomic`` is total and intron-bounded
instead, so both now decline.
"""
#: One step in from each sentinel — a plain number, refused on magnitude.
=
"""No panic, and no coordinate-shaped number either.
Reaching the assertion at all is half the test: a ``PanicException``
subclasses ``BaseException``, so ``pytest.raises(ProjectionError)``
does not catch it and the pre-fix run errors out here rather than
failing an assertion.
"""
"""The intron spans genomic 1101..2000, so 900 is the largest honourable
distance on either arm. An offset past it names no intronic base."""
"""The negative control for the bound above: a guard that refused every
large offset would pass both tests above and fail this one."""
assert ==
assert ==
# ---------------------------------------------------------------------------
# Escape 3: a value handed back that the caller cannot read
# ---------------------------------------------------------------------------
"""What comes back must mean what a caller would take it to mean."""
"""A sentinel returned as an ``int`` is indistinguishable from a distance.
Stated as a property of the *return value* rather than of the argument,
so it still holds if some future conversion path produces a sentinel it
was not handed.
"""
, =
return # refused at entry, which is the stronger outcome
assert not in
"""``downstream=True`` must not be answered as if it were ``False``.
``tx_to_cds`` discards the flag, so before this change the two calls
returned the same triple and a caller asking about ``n.*195`` was told
about ``n.195``. Repairing the conversion is separate work; until it
lands, the honest answer at the boundary is a refusal rather than a
confidently wrong coordinate.
"""
assert ==
assert in
"""The default path keeps working, positionally and by keyword."""
assert ==
assert ==
assert ==
# ---------------------------------------------------------------------------
# The docstring defect riding along
# ---------------------------------------------------------------------------
"""Every numeric entry point names the basis of both sides.
All five previously left at least one side undocumented: ``c_to_g`` and
``c_to_n`` documented neither, and ``c_to_p`` / ``g_to_c`` / ``n_to_c``
documented the input's basis and not the output's. A caller cannot use an
integer coordinate whose basis is not stated, and the ambiguity is exactly
what let these arguments go unvalidated.
"""
= .__doc__
assert is not None, f
# Both the Args and the Returns section must say which basis they are
# on; "1-based" is the vocabulary the rest of this class already uses.
# Assert the delimiters exist before slicing on them. Without this a
# missing ``Returns:`` raises a bare ``IndexError: list index out of
# range`` naming neither the method nor the heading, and — the quieter
# half — a missing ``Raises:`` does not fail at all: the second split
# returns a one-element list, so ``returns_section`` silently widens to
# the whole docstring tail and can be satisfied by text from a section
# this test is not looking at.
assert in , f
=
=
assert in , f
assert in , f