11"""Basic result reporters."""
22
33import abc
4+ import contextlib
45import csv
56import json
67import typing
1819T_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+
2134class 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:
253273class _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 )
0 commit comments