@@ -62,13 +62,14 @@ class ReportFile(object):
62
62
"_lines" ,
63
63
"_ignore" ,
64
64
"_totals" ,
65
+ "__present_sessions" ,
65
66
]
66
67
67
68
def __init__ (
68
69
self ,
69
- name ,
70
- totals = None ,
71
- lines = None ,
70
+ name : str ,
71
+ totals : ReportTotals | list | None = None ,
72
+ lines : list [ None | str | ReportLine ] | str | None = None ,
72
73
ignore = None ,
73
74
):
74
75
"""
@@ -82,26 +83,69 @@ def __init__(
82
83
{eof:N, lines:[1,10]}
83
84
"""
84
85
self .name = name
86
+ self ._details : dict [str , Any ] = {}
87
+
85
88
# lines = [<details dict()>, <Line #1>, ....]
89
+ self ._lines : list [None | str | ReportLine ] = []
86
90
if lines :
87
91
if isinstance (lines , list ):
88
- self ._details = None
89
92
self ._lines = lines
90
93
91
94
else :
92
95
lines = lines .splitlines ()
93
- self ._details = orjson .loads (lines .pop (0 ) or "null" )
96
+ if detailsline := lines .pop (0 ):
97
+ self ._details = orjson .loads (detailsline ) or {}
94
98
self ._lines = lines
95
- else :
96
- self ._details = {}
97
- self ._lines = []
98
99
99
100
self ._ignore = _ignore_to_func (ignore ) if ignore else None
100
101
102
+ # The `_totals` and `__present_sessions` fields are cached values for the
103
+ # `totals` and `_present_sessions` properties respectively.
104
+ # The values are loaded at initialization time, or calculated from line data on-demand.
105
+ # All mutating methods (like `append`, `merge`, etc) will either re-calculate these values
106
+ # directly, or clear them so the `@property` accessors re-calculate them when needed.
107
+
108
+ self ._totals : ReportTotals | None = None
101
109
if isinstance (totals , ReportTotals ):
102
110
self ._totals = totals
103
- else :
104
- self ._totals = ReportTotals (* totals ) if totals else None
111
+ elif totals :
112
+ self ._totals = ReportTotals (* totals )
113
+
114
+ self .__present_sessions : set [int ] | None = None
115
+ if present_sessions := self ._details .get ("present_sessions" ):
116
+ self .__present_sessions = set (present_sessions )
117
+
118
+ def _invalidate_caches (self ):
119
+ self ._totals = None
120
+ self .__present_sessions = None
121
+
122
+ @property
123
+ def _present_sessions (self ):
124
+ if self .__present_sessions is None :
125
+ self .__present_sessions = set ()
126
+ for _ , line in self .lines :
127
+ self .__present_sessions .update (int (s .id ) for s in line .sessions )
128
+ return self .__present_sessions
129
+
130
+ @property
131
+ def details (self ):
132
+ self ._details ["present_sessions" ] = sorted (self ._present_sessions )
133
+ return self ._details
134
+
135
+ @property
136
+ def totals (self ):
137
+ if not self ._totals :
138
+ self ._totals = self ._process_totals ()
139
+ return self ._totals
140
+
141
+ def _process_totals (self ) -> ReportTotals :
142
+ return get_line_totals (line for _ln , line in self .lines )
143
+
144
+ def _encode (self ) -> str :
145
+ details = orjson .dumps (self .details , option = orjson_option )
146
+ return (
147
+ details + b"\n " + b"\n " .join (_dumps_not_none (line ) for line in self ._lines )
148
+ ).decode ()
105
149
106
150
def __repr__ (self ):
107
151
try :
@@ -176,6 +220,7 @@ def __setitem__(self, ln, line):
176
220
self ._lines .extend ([EMPTY ] * (ln - length ))
177
221
178
222
self ._lines [ln - 1 ] = line
223
+ self ._invalidate_caches ()
179
224
return
180
225
181
226
def __delitem__ (self , ln : int ):
@@ -190,11 +235,12 @@ def __delitem__(self, ln: int):
190
235
self ._lines .extend ([EMPTY ] * (ln - length ))
191
236
192
237
self ._lines [ln - 1 ] = EMPTY
238
+ self ._invalidate_caches ()
193
239
return
194
240
195
241
def __len__ (self ):
196
242
"""Returns count(number of lines with coverage data)"""
197
- return len ([ _f for _f in self ._lines if _f ] )
243
+ return sum ( 1 for _f in self ._lines if _f )
198
244
199
245
@property
200
246
def eof (self ):
@@ -268,6 +314,8 @@ def append(self, ln, line):
268
314
self ._lines [ln - 1 ] = merge_line (_line , line )
269
315
else :
270
316
self ._lines [ln - 1 ] = line
317
+
318
+ self ._invalidate_caches ()
271
319
return True
272
320
273
321
def merge (self , other_file , joined = True ):
@@ -316,28 +364,9 @@ def merge(self, other_file, joined=True):
316
364
for before , after in zip_longest (self , other_file )
317
365
]
318
366
319
- self ._totals = None
367
+ self ._invalidate_caches ()
320
368
return True
321
369
322
- @property
323
- def details (self ):
324
- return self ._details
325
-
326
- def _encode (self ) -> str :
327
- details = orjson .dumps (self .details , option = orjson_option )
328
- return (
329
- details + b"\n " + b"\n " .join (_dumps_not_none (line ) for line in self ._lines )
330
- ).decode ()
331
-
332
- @property
333
- def totals (self ):
334
- if not self ._totals :
335
- self ._totals = self ._process_totals ()
336
- return self ._totals
337
-
338
- def _process_totals (self ) -> ReportTotals :
339
- return get_line_totals (line for _ln , line in self .lines )
340
-
341
370
def does_diff_adjust_tracked_lines (self , diff , future_file ):
342
371
for segment in diff ["segments" ]:
343
372
# loop through each line
@@ -385,10 +414,11 @@ def shift_lines_by_diff(self, diff, forward=True) -> None:
385
414
except (ValueError , KeyError , TypeError , IndexError ):
386
415
log .exception ("Failed to shift lines by diff" )
387
416
pass
417
+ self ._invalidate_caches ()
388
418
389
419
@classmethod
390
420
def line_without_labels (
391
- cls , line , session_ids_to_delete : list [int ], label_ids_to_delete : list [int ]
421
+ cls , line , session_ids_to_delete : set [int ], label_ids_to_delete : set [int ]
392
422
):
393
423
new_datapoints = (
394
424
[
@@ -401,7 +431,7 @@ def line_without_labels(
401
431
else None
402
432
)
403
433
remaining_session_ids = set (dp .sessionid for dp in new_datapoints )
404
- removed_session_ids = set ( session_ids_to_delete ) - remaining_session_ids
434
+ removed_session_ids = session_ids_to_delete - remaining_session_ids
405
435
if set (s .id for s in line .sessions ) & removed_session_ids :
406
436
new_sessions = [s for s in line .sessions if s .id not in removed_session_ids ]
407
437
else :
@@ -424,6 +454,38 @@ def line_without_labels(
424
454
sessions = new_sessions ,
425
455
)
426
456
457
+ def delete_labels (
458
+ self ,
459
+ session_ids_to_delete : list [int ] | set [int ],
460
+ label_ids_to_delete : list [int ] | set [int ],
461
+ ):
462
+ """
463
+ Given a list of session_ids and label_ids to delete, remove all datapoints
464
+ that belong to at least 1 session_ids to delete and include at least 1 of the label_ids to be removed.
465
+ """
466
+ session_ids_to_delete = set (session_ids_to_delete )
467
+ label_ids_to_delete = set (label_ids_to_delete )
468
+ for index , line in self .lines :
469
+ if line .datapoints is not None :
470
+ if any (
471
+ (
472
+ dp .sessionid in session_ids_to_delete
473
+ and label_id in label_ids_to_delete
474
+ )
475
+ for dp in line .datapoints
476
+ for label_id in dp .label_ids
477
+ ):
478
+ # Line fits change requirements
479
+ new_line = self .line_without_labels (
480
+ line , session_ids_to_delete , label_ids_to_delete
481
+ )
482
+ if new_line == EMPTY :
483
+ del self [index ]
484
+ else :
485
+ self [index ] = new_line
486
+
487
+ self ._invalidate_caches ()
488
+
427
489
@classmethod
428
490
def line_without_multiple_sessions (
429
491
cls , line : ReportLine , session_ids_to_delete : set [int ]
@@ -446,6 +508,30 @@ def line_without_multiple_sessions(
446
508
datapoints = new_datapoints ,
447
509
)
448
510
511
+ def delete_multiple_sessions (self , session_ids_to_delete : set [int ]):
512
+ current_sessions = self ._present_sessions
513
+ new_sessions = current_sessions .difference (session_ids_to_delete )
514
+ if current_sessions == new_sessions :
515
+ return # nothing to do
516
+
517
+ self ._invalidate_caches ()
518
+
519
+ if not new_sessions :
520
+ self ._lines = [] # no remaining sessions means no line data
521
+ return
522
+
523
+ for index , line in self .lines :
524
+ if any (s .id in session_ids_to_delete for s in line .sessions ):
525
+ new_line = self .line_without_multiple_sessions (
526
+ line , session_ids_to_delete
527
+ )
528
+ if new_line == EMPTY :
529
+ del self [index ]
530
+ else :
531
+ self [index ] = new_line
532
+
533
+ self .__present_sessions = current_sessions
534
+
449
535
450
536
def chunks_from_storage_contains_header (chunks : str ) -> bool :
451
537
try :
0 commit comments