"""
The `Context` class and related functions which form the backbone of the
testing process.

contexts.py

A `Context` represents a specific situation derived from
other contexts and ultimately submitted and/or solution code.
`potluck.rubrics.Goal` objects actually evaluate things, but they depend
on one or more contexts to provide material to be evaluated.

For example, a typical unit test consists of a
`potluck.rubrics.ComparisonTest` which runs a checker function on the
'value' and 'ref_value' context keys in a `Context` which is created by
running the function to test with particular arguments (it will run the
submitted function to generate a 'value' entry and the solution function
to generate a `ref_value` entry). This `Context` will in turn depend on
another `Context` which loads the submitted and solution modules so that
they are ready for testing, etc.

In general, `Context` objects will form a directed acyclic graph of
dependencies, and because they cache their results, duplicate work is
avoided. `Context` objects also have user-facing text to describe how
they are derived, as well as the ability to explain their results to the
user.

Although `Context` objects represent the abstract idea of a certain test
result, the actual value of that result is stored in a context
dictionary, which is created and cached by the `Context` and supplied to
`Goal` objects on-demand.
"""

import copy
import os
import json
import time
import re

from . import html_tools
from . import phrasing
from . import load
from . import patterns
from . import mast
from . import context_utils


#---------#
# Globals #
#---------#

RELEVANT_FILENAME = None
"""
A string (or None before it gets set) which holds the file name given to
the most recent instantiation of a `FileContext`, which is thus also
almost certainly the filename that will be used in any new contexts that
are created (unless hijinks have ensued).
"""


#----------------------------------------#
# Context class and associated functions #
#----------------------------------------#

def show_all_context_values(context):
    """
    The default display_product function for a Context, this creates a
    separate details node for each context key and puts a string
    representation of the value for that key in an inner <pre> tag.

    Generally when a context builder creates only a single key, you
    should use the output of build_context_value_displayer with that
    specific key as an argument instead.

    This function returns an HTML code string.
    """
    result = ""
    for key in context:
        if isinstance(context[key], str):
            rep = context[key]
        else:
            try:
                rep = json.dumps(context[key], indent=2)
            except TypeError:
                rep = repr(context[key])
        result += f"""
<details class='context-item'>
  <summary>{key}</summary>
  <pre>{rep}</pre>
</details>
"""
    return result


class Context:
    """
    Represents some kind of product of submitted code, like an
    AST tree, or produced output. Contexts can specialize each other, so
    for example a regular expression could be used to transform a context
    containing all output from a program into a context containing just a
    single line of output. Context objects organize context_builder
    functions into hierarchies, and manage caching of their results.

    A `Context` object also needs to know how to render its results into
    HTML to display to the user as part of the feedback process.

    A `Context` object, via its context_builder function, knows how to
    produce, on demand, a dictionary mapping context keys to values.
    The base context (and therefore all derived contexts) will always
    include the following `potluck.context_utils.BASE_CONTEXT_SLOTS`:

        - "task_info": A task info dictionary for the current task, which
            will have a "specification" key containing the specification
            module for this task.
        - "username": The user who submitted the code we're evaluating.
        - "submission_root": The root directory within which the
            submission we're evaluating can be found.
        - "default_file": The official name of the default file to evaluate.
        - "actual_file": The actual name of the default file as submitted.

    The typical slots of derived context dictionaries include:

        - "filename": The file name (without path) of the file we're
            evaluating.
        - "file_path": The full path to the file we're evaluating.
        - "source": A string containing the source code of the submitted
            module.
        - "parse_errors": A list of Exception objects which were
            generated but ignored when parsing the submitted code.
        - "defs": A mapping from function names to AST nodes covering
            every `def` node in the submitted code (but note that
            functions with identical names will shadow each other in this
            map).
        - "scope": an AST node to check within (e.g., for a function
            definition check). See ImplementationCheck for an example of
            how these contexts are created and used.
        - "docstrings": A mapping from function names to documentation
            strings for those functions.
        - "top_scope": The top-level AST node for the submitted code.
        - "module": a loaded (submitted or solution) module
            (for a suite of tests that relies on the code).
        - "output": a string containing output from a function call.
            This may be the full output, or may be filtered by additional
            Context objects to contain just part of the full output.
        - "value": arbitrary Python value returned from a function call.
        - "trace": a trace list recording certain function calls made
            during a test, along with arbitrary state snapshots (see
            `potluck.harness.tracing_function_calls`).
        - "image_info": an image info dictionary that include a filename
            which points to an image generated by a function call.
        - Other keys not defined here may be established and/or used by
            certain Context or Goal classes.
        - Versions of the keys above with "ref_" as a prefix hold
            equivalent values derived from solution instead of submitted
            code.

    Note: For various reasons, the context building process creates
    shallow copies of contexts, not deep copies (the context values, such
    as modules, are often not deep-copyable). Accordingly, it is possible
    for changes to some sub-component of a context to be seen by other
    places where that context is being used, and thus YOU SHOULD NEVER
    MUTATE A CONTEXT VALUE. Context builder functions may safely
    synthesize new values based on old ones, and may freely update
    context keys with new values, but should not mutate the values under
    those keys.
    """
    def __init__(
        self,
        description=(
            "UNSPECIFIED TOPIC",
            "UNSPECIFIED DETAILS",
            "UNSPECIFIED FULL TOPIC",
            "UNSPECIFIED FULL DETAILS"
        ),
        builder=None,
        display_product=show_all_context_values,
        depends=None,
        failure_explanation=None,
        cache_values=True,
        base=None,
        hidden=False
    ):
        """
        You must supply a description pair (topic + details), triple
        (topic + details + feedback topic), or quad (topic + details +
        feedback topic + feedback details). Each item must be an HTML
        string to be displayed to the students as part of the rubric; the
        feedback versions if provided are used instead of the originals
        when generating a feedback document as opposed to a blank rubric.

        You must also supply a context_builder function, to be run when
        this context is required. If depends is supplied, it should be a
        list of Context objects, and before this context is established,
        each of those contexts will be created and merged in turn, to
        establish the prev_context argument to the context_builder
        function.

        You may supply a failure_explanation, which can be either a
        string, or a function that will be given the context in which the
        failure occurred and an Exception object (with an html_tb
        attached) and expected to return a string. This will be used to
        set the error message for a ContextCreationError thrown when the
        context_builder crashes, and that can ultimately become part of
        an explanation for a failed goal.

        If cache_values is given as False, the context_builder function
        will be re-run every time this context is requested, but by
        default, the result will be run only when one of our dependencies
        has a newer timestamp than our cached value.

        To supply seed values to bootstrap context creation, a Context
        may have a base value, which is used to start the
        dependency-merging process to provide a context for its builder.

        If hidden is given as True, this Context will not show up in the
        Contexts list, and will only be visible as a context associated
        with a goal when that goal is evaluated (use with care).
        """
        self.description = description
        if builder is None:
            raise ValueError(
                "Must specify a context builder when creating a Context."
            )
        self.builder = builder
        self.display_product = display_product
        self.depends = depends or []
        self.failure_explanation = failure_explanation
        self.cache_values = cache_values
        self.base = base or {}
        self.hidden = hidden

        self.cached_value = None
        self.cache_timestamp = None

    def __copy__(self):
        """
        Contexts may not be copied. They are entangled with each other,
        and we also don't want to create multiple copies which will
        duplicate the same work.
        """
        raise NotImplementedError("Contexts may not be copied.")

    def __deepcopy__(self, memo):
        """
        Contexts may not be copied (see __copy__).
        """
        raise NotImplementedError("Contexts may not be copied.")

    def changed_at(self):
        """
        Returns the timestamp of the most recent update to our cached
        context dictionary, so that contexts that depend on this one can
        know if they need to update themselves. May return None if no
        cached value has been created yet.
        """
        return self.cache_timestamp

    def clear_cache(self):
        """
        Removes cached info & resets cache timestamp.
        """
        self.cached_value = None
        self.cache_timestamp = None

    def burn_cache(self):
        """
        Clears our cache, and burns caches of our dependencies.
        """
        self.clear_cache()
        for dep in self.depends:
            dep.burn_cache()

    def create(self, base_context):
        """
        Creates and returns a context dictionary, using our builder
        function. If caching is enabled, a (shallow copy of a) cached
        value will be returned if we think that it was created after any
        changes in our dependencies.

        A base context is required, and should be a dictionary; normally
        it should have all the `potluck.context_utils.BASE_CONTEXT_SLOTS`
        already populated.
        """
        rerun = False
        if not self.cache_values:
            rerun = True
        else:
            # Check whether we even have a result yet:
            if self.cache_timestamp is None:
                rerun = True
            else:
                # Check timestamps of our dependencies
                for dep in self.depends:
                    when = dep.changed_at()
                    if when is None or when > self.cache_timestamp:
                        rerun = True
                        break

        if not rerun:
            # Return a shallow copy of our cached value
            result = copy.copy(base_context)
            result.update(self.cached_value)
            return result
        else:
            # Create new cached value and update our timestamp
            prev_context = copy.copy(base_context)
            prev_context.update(self.base)
            for dep in self.depends:
                try:
                    prev_context.update(dep.create(base_context))
                except Exception as e:
                    e.html_tb = html_tools.html_traceback()
                    raise context_utils.ContextCreationError(
                        self,
                        "Dependency failed.",
                        e
                    )
            try:
                our_results = self.builder(prev_context)
                prev_context.update(our_results)
                self.cached_value = prev_context
            except Exception as e:
                e.html_tb = html_tools.html_traceback()
                if isinstance(self.failure_explanation, str):
                    msg = self.failure_explanation
                elif self.failure_explanation:
                    msg = self.failure_explanation(prev_context, e)
                else:
                    msg = "Test setup failed."
                raise context_utils.ContextCreationError(self, msg, e)
            self.cache_timestamp = time.time()
            # Return shallow copy of cached value
            result = {}
            result.update(self.cached_value)
            return result

    def deps_are_a_stick(self):
        """
        Returns true if the transitive dependencies of this context form
        a stick, not a real tree (i.e., each context, including this one,
        has exactly 1 or 0 dependencies).
        """
        return (
            len(self.depends) in (0, 1)
        and all(dep.deps_are_a_stick for dep in self.depends)
        )

    def rubric_topic(self):
        """
        Gets the rubric version of this Context's topic.
        """
        return self.description[0]

    def rubric_details(self):
        """
        Gets the rubric version of this Context's details.
        """
        return self.description[1]

    def feedback_topic(self):
        """
        Gets the feedback version of this Context's topic, or just the
        normal topic if there is no feedback version.
        """
        return self.description[::2][-1]

    def feedback_details(self):
        """
        Gets the feedback version of this Context's details, or just the
        normal details if there is no feedback version.
        """
        return self.description[1::2][-1]

    def html_topic(self, in_feedback=False):
        """
        Returns an HTML string representing just this context object as a
        div.topic, without including information about dependencies. If
        in_feedback is given as True, the feedback version of the topic
        and details is shown instead of the normal (rubric) version.

        Details are included behind a help button.
        """
        if in_feedback:
            topic = self.feedback_topic()
            details = self.feedback_details()
        else:
            topic = self.rubric_topic()
            details = self.rubric_details()
        return '<div class="topic">{}</div>'.format(
            topic + ' ' + html_tools.create_help_button(details)
        )

    def html_context_tree(self, in_feedback=False):
        """
        Produces an HTML string which shows this context and those that
        it depends on in a tree structure, with dependencies nested
        inside the contexts that depend on them. In the special case of a
        stick, a different format is used.

        If in_feedback is given as true, the feedback topic and details
        values from the description are used if present, instead of the
        normal (rubric) topic and details.
        """
        if self.deps_are_a_stick():
            if self.depends:
                dep_chain = (
                    '(depends on <div class="context_depends">{}</div>)'
                ).format(
                    self.depends[0].html_context_tree(in_feedback)
                    # there is only one
                )
                return (
                    '<details class="context">\n<summary>{}</summary>\n'
                  + '{}\n</details>'
                ).format(
                    self.html_topic(in_feedback),
                    dep_chain
                )
            else:
                return '<div class="context">\n{}\n</div>'.format(
                    self.html_topic(in_feedback)
                )
        else:
            # Dependencies are a tree...
            dep_entries = '<br>\n'.join(
                dep.html_context_tree(in_feedback)
                for dep in self.depends
            )
            depends_full = (
                '<div class="context_depends">\n'
              + 'Depends on:<br>\n'
              + '{}\n'
              + '</div>'
            ).format(dep_entries) if self.depends else ''
            return (
                '<details class="context">\n<summary>{}</summary>\n'
              + '{}\n</details>'
            ).format(
                self.html_topic(in_feedback),
                depends_full
            )

    def html_representation(self, base_context):
        """
        Builds an HTML representation of this context's result using the
        display_product function. A base context is required because
        create is used to fetch the current value (or build a new one).
        """
        try:
            return self.display_product(self.create(base_context))
        except context_utils.ContextCreationError as e:
            return f"""\
<div class="context_error">
  <h3>An error was encountered while attempting to run this test.</h3>
<pre>
{e}
</pre>
</div>
"""


def build_context_graph(all_context_objs):
    """
    Takes a list of Context objects and traces dependencies to build a
    top-down rather than bottom-up graph. The graph consists of a list of
    top-level node dictionaries with the following keys:

        context: The Context object for this node.
        children: A list containing pointers to the node dictionaries of
            each Context that depends on this one.
        parents: A list containing pointers to the node dictionaries of
            each Context that this one depends on.
        level: An integer indicating the longest chain of ancestors that
            this node has (0 for nodes without dependencies).

    The exact same node dictionary may appear as a child and/or parent of
    multiple other nodes.

    The ordering of children will be the same as the ordering of context
    dependencies, while the ordering of the top-level nodes will match
    the order that they occur within the given all_context_objs list.
    """

    dict_cache = {}

    def dict_for(cobj):
        """
        Returns the dictionary for the given Context object, either by
        creating it or remembering one already constructed during the
        current construction process. For new encounters, the children
        value will be an empty list.
        """
        nonlocal dict_cache
        if id(cobj) in dict_cache:
            return dict_cache[id(cobj)]
        else:
            result = { "context": cobj, "children": [], "parents": [] }
            dict_cache[id(cobj)] = result
            return result

    nodeps = []
    for ctx in all_context_objs:
        d = dict_for(ctx)
        if ctx.depends:
            for dep in ctx.depends:
                pd = dict_for(dep)
                pd["children"].append(d)
                d["parents"].append(pd)
        else:
            nodeps.append(d)

    # Loop again and assign "level" values now that the graph is complete
    for ctx in all_context_objs:
        d = dict_for(ctx)
        assign_cgraph_level(d)

    return nodeps


def assign_cgraph_level(node):
    """
    Determines the level of a node in a contexts graph, which is 0 for
    nodes without parents, and one plus the highest parent level for all
    other nodes.

    Store this value in the "level" slot of the node, and does the same
    for any ancestor nodes encountered.

    Returns an already-computed level if there is one, or else returns
    the level value that it assigns to the node.
    """
    if "level" in node:
        return node["level"]
    else:
        if len(node["parents"]) == 0:
            node["level"] = 0
        else:
            mp = 0
            for pn in node["parents"]:
                pl = assign_cgraph_level(pn)
                if pl > mp:
                    mp = pl
            node["level"] = mp + 1

        return node["level"]


def render_context_graph(cgraph, in_feedback=False):
    """
    Renders a context graph (see build_context_graph) into HTML.

    The in_feedback argument controls whether context items should show
    their full or redacted versions.
    """
    result = '<ol class="context_list">'
    for cnode in cgraph:
        result += '\n<li>{}</li>'.format(
            html_tools.build_html_details(
                cnode["context"].html_topic(in_feedback),
                render_context_graph(cnode["children"], in_feedback)
            )
            if cnode.get("children")
            else cnode["context"].html_topic(in_feedback)
        )

    result += '\n</ol>' # close .context_list ol
    return result


def list_and_render_contexts(cgraph, base_context=None):
    """
    Transforms a context graph (see build_context_graph) into a list of
    context summaries, which are dictionaries with the following keys:

        description: An HTML code string that describes the context item,
            including a detailed description behind a help button.
        depends: A list containing integer indices into the entire
            context list of the other contexts on which this context
            depends. Will be empty for contexts without dependencies.
        level: An integer specifying how far this context should be
            indented, which will be one greater than the maximum level of
            contexts that this one depends on, starting with 0 for
            contexts that have no dependencies.
        value: An HTML code string that displays or summarizes the value
            produced by this context. This will be the result of the
            context's display_product function run on the most recent
            result of that context (or on a fresh result if caching is
            disabled for the context).

    The base_context argument is used to generate context results (will
    only happen if cached results are not available or caching is
    disabled for one or more contexts). If omitted or specified
    explicitly as None, the result will have empty strings in each
    "value" slot, and the descriptions will use redacted topic and
    detail values; use this when generating a list for a rubric rather
    than for a feedback document.

    The contexts in the list will be ordered according to their original
    order in a depth-first traversal of the given context graph with
    contexts that are children of multiple higher-level contexts
    appearing only once under the *first* context that they depend on.
    """
    result = []

    # Produce a flat list of nodes in depth-first order
    nodeslist = []

    def visit(node):
        nonlocal nodeslist
        nodeslist.append(node)
        for child in node["children"]:
            visit(child)

    # Iterate over top-level nodes to produce the nodes list
    for topnode in cgraph:
        visit(topnode)

    # A mapping from node IDs to their indices in the nodeslist
    indexmap = {
        id(cnode): idx
        for idx, cnode in enumerate(nodeslist)
    }

    # Iterate over all nodes in order
    for cnode in nodeslist:
        result.append(
            {
                "description": cnode["context"].html_topic(
                    base_context is not None
                ),
                "depends": [
                    indexmap[id(child)]
                    for child in cnode["children"]
                ],
                "level": cnode["level"],
                "value": (
                    cnode["context"].html_representation(base_context)
                    if base_context is not None
                    else ""
                ),
            }
        )

    return result


#-----------------------------#
# Automatic Context Mechanism #
#-----------------------------#

class AutoContext(Context):
    """
    A `AutoContext` provides a way to automatically create dependencies
    on common contexts without saving them in variables that have to get
    passed around everywhere. When a `AutoContext` is created, it
    registers itself as the current provider for certain context slots,
    overwriting any previously-registered provider for those slots.
    Then, another context may use the `auto` function to create a list
    of `Context` objects to use as dependencies based on the slots it
    needs; this list will be populated with the set of
    most-recently-registered `AutoContext` objects for each slot
    requested.

    In addition to inheriting from `AutoContext`, `Context`s which
    should be automatic must call their own `register` method and
    provide it with one or more slot name strings as arguments.
    """
    _registry = {}
    _on_demand = {}

    def reset(relevant_filename=None):
        """
        This is a class method which resets the automatic contexts
        registry, erasing all registered `Context` objects. Used prior
        to loading a new task spec.

        Note that this does not reset the on-demand factory functions
        registry.

        A `relevant_filename` may be provided to set the global
        RELEVANT_FILENAME variable; setting this to the default filename
        for the task we're about to load is helpful since it prevents the
        spec from treating things created after an explicit-default file
        context as different from things created before any file contexts
        have been declared. If no `relevant_filename` is provided, the
        global will be set back to its default of None.
        """
        global RELEVANT_FILENAME
        RELEVANT_FILENAME = relevant_filename
        AutoContext._registry = {}

    def on_demand(factory, *slots):
        """
        This class method registers a given factory function as the
        on-demand provider of one or more slots.

        The factory function must be able to run with no arguments and
        must produce a `Context` object which can create the requested
        slot(s).

        Essentially, if `auto` is used to request an automatic context
        for a slot, but no such context has been registered, an
        on-demand factory function may be called to construct such a
        context automatically in some simple cases. Calling this
        function a second time re-using an old slot value will overwrite
        the factory function for that slot.
        """
        for slot in slots:
            AutoContext._on_demand[slot] = factory

    def register(self, *slots):
        """
        Registers this auto context as the current provider for one or
        more slots (named using strings). Subsequent calls to `auto`
        that include one or more of those slots will include this object
        in the resulting list.

        The slots for which a context registers itself are stored in the
        context's `_provides` attribute.
        """
        for slot in slots:
            AutoContext._registry[slot] = self

        self._provides = slots

    def refresh(self):
        """
        Any `AutoContext` may call this method to re-register itself as
        the current provider of all of its relevant slots. The slots to
        register under are remembered from the initial call to
        `register`.
        """
        self.register(*self._provides)


def auto(*slots):
    """
    This function returns a list of `Context` objects, suitable for use
    as all or part of a `depends` value for the creation of a new
    `Context`, and/or as all or part of a `test_in` value for the
    creation of a new `potluck.rubrics.Goal`. It does this by looking for
    the most recent `AutoContext` objects that have registered themselves
    as providers of the given slot or slots (these are just strings).

    If no `AutoContext` has registered itself for a given slot, but
    there is an on-demand factory registered for that slot, the factory
    function will be called to generate a `Context` to use as a
    dependency (which should also end up registering the resulting
    `Context` under the appropriate slot(s)).

    If multiple slots are given and one `Context` is registered under
    several of them, that `Context` will only appear in the resulting
    list once. Likewise, if an on-demand factory function creates a
    `Context` which registers itself for several slots, and one or more
    other slots in that list are also being requested, the factory
    function will not be re-run for the subsequent slots, and the
    resulting `Context` will only appear in the result list once.

    If no slots are given, this function returns an empty list.

    If a slot is requested for which there is no currently-registered
    `AutoContext` and for which there is no registered on-demand
    `Context` factory, an `ContextError` will be raised.
    """
    result = []
    for slot in slots:
        if slot in AutoContext._registry:
            match = AutoContext._registry[slot]
            if match not in result:
                result.append(match)
        elif slot in AutoContext._on_demand:
            created = AutoContext._on_demand[slot]()
            if created not in result:
                result.append(created)
        else:
            raise context_utils.ContextError(
                f"Automatic context request for slot '{slot}' could not"
                f" be fulfilled because no AutoContext was registered"
                f" for that slot, and no on-demand Context factory"
                f" function was available for that slot either."
            )

    return result


#-------------------------#
# Derived Context classes #
#-------------------------#


class FileContext(AutoContext):
    """
    Establishes a 'filename' context slot that holds the name of a
    specific file being evaluated. By default, the created `Context` has
    no dependencies and is hidden.

    The filename provided should be relative to the submission root,
    which is either the directory where the target file exists, or the
    target directory for multi-file submissions. If no target file is
    identified, the default_file context slot value is used to target
    the submission's default file.

    Establishes 'ref_*' slots for each normal slot it establishes.
    """
    def __init__(self, target_file=None, depends=None, hidden=True):
        """
        A target_file string specifies the path to the target file that
        this context will focus evaluation on, relative to the
        submission root. If not provided, the "default_file" context
        slot value will be used.

        Both 'filename' and 'file_path' context slots will be
        established by this builder; the former holds just the file (or
        directory) name of the target file, while the latter holds a
        full path to the file.

        It's up to other `Context`s to make use of the slots established
        by this one.
        """
        global RELEVANT_FILENAME
        # Set the relevant filename global, or fetch it
        if target_file is not None:
            RELEVANT_FILENAME = target_file
        elif RELEVANT_FILENAME is not None:
            target_file = RELEVANT_FILENAME

        self.target_file = target_file

        # First, create our context-builder function for this instance
        def establish_context(prev_context):
            """
            Establishes 'filename' and 'file_path' slots based on the
            target_file value given to the `FileContext` that this is
            the context builder function for. Also establishes
            'ref_filename' and 'ref_file_path' slots pointing to the
            equivalent file in the solution code.
            """
            task_info = context_utils.extract(prev_context, "task_info")
            soln_root = task_info["specification"].soln_path
            submission_root = context_utils.extract(
                prev_context,
                "submission_root"
            )
            # Revise our description when we build our context if we're
            # forced to fetch the default file:
            file_target = self.target_file
            default_filename = context_utils.extract(
                prev_context,
                "default_file"
            )
            if file_target is None:
                file_target = default_filename
                self.description = (
                    f"File '{file_target}'",
                    f"We will evaluate your submitted file '{file_target}'.",
                )
            if file_target == default_filename:
                actual_filename = context_utils.extract(
                    prev_context,
                    "actual_file"
                )
                full_target = os.path.join(
                    submission_root,
                    actual_filename
                )
                # Even if we're grading a custom-named file, the solution
                # won't have a custom name...
                ref_target = os.path.join(soln_root, default_filename)
                target_filename = default_filename
            else:
                # TODO: How can we properly handle ref targets for
                # custom-named files here?
                full_target = os.path.join(submission_root, file_target)
                ref_target = os.path.join(soln_root, file_target)
                _, target_filename = os.path.split(full_target)
            if not os.path.exists(submission_root):
                # If the submission root directory is missing...
                raise context_utils.ContextCreationError(
                    self,
                    (
                        f"Submission root directory"
                        f" '{submission_root}' does not exist."
                    )
                )
            elif not os.path.exists(full_target):
                # If the target file is missing
                raise context_utils.ContextCreationError(
                    self,
                    (
                        f"Target submission file"
                        f" '{full_target}' does not exist."
                    )
                )
            elif not os.path.exists(ref_target):
                # If there is no equivalent solution file
                raise context_utils.ContextCreationError(
                    self,
                    f"No solution file '{ref_target}' is available."
                )
            # else both submission root and full target exist

            return {
                "filename": target_filename,
                "file_path": full_target,
                "ref_filename": target_filename,
                "ref_file_path": ref_target
            }

        # Now we can call the super-class init with the context builder
        # function we just created.
        super().__init__(
            description=(
                f"File '{target_file}'",
                f"We will evaluate your submitted file '{target_file}'."
            ),
            builder=establish_context,
            display_product=lambda context: (
                f"Evaluating '{context['filename']}'"
            ),
            depends=depends,
            hidden=hidden
        )

        # Finally, we can register ourselves as an auto-context for the
        # "filename" and "file_path" slots.
        self.register(
            "filename",
            "file_path",
            "ref_filename",
            "ref_file_path"
        )


# Register a factory for a default FileContext as an on-demand option
# for "filename" and "file_path" slots:
AutoContext.on_demand(
    (lambda: FileContext()),
    "filename", "file_path", "ref_filename", "ref_file_path"
)


class CodeContext(AutoContext):
    """
    Requires "filename" and "file_path" slots (see `FileContext`), and
    establishes a "source" slot which contains the raw text of the
    target file, along with a "scope" slot which contains the parsed AST
    from the code.

    If the code cannot be parsed due to a `SyntaxError` or the like, a
    `ContextCreationError` will be generated naming the parsing error as
    its cause, although note that `potluck.load.fix_parse` is used which
    will attempt to steamroll some kinds of parsing errors while
    generating associated warnings.
    """
    # TODO: Create a default no-parse-errors Goal which uses the
    # parse_errors context key...
    def __init__(self, depends=None, hidden=False, prep=None):
        """
        Dependencies are optional; if not specified `auto` will be used
        to fill them in. `hidden` may be provided; by default this
        context is not hidden. A `prep` function may be provided; it will
        be applied to the source code string and its result will be used
        instead of the original source.
        """
        # First, create our context builder
        def establish_context(prev_context):
            """
            Establishes the following context slots based on the
            "file_path" slot, by reading the indicated file:

                original_source: The raw file contents.
                source: Possibly-edited (to steamroll syntax errors or by
                    a prep function) file contents.
                scope: An AST module node resulting from parsing the
                    modified file contents.
                top_scope: Same as above (but not designed to be
                    modified).
                parse_errors: A list of Exception objects that were
                    'successfully' steamrolled by editing the source code.
            """
            filename = context_utils.extract(prev_context, "filename")
            target = context_utils.extract(prev_context, "file_path")
            with open(target, 'r', encoding="utf-8") as fin:
                original_source = fin.read()

            if prep:
                source = prep(original_source)
            else:
                source = original_source

            try:
                fixed, node, errors = load.fix_parse(source, filename)
            except Exception as e:
                raise context_utils.ContextCreationError(
                    self,
                    f"Unable to parse submitted file '{filename}'.",
                    cause=e
                )

            return {
                "original_source": original_source,
                "source": fixed,
                "scope": node,
                "top_scope": node,
                "parse_errors": errors
            }

        # Figure out if we need to use automatic dependencies:
        if depends is None:
            depends = auto("filename", "file_path")

        # Now we can call the super constructor
        super().__init__(
            description=(
                "Code in the target file",
                (
                    "We will parse the code in the target file and pay"
                    "attention to how it was written."
                ),
                "Code in the target file",
                (
                    "We parsed the code in the target file and paid"
                    "attention to how it was written."
                ),
            ),
            builder=establish_context,
            display_product=lambda context: (
                f"The code for '{context['filename']}' (shown elsewhere)."
            ),
            depends=depends,
            hidden=hidden
        )

        # Finally, register ourselves as an auto provider for the slots
        # that we generate:
        self.register(
            "original_source",
            "source",
            "scope",
            "top_scope",
            "parse_errors"
        )


# Register a factory for a default CodeContext as an on-demand option
# for the slots it can generate.
AutoContext.on_demand(
    (lambda: CodeContext()),
    "original_source",
    "source",
    "scope",
    "top_scope",
    "parse_errors"
)


class SolnCodeContext(AutoContext):
    """
    Requires "ref_filename" and "ref_file_path" slots (see
    `FileContext`), and establishes a "ref_source" slot which contains
    the raw text of the equivalent file from the solution code, along
    with a "ref_scope" slot which contains the parsed AST from the
    solution code.

    "task_info" and "submission_root" slots are also required, but those
    should always be present.

    If the solution code cannot be parsed due to a `SyntaxError` or the
    like or because no equivalent solution file exists, a
    `ContextCreationError` will be generated naming the relevant error as
    its cause.
    """
    def __init__(self, depends=None, hidden=False):
        """
        Dependencies are optional; if not specified `auto` will be used
        to fill them in. `hidden` may be provided; by default this
        context is not hidden.
        """
        # First, create our context builder
        def establish_context(prev_context):
            """
            Establishes the following context slots based on the
            "ref_file_path" slot, by reading the solution version of the
            indicated file:

                ref_source: The source of the solution file.
                ref_scope: An AST module node resulting from parsing the
                    solution file.
                ref_top_scope: As above, but won't be modified.
            """
            soln_equivalent = context_utils.extract(
                prev_context,
                "ref_file_path"
            )
            ref_filename = context_utils.extract(
                prev_context,
                "ref_filename"
            )

            if not os.path.isfile(soln_equivalent):
                raise context_utils.ContextCreationError(
                    self,
                    f"Target file {soln_equivalent} does not exist in the"
                    f" solution directory."
                )

            with open(soln_equivalent, 'r', encoding="utf-8") as fin:
                contents = fin.read()

            try:
                node = mast.parse(contents, filename=ref_filename)
            except Exception as e:
                raise context_utils.ContextCreationError(
                    self,
                    f"Unable to parse solution file '{soln_equivalent}'.",
                    cause=e
                )

            return {
                "ref_source": contents,
                "ref_scope": node,
                "ref_top_scope": node
            }

        # Figure out if we need to use automatic dependencies:
        if depends is None:
            depends = auto("ref_filename", "ref_file_path")

        # Now we can call the super constructor
        super().__init__(
            description=(
                "Code in the solution file",
                "We will parse the code in the solution file.",
            ),
            builder=establish_context,
            display_product=lambda context: (
                f"The solution code for '{context['filename']}'"
                f" (available after the revision period is over)."
            ),
            depends=depends,
            hidden=hidden
        )

        # Finally, register ourselves as an auto provider for the slots
        # that we generate:
        self.register("ref_source", "ref_scope", "ref_top_scope")


# Register a factory for a default CodeContext as an on-demand option
# for the slots it can generate.
AutoContext.on_demand(
    (lambda: SolnCodeContext()),
    "ref_source", "ref_scope", "ref_top_scope"
)


class ModuleContext(AutoContext):
    """
    Requires a "top_scope" slot (see `CodeContext` which must hold an
    entire module's AST, and creates a "module" slot which holds the
    module object that results from running that code.
    """
    _from = "top_scope"
    _to = "module"
    _description = (
        "The values defined by the code.",
        (
            "We will run your code and so that we can run tests on the"
            " values it defines."
        )
    )

    def display_result(self, context):
        """
        Context result display function which lists names defined in the
        loaded module.
        """
        loaded = context[self._to]
        defined = [
            name
            for name in dir(loaded)
            if not name.startswith("__") or not name.endswith("__")
        ]
        if len(defined) == 0:
            return "No values were defined in the file."
        else:
            return (
                "The following values were defined in the file:\n"
              + html_tools.build_list(
                    "<code>{}</code>".format(name)
                    for name in defined
                )
            )

    def __init__(self, depends=None, hidden=False, prep=None, wrap=None):
        """
        Dependencies are optional; if not specified `auto` will be used
        to fill them in. `hidden` may be provided; by default this
        context is not hidden.

        `prep` may be supplied; it is a function which receives the
        current context dictionary and will be run before the module is
        loaded.

        `wrap` may be supplied; it is a function which will be given the
        module once it's loaded and its return value will be used instead
        of the original module.
        """
        # First, create our context builder
        def establish_context(prev_context):
            """
            Establishes the "module" context slot by executing the code
            in the "top_scope" slot. Actually, uses self._from and
            self._to to determine the slots to read/write, since it can
            also be used to create "ref_module" from "ref_top_scope".
            """
            # Fetch the AST node that we'd like to turn into a module
            node = context_utils.extract(prev_context, self._from)

            # Prefix the file name so that submitted and solution
            # modules with the same name don't collide
            filename = context_utils.extract(prev_context, "filename")
            if self._from == "top_scope":
                prefix = "subm_"
            elif self._from == "ref_top_scope":
                prefix = "soln_"
            else:
                prefix = "loaded_"
            full_name = prefix + filename

            # Run the prep function if one was supplied
            if prep is not None:
                prep(prev_context)

            # Actually load the module
            try:
                module = load.create_module_from_code(node, full_name)
            except Exception as e:
                raise context_utils.ContextCreationError(
                    self,
                    "Unable to run code.",
                    e
                )

            # Wrap our module result if necessary
            if wrap is not None:
                module = wrap(module)

            # Return our new slot
            return { self._to: module }

        # Figure out if we need to use automatic dependencies:
        if depends is None:
            depends = auto(self._from)
            # Our dependency on the "filename" slot is implicit

        # Now we can call the super constructor
        super().__init__(
            description=self._description,
            builder=establish_context,
            display_product=self.display_result,
            depends=depends,
            hidden=hidden
        )

        # Finally, register ourselves as an auto provider for the slots
        # that we generate:
        self.register(self._to)


# Register a factory for a default ModuleContext as an on-demand option
# for the "module" slot.
AutoContext.on_demand((lambda: ModuleContext()), "module")


class SolnModuleContext(ModuleContext):
    """
    Works like `ModuleContext`, but for the solution module: Requires a
    "ref_top_scope" slot (see `SolnCodeContext`) which must hold the
    solution module's AST, and creates a "ref_module" slot which holds
    the module object that results from running that code.
    """
    _from = "ref_top_scope"
    _to = "ref_module"
    _description = (
        "The values defined by the solution code.",
        (
            "We will run the solution code and so that we can compare"
            " its results to the results of your code."
        )
    )

    # Note: just by overriding these fields we've done all that we need
    # to to change where we're reading from and where we're putting our
    # results.


# Register a factory for a default SolnModuleContext as an on-demand
# option for the "ref_module" slot.
AutoContext.on_demand((lambda: SolnModuleContext()), "ref_module")


class DefinitionsContext(AutoContext):
    """
    Creates a "defs" slot based on a "top_scope" slot which holds a
    mapping from function names to AST nodes covering every `def` which
    occurs in the provided scope, including nested and method
    definitions.

    Note that nested definitions may shadow exterior definitions in the
    map if they have the same name. (TODO: Not that?)
    """
    _from = "top_scope"
    _to = "defs"

    def __init__(self, depends=None, hidden=False):
        """
        Dependencies may be supplied and the context may be hidden. If no
        dependencies are given, an auto-dependency for the "top_scope"
        slot will be generated.
        """
        def establish_context(prev_context):
            """
            This context_builder function depends on a "scope" context
            holding an AST node, and adds a "defs" context item which
            contains a dicitonary of all AST nodes in that scope which
            are function definitions (includes interior defs). The keys
            of the dictionary are the names of the functions. Lambdas
            are not included in the list.
            """
            within = context_utils.extract(prev_context, self._from)

            # Find definition AST nodes
            alldefs = set()
            for pat in patterns.ALL_DEF_PATTERNS:
                alldefs |= set(
                    node
                    for node, bindings in mast.findall(within, pat)
                )

            # Create the mapping
            defmap = {}
            for defn in alldefs:
                defmap[defn.name] = defn

            # Return our resulting slot
            return { self._to: defmap }

        # Figure out if we need to use automatic dependencies:
        if depends is None:
            depends = auto(self._from)

        # Now we can call the super constructor
        super().__init__(
            description=(
                "The function definitions in the code",
                (
                    "We will inspect the code and extract all of the"
                    " function definitions."
                )
            ),
            builder=establish_context,
            display_product=lambda context: (
                "The following functions were defined:\n"
              + html_tools.build_list(
                    f"<code>{name}</code>"
                    for name in context[self._to]
                )
            ),
            depends=depends,
            hidden=hidden
        )

        # Finally, register ourselves as an auto provider for the slot
        # that we generate:
        self.register(self._to)


# Register a factory for a default DefinitionsContext as an on-demand
# option for the "defs" slot.
AutoContext.on_demand((lambda: DefinitionsContext()), "defs")


class SolnDefinitionsContext(DefinitionsContext):
    """
    Works like `DefinitionsContext` but extracts a "ref_defs" slot from
    the "ref_top_scope" slot.
    """
    _from = "ref_top_scope"
    _to = "ref_defs"


# Register a factory for a default SolnDefinitionsContext as an on-demand
# option for the "ref_defs" slot.
AutoContext.on_demand((lambda: DefinitionsContext()), "ref_defs")


class DocstringsContext(AutoContext):
    """
    Establishes a "docstrings" slot based on "defs" and "module" slots,
    which contains a mapping from function names to their docstrings.
    This mapping will only include functions defined at the top level of
    the module.
    """

    def __init__(self, depends=None, hidden=False):
        """
        May have non-automatic dependencies and/or be hidden. If manual
        dependencies are provided, make sure they establish the "defs"
        and "module" slots.
        """
        def establish_context(prev_context):
            """
            This context_builder requires *both* a "defs" node (see
            `DefinitionsContext`) *and* a "module" node (see
            `ModuleContext`), because it makes use of both the AST and
            the actual imported module.

            It uses the defs map to figure out what functions to look
            for, and then for every function defined *at the top level*
            of the submitted code, it looks up the docstring from the
            module object, returning a mapping from function names to
            docstrings. If there are functions defined inside of other
            functions, they will not show up in the resulting
            "docstrings" context item.
            """
            defs = context_utils.extract(prev_context, "defs")
            submitted = context_utils.extract(prev_context, "module")

            docsmap = {}

            for fname in defs:
                fcn = getattr(submitted, fname, None)
                if fcn:
                    # this function is defined at the module level
                    doc = getattr(fcn, "__doc__", None) or ""
                    doc = doc.strip()
                    docsmap[fname] = doc

            return { "docstrings": docsmap }

        # Figure out if we need to use automatic dependencies:
        if depends is None:
            depends = auto("defs", "module")

        super().__init__(
            description=(
                "The function docstrings",
                (
                    "We will extract the docstrings from each function"
                    " defined by your code."
                )
            ),
            builder=establish_context,
            display_product=lambda context: (
                "The following docstrings were found:\n"
              + html_tools.build_list(
                    (
                        f"Function <code>{name}</code>:<br>"
                        f"<pre>{doc}</pre>"
                    )
                    for name, doc in context["docstrings"].items()
                )
            ),
            depends=depends,
            hidden=hidden
        )

        # Finally, register ourselves as an auto provider for the slot
        # that we generate:
        self.register("docstrings")


# Register a factory for a default DocstringsContext as an on-demand
# option for the "docstrings" slot.
AutoContext.on_demand((lambda: DocstringsContext()), "docstrings")


#--------------------------------------------------#
# Functions for displaying context builder results #
#--------------------------------------------------#

def build_context_value_displayer(
    key,
    compare_ref=True,
    include_diff=True,
    labels=["Your value", "Solution value", "Comparison"]
):
    """
    Creates a display_product function which will show the contents of a
    single specific context key, and by default, will include multiple
    tabs that show the value, the reference value, and a diff of the two
    values. String values are shown as-is; non-string values are
    converted to strings using json.dumps.

    Set compare_ref to False to simply show the value for the specified
    key, and set include_diff to False when compare_ref is True to omit
    the difference tab in the comparison.

    Custom labels for the two values and their difference (second and/or
    third labels may be ignored depending on other flags) may be given
    using the labels argument.

    Returns a function suitable for use as the display_product argument
    to a Context.
    """
    def display_context_value(context):
        """
        A function that returns HTML code which displays the value of a
        single specific context key, possibly with tabs to view the value
        produced by submitted code, the reference value, and the
        difference between the two (as a diff).

        See build_context_value_displayer, which created this function.
        """
        if not compare_ref:
            if key in context:
                # simply return a single <pre> containing a representation of
                # the value
                if isinstance(context[key], str):
                    rep = context[key]
                else:
                    try:
                        rep = json.dumps(context[key], indent=2)
                    except TypeError:
                        rep = repr(context[key])
                return f"<pre class='context-value'>{rep}</pre>"
            else:
                # failed to create the context key we're looking for!
                return (
                    f"<div class='context-missing'>Failed to establish"
                    f" context '{key}'!</div>"
                )
        else:
            if key in context:
                if isinstance(context[key], str):
                    rep = context[key]
                else:
                    try:
                        rep = json.dumps(context[key], indent=2)
                    except TypeError:
                        rep = repr(context[key])
                rep_html = f"<pre class='context-value'>{rep}</pre>"
            else:
                rep = ""
                rep_html = (
                    f"<div class='context-missing'>Failed to establish"
                    f" context '{key}'!</div>"
                )

            if "ref_" + key in context:
                if isinstance(context["ref_" + key], str):
                    ref_rep = context["ref_" + key]
                else:
                    try:
                        ref_rep = json.dumps(context["ref_" + key], indent=2)
                    except TypeError:
                        ref_rep = repr(context["ref_" + key], indent=2)
                ref_rep_html = f"<pre class='context-value'>{ref_rep}</pre>"
            else:
                ref_rep = ""
                ref_rep_html = (
                    f"<div class='context-missing'>Failed to establish"
                    f" context 'ref_{key}'!</div>"
                )

            if include_diff:
                # Include a tab for the differences
                diff = html_tools.html_diff_table(
                    rep,
                    ref_rep,
                    out_title=labels[0],
                    ref_title=labels[1]
                )
                return html_tools.build_html_tabs(
                    [
                        (labels[0], rep_html),
                        (labels[1], ref_rep_html),
                        (labels[2], diff),
                    ]
                )
            else:
                # No tab for the differences
                return html_tools.build_html_tabs(
                    [
                        (labels[0], rep_html),
                        (labels[1], ref_rep_html),
                    ]
                )

    return display_context_value


def build_simple_context_value_displayer(
    key,
    compare_ref=True,
    labels=["Your value", "Solution value"]
):
    """
    Creates a display_product function similar to the
    build_context_value_displayer result, but for simple values which
    don't need a <pre> wrapper and which can be displayed side-by-side
    (e.g., numbers). No diff is included, as it's presumed that any
    differences will be obvious, and values are converted to strings
    using str() instead of json.dumps.

    Set compare_ref to False to include only the main value.

    Custom labels for the two values may be given using the labels
    argument. These are not used if compare_ref is False.

    Returns a function suitable for use as the display_product argument
    to a Context.
    """
    def display_context_value(context):
        """
        A function that returns HTML code which displays the value of a
        single specific context key, possibly side-by-side with the
        corresponding reference value.

        See build_simple_context_value_displayer, which created this function.
        """
        if not compare_ref:
            if key in context:
                return str(context[key])
            else:
                return (
                    f"<div class='context-missing'>Failed to establish"
                    f" context '{key}'!</div>"
                )
        else:
            if key in context:
                rep = "<code>{}</code>".format(repr(context[key]))
            else:
                rep = (
                    f"<div class='context-missing'>Failed to establish"
                    f" context '{key}'!</div>"
                )

            if "ref_" + key in context:
                ref_rep = "<code>{}</code>".format(repr(context["ref_" + key]))
            else:
                ref_rep = (
                    f"<div class='context-missing'>Failed to establish"
                    f" context 'ref_{key}'!</div>"
                )

            return f"""
<table class='context-values'>
  <tbody>
    <tr> <th>{labels[0]}</th> <td>{rep}</td>  </tr>
    <tr> <th>{labels[1]}</th> <td>{ref_rep}</td>  </tr>
  </tbody>
</table>
"""

    return display_context_value


def create_distribution_result_displayer(context_key="distribution"):
    """
    Creates a distribution results display function, which will read
    values from the given context key ("distribution" by default). Also
    reads a value from the matching "ref_" key.
    """
    def display_distribution_results(context):
        """
        Displays the 'distribution' and 'ref_distribution' context keys
        side-by-side.
        """
        sub_dist = context[context_key]["results"]
        ref_dist = context["ref_" + context_key]["results"]

        all_results = set(sub_dist) | set(ref_dist)

        n_samples = context[context_key]["trials"]

        rows = []
        for result in sorted(all_results):
            rows.append(
                (
                    "<tr> <td>{result}</td> <td>{n}</td>"
                  + " <td>{ref_n}</td> </tr>"
                ).format(
                    result=repr(result),
                    n=repr(sub_dist.get(result, 0)),
                    ref_n=repr(ref_dist.get(result, 0))
                )
            )

        return """
The distribution of results from your function and the solution function
after {n_samples} trials (note: distributions may differ somewhat due to
random chance.):
<table class='result_distribution'>
  <thead>
    <tr>
      <th>Result value</th>
      <th>Observed count</th>
      <th>Solution count</th>
    </tr>
  </thead>
  <tbody>
    {rows}
  </tbody>
</table>
""".format(n_samples=n_samples, rows='\n    '.join(rows))

    return display_distribution_results


class SiftedContext(Context):
    """
    Working from the "output" and "ref_output" slots (or some other
    custom list of slots), this `Context` creates "sifted" and
    "ref_sifted" slots which hold the results of matching a regular
    expression against the input value.
    """
    def __init__(
        self,
        pattern,
        depends,
        description=None,
        slot_map={"output": "sifted", "ref_output": "ref_sifted"},
        first_match=False,
        require_match=True,
        use_matchobjs=False
    ):
        """
        Dependencies must be supplied. A custom description may be
        supplied (and is often useful). A custom slot map may be supplied
        to specify which incoming slots to process, and for each incoming
        slot, which new slot to create to store the result from that
        slot.
        """
        if isinstance(pattern, str):
            pattern = re.compile(pattern)

        def establish_context(prev_context):
            """
            This context_builder function processes a custom list of
            slots by applying a regular expression to them.
            """
            result = {}
            # Process each requested slot
            for from_slot in slot_map:
                to_slot = slot_map[from_slot]

                # Grab our input value
                value = context_utils.extract(prev_context, from_slot)
                if not isinstance(value, str):
                    raise TypeError(
                        f"SiftedContext can only refine string values,"
                        f" but was asked to refine value of slot"
                        f" {from_slot} which was a {type(value)}."
                    )

                # Apply our regular expression
                matches = pattern.finditer(value)

                # Grab the first match if that's what we need
                if first_match:
                    try:
                        first = next(matches)
                    except StopIteration:
                        raise ValueError(
                            f"While refining '{from_slot}' context,"
                            f" found no matches for pattern."
                        )
                    # Add either the match object or matching string
                    if use_matchobjs:
                        result[to_slot] = first
                    else:
                        result[to_slot] = first.group()
                else: # Grab all matches
                    if use_matchobjs:
                        objs = [m for m in matches]
                    else:
                        objs = [m.group() for m in matches]

                    # list might be empty...
                    if require_match and len(objs) == 0:
                        raise ValueError(
                            f"While refining '{from_slot}' context,"
                            f" found no matches for pattern."
                        )

                    result[to_slot] = objs

            # Return our results
            return result

        def display_result(context):
            """
            Displays the results of slot sifting as a tabbed HTML
            structure with one tab per input slot.
            """
            tablist = []
            for from_slot in slot_map:
                to_slot = slot_map[from_slot]
                result = context_utils.extract(context, to_slot)

                if isinstance(result, re.Match):
                    display = f"<pre>{result.group(0)}</pre>"
                elif isinstance(result, list):
                    if len(result) == 0:
                        display = "&lt;no matches&gt;"
                    elif isinstance(result[0], str):
                        display = html_tools.build_list(
                            [
                                f"<pre>{entry}</pre>"
                                for entry in result
                            ]
                        )
                    else: # results are Match objects
                        display = html_tools.build_list(
                            [
                                f"<pre>{match.group(0)}</pre>"
                                for match in result
                            ]
                        )
                else: # results should be strings
                    display = f"<pre>{result}</pre>"

                tablist.append((from_slot, display))

            return (
                "Results for expression <pre><code>{expr}</code></pre>:<br>"
              + html_tools.build_html_tabs(tablist)
            )

        # Create a default description if necessary
        if description is None:
            stuff = phrasing.comma_list(
                slot
                for slot in slot_map
                if not slot.startswith("ref_")
            )
            description = (
                f"Certain parts of the {stuff}.",
                (
                    f"We will search for the pattern"
                    f"<pre><code>{pattern.pattern}</code></pre> within"
                    f" the {stuff} and inspect the results."
                )
            )

        # Now we can call the super constructor
        super().__init__(
            description=description,
            builder=establish_context,
            display_product=display_result,
            depends=depends
        )
