Source code for simplebench.reporters.json.reporter.reporter

"""Reporter for benchmark results using JSON files."""
from __future__ import annotations

import json
from argparse import Namespace
from io import StringIO
from pathlib import Path
from typing import TYPE_CHECKING, Any, ClassVar, TypeAlias

from simplebench.enums import Section
from simplebench.exceptions import SimpleBenchTypeError
from simplebench.reporters.log.report_log_metadata import ReportLogMetadata
from simplebench.reporters.protocols.reporter_callback import ReporterCallback
from simplebench.reporters.reporter import Reporter, ReporterOptions
from simplebench.type_proxies import is_case
from simplebench.utils import get_machine_info
from simplebench.validators import validate_type

from .config import JSONConfig
from .exceptions import _JSONReporterErrorTag
from .options import JSONOptions

Options: TypeAlias = JSONOptions


if TYPE_CHECKING:
    from simplebench.case import Case
    from simplebench.reporters.choice.choice import Choice
    from simplebench.session import Session


[docs] class JSONReporter(Reporter): """Class for outputting benchmark results to JSON files. It supports reporting statistics for various sections, either separately or together, to the filesystem, via a callback function, or to the console in JSON format. The JSON files are tagged with metadata comments including the case title, description, and units for clarity. **Defined command-line flags:** * ``--json: {filesystem, console, callback}`` (default=filesystem) Outputs statistical results to JSON. * ``--json-data: {filesystem, console, callback}`` (default=filesystem) Outputs results to JSON with full data. **Example usage:** .. code-block:: none program.py --json # Outputs results to JSON files in the filesystem (default). program.py --json filesystem # Outputs results to JSON files in the filesystem. program.py --json console # Outputs results to the console in JSON format. program.py --json callback # Outputs results via a callback function in JSON format. program.py --json filesystem console # Outputs results to both JSON files and the console. :ivar name: The unique identifying name of the reporter. :vartype name: str :ivar description: A brief description of the reporter. :vartype description: str :ivar choices: A collection of :class:`~simplebench.reporters.choices.Choices` instances defining the reporter instance, CLI flags, :class:`~simplebench.reporters.choice.Choice` name, supported :class:`~simplebench.enums.Section` objects, supported output :class:`~simplebench.enums.Target` objects, and supported output :class:`~simplebench.enums.Format` objects for the reporter. :vartype choices: ~simplebench.reporters.choices.Choices :ivar targets: The supported output targets for the reporter. :vartype targets: set[~simplebench.enums.Target] :ivar formats: The supported output formats for the reporter. :vartype formats: set[~simplebench.enums.Format] """ _OPTIONS_TYPE: ClassVar[type[JSONOptions]] = JSONOptions # type: ignore[reportIncompatibleVariableOveride] """:ivar: The type of :class:`~.ReporterOptions` used by the :class:`~.JSONReporter`. :vartype: ~typing.ClassVar[type[~.JSONOptions]] """ _OPTIONS_KWARGS: ClassVar[dict[str, Any]] = {'full_data': False} """:ivar: The default keyword arguments for the :class:`~.JSONReporter` options. .. code-block:: python {"full_data": False} :vartype: ~typing.ClassVar[dict[str, ~typing.Any]] """ def __init__(self, config: JSONConfig | None = None) -> None: """Initialize the JSONReporter. .. note:: The exception documentation below refers to validation of subclass configuration class variables ``_OPTIONS_TYPE`` and ``_OPTIONS_KWARGS``. These must be correctly defined in any subclass of :class:`~.JSONReporter` to ensure proper functionality. :param config: An optional configuration object to override default reporter settings. If not provided, default settings will be used. :type config: JSONConfig | None :raises ~simplebench.exceptions.SimpleBenchTypeError: If the subclass configuration types are invalid. :raises ~simplebench.exceptions.SimpleBenchValueError: If the subclass configuration values are invalid. """ if config is None: config = JSONConfig() super().__init__(config)
[docs] def run_report(self, *, args: Namespace, log_metadata: ReportLogMetadata, case: Case, choice: Choice, path: Path | None = None, session: Session | None = None, callback: ReporterCallback | None = None ) -> None: """Output the benchmark results to a file as tagged JSON if available. This method is called by the base class's :meth:`~.Reporter.report` method after validation. The base class handles validation of the arguments, so subclasses can assume the arguments are valid without a large amount of boilerplate code. The base class also handles lazy loading of the reporter classes, so subclasses can assume any required imports are available. The :meth:`~.run_report` method's main responsibilities are to select the appropriate output method (``render_by_case()`` in this case) based on the provided arguments and to pass the actual rendering method to be used (the :meth:`~.render` method in this case). The rendering method must conform with the :class:`~simplebench.reporters.protocols.reporter_renderer.ReportRenderer` protocol. :param args: The parsed command-line arguments. :param log_metadata: The :class:`~.ReportLogMetadata` instance containing metadata about the report being generated. :param case: The :class:`~simplebench.case.Case` instance representing the benchmarked code. :param choice: The :class:`~simplebench.reporters.choice.Choice` instance specifying the report configuration. :param path: The path to the directory where the JSON file(s) will be saved. :param reports_log_path: The path to the reports log file. :param session: The :class:`~simplebench.session.Session` instance containing benchmark results. :param callback: A callback function for additional processing of the report. The function should accept two arguments: the :class:`~simplebench.case.Case` instance and the JSON data as a string. Leave as ``None`` if no callback is needed. """ self.render_by_case( renderer=self.render, log_metadata=log_metadata, args=args, case=case, choice=choice, path=path, session=session, callback=callback)
[docs] def render(self, *, case: Case, section: Section, options: ReporterOptions) -> str: """Convert the Case data for all sections to a JSON string. Machine info is included in the JSON output under the 'metadata' key. :param case: The :class:`~simplebench.case.Case` instance holding the benchmarked code statistics. :param section: The :class:`~simplebench.enums.Section` to render (ignored, all sections are included). :param options: The :class:`~.JSONOptions` instance specifying rendering options or ``None`` if not provided. (:class:`~.JSONOptions` is a subclass of :class:`~.ReporterOptions`.) :return: The JSON string representation of the :class:`~simplebench.case.Case` data. """ # is_* checks provide deferred import validation to avoid circular imports if not is_case(case): raise SimpleBenchTypeError( f"'case' argument must be a Case instance, got {type(case)}", tag=_JSONReporterErrorTag.RENDER_INVALID_CASE) section = validate_type(section, Section, 'section', _JSONReporterErrorTag.RENDER_INVALID_SECTION) options = validate_type(options, Options, 'options', _JSONReporterErrorTag.RENDER_INVALID_OPTIONS) full_data: bool = options.full_data if isinstance(options, Options) else False with StringIO() as jsonfile: case_dict = case.as_dict(full_data=full_data) try: case_dict['metadata'] = get_machine_info() json.dump(case_dict, jsonfile, indent=4) jsonfile.seek(0) except Exception as exc: raise SimpleBenchTypeError( f'Error generating JSON output for case {case.title}: {exc}', tag=_JSONReporterErrorTag.JSON_OUTPUT_ERROR) from exc return jsonfile.read()