# Authors: The MNE-Python contributors. # License: BSD-3-Clause # Copyright the MNE-Python contributors. from __future__ import annotations # only needed for Python ≤ 3.9 import datetime import functools import uuid from dataclasses import dataclass from typing import Any, Literal from .._fiff.pick import channel_type from ..defaults import _handle_default _COLLAPSED = False # will override in doc build def _format_number(value: int | float) -> str: """Insert thousand separators.""" return f"{value:,}" def _append_uuid(string: str, sep: str = "-") -> str: """Append a UUID to a string.""" return f"{string}{sep}{uuid.uuid4()}" def _data_type(obj) -> str: """Return the qualified name of a class.""" return obj.__class__.__qualname__ def _dt_to_str(dt: datetime.datetime) -> str: """Convert a datetime object to a human-readable string representation.""" return dt.strftime("%Y-%m-%d at %H:%M:%S %Z") def _format_baseline(inst) -> str: """Format the baseline time period.""" if inst.baseline is None: baseline = "off" else: baseline = ( f"{round(inst.baseline[0], 3):.3f} – {round(inst.baseline[1], 3):.3f} s" ) return baseline def _format_metadata(inst) -> str: """Format metadata representation.""" if inst.metadata is None: metadata = "No metadata set" else: metadata = f"{inst.metadata.shape[0]} rows × {inst.metadata.shape[1]} columns" return metadata def _format_time_range(inst) -> str: """Format evoked and epochs time range.""" tr = f"{round(inst.tmin, 3):.3f} – {round(inst.tmax, 3):.3f} s" return tr def _format_projs(info) -> list[str]: """Format projectors.""" projs = [f'{p["desc"]} ({"on" if p["active"] else "off"})' for p in info["projs"]] return projs @dataclass class _Channel: """A channel in a recording.""" index: int name_html: str type: str type_pretty: str status: Literal["good", "bad"] def _format_channels(info) -> dict[str, dict[Literal["good", "bad"], list[str]]]: """Format channel names.""" ch_types_pretty: dict[str, str] = _handle_default("titles") channels = [] if info.ch_names: for ch_index, ch_name in enumerate(info.ch_names): ch_type = channel_type(info, ch_index) ch_type_pretty = ch_types_pretty.get(ch_type, ch_type.upper()) ch_status = "bad" if ch_name in info["bads"] else "good" channel = _Channel( index=ch_index, name_html=ch_name.replace(" ", " "), type=ch_type, type_pretty=ch_type_pretty, status=ch_status, ) channels.append(channel) # Extract unique channel types and put them in the desired order. ch_types = list(set([c.type_pretty for c in channels])) ch_types = [c for c in ch_types_pretty.values() if c in ch_types] channels_formatted = {} for ch_type in ch_types: goods = [c for c in channels if c.type_pretty == ch_type and c.status == "good"] bads = [c for c in channels if c.type_pretty == ch_type and c.status == "bad"] if ch_type not in channels_formatted: channels_formatted[ch_type] = {"good": [], "bad": []} channels_formatted[ch_type]["good"] = goods channels_formatted[ch_type]["bad"] = bads return channels_formatted def _has_attr(obj: Any, attr: str) -> bool: """Check if an object has an attribute `obj.attr`. This is needed because on dict-like objects, Jinja2's `obj.attr is defined` would check for `obj["attr"]`, which may not be what we want. """ return hasattr(obj, attr) @functools.lru_cache(maxsize=2) def _get_html_templates_env(kind): # For _html_repr_() and mne.Report assert kind in ("repr", "report"), kind import jinja2 templates_env = jinja2.Environment( loader=jinja2.PackageLoader( package_name="mne.html_templates", package_path=kind ), autoescape=jinja2.select_autoescape(default=True, default_for_string=True), ) if kind == "report": templates_env.filters["zip"] = zip templates_env.filters["format_number"] = _format_number templates_env.filters["append_uuid"] = _append_uuid templates_env.filters["data_type"] = _data_type templates_env.filters["dt_to_str"] = _dt_to_str templates_env.filters["format_baseline"] = _format_baseline templates_env.filters["format_metadata"] = _format_metadata templates_env.filters["format_time_range"] = _format_time_range templates_env.filters["format_projs"] = _format_projs templates_env.filters["format_channels"] = _format_channels templates_env.filters["has_attr"] = _has_attr return templates_env def _get_html_template(kind, name): return _RenderWrap( _get_html_templates_env(kind).get_template(name), collapsed=_COLLAPSED, ) class _RenderWrap: """Class that allows functools.partial-like wrapping of jinja2 Template.render().""" def __init__(self, template, **kwargs): self._template = template self._kwargs = kwargs def render(self, *args, **kwargs): return self._template.render(*args, **kwargs, **self._kwargs)