[074d3d]: / mne / html_templates / _templates.py

Download this file

172 lines (127 with data), 5.3 kB

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
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)