# Copyright French Alternative Energies and Atomic Energy Commission
# Contributors: valjean developers
# valjean-support@cea.fr
#
# This software is a computer program whose purpose is to analyze and
# post-process numerical simulation results.
#
# This software is governed by the CeCILL license under French law and abiding
# by the rules of distribution of free software. You can use, modify and/ or
# redistribute the software under the terms of the CeCILL license as circulated
# by CEA, CNRS and INRIA at the following URL: http://www.cecill.info.
#
# As a counterpart to the access to the source code and rights to copy, modify
# and redistribute granted by the license, users are provided only with a
# limited warranty and the software's author, the holder of the economic
# rights, and the successive licensors have only limited liability.
#
# In this respect, the user's attention is drawn to the risks associated with
# loading, using, modifying and/or developing or reproducing the software by
# the user in light of its specific status of free software, that may mean that
# it is complicated to manipulate, and that also therefore means that it is
# reserved for developers and experienced professionals having in-depth
# computer knowledge. Users are therefore encouraged to load and test the
# software's suitability as regards their requirements in conditions enabling
# the security of their systems and/or data to be ensured and, more generally,
# to use and operate it in the same conditions as regards security.
#
# The fact that you are presently reading this means that you have had
# knowledge of the CeCILL license and that you accept its terms.
'''Module containing all available methods to convert a test result in a table
to be converted in rst.
'''
import logging
from itertools import chain
import numpy as np
from ..cosette.task import TaskStatus
from ..gavroche.diagnostics.stats import TestOutcome, classification_counts
from .templates import TableTemplate, TextTemplate
from .verbosity import Verbosity
# turn off pylint warnings about invalid names in this file; there are just too
# many long function names and they cannot be renamed because
# javert.representation looks for them by programmatically constructing their
# name based on the name of the test result class, the verbosity, etc.
# pylint: disable=invalid-name
LOGGER = logging.getLogger(__name__)
[docs]def repr_bins(dsref):
'''Representation of bins in tables.
When bins are given by edges, representation is ``min - max``, when they
are given at center, representation is ``center``.
Trivial dimensions are not represented, i.e. dimensions where there is only
one bin.
If there are more than one non-trivial dimensions, some repetition is
expected. For example with two non-trivial dimensions of two bins each one
point will be defined by its coordinated in the two dimensions and we
expect all the bins to possibily be shown in a table. We expected 4 bins
and their associated values in that case.
Let's consider the following dataset:
>>> from valjean.eponine.dataset import Dataset
>>> import numpy as np
>>> from collections import OrderedDict
>>> vals = np.arange(6).reshape(1, 2, 1, 3, 1)
>>> errs = np.array([0.1]*6).reshape(1, 2, 1, 3, 1)
>>> bins = OrderedDict([('bacon', np.array([0, 1])),
... ('egg', np.array([0, 2, 4])),
... ('sausage', np.array([10, 20])),
... ('spam', np.array([-5, 0, 5])),
... ('tomato', np.array([-2, 2]))])
>>> ds = Dataset(vals, errs, bins=bins)
>>> names, rbins = repr_bins(ds)
>>> print(list(ds.bins.keys()))
['bacon', 'egg', 'sausage', 'spam', 'tomato']
>>> print(names)
['egg', 'spam']
>>> print(ds.shape)
(1, 2, 1, 3, 1)
>>> print([rb.shape for rb in rbins])
[(1, 2, 1, 3, 1), (1, 2, 1, 3, 1)]
``'bacon'`` and ``'sausage'`` are trivial dimensions, so won't be
represented in the table, but we expect 6 values corresponding to 2 bins in
``'egg'`` and 3 in ``'spam'``. Each value corresponds to a line in the
table so each columns should have the same size and the same shape, the
shape of the given dataset without trivial dimensions. We then have 3
``'spam'`` bins in each ``'egg'`` bins or 2 ``'egg'`` bins in each
``'spam'`` bins. Each couple appears only once.
>>> for name, rbin in zip(names, rbins):
... print(name, ':', rbin.flatten())
egg : ['0 - 2' '0 - 2' '0 - 2' '2 - 4' '2 - 4' '2 - 4']
spam : ['-5' '0' '5' '-5' '0' '5']
As expected from the bins, ``'egg'`` bins are given by edges
(``min - max``) while ``'spam'`` bins are given by center (``center``).
:param dsref: dataset
:type dsref: Dataset
:returns: list of the non-trivial dimensions and a tuple of the bins
:rtype: (list(str), tuple(numpy.ndarray))
The tuple must have the same length as the list of dimensions and the bins
inside must have the same shape as ``dsref.value``.
'''
bins, dim_names = [], []
for idim, dim in enumerate(dsref.bins.keys()):
# reject trivial dimensions (1 bin or no bin)
if dsref.value.shape[idim] < 2:
continue
dim_names.append(dim)
shape = [dsref.value.shape[idim]] + [1] * (dsref.ndim - 1 - idim)
if dsref.value.shape[:idim] == tuple([1]*idim):
shape = [1]*idim + shape
if dsref.bins[dim].size == dsref.value.shape[idim]+1:
dbins = [f"{a:.4g} - {b:.4g}"
for a, b in zip(dsref.bins[dim][:-1],
dsref.bins[dim][1:])]
else:
dbins = ([f"{a:.4g}" for a in dsref.bins[dim]]
if dsref.bins[dim].dtype.kind != 'U'
else dsref.bins[dim])
bins.append(np.array(dbins).reshape(shape))
bbins = np.broadcast_arrays(*bins)
return dim_names, tuple(bbins)
[docs]def repr_testresultequal(result, verbosity=Verbosity.DEFAULT):
'''Represent the result of a :class:`~.TestEqual` test.
:param TestResultEqual result: a test result.
:param Verbosity verbosity: verbosity level
:returns: list of templates representing a :class:`~.TestResultEqual`
'''
LOGGER.debug("IN repr_testresultequal, %s, res = %s",
verbosity, bool(result))
if bool(result):
if verbosity != Verbosity.FULL_DETAILS:
return []
return repr_equal(result)
if verbosity.value < Verbosity.DEFAULT.value:
return repr_equal_summary(result)
return repr_equal(result)
[docs]def repr_equal_summary(result):
'''Function to generate a summary table for the equal test (only tells if
the test was successful or not).
:param TestResultEqual result: a test result.
:rtype: list(TextTemplate)
'''
LOGGER.debug("repr_equal_summary found")
if result:
return [TextTemplate('Equal test: OK\n\n')]
return [TextTemplate('.. role:: hl\n\n'
'Equal test: :hl:`KO`\n\n')]
[docs]def repr_equal(result):
'''Representation of equal test.
:param TestResultEqual result: a test result.
:returns: Representation of a :class:`~.TestResultEqual` as a table.
:rtype: list(TableTemplate)
'''
LOGGER.debug("In repr_equal")
nbins, bins = repr_bins(result.test.dsref)
dscols = tuple((ds.value, eq)
for ds, eq in zip(result.test.datasets, result.equal))
heads = nbins + [result.test.dsref.name]
for ds in result.test.datasets:
heads.extend([ds.name,
f'equal({ds.name})?'
if len(result.test.datasets) > 1 else 'equal?'])
falses = np.full_like(result.test.dsref.value, False, dtype=bool)
highlights = [falses] * (len(nbins) + 1)
for equal in result.equal:
highlights.extend([falses, np.logical_not(equal)])
table_template = TableTemplate(
*bins, result.test.dsref.value, *chain.from_iterable(dscols),
highlights=highlights, headers=heads)
return [table_template]
[docs]def repr_testresultapproxequal(result, verbosity=Verbosity.DEFAULT):
'''Represent the result of a :class:`~.TestApproxEqual` test.
:param TestResultApproxEqual result: a test result.
:param Verbosity verbosity: verbosity level
:returns: list of templates representing a :class:`~.TestResultApproxEqual`
'''
if verbosity == Verbosity.SILENT and bool(result):
return []
if verbosity == Verbosity.SUMMARY:
return repr_approx_equal_summary(result)
return repr_approx_equal(result)
[docs]def repr_approx_equal_summary(result):
'''Function to generate a summary table for the approx equal test (only
tells if the test was successful or not).
:param TestResultApproxEqual result: a test result.
:rtype: list(TextTemplate)
'''
LOGGER.debug("repr_approx_equal_summary found")
if result:
return [TextTemplate('Approx equal test: OK\n\n')]
return [TextTemplate('.. role:: hl\n\n'
'Approx equal test: :hl:`KO`\n\n')]
[docs]def repr_approx_equal(result):
'''Representation of approx equal test.
:param TestResultApproxEqual result: a test result.
:returns: Representation of a :class:`~.TestResultApproxEqual` as a table.
:rtype: list(TableTemplate)
'''
LOGGER.debug("In repr_approx_equal")
nbins, bins = repr_bins(result.test.dsref)
dscols = tuple((ds.value, eq)
for ds, eq in zip(result.test.datasets,
result.approx_equal))
heads = nbins + [result.test.dsref.name]
for ds in result.test.datasets:
heads.extend([ds.name,
f'approx equal({ds.name})?'
if len(result.test.datasets) > 1
else 'approx equal?'])
falses = np.full_like(result.test.dsref.value, False, dtype=bool)
highlights = [falses] * (len(nbins) + 1)
for approx_equal in result.approx_equal:
highlights.extend([falses, np.logical_not(approx_equal)])
table_template = TableTemplate(
*bins, result.test.dsref.value, *chain.from_iterable(dscols),
highlights=highlights, headers=heads)
return [table_template]
[docs]def repr_testresultstudent(result, verbosity=Verbosity.DEFAULT):
'''Represent the result of a :class:`~.TestStudent` test.
:param TestResultStudent result: a test result.
:param Verbosity verbosity: verbosity level
:returns: list of templates representing a :class:`~.TestResultStudent`
'''
LOGGER.debug("student repr, %s, res = %s", verbosity, bool(result))
if verbosity == Verbosity.SILENT:
return []
if verbosity == Verbosity.SUMMARY:
return repr_student_summary(result)
if verbosity in (Verbosity.DEFAULT, Verbosity.INTERMEDIATE):
return repr_student_intermediate(result)
return repr_student(result)
def _student_heads(test_res, bin_names):
'''Build the column names for Student test representation.
Reference appears first then all the tested datasets. They are identified
by their name if unique, else their index in the list of datasets is used.
:param TestStudent test_res: the test (not the result)
:param list(str) bin_names: names of the bins (dimensions)
:returns: list of headers
'''
heads = ['v('+test_res.dsref.name+')', 'σ('+test_res.dsref.name+')']
for _ds in test_res.datasets:
heads.extend(['v('+_ds.name+')', 'σ('+_ds.name+')'])
if len(test_res.datasets) > 1:
heads.extend([r't('+_ds.name+')', 'Student('+_ds.name+')?'])
else:
heads.extend(['t', 'Student?'])
heads = bin_names + heads
return heads
[docs]def repr_student(result):
'''Representation of Student test result.
:param TestResultStudent result: a test result.
:returns: Representation of a :class:`~.TestResultStudent` as a table.
:rtype: list(TableTemplate)
'''
LOGGER.debug("In repr_student")
oracles = result.oracles()
nbins, bins = repr_bins(result.test.dsref)
dscols = tuple((ds.value, ds.error, tstud, studbool)
for ds, tstud, studbool in zip(result.test.datasets,
result.tstud,
oracles))
falses = np.full_like(result.test.dsref.value, False, dtype=bool)
heads = _student_heads(result.test, nbins)
highlights = [falses]*(len(nbins) + 2)
for oracle in oracles:
highlights += [falses, falses, falses, np.logical_not(oracle)]
table_template = TableTemplate(
*bins, result.test.dsref.value, result.test.dsref.error,
*chain.from_iterable(dscols),
highlights=highlights, headers=heads)
return [table_template]
[docs]def repr_student_silent(_result):
'''Function to generate a silent table for the Student test (only tells if
the test was successful or not).
:param TestResultStudent result: a Student test result.
:returns: empty list
'''
LOGGER.debug("student silent")
return []
[docs]def repr_student_summary(result):
'''Function to generate a summary table for the Student test (only tells if
the test was successful or not).
:param TestResultStudent result: a Student test result.
:rtype: list(TextTemplate)
'''
LOGGER.debug("repr_student_summary found")
if result:
return [TextTemplate('Student test: OK\n\n')]
return [TextTemplate('.. role:: hl\n\n'
'Student test: :hl:`KO`\n\n')]
[docs]def repr_testresultbonferroni(result, verbosity=Verbosity.DEFAULT):
'''Represent the result of a :class:`~.TestBonferroni` test.
Only represents the Bonferroni result, not the input test result.
:param TestResultBonferroni result: a test result.
:param Verbosity verbosity: verbosity level
:returns: list of templates representing a :class:`~.TestResultBonferroni`
'''
LOGGER.debug("bonf repr, %s, res = %s", verbosity, bool(result))
if verbosity == Verbosity.SILENT and bool(result):
return []
if verbosity == Verbosity.SUMMARY:
return repr_bonferroni_summary(result)
return repr_bonferroni(result)
[docs]def repr_bonferroni(result):
'''Reprensetation of Bonferroni test result.
Only represents the Bonferroni result, not the input test result.
:param TestResultBonferroni result: a test result.
:returns: Representation of a :class:`~.TestResultBonferroni` as a table.
:rtype: list(TableTemplate)
'''
ndatasets = len(result.first_test_res.test.datasets)
oracles = list(result.oracles())
highlights = [[False] * ndatasets] * 5 # 5 non-highlighted columns
highlights.append([not oracle for oracle in oracles])
table_template = TableTemplate(
[f"{result.first_test_res.test.dsref.name} vs {dtest.name}"
for dtest in result.first_test_res.test.datasets],
[result.test.ntests] * ndatasets,
[result.test.alpha] * ndatasets,
[result.test.bonf_signi_level] * ndatasets,
[min(pval) for pval in result.first_test_res.pvalue],
oracles,
highlights=highlights,
headers=['test', 'ndf', 'α', 'α(Bonferroni)', 'min(p-value)',
'Bonferroni?'])
return [table_template]
[docs]def repr_bonferroni_summary(result):
'''Represent the result of a :class:`~.TestBonferroni` test for the
SUMMARY level of verbosity.
:param TestResultBonferroni result: a test result.
:returns: Representation of a :class:`~.TestResultBonferroni` as a table.
:rtype: list(TextTemplate)
'''
if result:
return [TextTemplate('Bonferroni test: OK\n\n')]
return [TextTemplate('.. role:: hl\n\n'
'Bonferroni test: :hl:`KO`\n\n')]
[docs]def repr_testresultholmbonferroni(result, verbosity=Verbosity.DEFAULT):
'''Represent the result of a :class:`~.TestHolmBonferroni` test.
:param TestResultHolmBonferroni result: a test result.
:param Verbosity verbosity: verbosity level
:returns: list of templates representing a
:class:`~.TestResultHolmBonferroni`
'''
LOGGER.debug("HB res, %s, res = %s", verbosity, bool(result))
if verbosity == Verbosity.SILENT:
if bool(result):
return []
return repr_holm_bonferroni_summary(result)
if verbosity == Verbosity.SUMMARY:
return repr_holm_bonferroni_summary(result)
return repr_holm_bonferroni(result)
[docs]def repr_holm_bonferroni(result):
'''Reprensetation of Holm-Bonferroni test result.
Only represents the Holm-Bonferroni result, not the input test result.
:param TestResultHolmBonferroni result: a test result.
:returns: Representation of a :class:`~.TestResultHolmBonferroni` as a
table.
:rtype: list(TableTemplate)
'''
ndatasets = len(result.first_test_res.test.datasets)
oracles = list(result.oracles())
highlights = [[False] * ndatasets] * 6 # 6 non-highlighted columns
highlights.append([not oracle for oracle in oracles])
table_template = TableTemplate(
[f"{result.first_test_res.test.dsref.name} vs {dtest.name}"
for dtest in result.first_test_res.test.datasets],
[result.test.ntests] * ndatasets,
[result.test.alpha] * ndatasets,
[np.amin(pval) for pval in result.first_test_res.pvalue],
[np.amin(alpha_i) for alpha_i in result.alphas_i],
list(result.nb_rejected),
oracles,
highlights=highlights,
headers=['test', 'ndf', 'α', 'min(p-value)', 'min(α)',
'N rejected', 'Holm-Bonferroni?'])
return [table_template]
[docs]def repr_holm_bonferroni_summary(result):
'''Represent the result of a :class:`~.TestHolmBonferroni` test for the
SUMMARY level of verbosity.
:param TestResultHolmBonferroni result: a test result.
:returns: Representation of a :class:`~.TestResultHolmBonferroni` as a
table.
:rtype: list(TextTemplate)
'''
if result:
return [TextTemplate('Holm-Bonferroni test: OK\n\n')]
return [TextTemplate('.. role:: hl\n\n'
'Holm-Bonferroni test: :hl:`KO`\n\n')]
[docs]def percent_fmt(num, den):
'''Format a fraction as a percentage. Example:
>>> percent_fmt(2, 4)
'2/4 (50.0%)'
>>> percent_fmt(0, 3)
'0/3 (0.0%)'
>>> percent_fmt(7, 7)
'7/7 (100.0%)'
>>> percent_fmt(0, 0)
'0/0 (???%)'
:param int num: the numerator.
:param int den: the denominator.
:rtype: str
'''
nbsp = ' '
if den != 0:
percent = 100.0 * num / den
return f'{num:d}/{den:d}{nbsp}({percent:.1f}%)'
return f'{num:d}/{den:d}{nbsp}(???%)'
[docs]def repr_testresultstatstasks(result, verbosity=Verbosity.DEFAULT):
'''Represent a :class:`~.TestResultStatsTasks` as a table. The table
breaks down the tasks by status.
:param TestResultStatsTasks result: the test result to represent.
:param Verbosity verbosity: verbosity level
:returns: list of templates representing the test result.
'''
LOGGER.debug("In repr_testresultstatstasks")
if verbosity == Verbosity.SILENT and bool(result):
return []
return repr_testresultstats(result, TaskStatus.DONE, 'tasks')
[docs]def repr_testresultstatstests(result, verbosity=Verbosity.DEFAULT):
'''Represent a :class:`~.TestResultStatsTests` as a table. The table
breaks down the tests by success status.
:param TestResultStatsTests result: the test result to represent.
:param Verbosity verbosity: verbosity level
:returns: the tables representing the test result.
:rtype: list(TableTemplate)
'''
LOGGER.debug("In repr_testresultstatstests")
if verbosity == Verbosity.SILENT and bool(result):
return []
return repr_testresultstats(result, TestOutcome.SUCCESS, 'tests')
[docs]def repr_testresultstats(result, status_ok, label):
'''Helper function for :func:`repr_testresultstatstests` and
:func:`repr_testresultstatstasks`. It generates a table with the
`status_ok` value in the first row. Non-null results in other rows are
considered as failures, and are highlighted if the count is non-zero. Null
results are omitted from the table.
:param result: the test result to represent.
:type result: TestResultStatsTasks or TestResultStatsTests
:param status_ok: the status value that must be considered as a success.
:param str label: the type of things that we are testing (``'tests'`` or
``'tasks'``)
:returns: the tables representing the test result.
:rtype: list(TableTemplate)
'''
classify = result.classify
statuses, counts = classification_counts(classify, status_ok)
statuses_txt = [status.name for status in statuses]
n_tasks = sum(counts)
statuses_txt.append('total')
counts.append(n_tasks)
percents = [percent_fmt(count, n_tasks) for count in counts]
hl_column = [False]
hl_column.extend(count > 0 for count in counts[1:-1])
table = TableTemplate(statuses_txt, percents, headers=['status', 'counts'],
highlights=[hl_column, hl_column])
text = []
for status, status_txt in zip(statuses, statuses_txt):
if status_ok == status:
continue
text.append(f'List of {label} with status {status_txt}:\n\n')
for item in sorted(classify[status]):
if item.fingerprint:
text.append(f'#. :ref:`{item.name} '
f'<anchor_{item.fingerprint}>`')
else:
text.append(f'#. {item.name}')
text.append('\n')
return [table, TextTemplate('\n'.join(text))]
[docs]def repr_testresultstatstestsbylabels(result, verbosity=Verbosity.DEFAULT):
'''Represent a :class:`~.TestResultStatsTestsByLabels` as tables. Shape of
the table may change according to the number of flags required.
:param TestResultStatsTestsByLabels result: the test result to represent.
:param Verbosity verbosity: verbosity level
:returns: the tables representing the test result.
:rtype: list(TableTemplate)
'''
LOGGER.debug("In repr_testresultstatstestsbylabels")
if verbosity == Verbosity.SILENT and bool(result):
return []
if verbosity == Verbosity.SUMMARY:
return repr_testresultstatsbylabels_summary(result)
return repr_testresultstatsbylabels(result)
def _sbl_labels_1column(labels, results, oracles):
'''Build a :class:`TableTemplate` from the results where labels are
concantenated in one column.
:param tuple(str) labels: labels used to make statistics
:param dict results: result from :class:`TestResultStatsTestsByLabels`
:param list(bool) oracles: result of each individual test
:rtype: TableTemplate
'''
lnames, lok, lko, hlight = [], [], [], []
for res, ora in zip(results, oracles):
lnames.append('/'.join(res['labels']))
lok.append(percent_fmt(res['OK'], res['total']))
lko.append(percent_fmt(res['KO'], res['total']))
hlight.append([not ora])
return TableTemplate(
lnames, lok, lko,
headers=['/'.join(labels), r'% success', r'% failure'],
highlights=[hlight]*3)
def _sbl_1colbylabel(labels, results, oracles):
'''Build a :class:`TableTemplate` from the results where each label will be
represented in a separate column.
:param tuple(str) labels: labels used to make statistics
:param dict results: result from :class:`TestResultStatsTestsByLabels`
:param list(bool) oracles: result of each individual test
:rtype: TableTemplate
'''
lnames = [[] for _ in range(len(labels))]
lok, lko, hlight = [], [], []
for res, ora in zip(results, oracles):
for i, lab in enumerate(res['labels']):
lnames[i].append(lab)
lok.append(percent_fmt(res['OK'], res['total']))
lko.append(percent_fmt(res['KO'], res['total']))
hlight.append([not ora])
return TableTemplate(
*lnames, lok, lko,
headers=[*labels] + [r'% success', r'% failure'],
highlights=[hlight]*(len(labels)+2))
[docs]def repr_testresultstatsbylabels(result):
'''Function to print detailed statistics on tests, per category and sample
run.
:param TestResultStatsTestsByLabels result: the test result to represent.
:returns: the tables representing the test result.
:rtype: list(TableTemplate)
'''
LOGGER.debug("In repr_testresultstatsbylabels")
res = []
res.append(_sbl_1colbylabel(
result.test.by_labels, result.classify, result.oracles()))
if result.nb_missing_labels() != 0:
res.append(TextTemplate('At least one of the labels used for sorting '
f'{result.test.by_labels} is missing in '
f'{result.nb_missing_labels()} tests\n\n'))
return res
[docs]def repr_testresultstatsbylabels_summary(result):
'''Function to print detailed statistics on tests, per category and sample
run in summary case: only print failed cases.
:param TestResultStatsTestsByLabels result: the test result to represent.
:returns: the tables representing the test result.
:rtype: list(TableTemplate), list(TextTemplate)
'''
LOGGER.debug("In repr_testresultstatsbylabels")
res = []
lnames = [[] for _ in range(len(result.test.by_labels))]
lok, lko, hlight = [], [], []
for resu, ora in zip(result.classify, result.oracles()):
if ora:
continue
for i, lab in enumerate(resu['labels']):
lnames[i].append(lab)
lok.append(percent_fmt(resu['OK'], resu['total']))
lko.append(percent_fmt(resu['KO'], resu['total']))
hlight.append([not ora])
tabtemp = TableTemplate(
*lnames, lok, lko,
headers=[*result.test.by_labels] + [r'% success', r'% failure'],
highlights=[hlight]*(len(result.test.by_labels)+2))
if lok:
res.append(tabtemp)
else:
res.append(TextTemplate('No failed test found.\n\n'))
if result.nb_missing_labels() != 0:
res.append(TextTemplate('At least one of the labels used for sorting '
f'{result.test.by_labels} is missing in '
f'{result.nb_missing_labels()} tests\n\n'))
return res
[docs]def repr_testresultexternal(_result, _verbosity=Verbosity.DEFAULT):
'''Represent external test as tables -> no table done.
If tables are required they are already done. Their representation in the
report is done by :class:`.ExternalRepresenter`.
:returns: empty list
'''
return []
[docs]def repr_testresultfailed(result, _verbosity=Verbosity.DEFAULT):
'''Represent a failed result as rst text.
:param TestResultFailed result: a failed test result.
:rtype: list(TextTemplate)
'''
LOGGER.debug("In repr_testresultfailed")
defmsg = ' failed with message:'
cname = result.test.__class__.__name__
text = f':hl:`{cname}{defmsg}`\n{result.msg}\n'
return [TextTemplate('.. role:: hl\n\n' + text.replace('\n', '\n\n'))]