initial commit
This commit is contained in:
171
mne/html_templates/_templates.py
Normal file
171
mne/html_templates/_templates.py
Normal file
@@ -0,0 +1,171 @@
|
||||
# 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)
|
||||
Reference in New Issue
Block a user