Source code for valjean.javert.rst
# 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.
'''This module provides the classes to convert test results to reStructuredText
format. reStructuredText_ is a text markup language meant for easy human
consumption. The `reStructuredText primer`_ provides a good introduction to its
syntax.
.. _reStructuredText: https://docutils.sourceforge.io/rst.html
.. _reStructuredText primer: https://
docutils.sourceforge.io/docs/user/rst/quickstart.html
.. _pkg_resources: https://setuptools.pypa.io/en/latest/pkg_resources.html
'''
import logging
from datetime import datetime
from collections import defaultdict
from pathlib import Path
import multiprocessing as mp
import pkg_resources as pkg
import numpy as np
from ..fingerprint import fingerprint
from ..path import ensure, sanitize_filename
from ..cosette.task import TaskStatus
from ..cosette.pythontask import PythonTask
from ..gavroche.test import TestResult
from .formatter import Formatter
from .mpl import MplPlot
from .test_report import TestReport, TestReportTask
from .verbosity import Verbosity
LOGGER = logging.getLogger(__name__)
[docs]class Rst:
'''Class to convert :class:`~.TestResult` objects into `reStructuredText`_
format.'''
[docs] def __init__(self, representation, *, n_workers=None):
'''Initialize a class instance with the given representation.
:param Representation representation: A representation.
'''
self.representation = representation
self.formatter = RstFormatter()
self.plots = {}
self.tree_dict = defaultdict(list)
self.text_dict = defaultdict(list)
self.n_workers = n_workers
[docs] def clear(self):
'''Clear the content of `self`.'''
self.plots.clear()
self.tree_dict.clear()
self.text_dict.clear()
[docs] def format_report(self, *, report, author, version):
'''Format a report.
Most of the work is actually done in the :meth:`format_report_rec`
method.
:param TestReport report: the report to format.
:returns: the formatted report.
:rtype: FormattedRst
'''
self.clear()
self.format_report_rec(report=report, tree=())
fmt_report = FormattedRst(author=author, title=report.title,
version=version,
tree_dict=self.tree_dict,
text_dict=self.text_dict,
plots=self.plots,
n_workers=self.n_workers)
LOGGER.debug('formatted report: %s', fmt_report)
return fmt_report
[docs] def format_report_rec(self, *, report, tree):
'''Recursively format report sections and populate the
``tree_dict`` and ``text_dict`` attributes.
Report sections, as represented by the :class:`~.TestReport` class, are
essentially trees, with :class:`~.TestResult` objects playing the role
of the tree leaves and :class:`~.TestReport` objects playing the role
of the tree nodes. This method performs a recursive, pre-order,
depth-first traversal of the tree and fills the ``tree_dict`` and
``text_dict`` dictionaries with the information that is necessary
to instantiate the final :class:`FormattedRst` object.
Given a report object, this method constructs a dictionary key by
concatenating in a tuple the titles of the parent reports and the
present report. The tuple containing the parent reports' titles is
recursively passed as the `tree` argument (and it is initialized to the
empty tuple in the call from :meth:`format_report`). For instance,
consider the following simplified report structure:
>>> from valjean.javert.test_report import TestReport
>>> subsub1 = TestReport(title='SubSub1')
>>> subsub2 = TestReport(title='SubSub2')
>>> sub1 = TestReport(title='Sub1', content=[subsub1, subsub2])
>>> sub2 = TestReport(title='Sub2')
>>> main = TestReport(title='Main', content=[sub1, sub2],
... text='Si six scies scient six cyprès, '
... 'six-cent-six scies scient '
... 'six-cent-six cyprès.')
We instantiate an :class:`Rst` object and we format the report:
>>> import valjean.javert.representation as rpr
>>> from valjean.javert.rst import Rst
>>> rst = Rst(rpr.Representation(rpr.FullRepresenter()))
>>> formatted_report = rst.format_report(report=main, author='Me',
... version='0.1')
Let us look at the dictionary keys:
>>> print(sorted(rst.text_dict.keys()))
[(), ('Sub1',), ('Sub1', 'SubSub1'), ('Sub1', 'SubSub2'), \
('Sub2',)]
The empty tuple, ``()``, is associated to the main report. The other
keys represent subreports. The length of the tuple corresponds to the
depth of the nested report; for instance, the ``'SubSub2'`` section
appears in the ``('Sub1', 'SubSub2')`` tuple with length 2, because
``'SubSub2'`` is nested twice (``Main``/``Sub1``/``SubSub2``).
The values of the ``text_dict`` dictionary are lists of strings
representing the text of the given section. For instance, here is what
the main section looks like:
.. doctest::
>>> print('\\n'.join(rst.text_dict[()]))
Main
====
<BLANKLINE>
Si six scies scient six cyprès, six-cent-six scies scient \
six-cent-six cyprès.
<BLANKLINE>
The ``tree_dict`` dictionary associates a tuple representing a
report section to the list of tuples that represent the subsections:
>>> print(rst.tree_dict[()])
[('Sub1',), ('Sub2',)]
>>> print(rst.tree_dict[('Sub1',)])
[('Sub1', 'SubSub1'), ('Sub1', 'SubSub2')]
If a section does not have any subsection, the corresponding tuple does
not appear in the dictionary:
>>> ('Sub2',) in rst.tree_dict
False
The ``tree_dict`` and ``text_dict`` dictionaries contain most
of the information required by :class:`FormattedRst` to write out the
report in the form of a `reStructuredText`_ file tree (see
:meth:`~.FormattedRst.write`).
'''
LOGGER.debug('formatting tree: %s', tree)
LOGGER.debug('formatting report: %s', report.title)
report_text = self.format_section(report, depth=len(tree))
self.text_dict[tree].extend(report_text)
for stuff in report.content:
if isinstance(stuff, TestReport):
subtree = tree + (stuff.title,)
self.tree_dict[tree].append(subtree)
self.format_report_rec(report=stuff, tree=subtree)
elif isinstance(stuff, TestResult):
LOGGER.debug('tree: %s', tree)
LOGGER.debug('tree_dict: %s', dict(self.tree_dict))
assert tree in self.text_dict
res_text = self.format_result(stuff)
self.text_dict[tree].extend(res_text)
else:
raise TypeError('expected TestReport or TestResult in '
f'TestReport content, found {type(stuff)} '
'instead')
[docs] def format_section(self, section, *, depth):
'''Format a report section.
:param TestReport report: the report section to format.
:returns: the formatted report section.
:rtype: str
'''
lines = [self.formatter.header(section.title, depth),
self.formatter.text(section.text),
'']
LOGGER.debug('formatted section: %s', lines)
return lines
[docs] def format_result(self, result):
'''Format one test result.
:param TestResult result: A :class:`~.TestResult`.
:returns: the formatted test result.
:rtype: str
'''
lines = [self.formatter.anchor(fingerprint(result.test)),
self.formatter.text(result.test.description), '']
res_repr = self.representation(result)
if (not res_repr
and self.representation.verbosity in (Verbosity.SILENT,
Verbosity.SUMMARY)):
return []
for template in res_repr:
fmt = self.formatter.template(template)
lines.append(str(fmt))
if isinstance(fmt, RstPlot):
self.plots[fmt.fingerprint] = fmt.mpl_plot
LOGGER.debug('formatted result: %s', lines)
return lines
[docs]class RstFormatter(Formatter):
'''Class that dispatches the task of formatting templates as
`reStructuredText`_. The concrete formatting is handled by separate classes
(:class:`RstTable`...).
'''
HEADER_CHARS = ('=', '-', '`', "'", '"')
[docs] def header(self, name, depth):
'''Produce the header for formatting a :class:`~.TestResult`.
:param str name: A header name
:param int depth: the depth of this header, 0 being the top.
:returns: the test header, for inclusion in a reST document.
:rtype: str
'''
if depth >= len(self.HEADER_CHARS):
raise ValueError(f'maximum depth exceeded: {depth} > '
f'{self.HEADER_CHARS}')
lines = [name]
lines.append(self.HEADER_CHARS[depth]*len(name))
lines.append('')
return '\n'.join(lines)
[docs] def text(self, text):
'''Format some text.
:param str text: some text to include.
:returns: the text, in rst format.
:rtype: str
'''
return text
[docs] @staticmethod
def anchor(fingerprint):
'''Format an anchor with the given fingerprint.'''
return f'\n.. _anchor_{fingerprint}:\n\n'
[docs] @staticmethod
def format_tabletemplate(table):
'''Format a :class:`~.TableTemplate`.
:param TableTemplate table: A table.
:returns: the reST table.
:rtype: RstTable
'''
return RstTable(table)
[docs] @staticmethod
def format_plottemplate(plot):
'''Format a :class:`~.PlotTemplate`.
:param PlotTemplate plot: A plot.
:returns: the formatted plot
:rtype: RstPlot
'''
return RstPlot(plot)
[docs] @staticmethod
def format_texttemplate(text):
'''Format a :class:`~.TextTemplate`.
:param TextTemplate text: A text with its highlight positions
:returns: the formatted text (text itself)
:rtype: str
'''
return RstText(text)
[docs]class RstTable:
'''Convert a :class:`~.TableTemplate` into a `reStructuredText`_ table.'''
HIGHLIGHT_ROLE = 'hl'
COL_SEP = ' '
[docs] def __init__(self, table, num_fmt='{:11.6g}'):
'''Construct an :class:`RstTable` from the given
:class:`~.templates.TableTemplate`.
:param TableTemplate table: The table to convert.
:param str num_fmt: A :func:`format` string to specify how numerical
table entries should be represented. The default
value for this option is ``'{:11.6g}'``.
'''
self.table = table
self.num_fmt = num_fmt
[docs] def __str__(self):
'''Yield the table, as a reST string. This is probably the method that
you want to call.'''
columns = self.table.columns
highlights = self.table.highlights
rows = list(self.format_columns(columns, highlights, self.num_fmt))
table = self.tabularize(self.table.headers, rows, indent=4)
header = ('.. role:: ' + RstTable.HIGHLIGHT_ROLE + '\n\n'
'.. table::\n :widths: auto\n\n')
return header + table + '\n'
[docs] @classmethod
def tabularize(cls, headers, rows, *, indent=0):
'''Transform a list of headers and a list of rows into a nice reST
table.
The `headers` argument must be a list of strings. The `rows` argument
represents the table rows, as a list of lists of strings. Each sublist
represents a table row, and it must have the same length as `headers`,
or terrible things will happen.
The column widths are automatically computed to accommodate the largest
template in each column. Smaller templates are automatically
right-justified.
.. doctest::
>>> headers = ['name', 'quest', 'favourite colour']
>>> rows = [['Lancelot', 'to seek the Holy Grail', 'blue'],
... ['Galahad', 'to seek the Holy Grail', 'yellow']]
>>> table = RstTable.tabularize(headers, rows)
>>> print(table)
======== ====================== ================
name quest favourite colour
======== ====================== ================
Lancelot to seek the Holy Grail blue
Galahad to seek the Holy Grail yellow
======== ====================== ================
<BLANKLINE>
You can also indent the table by a given amount of spaces with the
`indent` keyword argument:
.. doctest::
>>> table = RstTable.tabularize(headers, rows, indent=4)
>>> print(table)
======== ====================== ================
name quest favourite colour
======== ====================== ================
Lancelot to seek the Holy Grail blue
Galahad to seek the Holy Grail yellow
======== ====================== ================
<BLANKLINE>
:param list(str) headers: The table headers.
:param list(list(str)) rows: The table rows.
:returns: The reST table, as a string.
'''
widths = cls.compute_column_widths(headers, rows)
LOGGER.debug('widths: %s', widths)
sep_row = cls.COL_SEP.join('='*w for w in widths)
header_row = cls.COL_SEP.join(f'{header:^{w}}'
for w, header in zip(widths, headers))
lines = [' '*indent + sep_row, header_row, sep_row]
lines.extend(cls.concat_rows(widths, rows))
lines.append(sep_row)
lines.append('') # in order to get 2 tables
lines_sep = '\n' + ' '*indent
return lines_sep.join(lines)
[docs] @staticmethod
def transpose(columns):
r'''Given a matrix as a list of columns, yield the matrix rows.
(Equivalently, if the matrix is given as a list of rows, yield the
columns). For instance, consider the following list:
>>> matrix = [(11, 21, 31), (12, 22, 32), (13, 23, 33)]
>>> for column in matrix:
... print(' '.join(str(elem) for elem in column))
11 21 31
12 22 32
13 23 33
If the tuples are interpreted as columns, `matrix` represents the
following matrix:
.. math::
\begin{pmatrix}11&12&13\\21&22&23\\31&32&33\end{pmatrix}
Transposing it yields:
>>> transposed = RstTable.transpose(matrix)
>>> for row in transposed:
... print(' '.join(str(elem) for elem in row))
11 12 13
21 22 23
31 32 33
.. note::
The matrix elements returned by :meth:`transpose` are actually
0-dimensional :class:`numpy.ndarray` objects. For the purpose of
their further manipulation this is of little consequence, as they
transparently support most numerical operations.
:param columns: An iterable yielding columns
:type columns: list(list) or list(tuple) or list(numpy.ndarray)
:raises ValueError: if the columns do not have the same length.
:returns: a generator for the rows of the transposed matrix.
'''
LOGGER.debug('columns: %s, %s', type(columns), len(columns))
try:
yield from np.nditer(columns)
except ValueError as exc:
raise ValueError('all columns must have the same length') from exc
[docs] @classmethod
def format_columns(cls, columns, highlights, num_fmt):
'''Transform a bunch of columns (containing arbitrary data: floats,
bools, ints, strings...) into an iterable of lists of (optionally
highlighted) strings representing the table **rows**.
Example:
>>> cols = [('European swallow', 'African swallow'),
... (27.7, 35.1),
... ('km/h', 'km/h')]
>>> highs = [[False, False], [False, True], [False, True]]
>>> for row in RstTable.format_columns(cols, highs, '{:13.8f}'):
... print(row)
['European swallow', ' 27.70000000', 'km/h']
['African swallow', ':hl:`35.10000000`', ':hl:`km/h`']
:param columns: An iterable yielding columns. The table columns may
contain any kind of data; if the data type is numeric,
it will be formatted according to the `num_fmt`
argument; other types will just get stringified
(:class:`str`).
:param highlights: An iterable yielding a collection of bools for each
column. The `j`-th element of the `i`-th iteration
decides whether the element in the `j`-th column of
the `i`-th row should be highlighted.
:param num_fmt: The format string to be used for numerical types.
:returns: a generator yielding lists of strings, representing the table
rows.
'''
def _format_val(val):
try:
type_ = val.dtype.type
except AttributeError:
return str(val)
if issubclass(type_, (float, np.inexact)):
return num_fmt.format(val)
return str(val)
for row, hlrow in zip(cls.transpose(columns),
cls.transpose(highlights)):
yield [cls.highlight(_format_val(val), high)
for val, high in zip(row, hlrow)]
[docs] @staticmethod
def compute_column_widths(headers, rows):
'''Compute the width of the columns that are necessary to accommodate
all the headers and table rows.
>>> headers = ['swallow sub-species', 'airspeed', 'laden']
>>> rows = [['European', '27.7 km/h', 'no'],
... ['European', '22.0 km/h', 'yes'],
... ['African', '35.1 km/h', 'no'],
... ['African', '26.2 km/h', 'yes']]
>>> RstTable.compute_column_widths(headers, rows)
[19, 9, 5]
>>> [len('swallow sub-species'), len('27.7 km/h'), len('laden')]
[19, 9, 5]
:param list(str) headers: A list of column headers.
:param list(list(str)) rows: A list of table rows.
:returns: a list of integers indicating how wide each columns must be
to accommodate all the table elements and the headers.
:rtype: list(int)
'''
col_widths = [len(header) for header in headers]
for row in rows:
for i, val in enumerate(row):
col_widths[i] = max(col_widths[i], len(val))
return col_widths
[docs] @classmethod
def highlight(cls, val, flag):
'''Wrap `val` in highlight reST markers if flag is `True`.
>>> RstTable.highlight('dis', False)
'dis'
>>> RstTable.highlight('DAT', True)
':hl:`DAT`'
'''
if flag:
return f':{cls.HIGHLIGHT_ROLE}:`{val.strip()}`'
return val
[docs] @classmethod
def concat_rows(cls, widths, rows, just='>'):
'''Concatenate the given rows and justify them according to the `just`
parameter (see the :ref:`python:formatspec`). The columns widths (for
justification) must be provided using the `width` argument.
:param list(int) widths: The list of columns widths.
:param list(list(str)) rows: The list of rows; each row must be a list
of strings.
:param str just: A format character for the justification (usually one
of '<', '^', '>').
:returns: the concatenated, justified rows.
:rtype: str
'''
for row in rows:
centered = list(f'{val:{just}{width}}'
for width, val in zip(widths, row))
yield cls.COL_SEP.join(centered)
[docs]class RstPlot:
'''This class models a plot in an `reStructuredText`_ document. It converts
a :class:`~.PlotTemplate` object into an :class:`~.MplPlot`, and it
provides the ``.. image::`` directive to include in the .rst file.
'''
[docs] def filename(self):
'''Make up a(n almost) unique filename for this plot.
:returns: the filename from fingerprint (png format)
:rtype: str
'''
return f'plot_{self.fingerprint}.png'
[docs]class RstText:
'''Construct an :class:`RstText` from the given
:class:`~.templates.TextTemplate`.
'''
[docs]class FormattedRst:
'''This class represents a formatted rst document tree, which typically
consists of an index file and of several sections.
'''
[docs] def __init__(self, *, author, title, version,
tree_dict, text_dict, plots, n_workers=None):
# pylint: disable=too-many-arguments
'''Create a :class:`FormattedRst` object. The `author`, `title` and
`version` arguments are expected to be strings and are
self-explanatory.
The `tree_dict` and `text_dict` arguments must be dictionaries. The
`tree_dict` dictionary represents the tree structure of the
`reStructuredText`_ document, and the `text_dict` represent the
contents of each section. The report sections, which appear as keys in
both dictionaries, are expected to be tuples of strings, with each
string representing an additional layer in the document hierarchy. The
section contents (the values of `text_dict`) are expected to be lists
of strings to be written to disk.
Finally, the `plots` argument is a list of the plots referenced by the
text sections. The plots will be written to disk with filenames of the
form :samp:`plot_{fingerprint}.png`, where `fingerprint` is the plot
fingerprint.
:param str author: the author of this report.
:param str title: the title of this report.
:param str version: the version number for this report.
:param dict tree_dict: dictionary mapping tuples of sections to lists
of tuples of sections.
:param dict text_dict: dictionary mapping tuples of sections to lists
of strings.
:param plots: list of plots to be written to disk.
:type plots: list(MplPlot)
:param n_workers: number of subprocesses to use to write out the plots
in parallel. If `None` is given, write the plots in sequential
mode.
:type n_workers: int or None
'''
if not isinstance(tree_dict, dict):
raise TypeError("expecting a dictionary as 'tree_dict'"
f", got {type(tree_dict)}")
if not isinstance(text_dict, dict):
raise TypeError("expecting a dictionary as 'text_dict'"
f", got {type(tree_dict)}")
self.author = author
self.title = title
self.version = version
self.tree_dict = tree_dict.copy()
self.text_dict = text_dict.copy()
self.plots = plots.copy()
self.n_workers = n_workers
[docs] def write(self, path):
'''Write the text files and the plots into the directory specified by
`path`.
:param path: path to the directory that will be written to. It is OK if
the directory does not exist.
:type path: str or pathlib.Path
'''
path = Path(path)
self.setup(path)
self._write_rec(tree=(), path=path)
items = [(plot, str(path / 'figures'
/ f'plot_{fingerprint}.png'))
for fingerprint, plot in self.plots.items()]
if self.n_workers is not None:
LOGGER.info('writing %d plots using %d subprocesses',
len(items), self.n_workers)
with mp.Pool(self.n_workers) as pool:
pool.map(self._static_writer, items)
else:
LOGGER.info('writing %d plots in sequential mode', len(items))
for item in items:
self._static_writer(item)
@staticmethod
def _static_writer(plot_path_pair):
plot_path_pair[0].save(plot_path_pair[1])
def _write_rec(self, *, tree, path):
if tree:
tree_path = self.tree_to_path(base=path, tree=tree)
else:
tree_path = path / 'index'
LOGGER.note('writing tree_path: %s', tree_path)
ensure(tree_path.parent, is_dir=True)
subtrees = self.tree_dict[tree]
write_path = tree_path.with_name(tree_path.name + '.rst')
with write_path.open('w') as tree_file:
text = '\n'.join(self.text_dict[tree])
tree_file.write(text)
if subtrees:
toc = self.toc('Contents', subtrees)
tree_file.write(toc)
for subtree in subtrees:
self._write_rec(tree=subtree, path=path)
[docs] @staticmethod
def tree_to_path(*, base, tree):
'''Convert a tree to a file path.
:param base: the base path for all subtrees.
:type base: pathlib.Path
:param tuple(str) tree: a sequence of tree nodes, starting from the
tree root.
'''
paths = [sanitize_filename(node) for node in tree]
return base.joinpath(*paths)
[docs] def toc(self, toc_title, subtrees):
'''Build an rst table of contents.
:param str toc_title: the title for the table of contents (e.g.
``'Contents'``).
'''
lines = ['\n\n.. toctree::\n :titlesonly:\n '
f':caption: {toc_title}:\n']
for subtree in subtrees:
# need to remove upper level to subtree in toc else they appear N
# times in the toc, depending on level, breaking the links
fname = str(self.tree_to_path(base=Path(''), tree=subtree[-2:]))
line = ' '*4 + fname
lines.append(line)
lines.append('')
lines.append('')
return '\n'.join(lines)
[docs] def setup(self, path):
'''Set up the output directory for :meth:`write`.
:param pathlib.Path path: the path to the directory.
'''
ensure(path, is_dir=True)
ensure(path / '.static', is_dir=True)
ensure(path / '.templates', is_dir=True)
ensure(path / 'figures', is_dir=True)
year = datetime.now().year
self.configure(resource='conf.py.template', dest=path / 'conf.py',
author=self.author, project=self.title,
version=self.version, theme='alabaster',
theme_options={}, year=year)
self.configure(resource='valjean.css',
dest=path / '.static' / 'valjean.css',
formatting=False)
[docs] @staticmethod
def configure(resource, dest, formatting=True, **kwargs):
'''Copy a package resource to the specified destination, optionally
formatting the resource content using :meth:`str.format`.
For more information about resources, see `pkg_resources`_.
:param str resource: the name of the resource.
:param pathlib.Path dest: the destination path.
:param bool formatting: whether formatting should be applied.
:param dict kwargs: any additional keyword arguments will be passed to
the formatting.
'''
assert pkg.resource_exists('valjean.javert.resources.rst', resource)
res_template = pkg.resource_string('valjean.javert.resources.rst',
resource).decode('utf-8')
res_str = res_template.format(**kwargs) if formatting else res_template
with dest.open('w') as res_file:
res_file.write(res_str)
[docs]class RstTestReportTask(PythonTask):
'''Task class that transforms a list of tests into a test report.
:class:`~.TestResult` objects in the environment.'''
[docs] @classmethod
def from_tasks(cls, name, *, make_report, eval_tasks, representation,
author, version, kwargs=None, deps=None, soft_deps=None):
# pylint: disable=too-many-arguments
'''Construct an :class:`RstTestReportTask` from a list of test
evaluation tasks and a function to classify test results and put them
in test reports.
'''
report_name = 'report-' + name
report_task = TestReportTask(report_name, make_report=make_report,
eval_tasks=eval_tasks, kwargs=kwargs)
return cls(name, report_task=report_task,
representation=representation, author=author,
version=version, deps=deps, soft_deps=soft_deps)
[docs] def __init__(self, name, *, report_task, representation, author, version,
deps=None, soft_deps=None):
# pylint: disable=too-many-arguments
def write_rst(*, env, config):
report = env[report_task.name]['result']
if 'args' in config and 'workers' in config['args']:
n_workers = config['args']['workers']
LOGGER.debug('will use %d subprocesses to write the report',
n_workers)
else:
n_workers = None
LOGGER.debug('will write the report in sequential mode')
rst = Rst(representation, n_workers=n_workers)
fmt_report = rst.format_report(report=report, author=author,
version=version)
report_root = Path(config.query('path', 'report-root'))
report_path = report_root / sanitize_filename(self.name)
ensure(report_path, is_dir=True)
fmt_report.write(report_path)
env_up = {self.name: {'result': fmt_report,
'output_dir': str(report_path)}}
return env_up, TaskStatus.DONE
deps = [] if deps is None else deps
deps.append(report_task)
super().__init__(name, write_rst, deps=deps, soft_deps=soft_deps,
env_kwarg='env', config_kwarg='config')