6
6
__all__ = ['Alignment' ]
7
7
8
8
import bisect
9
- from numbers import Real
10
- from typing import Callable , Iterable , List , Optional , Sequence , Tuple , TypeVar , cast , overload
9
+ from typing import Any , Callable , Iterable , Iterator , List , Optional , Sequence , Tuple , TypeVar , Union , cast , overload
11
10
12
- from ._typing import Bounds , Range
11
+ from ._typing import AnyBounds , Bounds , Index , Range
13
12
14
13
15
14
T = TypeVar ('T' )
16
15
U = TypeVar ('U' )
16
+ Real = Union [int , float ]
17
17
CostFn = Callable [[Optional [T ], Optional [U ]], Real ]
18
18
19
19
@@ -108,15 +108,15 @@ def _create(cls, original: List[int], modified: List[int]) -> Alignment:
108
108
result ._modified = modified
109
109
return result
110
110
111
- def __str__ (self ):
111
+ def __str__ (self ) -> str :
112
112
i , j = self ._original [0 ], self ._original [- 1 ]
113
113
k , l = self ._modified [0 ], self ._modified [- 1 ]
114
114
if self ._original == list (range (i , j + 1 )) and self ._modified == list (range (k , l + 1 )):
115
115
return f'[{ i } :{ j } ⇋{ k } :{ l } ]'
116
116
else :
117
117
return '[' + ', ' .join (f'{ i } ⇋{ j } ' for i , j in self ) + ']'
118
118
119
- def __repr__ (self ):
119
+ def __repr__ (self ) -> str :
120
120
i , j = self ._original [0 ], self ._original [- 1 ]
121
121
if self ._original == list (range (i , j + 1 )) and self ._modified == list (range (i , j + 1 )):
122
122
if i == 0 :
@@ -126,17 +126,17 @@ def __repr__(self):
126
126
else :
127
127
return 'Alignment([' + ', ' .join (map (repr , self )) + '])'
128
128
129
- def __eq__ (self , other ) :
129
+ def __eq__ (self , other : Any ) -> bool :
130
130
if isinstance (other , Alignment ):
131
131
return (self ._original , self ._modified ) == (other ._original , other ._modified )
132
132
else :
133
133
return NotImplemented
134
134
135
135
@classmethod
136
- def _parse_args (cls , args : Tuple ) -> Bounds :
136
+ def _parse_bounds (cls , args : Tuple [ AnyBounds , ...] ) -> Bounds :
137
137
l = len (args )
138
138
if l == 0 :
139
- return None , None
139
+ raise TypeError ( 'Not enough arguments' )
140
140
elif l == 1 :
141
141
arg = args [0 ]
142
142
if isinstance (arg , range ):
@@ -154,23 +154,27 @@ def _parse_args(cls, args: Tuple) -> Bounds:
154
154
else :
155
155
raise TypeError ('Too many arguments' )
156
156
157
+ @classmethod
158
+ def _parse_optional_bounds (cls , args : Tuple [AnyBounds , ...]) -> Union [Bounds , Tuple [None , None ]]:
159
+ if len (args ) == 0 :
160
+ return None , None
161
+ else :
162
+ return cls ._parse_bounds (args )
163
+
157
164
@overload
158
165
@classmethod
159
- def identity (cls , length : int ) -> Alignment :
160
- ...
166
+ def identity (cls , __length : int ) -> Alignment : ...
161
167
162
168
@overload
163
169
@classmethod
164
- def identity (cls , start : int , stop : int ) -> Alignment :
165
- ...
170
+ def identity (cls , __start : int , __stop : int ) -> Alignment : ...
166
171
167
172
@overload
168
173
@classmethod
169
- def identity (cls , bounds : Range ) -> Alignment :
170
- ...
174
+ def identity (cls , __bounds : Range ) -> Alignment : ...
171
175
172
176
@classmethod
173
- def identity (cls , * args ) :
177
+ def identity (cls , * args : Union [ int , range , slice , Bounds ]) -> Alignment :
174
178
"""
175
179
Create an identity alignment, which maps all intervals to themselves. You can pass the size of the sequence:
176
180
@@ -188,7 +192,7 @@ def identity(cls, *args):
188
192
Alignment.identity(1, 5)
189
193
"""
190
194
191
- start , stop = cls ._parse_args (args )
195
+ start , stop = cls ._parse_bounds (args )
192
196
values = list (range (start , stop + 1 ))
193
197
return cls ._create (values , values )
194
198
@@ -203,12 +207,12 @@ def _infer_costs(cls, original: Sequence[T], modified: Sequence[U], cost_fn: Cos
203
207
https://en.wikipedia.org/wiki/Wagner%E2%80%93Fischer_algorithm
204
208
"""
205
209
206
- row = [0 ]
210
+ row : List [ Real ] = [0 ]
207
211
for i , m in enumerate (modified ):
208
212
cost = row [i ] + cost_fn (None , m )
209
213
row .append (cost )
210
214
211
- prev = [0 ] * len (row )
215
+ prev : List [ Real ] = [0 ] * len (row )
212
216
213
217
for o in original :
214
218
prev , row = row , prev
@@ -228,7 +232,7 @@ def _infer_matrix(cls, original: Sequence[T], modified: Sequence[U], cost_fn: Co
228
232
The Needleman–Wunsch or Wagner–Fischer algorithm, using the entire matrix to compute the optimal alignment.
229
233
"""
230
234
231
- row = [(0 , - 1 , - 1 )]
235
+ row : List [ Tuple [ Real , int , int ]] = [(0 , - 1 , - 1 )]
232
236
for j , m in enumerate (modified ):
233
237
cost = row [j ][0 ] + cost_fn (None , m )
234
238
row .append ((cost , 0 , j ))
@@ -325,29 +329,31 @@ def infer(cls, original: Sequence[T], modified: Sequence[U], cost_fn: Optional[C
325
329
"""
326
330
327
331
if cost_fn is None :
328
- cost_fn = lambda a , b : int (a != b )
332
+ real_cost_fn : CostFn [T , U ] = lambda a , b : int (a != b )
333
+ else :
334
+ real_cost_fn = cost_fn
329
335
330
336
if len (original ) < len (modified ):
331
- swap = True
332
- swapped_cost_fn = lambda a , b : cost_fn (b , a )
337
+ swapped_cost_fn = lambda a , b : real_cost_fn (b , a )
333
338
result = cls ._infer_recursive (modified , original , swapped_cost_fn )
339
+ return Alignment (result ).inverse ()
334
340
else :
335
- swap = False
336
- result = cls . _infer_recursive ( original , modified , cost_fn )
341
+ result = cls . _infer_recursive ( original , modified , real_cost_fn )
342
+ return Alignment ( result )
337
343
338
- alignment = Alignment (result )
339
- if swap :
340
- return alignment .inverse ()
341
- else :
342
- return alignment
343
-
344
- def __iter__ (self ):
344
+ def __iter__ (self ) -> Iterator [Tuple [int , int ]]:
345
345
return zip (self ._original , self ._modified )
346
346
347
- def __len__ (self ):
347
+ def __len__ (self ) -> int :
348
348
return len (self ._original )
349
349
350
- def __getitem__ (self , index ):
350
+ @overload
351
+ def __getitem__ (self , index : int ) -> Bounds : ...
352
+
353
+ @overload
354
+ def __getitem__ (self , index : slice ) -> Alignment : ...
355
+
356
+ def __getitem__ (self , index : Index ) -> Union [Bounds , Alignment ]:
351
357
"""
352
358
Indexing an alignment returns the nth pair of aligned positions:
353
359
@@ -398,15 +404,15 @@ def _search(self, source: List[int], start: int, stop: int) -> Bounds:
398
404
399
405
return first , last
400
406
401
- def _bounds (self , source : List [int ], target : List [int ], args : Tuple ) -> Bounds :
402
- start , stop = self ._parse_args (args )
403
- if start is None :
407
+ def _bounds (self , source : List [int ], target : List [int ], args : Tuple [ AnyBounds , ...] ) -> Bounds :
408
+ start , stop = self ._parse_optional_bounds (args )
409
+ if start is None or stop is None :
404
410
i , j = 0 , - 1
405
411
else :
406
412
i , j = self ._search (source , start , stop )
407
413
return (target [i ], target [j ])
408
414
409
- def original_bounds (self , * args ) -> Bounds :
415
+ def original_bounds (self , * args : AnyBounds ) -> Bounds :
410
416
"""
411
417
Maps a subrange of the modified sequence to the original sequence. Can be called with either two arguments:
412
418
@@ -430,19 +436,19 @@ def original_bounds(self, *args) -> Bounds:
430
436
431
437
return self ._bounds (self ._modified , self ._original , args )
432
438
433
- def original_range (self , * args ) -> range :
439
+ def original_range (self , * args : AnyBounds ) -> range :
434
440
"""
435
441
Like :meth:`original_bounds`, but returns a :class:`range`.
436
442
"""
437
443
return range (* self .original_bounds (* args ))
438
444
439
- def original_slice (self , * args ) -> slice :
445
+ def original_slice (self , * args : AnyBounds ) -> slice :
440
446
"""
441
447
Like :meth:`original_bounds`, but returns a :class:`slice`.
442
448
"""
443
449
return slice (* self .original_bounds (* args ))
444
450
445
- def modified_bounds (self , * args ) -> Bounds :
451
+ def modified_bounds (self , * args : AnyBounds ) -> Bounds :
446
452
"""
447
453
Maps a subrange of the original sequence to the modified sequence. Can be called with either two arguments:
448
454
@@ -466,19 +472,19 @@ def modified_bounds(self, *args) -> Bounds:
466
472
467
473
return self ._bounds (self ._original , self ._modified , args )
468
474
469
- def modified_range (self , * args ) -> range :
475
+ def modified_range (self , * args : AnyBounds ) -> range :
470
476
"""
471
477
Like :meth:`modified_bounds`, but returns a :class:`range`.
472
478
"""
473
479
return range (* self .modified_bounds (* args ))
474
480
475
- def modified_slice (self , * args ) -> slice :
481
+ def modified_slice (self , * args : AnyBounds ) -> slice :
476
482
"""
477
483
Like :meth:`modified_bounds`, but returns a :class:`range`.
478
484
"""
479
485
return slice (* self .modified_bounds (* args ))
480
486
481
- def slice_by_original (self , * args ) -> Alignment :
487
+ def slice_by_original (self , * args : AnyBounds ) -> Alignment :
482
488
"""
483
489
Slice this alignment by a span of the original sequence.
484
490
@@ -490,14 +496,14 @@ def slice_by_original(self, *args) -> Alignment:
490
496
The slice of this alignment that corresponds with the given span of the original sequence.
491
497
"""
492
498
493
- start , stop = self ._parse_args (args )
499
+ start , stop = self ._parse_bounds (args )
494
500
first , last = self ._search (self ._original , start , stop )
495
501
original = self ._original [first :last + 1 ]
496
502
original = [min (max (i , start ), stop ) for i in original ]
497
503
modified = self ._modified [first :last + 1 ]
498
504
return self ._create (original , modified )
499
505
500
- def slice_by_modified (self , * args ) -> Alignment :
506
+ def slice_by_modified (self , * args : AnyBounds ) -> Alignment :
501
507
"""
502
508
Slice this alignment by a span of the modified sequence.
503
509
@@ -509,14 +515,14 @@ def slice_by_modified(self, *args) -> Alignment:
509
515
The slice of this alignment that corresponds with the given span of the modified sequence.
510
516
"""
511
517
512
- start , stop = self ._parse_args (args )
518
+ start , stop = self ._parse_bounds (args )
513
519
first , last = self ._search (self ._modified , start , stop )
514
520
original = self ._original [first :last + 1 ]
515
521
modified = self ._modified [first :last + 1 ]
516
522
modified = [min (max (i , start ), stop ) for i in modified ]
517
523
return self ._create (original , modified )
518
524
519
- def __add__ (self , other ) :
525
+ def __add__ (self , other : Any ) -> Alignment :
520
526
"""
521
527
Concatenate two alignments.
522
528
"""
0 commit comments