Skip to content
This repository was archived by the owner on May 5, 2025. It is now read-only.

Commit 3b3bf9c

Browse files
committed
Merge EditableReport into Report
The primary usecase for `EditableReport` was to always keep a list of "partially parsed" `ReportFile`s around, as well as making sure that `totals` (and to some extent `file_totals`) are uptodate. This now moves all the methods from `EditableReport` onto `Report`, and introduces a new method to iterate over "partially parsed" `ReportFile`s, making sure those are being kept around as well.
1 parent 676f04d commit 3b3bf9c

File tree

3 files changed

+114
-145
lines changed

3 files changed

+114
-145
lines changed

shared/reports/editable.py

Lines changed: 3 additions & 113 deletions
Original file line numberDiff line numberDiff line change
@@ -1,115 +1,5 @@
1-
import dataclasses
2-
import logging
3-
4-
import sentry_sdk
5-
61
from shared.reports.resources import Report, ReportFile
72

8-
log = logging.getLogger(__name__)
9-
10-
EditableReportFile = ReportFile # re-export
11-
12-
13-
class EditableReport(Report):
14-
file_class = EditableReportFile
15-
16-
def __init__(self, *args, **kwargs):
17-
super().__init__(*args, **kwargs)
18-
self.turn_chunks_into_reports()
19-
20-
def merge(self, new_report, joined=True):
21-
super().merge(new_report, joined)
22-
for file in self:
23-
if isinstance(file, ReportFile):
24-
self._chunks[self._files.get(file.name).file_index] = file
25-
26-
def turn_chunks_into_reports(self):
27-
filename_mapping = {
28-
file_summary.file_index: filename
29-
for filename, file_summary in self._files.items()
30-
}
31-
for chunk_index in range(len(self._chunks)):
32-
filename = filename_mapping.get(chunk_index)
33-
file_summary = self._files.get(filename)
34-
chunk = self._chunks[chunk_index]
35-
if chunk is not None and file_summary is not None:
36-
if isinstance(chunk, ReportFile):
37-
chunk = chunk._lines
38-
report_file = ReportFile(
39-
name=filename,
40-
totals=file_summary.file_totals,
41-
lines=chunk,
42-
)
43-
self._chunks[chunk_index] = report_file
44-
else:
45-
self._chunks[chunk_index] = None
46-
47-
def delete_labels(self, sessionids, labels_to_delete):
48-
self._totals = None
49-
for file in self._chunks:
50-
if file is not None:
51-
file.delete_labels(sessionids, labels_to_delete)
52-
if file:
53-
self._files[file.name] = dataclasses.replace(
54-
self._files[file.name],
55-
file_totals=file.totals,
56-
)
57-
else:
58-
del self[file.name]
59-
return sessionids
60-
61-
def delete_multiple_sessions(self, session_ids_to_delete: list[int] | set[int]):
62-
session_ids_to_delete = set(session_ids_to_delete)
63-
self._totals = None
64-
for sessionid in session_ids_to_delete:
65-
self.sessions.pop(sessionid)
66-
67-
for file in self._chunks:
68-
if file is not None:
69-
file.delete_multiple_sessions(session_ids_to_delete)
70-
if file:
71-
self._files[file.name] = dataclasses.replace(
72-
self._files[file.name],
73-
file_totals=file.totals,
74-
)
75-
else:
76-
del self[file.name]
77-
78-
@sentry_sdk.trace
79-
def change_sessionid(self, old_id: int, new_id: int):
80-
"""
81-
This changes the session with `old_id` to have `new_id` instead.
82-
It patches up all the references to that session across all files and line records.
83-
84-
In particular, it changes the id in all the `LineSession`s and `CoverageDatapoint`s,
85-
and does the equivalent of `calculate_present_sessions`.
86-
"""
87-
session = self.sessions[new_id] = self.sessions.pop(old_id)
88-
session.id = new_id
89-
90-
report_file: EditableReportFile
91-
for report_file in self._chunks:
92-
if report_file is None:
93-
continue
94-
95-
all_sessions = set()
96-
97-
for idx, _line in enumerate(report_file._lines):
98-
if not _line:
99-
continue
100-
101-
# this turns the line into an actual `ReportLine`
102-
line = report_file._lines[idx] = report_file._line(_line)
103-
104-
for session in line.sessions:
105-
if session.id == old_id:
106-
session.id = new_id
107-
all_sessions.add(session.id)
108-
109-
if line.datapoints:
110-
for point in line.datapoints:
111-
if point.sessionid == old_id:
112-
point.sessionid = new_id
113-
114-
report_file._invalidate_caches()
115-
report_file.__present_sessions = all_sessions
3+
# re-export to avoid having to patch the whole world:
4+
EditableReportFile = ReportFile
5+
EditableReport = Report

shared/reports/resources.py

Lines changed: 110 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from fractions import Fraction
66
from itertools import filterfalse, zip_longest
77
from types import GeneratorType
8-
from typing import Any, cast
8+
from typing import Any, Generator, cast
99

1010
import orjson
1111
import sentry_sdk
@@ -599,7 +599,6 @@ def parse_chunks(chunks: str) -> tuple[list[str], ReportHeader]:
599599

600600

601601
class Report(object):
602-
file_class = ReportFile
603602
_files: dict[str, ReportFileSummary]
604603
_header: ReportHeader
605604

@@ -618,22 +617,57 @@ def __init__(
618617
self.sessions = get_sessions(sessions) if sessions else {}
619618

620619
# ["<json>", ...]
620+
self._chunks: list[str | ReportFile]
621621
self._chunks, self._header = (
622622
parse_chunks(chunks)
623623
if chunks and isinstance(chunks, str)
624624
else (chunks or [], ReportHeader())
625625
)
626626

627627
# <ReportTotals>
628+
self._totals: ReportTotals | None = None
628629
if isinstance(totals, ReportTotals):
629630
self._totals = totals
630631
elif totals:
631632
self._totals = ReportTotals(*migrate_totals(totals))
632-
else:
633-
self._totals = None
634633

635634
self.diff_totals = diff_totals
636635

636+
def _invalidate_caches(self):
637+
self._totals = None
638+
639+
@property
640+
def totals(self):
641+
if not self._totals:
642+
self._totals = self._process_totals()
643+
return self._totals
644+
645+
def _process_totals(self):
646+
"""Runs through the file network to aggregate totals
647+
returns <ReportTotals>
648+
"""
649+
650+
def _iter_totals():
651+
for filename, data in self._files.items():
652+
if data.file_totals is None:
653+
yield self.get(filename).totals
654+
else:
655+
yield data.file_totals
656+
657+
totals = agg_totals(_iter_totals())
658+
totals.sessions = len(self.sessions)
659+
return totals
660+
661+
def _iter_parsed_files(self) -> Generator[ReportFile, None, None]:
662+
for name, summary in self._files.items():
663+
idx = summary.file_index
664+
file = self._chunks[idx]
665+
if not isinstance(file, ReportFile):
666+
file = self._chunks[idx] = ReportFile(
667+
name=name, totals=summary.file_totals, lines=file
668+
)
669+
yield file
670+
637671
@property
638672
def header(self) -> ReportHeader:
639673
return self._header
@@ -796,7 +830,7 @@ def get(self, filename, _else=None, bind=False):
796830
lines = None
797831
if isinstance(lines, ReportFile):
798832
return lines
799-
report_file = self.file_class(
833+
report_file = ReportFile(
800834
name=filename,
801835
totals=_file.file_totals,
802836
lines=lines,
@@ -866,29 +900,6 @@ def get_file_totals(self, path: str) -> ReportTotals | None:
866900
else:
867901
return ReportTotals(*totals)
868902

869-
@property
870-
def totals(self):
871-
if not self._totals:
872-
# reprocess totals
873-
self._totals = self._process_totals()
874-
return self._totals
875-
876-
def _process_totals(self):
877-
"""Runs through the file network to aggregate totals
878-
returns <ReportTotals>
879-
"""
880-
881-
def _iter_totals():
882-
for filename, data in self._files.items():
883-
if data.file_totals is None:
884-
yield self.get(filename).totals
885-
else:
886-
yield data.file_totals
887-
888-
totals = agg_totals(_iter_totals())
889-
totals.sessions = len(self.sessions)
890-
return totals
891-
892903
def next_session_number(self):
893904
start_number = len(self.sessions)
894905
while start_number in self.sessions or str(start_number) in self.sessions:
@@ -921,7 +932,7 @@ def __iter__(self):
921932
if isinstance(report, ReportFile):
922933
yield report
923934
else:
924-
yield self.file_class(
935+
yield ReportFile(
925936
name=filename,
926937
totals=_file.file_totals,
927938
lines=report,
@@ -1239,6 +1250,76 @@ def _passes_integrity_analysis(self):
12391250
return False
12401251
return True
12411252

1253+
def delete_labels(
1254+
self, sessionids: list[int] | set[int], labels_to_delete: list[int] | set[int]
1255+
):
1256+
for file in self._iter_parsed_files():
1257+
file.delete_labels(sessionids, labels_to_delete)
1258+
if file:
1259+
self._files[file.name] = dataclasses.replace(
1260+
self._files[file.name],
1261+
file_totals=file.totals,
1262+
)
1263+
else:
1264+
del self[file.name]
1265+
1266+
self._invalidate_caches()
1267+
return sessionids
1268+
1269+
def delete_multiple_sessions(self, session_ids_to_delete: list[int] | set[int]):
1270+
session_ids_to_delete = set(session_ids_to_delete)
1271+
for sessionid in session_ids_to_delete:
1272+
self.sessions.pop(sessionid)
1273+
1274+
for file in self._iter_parsed_files():
1275+
file.delete_multiple_sessions(session_ids_to_delete)
1276+
if file:
1277+
self._files[file.name] = dataclasses.replace(
1278+
self._files[file.name],
1279+
file_totals=file.totals,
1280+
)
1281+
else:
1282+
del self[file.name]
1283+
1284+
self._invalidate_caches()
1285+
1286+
@sentry_sdk.trace
1287+
def change_sessionid(self, old_id: int, new_id: int):
1288+
"""
1289+
This changes the session with `old_id` to have `new_id` instead.
1290+
It patches up all the references to that session across all files and line records.
1291+
1292+
In particular, it changes the id in all the `LineSession`s and `CoverageDatapoint`s,
1293+
and does the equivalent of `calculate_present_sessions`.
1294+
"""
1295+
session = self.sessions[new_id] = self.sessions.pop(old_id)
1296+
session.id = new_id
1297+
1298+
for file in self._iter_parsed_files():
1299+
all_sessions = set()
1300+
1301+
for idx, _line in enumerate(file._lines):
1302+
if not _line:
1303+
continue
1304+
1305+
# this turns the line into an actual `ReportLine`
1306+
line = file._lines[idx] = file._line(_line)
1307+
1308+
for session in line.sessions:
1309+
if session.id == old_id:
1310+
session.id = new_id
1311+
all_sessions.add(session.id)
1312+
1313+
if line.datapoints:
1314+
for point in line.datapoints:
1315+
if point.sessionid == old_id:
1316+
point.sessionid = new_id
1317+
1318+
file._invalidate_caches()
1319+
file.__present_sessions = all_sessions
1320+
1321+
self._invalidate_caches()
1322+
12421323

12431324
def _ignore_to_func(ignore):
12441325
"""Returns a function to determine whether a a line should be saved to the ReportFile

tests/benchmarks/test_report.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
import zstandard as zstd
44

55
from shared.reports.carryforward import generate_carryforward_report
6-
from shared.reports.editable import EditableReport
76
from shared.reports.readonly import ReadOnlyReport
87
from shared.reports.resources import Report
98
from shared.torngit.base import TorngitBaseAdapter
@@ -21,10 +20,9 @@ def read_fixture(name: str) -> bytes:
2120
pytest.param(Report, False, id="Report"),
2221
pytest.param(ReadOnlyReport, False, id="ReadOnlyReport"),
2322
pytest.param(ReadOnlyReport, True, id="Rust ReadOnlyReport"),
24-
pytest.param(EditableReport, False, id="EditableReport"),
2523
]
2624

27-
EDITABLE_VARIANTS = [Report, EditableReport]
25+
EDITABLE_VARIANTS = [Report]
2826

2927

3028
def init_mocks(mocker, should_load_rust) -> tuple[bytes, bytes]:

0 commit comments

Comments
 (0)