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 __init__(self, plot): self.fingerprint = fingerprint(plot) self.mpl_plot = MplPlot(plot)
[docs] def __str__(self): return f'.. image:: /figures/{self.filename()}\n'
[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] def __init__(self, text): self.text = text
[docs] def __str__(self): return self.text.text
[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')