File: _templates.py

package info (click to toggle)
python-mne 1.9.0-2
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 131,492 kB
  • sloc: python: 213,302; javascript: 12,910; sh: 447; makefile: 144
file content (171 lines) | stat: -rw-r--r-- 5,319 bytes parent folder | download
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)