Skip to content

Commit e186b07

Browse files
committed
chore: add callback/multiplexing reporters, cleanup reporter annotations
Additionally, split streaming reporters out as a subclass derivative. Signed-off-by: Brian Harring <ferringb@gmail.com>
1 parent 48dbd5f commit e186b07

3 files changed

Lines changed: 129 additions & 34 deletions

File tree

src/pkgcheck/objects.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ def _find_classes(module, matching_cls, skip=()): # pragma: no cover
4242
and issubclass(cls, matching_cls)
4343
and cls.__name__[0] != "_"
4444
and cls not in skip
45+
and not inspect.isabstract(cls)
4546
):
4647
yield cls
4748

@@ -197,4 +198,4 @@ def default(self):
197198

198199
KEYWORDS = _KeywordsLazyDict("KEYWORDS", ("checks", "results.Result"))
199200
CHECKS = _ChecksLazyDict("CHECKS", ("checks", "checks.Check"))
200-
REPORTERS = _LazyDict("REPORTERS", ("reporters", "reporters.Reporter"))
201+
REPORTERS = _LazyDict("REPORTERS", ("reporters", "reporters.StreamReporter"))

src/pkgcheck/reporters.py

Lines changed: 89 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"""Basic result reporters."""
22

33
import abc
4+
import contextlib
45
import csv
56
import json
67
import typing
@@ -18,43 +19,41 @@
1819
T_report_func: typing.TypeAlias = typing.Callable[[Result], None]
1920

2021

22+
class ReportFuncShim:
23+
"""Compatibility shim while migrating endusers away from using .report()"""
24+
25+
__slots__ = ("report",)
26+
27+
def __init__(self, report: T_report_func) -> None:
28+
self.report = report
29+
30+
def __call__(self, result: Result) -> None:
31+
self.report(result)
32+
33+
2134
class Reporter(abc.ABC, immutable.Simple):
2235
"""Generic result reporter."""
2336

24-
__slots__ = ("report", "_current_generator", "out")
37+
__slots__ = ("report", "_current_generator")
2538

2639
priority: int # used by the config system
2740
_current_generator: T_process_report | None
2841

29-
def __init__(self, out: snakeoil_Formatter):
30-
"""Initialize
31-
32-
:type out: L{snakeoil.formatters.Formatter}
33-
"""
34-
self.out = out
42+
def __init__(self) -> None:
3543
self._current_generator = None
3644

3745
@immutable.Simple.__allow_mutation_wrapper__
38-
def __enter__(self) -> T_report_func:
39-
self.out.flush()
46+
def __enter__(self) -> ReportFuncShim:
4047
self._current_generator = self._consume_reports_generator()
4148
# start the generator
4249
next(self._current_generator)
43-
44-
# make a class so there's no intermediate frame relaying for __call__. Optimization.
45-
class reporter:
46-
__slots__ = ()
47-
report: T_report_func = staticmethod(self._current_generator.send)
48-
__call__: T_report_func = staticmethod(self._current_generator.send)
49-
50-
return reporter()
50+
return ReportFuncShim(self._current_generator.send)
5151

5252
@immutable.Simple.__allow_mutation_wrapper__
53-
def __exit__(self, *exc_info):
53+
def __exit__(self, typ, value, traceback):
5454
# shut down the generator so it can do any finalization
5555
self._current_generator.close() # pyright: ignore[reportOptionalMemberAccess]
5656
self._current_generator = None
57-
self.out.flush()
5857

5958
@abc.abstractmethod
6059
def _consume_reports_generator(self) -> T_process_report:
@@ -67,7 +66,28 @@ def _consume_reports_generator(self) -> T_process_report:
6766
"""
6867

6968

70-
class StrReporter(Reporter):
69+
class StreamReporter(Reporter):
70+
__slots__ = ("out",)
71+
out: snakeoil_Formatter
72+
73+
def __init__(self, out: snakeoil_Formatter):
74+
"""Initialize
75+
76+
:type out: L{snakeoil.formatters.Formatter}
77+
"""
78+
super().__init__()
79+
self.out = out
80+
81+
def __enter__(self) -> ReportFuncShim:
82+
self.out.flush()
83+
return super().__enter__()
84+
85+
def __exit__(self, typ, value, traceback) -> None:
86+
super().__exit__(typ, value, traceback)
87+
self.out.flush()
88+
89+
90+
class StrReporter(StreamReporter):
7191
"""Simple string reporter, pkgcheck-0.1 behaviour.
7292
7393
Example::
@@ -96,7 +116,7 @@ def _consume_reports_generator(self) -> T_process_report:
96116
self.out.stream.flush()
97117

98118

99-
class FancyReporter(Reporter):
119+
class FancyReporter(StreamReporter):
100120
"""Colored output grouped by result scope.
101121
102122
Example::
@@ -140,7 +160,7 @@ def _consume_reports_generator(self) -> T_process_report:
140160
self.out.stream.flush()
141161

142162

143-
class JsonReporter(Reporter):
163+
class JsonReporter(StreamReporter):
144164
"""Feed of newline-delimited JSON records.
145165
146166
Note that the format is newline-delimited JSON with each line being related
@@ -175,7 +195,7 @@ def _consume_reports_generator(self) -> T_process_report:
175195
self.out.stream.flush()
176196

177197

178-
class XmlReporter(Reporter):
198+
class XmlReporter(StreamReporter):
179199
"""Feed of newline-delimited XML reports."""
180200

181201
__slots__ = ()
@@ -185,9 +205,9 @@ def __enter__(self):
185205
self.out.write("<checks>")
186206
return super().__enter__()
187207

188-
def __exit__(self, *exc_info):
208+
def __exit__(self, typ, value, traceback):
189209
# finalize/close the generator, *then* close the xml.
190-
ret = super().__exit__(*exc_info)
210+
ret = super().__exit__(typ, value, traceback)
191211
self.out.write("</checks>")
192212
return ret
193213

@@ -221,7 +241,7 @@ def _consume_reports_generator(self) -> T_process_report:
221241
self.out.write(scope_map.get(result.scope, result_template) % d)
222242

223243

224-
class CsvReporter(Reporter):
244+
class CsvReporter(StreamReporter):
225245
"""Comma-separated value reporter, convenient for shell processing.
226246
227247
Example::
@@ -253,17 +273,17 @@ def _consume_reports_generator(self) -> T_process_report:
253273
class _ResultFormatter(Formatter):
254274
"""Custom string formatter that collapses unmatched variables."""
255275

256-
def get_value(self, key, args, kwds):
276+
def get_value(self, key, args, kwargs):
257277
"""Retrieve a given field value, an empty string is returned for unmatched fields."""
258278
if isinstance(key, str):
259279
try:
260-
return kwds[key]
280+
return kwargs[key]
261281
except KeyError:
262282
return ""
263283
raise base.PkgcheckUserException("FormatReporter: integer indexes are not supported")
264284

265285

266-
class FormatReporter(Reporter):
286+
class FormatReporter(StreamReporter):
267287
"""Custom format string reporter.
268288
269289
This formatter uses custom format string passed using the ``--format``
@@ -296,23 +316,25 @@ class DeserializationError(Exception):
296316
"""Exception occurred while deserializing a data stream."""
297317

298318

299-
class JsonStream(Reporter):
319+
class JsonStream(StreamReporter):
300320
"""Generate a stream of result objects serialized in JSON."""
301321

302322
__slots__ = ()
303323
priority = -1001
304324

305325
@staticmethod
306-
def to_json(obj):
326+
def to_json(obj) -> str | dict[str, str]:
307327
"""Serialize results and other objects to JSON."""
308328
if isinstance(obj, Result):
309329
d = {"__class__": obj.__class__.__name__}
310330
d.update(obj._attrs)
311331
return d
332+
# TODO: remove this pathway via using JSONDecoder with registered decoders.
333+
# tests for to_json force a cast, so remove that also.
312334
return str(obj)
313335

314336
@staticmethod
315-
def from_iter(iterable):
337+
def from_iter(iterable) -> typing.Generator[Result, None, None]:
316338
"""Deserialize results from a given iterable."""
317339
# avoid circular import issues
318340
from . import objects
@@ -332,7 +354,7 @@ def _consume_reports_generator(self) -> T_process_report:
332354
self.out.write(json.dumps(result, default=self.to_json))
333355

334356

335-
class FlycheckReporter(Reporter):
357+
class FlycheckReporter(StreamReporter):
336358
"""Simple line reporter done for easier integration with flycheck [#]_ .
337359
338360
.. [#] https://github.com/flycheck/flycheck
@@ -353,3 +375,37 @@ def _consume_reports_generator(self) -> T_process_report:
353375
else:
354376
lineno = getattr(result, "lineno", 0)
355377
self.out.write(f"{file}:{lineno}:{getattr(result, 'level')}:{message}")
378+
379+
380+
class CallbackReporter(Reporter):
381+
"""Reporter that calls back for every result"""
382+
383+
__slots__ = ("callbacks",)
384+
callbacks: list[T_report_func]
385+
386+
def __init__(self, *callbacks: T_report_func) -> None:
387+
self.callbacks = list(callbacks)
388+
389+
def _consume_reports_generator(self) -> T_process_report:
390+
while True:
391+
result = yield
392+
for callback in self.callbacks:
393+
callback(result)
394+
395+
396+
class MultiplexingReporter(Reporter):
397+
"""Reporter that multiplexes results to multiple Reporters"""
398+
399+
__slots__ = ("reporters",)
400+
reporters: list[Reporter]
401+
402+
def __init__(self, *args: Reporter) -> None:
403+
self.reporters = list(args)
404+
405+
def _consume_reports_generator(self) -> T_process_report:
406+
with contextlib.ExitStack() as context:
407+
callbacks = [context.enter_context(reporter) for reporter in self.reporters]
408+
while True:
409+
result = yield
410+
for report_func in callbacks:
411+
report_func(result)

tests/test_reporters.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,3 +217,41 @@ class TestFlycheckReporter(BaseReporter):
217217
foo-0.ebuild:7:warning:UnquotedVariable: unquoted variable D
218218
"""
219219
)
220+
221+
222+
class TestCallbackReporter:
223+
results = BaseReporter.results
224+
225+
def test_it(self):
226+
collected = [], []
227+
with reporters.CallbackReporter(*[l.append for l in collected]) as report:
228+
for result in self.results:
229+
report(result)
230+
assert list(self.results), list(self.results) == collected
231+
232+
233+
class TestMultiplexingReporter:
234+
results = BaseReporter.results
235+
236+
def test_it(self):
237+
collected = []
238+
context_checks = []
239+
context_check_results = []
240+
241+
class context_verifier:
242+
def __enter__(self):
243+
context_checks.append(True)
244+
return reporters.ReportFuncShim(context_check_results.append)
245+
246+
def __exit__(self, typ, value, traceback):
247+
context_checks.append(True)
248+
249+
with reporters.MultiplexingReporter(
250+
context_verifier(), reporters.CallbackReporter(collected.append)
251+
) as report:
252+
for result in self.results:
253+
report(result)
254+
255+
assert self.results == tuple(collected)
256+
assert [True, True] == context_checks
257+
assert self.results == tuple(context_check_results)

0 commit comments

Comments
 (0)