potluck.html_tools

Tools for building HTML strings.

html_tools.py

  1# -*- coding: utf-8 -*-
  2"""
  3Tools for building HTML strings.
  4
  5html_tools.py
  6"""
  7
  8import sys
  9import traceback
 10import difflib
 11import os
 12import re
 13import io
 14import base64
 15
 16from . import phrasing
 17from . import file_utils
 18
 19
 20# Figure out how to escape HTML (2/3 compatibility)
 21if sys.version_info[0] < 3:
 22    # use cgi.escape instead of html.escape
 23    import cgi
 24    escape = cgi.escape
 25else:
 26    # use html.escape
 27    import html
 28    escape = html.escape
 29
 30
 31#---------#
 32# Globals #
 33#---------#
 34
 35STATUS_SYMBOLS = {
 36    "unknown": " ", # non-breaking space
 37    "not applicable": "–",
 38    "failed": "✗",
 39    "partial": "~",
 40    "accomplished": "✓",
 41}
 42"""
 43The symbols used as shorthand icons for each goal accomplishment status.
 44"""
 45
 46SHORT_REPR_LIMIT = 160
 47"""
 48Limit in terms of characters before we try advanced formatting.
 49"""
 50
 51
 52#-----------#
 53# Functions #
 54#-----------#
 55
 56def len_as_text(html_string):
 57    """
 58    Returns an approximate length of the given string in characters
 59    assuming HTML tags would not be visible and HTML entities would be
 60    single characters.
 61    """
 62    without_tags = re.sub("<[^>]*>", "", html_string)
 63    without_entities = re.sub("&[#0-9A-Za-z]+;", ".", without_tags)
 64    return len(without_entities)
 65
 66
 67#---------------------------#
 68# Text wrapping/indentation #
 69#---------------------------#
 70
 71
 72def wrapped_fragments(line, width=80, indent=''):
 73    """
 74    Wraps a single line of text into one or more lines, yielding a
 75    sequence of strings. Breaks on spaces/tabs only, and does not
 76    attempt any kind of optimization but simply breaks greedily as close
 77    to the given width as possible. The given indentation will be
 78    prepended to each line, including the first.
 79    """
 80    # Apply indentation
 81    line = indent + line
 82
 83    if len(line) <= width:
 84        yield line
 85        return
 86
 87    # Look backwards from width for a place we can break at
 88    breakpoint = None
 89    for i in range(width, len(indent), -1):
 90        # (note: breaking on first char doesn't make sense)
 91        c = line[i]
 92        if c in ' \t':
 93            breakpoint = i
 94            break
 95
 96    # If we didn't find a breakpoint that would respect the
 97    # target width, find the first one we can
 98    if breakpoint is None:
 99        for i in range(width, len(line)):
100            c = line[i]
101            if c in ' \t':
102                breakpoint = i
103
104    if breakpoint is None:
105        # Can't find any breakpoint :'(
106        yield line
107    else:
108        first = line[:breakpoint]
109        rest = line[breakpoint + 1:]
110        yield first
111        # Note: avoiding yield from because we want Python2 compatibility
112        for item in wrapped_fragments(rest, width, indent):
113            yield item
114
115
116def wrap_text_with_indentation(text, width=80):
117    """
118    Word-wraps the given text, respecting indentation, to the given
119    width. If there is a line that's more-indented than the given width,
120    wrapping will happen as aggressively as possible after that. Wraps
121    only at spaces. Replicates tab/space mix of indentation in the
122    output.
123
124    This function does NOT optimize for line lengths, and instead just
125    breaks each line greedily as close to the given width as possible.
126    """
127    lines = text.splitlines()
128    wrapped = []
129    for line in lines:
130        if len(line) <= width:
131            wrapped.append(line)
132        else:
133            indentation = ''
134            for c in line:
135                if c in ' \t':
136                    indentation += c
137                else:
138                    break
139
140            wrapped.extend(
141                wrapped_fragments(
142                    line[len(indentation):],
143                    width,
144                    indentation
145                )
146            )
147
148    return '\n'.join(wrapped)
149
150
151def indent(string, indent):
152    """
153    Indents a string using spaces. Newlines will be '\\n' afterwards.
154    """
155    lines = string.splitlines()
156    return '\n'.join(' ' * indent + line for line in lines)
157
158
159def truncate(text, limit=50000, tag='\n...truncated...'):
160    """
161    Truncates the given text so that it does not exceed the given limit
162    (in terms of characters; default 50K)
163
164    If the text actually is truncated, the string '\\n...truncated...'
165    will be added to the end of the result to indicate this, but an
166    alternate tag may be specified; the tag characters are not counted
167    against the limit, so it's possible for this function to actually
168    make a string longer if it starts out longer than the limit by less
169    than the length of the tag.
170    """
171    if len(text) > limit:
172        return text[:limit] + tag
173    else:
174        return text
175
176
177#------------------#
178# Common HTML bits #
179#------------------#
180
181def create_help_button(help_content):
182    """
183    Returns an HTML string for a button that can be clicked to display
184    help. The button will have a question mark when collapsed and a dash
185    when expanded, and the help will be displayed in a box on top of
186    other content to the right of the help button.
187    """
188    return (
189        '<details class="help_button">\n'
190      + '<summary aria-label="More details"></summary>\n'
191      + '<div class="help">{}</div>\n'
192      + '</details>\n'
193    ).format(help_content)
194
195
196def build_list(items, ordered=False):
197    """
198    Builds an HTML ul tag, or an ol tag if `ordered` is set to True.
199    Items must be a list of (possibly HTML) strings.
200    """
201    tag = "ol" if ordered else "ul"
202    return (
203        "<{tag}>\n{items}\n</{tag}>"
204    ).format(
205        tag=tag,
206        items="\n".join("<li>{}</li>".format(item) for item in items)
207    )
208
209
210def build_html_details(title, content, classes=None):
211    """
212    Builds an HTML details element with the given title as its summary
213    and content inside. The classes are attached to the details element
214    if provided, and should be a string.
215    """
216    return (
217        '<details{}><summary>{}</summary>{}</details>'
218    ).format(
219        ' class="{}"'.format(classes) if classes is not None else "",
220        title,
221        content
222    )
223
224
225def build_html_tabs(tabs):
226    """
227    Builds an HTML structure which uses several tab elements and a
228    scrollable region to implement a multiple-tabs structure. The given
229    tabs list should contain title, content pairs, where both titles and
230    contents are strings that may contain HTML code.
231
232    `resources/potluck.js` defines the Javascript code necessary for the
233    tabs to work correctly.
234    """
235    tab_pieces = [
236        (
237            """
238<li
239 class='tab{selected}'
240 tabindex="0"
241 aria-label="Activate to display {title} after this list."
242>
243{title}
244</li>
245"""         .format(
246                selected=" selected" if i == 0 else "",
247                title=title
248            ),
249            """
250<section
251 class='tab-content{selected}'
252 tabindex="0"
253>
254{content}
255</section>
256"""         .format(
257                selected=" selected" if i == 0 else "",
258                content=content
259            )
260        )
261        for i, (title, content) in enumerate(tabs)
262    ]
263    tabs = '\n\n'.join([piece[0] for piece in tab_pieces])
264    contents = '\n\n'.join([piece[1] for piece in tab_pieces])
265    return """
266<div class="tabs" role="presentation">
267  <ul class="tabs-top">
268{tabs}
269  </ul>
270  <div class="tabs-bot" aria-live="polite">
271{contents}
272  </div>
273</div>
274""" .format(
275        tabs=tabs,
276        contents=contents
277    )
278
279
280def fileslug(filename):
281    """
282    Returns a safe-for-HTML-ID version of the given filename.
283    """
284    return filename\
285        .replace(os.path.sep, '-')\
286        .replace('.py', '')\
287        .replace('.', '-')
288
289
290def line_id(taskid, filename, lineno):
291    """
292    Generates an HTML ID for a code line, given the task ID, file name,
293    and line number.
294    """
295    return "{taskid}_{slug}_codeline_{lineno}".format(
296        taskid=taskid,
297        slug=fileslug(filename),
298        lineno=lineno
299    )
300
301
302def block_id(taskid, filename):
303    """
304    Generates an HTML ID for a code block, given the task ID and file
305    name.
306    """
307    return "{taskid}_code_{slug}".format(
308        taskid=taskid,
309        slug=fileslug(filename)
310    )
311
312
313def html_link_to_line(taskid, filename, lineno):
314    """
315    Returns an HTML anchor tag (as a string) that links to the specified
316    line of code in the specified file of the specified task.
317
318    `resources/potluck.js` includes code that will add click handlers to
319    these links so that the line being jumped to gets highlighted.
320    """
321    lineid = line_id(taskid, filename, lineno)
322    return (
323        '<a class="lineref" data-lineid="{lineid}"'
324        ' href="#{lineid}">{lineno}</a>'
325    ).format(
326        lineid=lineid,
327        lineno=lineno
328    )
329
330
331def html_output_details(raw_output, title="Output"):
332    """
333    Takes raw program output and turns it into an expandable details tag
334    with pre formatting.
335    """
336    return (
337        '<details>\n'
338      + '<summary>{}</summary>\n'
339      + '<pre class="printed_output">{}</pre>\n'
340        '</details>'
341    ).format(
342        title,
343        raw_output
344    )
345
346
347def html_diff_table(
348    output,
349    reference,
350    out_title='Actual output',
351    ref_title='Expected output',
352    joint_title=None,
353    line_limit=300,
354    trim_lines=1024
355):
356    """
357    Uses difflib to create and return an HTML string that encodes a table
358    comparing two (potentially multi-line) outputs. If joint_title is
359    given, the result is wrapped into a details tag with that title and
360    class diff_table.
361
362    If line_limit is not None (the default is 300) then only that many
363    lines of each output will be included, and a message about the
364    limitation will be added.
365
366    If the trim_lines value is None (the default is 1024) then the full
367    value of every line will be included no matter how long it is,
368    otherwise lines beyond that length will be trimmed to that length
369    (with three periods added to indicate this).
370    """
371    if isinstance(output, str):
372        output = output.split('\n')
373    if isinstance(reference, str):
374        reference = reference.split('\n')
375
376    if line_limit and len(output) > line_limit or len(reference) > line_limit:
377        result = "(comparing the first {})<br>\n".format(
378            phrasing.obj_num(line_limit, "line")
379        )
380        output = output[:line_limit]
381        reference = reference[:line_limit]
382    else:
383        result = ""
384
385    if (
386        trim_lines
387    and (
388            any(len(line) > trim_lines for line in output)
389         or any(len(line) > trim_lines for line in reference)
390        )
391    ):
392        result += (
393            "(comparing the first {} on each line)<br>\n"
394        ).format(phrasing.obj_num(trim_lines, "character"))
395        output = [
396            line[:trim_lines] + ('...' if len(line) > trim_lines else '')
397            for line in output
398        ]
399        reference = [
400            line[:trim_lines] + ('...' if len(line) > trim_lines else '')
401            for line in reference
402        ]
403
404    result += difflib.HtmlDiff().make_table(
405        output,
406        reference,
407        fromdesc=out_title,
408        todesc=ref_title,
409    )
410    if joint_title is None:
411        return result
412    else:
413        return build_html_details(joint_title, result, classes="diff_table")
414
415
416#---------------------#
417# Multimedia elements #
418#---------------------#
419
420def html_image(image, alt_text, classes=None):
421    """
422    Given a PIL image and associated alt text, builds an HTML image tag
423    that uses a data URL to display the provided image. A list of CSS
424    class strings may be provided to include in the tag.
425    """
426    src_bytes = io.BytesIO()
427    image.save(src_bytes, format="PNG")
428    data = base64.standard_b64encode(src_bytes.getvalue()).decode("utf-8")
429    if classes is not None:
430        class_attr = 'class="' + ' '.join(classes) + '" '
431    else:
432        class_attr = ''
433    return (
434        '<img {}src="data:image/png;base64,{}" alt="{}">'
435    ).format(class_attr, data, alt_text)
436
437
438def html_animation(frames, alt_text, classes=None, delays=10, loop=True):
439    """
440    Given a list of PIL images and associated alt text, builds an HTML
441    image tag that uses a data URL to display the provided images as
442    frames of an animation. A list of CSS class strings may be provided
443    to include in the tag.
444
445    If delays is provided, it specifies the delay in milliseconds between
446    each frame of the animation. A list of numbers with length equal to
447    the number of frames may be provided to set separate delays for each
448    frame, or a single number will set the same delay for each frame.
449
450    If loop is set to false, the animation will play only once (not
451    recommended).
452    """
453    src_bytes = io.BytesIO()
454    frames[0].save(
455        src_bytes,
456        format="GIF",
457        append_images=frames[1:],
458        save_all=True,
459        duration=delays,
460        loop=0 if loop else None # TODO: Does this work?
461    )
462    data = base64.standard_b64encode(src_bytes.getvalue()).decode("utf-8")
463    if classes is not None:
464        class_attr = 'class="' + ' '.join(classes) + '" '
465    else:
466        class_attr = ''
467    return (
468        '<img {}src="data:image/gif;base64,{}" alt="{}">'
469    ).format(class_attr, data, alt_text)
470
471
472def html_audio(raw_data, mimetype, label=None):
473    """
474    Returns an HTML audio tag that will play the provided audio data,
475    using a data URL. For reasons, the data URL is included twice, so the
476    data's size will be more than doubled in the HTML output (TODO: Don't
477    do that?!). If a string is specified as the label, an aria-label
478    value will be attached to the audio element, although this will
479    usually only be accessible to those using screen readers.
480    """
481    # Construct label attribute
482    label_attr = ''
483    if label is not None:
484        label_attr = ' aria-label="{}"'.format(label)
485
486    # Encode data and construct data URL
487    data = base64.standard_b64encode(raw_data).decode("utf-8")
488    data_url = 'data:{};base64,{}'.format(mimetype, data)
489
490    # Construct and return tag
491    return '''\
492<audio controls{label}><source type="{mime}" src="{data_url}">
493(Your browser does not support the audio element. Use this link to download the file and play it using a media player: <a href="{data_url}">download audio</a>)
494</audio>'''.format( # noqa E501
495        label=label_attr,
496        mime=mimetype,
497        data_url=data_url
498    )
499
500
501#-------------------#
502# Status indicators #
503#-------------------#
504
505def status_css_class(status):
506    """
507    Returns the CSS class used to indicate the given status.
508    """
509    return "status_" + status.replace(' ', '_')
510
511
512def build_status_indicator(status):
513    """
514    Builds a single div that indicates the given goal accomplishment
515    status.
516    """
517    status_class = status_css_class(status)
518    status_symbol = STATUS_SYMBOLS.get(status, "!")
519    return '<div class="goal_status {stc}">{sy}</div>'.format(
520        stc=status_class,
521        sy=status_symbol
522    )
523
524
525#------------------------------#
526# General Object Reprs in HTML #
527#------------------------------#
528
529
530def big_repr(thing):
531    """
532    A function that works like repr, but with some extra formatting for
533    special known cases when the results would be too large.
534    """
535    base = repr(thing)
536    if len(base) > SHORT_REPR_LIMIT:
537        if type(thing) in (tuple, list, set): # NOT isinstance
538            # left and right delimeters
539            ld, rd = repr(type(thing)())
540            stuff = ',\n'.join(big_repr(elem) for elem in thing)
541            return ld + '\n' + indent(stuff, 2) + '\n' + rd
542        elif type(thing) is dict: # NOT isinstance
543            stuff = ',\n'.join(
544                big_repr(key) + ": " + big_repr(value)
545                for key, value in thing.items()
546            )
547            return '{\n' + indent(stuff, 2) + '\n}'
548        elif type(thing) is str and '\n' in thing:
549            return "'''\\\n{thing}'''".format(thing=thing)
550        # else we fall out and return base
551
552    return base
553
554
555def build_display_box(text):
556    """
557    Creates a disabled textarea element holding the given text. This
558    allows for a finite-size element to contain (potentially a lot) of
559    text that the user can scroll through or even search through.
560    """
561    # TODO: A disabled textarea is such a hack! Instead, use an inner pre
562    # inside an outer div with CSS to make it scrollable.
563    return '<textarea class="display_box" disabled>{text}</textarea>'.format(
564        text=text
565    )
566
567
568def dynamic_html_repr(thing, reasonable=200, limit=10000):
569    """
570    A dynamic representation of an object which is simply a pre-formatted
571    string for objects whose representations are reasonably small, and
572    which turns into a display box for representations which are larger,
573    with text being truncated after a (very large) limit.
574
575    The threshold for reasonably small representations as well as the
576    hard limit may be customized; use None for the hard limit to disable
577    truncation entirely (but don't complain about file sizes if you do).
578    """
579    rep = big_repr(thing)
580    if len(rep) <= reasonable:
581        return '<pre class="short_repr">{rep}</pre>'.format(
582            rep=escape(rep)
583        )
584    else:
585        return build_display_box(escape(truncate(rep, limit)))
586
587
588#-------------------------------#
589# Formatting tracebacks as HTML #
590#-------------------------------#
591
592def html_traceback(exc=None, title=None, linkable=None):
593    """
594    In an exception handler, returns an HTML string that includes the
595    exception type, message, and traceback. Must be called from an except
596    clause, unless an exception object with a __traceback__ value is
597    provided.
598
599    If title is given and not None, a details tag will be returned
600    using that title, which can be expanded to show the traceback,
601    otherwise just a pre tag is returned containing the traceback.
602
603    If linkable is given, it must be a dictionary mapping filenames to
604    task IDs, and line numbers of those files which appear in the
605    traceback will be turned into links to those lines.
606    """
607    result = escape(string_traceback(exc))
608    if linkable:
609        for fn in linkable:
610            taskid = linkable[fn]
611
612            def replacer(match):
613                link = html_link_to_line(taskid, fn, int(match.group(1)))
614                return '{}&quot;, line {},'.format(fn, link)
615
616            pat = r'{}&quot;, line ([0-9]+),'.format(fn)
617            result = re.sub(pat, replacer, result)
618
619    pre = '<pre class="traceback">\n{}\n</pre>'.format(result)
620    if title is not None:
621        return build_html_details(title, pre, "error")
622    else:
623        return pre
624
625
626REWRITES = {
627    file_utils.potluck_src_dir(): "<potluck>"
628}
629"""
630The mapping from filenames to replacements to be used for rewriting
631filenames in tracebacks.
632"""
633
634
635def set_tb_rewrite(filename, rewrite_as):
636    """
637    Sets up a rewriting rule that will be applied to string and HTML
638    tracebacks. You must provide the filename that should be rewritten,
639    and the string to replace it with. Set rewrite_as to None to remove a
640    previously-established rewrite rule.
641    """
642    global REWRITES
643    REWRITES[filename] = rewrite_as
644
645
646def string_traceback(exc=None):
647    """
648    When called in an exception handler, returns a multi-line string
649    including what Python would normally print: the exception type,
650    message, and a traceback. You can also call it anywhere if you can
651    provide an exception object which has a __traceback__ value.
652
653    The traceback gets obfuscated by replacing full file paths that start
654    with the potluck directory with just the package directory and
655    filename, and by replacing full file paths that start with the spec
656    directory with just the task ID and then the file path from the spec
657    directory (usually starter/, soln/, or the submitted file itself).
658    """
659    sfd = os.path.split(file_utils.get_spec_file_name())[0]
660    rewrites = {}
661    rewrites.update(REWRITES)
662    if sfd:
663        rewrites[sfd] = "<task>"
664
665    if exc is None:
666        raw = traceback.format_exc()
667    else:
668        raw = ''.join(traceback.format_tb(exc.__traceback__) + [ str(exc) ])
669
670    return rewrite_traceback_filenames(raw, rewrites)
671
672
673def rewrite_traceback_filenames(raw_traceback, prefix_map=None):
674    """
675    Accepts a traceback as a string, and returns a modified string where
676    filenames have been altered by replacing the keys of the given
677    prefix_map with their values. In addition, filenames which include a
678    directory that ends in "__tmp" will have all directory entries up to
679    and including that one stripped from their path.
680
681    If no prefix map is given, the value of REWRITES will be used.
682    """
683    prefix_map = prefix_map or REWRITES
684    result = raw_traceback
685    for prefix in prefix_map:
686        replace = prefix_map[prefix]
687        result = result.replace(
688            'File "{prefix}'.format(prefix=prefix),
689            'File "{replace}'.format(replace=replace)
690        )
691
692    result = re.sub(
693        'File ".*__tmp' + os.path.sep,
694        'File "<submission>/',
695        result
696    )
697
698    return result
699
700
701#-----------------------------#
702# Function def/call templates #
703#-----------------------------#
704
705def function_def_code_tags(fn_name, params_pattern, announce=None):
706    """
707    Returns a tuple containing two strings of HTML code used to represent
708    the given function definition in both short and long formats. The
709    short format just lists the first acceptable definition, while the
710    long format lists all of them. Note that both fn_name and
711    params_pattern may be lists of strings instead of strings; see
712    function_def_patterns.
713    """
714    if isinstance(fn_name, str):
715        names = [fn_name]
716    else:
717        names = list(fn_name)
718
719    # If there are specific parameters we can give users more info about
720    # what they need to do.
721    if isinstance(params_pattern, str):
722        specific_names = [
723            "{}({})".format(name, params_pattern) for name in names
724        ]
725    else:
726        specific_names = names
727
728    # Figure out what we're announcing as:
729    if announce is None:
730        announce = specific_names[0]
731
732    # Make code tag and detailed code tag:
733    code_tag = "<code>{}</code>".format(announce)
734    details_code = phrasing.comma_list(
735        ["<code>{}</code>".format(n) for n in specific_names],
736        junction="or"
737    )
738
739    # Add a comment about the number of parameters required
740    if isinstance(params_pattern, int):
741        with_n = " with {} {}".format(
742            params_pattern,
743            phrasing.plural(params_pattern, "parameter")
744        )
745        code_tag += with_n
746        details_code += with_n
747
748    return code_tag, details_code
749
750
751def function_call_code_tags(fn_name, args_pattern, is_method=False):
752    """
753    Works like `potluck.patterns.function_call_patterns`, but generates a
754    pair of HTML strings with summary and detailed descriptions of the
755    function call. In that sense it's also similar to
756    `function_def_code_tags`, except that it works for a function call
757    instead of a function definition.
758
759    If the args_pattern is "-any arguments-", the parentheses for the
760    function call will be omitted entirely.
761    """
762    if isinstance(fn_name, str):
763        names = [fn_name]
764    else:
765        names = list(fn_name)
766
767    # If there are specific args we can give users more info about what
768    # they need to do.
769    if args_pattern == "-any arguments-":
770        specific_names = names
771    elif isinstance(args_pattern, str):
772        if is_method:
773            specific_names = [
774                ".{}({})".format(name, args_pattern)
775                for name in names
776            ]
777        else:
778            specific_names = [
779                "{}({})".format(name, args_pattern)
780                for name in names
781            ]
782    else:
783        specific_names = names
784
785    # Make code tag and detailed code tag:
786    code_tag = "<code>{}</code>".format(
787        escape(specific_names[0])
788    )
789
790    details_code = phrasing.comma_list(
791        [
792            "<code>{}</code>".format(escape(name))
793            for name in specific_names
794        ],
795        junction="or"
796    )
797
798    return code_tag, details_code
799
800
801def args_repr_list(args, kwargs):
802    """
803    Creates an HTML string representation of the given positional and
804    keyword arguments, as a bulleted list.
805    """
806    arg_items = []
807    for arg in args:
808        arg_items.append(dynamic_html_repr(arg))
809
810    for kw in kwargs:
811        key_repr = dynamic_html_repr(kw)
812        val_repr = dynamic_html_repr(kwargs[kw])
813        arg_items.append(key_repr + "=" + val_repr)
814
815    return build_list(arg_items)
STATUS_SYMBOLS = {'unknown': '\xa0', 'not applicable': '–', 'failed': '✗', 'partial': '~', 'accomplished': '✓'}

The symbols used as shorthand icons for each goal accomplishment status.

SHORT_REPR_LIMIT = 160

Limit in terms of characters before we try advanced formatting.

def len_as_text(html_string):
57def len_as_text(html_string):
58    """
59    Returns an approximate length of the given string in characters
60    assuming HTML tags would not be visible and HTML entities would be
61    single characters.
62    """
63    without_tags = re.sub("<[^>]*>", "", html_string)
64    without_entities = re.sub("&[#0-9A-Za-z]+;", ".", without_tags)
65    return len(without_entities)

Returns an approximate length of the given string in characters assuming HTML tags would not be visible and HTML entities would be single characters.

def wrapped_fragments(line, width=80, indent=''):
 73def wrapped_fragments(line, width=80, indent=''):
 74    """
 75    Wraps a single line of text into one or more lines, yielding a
 76    sequence of strings. Breaks on spaces/tabs only, and does not
 77    attempt any kind of optimization but simply breaks greedily as close
 78    to the given width as possible. The given indentation will be
 79    prepended to each line, including the first.
 80    """
 81    # Apply indentation
 82    line = indent + line
 83
 84    if len(line) <= width:
 85        yield line
 86        return
 87
 88    # Look backwards from width for a place we can break at
 89    breakpoint = None
 90    for i in range(width, len(indent), -1):
 91        # (note: breaking on first char doesn't make sense)
 92        c = line[i]
 93        if c in ' \t':
 94            breakpoint = i
 95            break
 96
 97    # If we didn't find a breakpoint that would respect the
 98    # target width, find the first one we can
 99    if breakpoint is None:
100        for i in range(width, len(line)):
101            c = line[i]
102            if c in ' \t':
103                breakpoint = i
104
105    if breakpoint is None:
106        # Can't find any breakpoint :'(
107        yield line
108    else:
109        first = line[:breakpoint]
110        rest = line[breakpoint + 1:]
111        yield first
112        # Note: avoiding yield from because we want Python2 compatibility
113        for item in wrapped_fragments(rest, width, indent):
114            yield item

Wraps a single line of text into one or more lines, yielding a sequence of strings. Breaks on spaces/tabs only, and does not attempt any kind of optimization but simply breaks greedily as close to the given width as possible. The given indentation will be prepended to each line, including the first.

def wrap_text_with_indentation(text, width=80):
117def wrap_text_with_indentation(text, width=80):
118    """
119    Word-wraps the given text, respecting indentation, to the given
120    width. If there is a line that's more-indented than the given width,
121    wrapping will happen as aggressively as possible after that. Wraps
122    only at spaces. Replicates tab/space mix of indentation in the
123    output.
124
125    This function does NOT optimize for line lengths, and instead just
126    breaks each line greedily as close to the given width as possible.
127    """
128    lines = text.splitlines()
129    wrapped = []
130    for line in lines:
131        if len(line) <= width:
132            wrapped.append(line)
133        else:
134            indentation = ''
135            for c in line:
136                if c in ' \t':
137                    indentation += c
138                else:
139                    break
140
141            wrapped.extend(
142                wrapped_fragments(
143                    line[len(indentation):],
144                    width,
145                    indentation
146                )
147            )
148
149    return '\n'.join(wrapped)

Word-wraps the given text, respecting indentation, to the given width. If there is a line that's more-indented than the given width, wrapping will happen as aggressively as possible after that. Wraps only at spaces. Replicates tab/space mix of indentation in the output.

This function does NOT optimize for line lengths, and instead just breaks each line greedily as close to the given width as possible.

def indent(string, indent):
152def indent(string, indent):
153    """
154    Indents a string using spaces. Newlines will be '\\n' afterwards.
155    """
156    lines = string.splitlines()
157    return '\n'.join(' ' * indent + line for line in lines)

Indents a string using spaces. Newlines will be '\n' afterwards.

def truncate(text, limit=50000, tag='\n...truncated...'):
160def truncate(text, limit=50000, tag='\n...truncated...'):
161    """
162    Truncates the given text so that it does not exceed the given limit
163    (in terms of characters; default 50K)
164
165    If the text actually is truncated, the string '\\n...truncated...'
166    will be added to the end of the result to indicate this, but an
167    alternate tag may be specified; the tag characters are not counted
168    against the limit, so it's possible for this function to actually
169    make a string longer if it starts out longer than the limit by less
170    than the length of the tag.
171    """
172    if len(text) > limit:
173        return text[:limit] + tag
174    else:
175        return text

Truncates the given text so that it does not exceed the given limit (in terms of characters; default 50K)

If the text actually is truncated, the string '\n...truncated...' will be added to the end of the result to indicate this, but an alternate tag may be specified; the tag characters are not counted against the limit, so it's possible for this function to actually make a string longer if it starts out longer than the limit by less than the length of the tag.

def create_help_button(help_content):
182def create_help_button(help_content):
183    """
184    Returns an HTML string for a button that can be clicked to display
185    help. The button will have a question mark when collapsed and a dash
186    when expanded, and the help will be displayed in a box on top of
187    other content to the right of the help button.
188    """
189    return (
190        '<details class="help_button">\n'
191      + '<summary aria-label="More details"></summary>\n'
192      + '<div class="help">{}</div>\n'
193      + '</details>\n'
194    ).format(help_content)

Returns an HTML string for a button that can be clicked to display help. The button will have a question mark when collapsed and a dash when expanded, and the help will be displayed in a box on top of other content to the right of the help button.

def build_list(items, ordered=False):
197def build_list(items, ordered=False):
198    """
199    Builds an HTML ul tag, or an ol tag if `ordered` is set to True.
200    Items must be a list of (possibly HTML) strings.
201    """
202    tag = "ol" if ordered else "ul"
203    return (
204        "<{tag}>\n{items}\n</{tag}>"
205    ).format(
206        tag=tag,
207        items="\n".join("<li>{}</li>".format(item) for item in items)
208    )

Builds an HTML ul tag, or an ol tag if ordered is set to True. Items must be a list of (possibly HTML) strings.

def build_html_details(title, content, classes=None):
211def build_html_details(title, content, classes=None):
212    """
213    Builds an HTML details element with the given title as its summary
214    and content inside. The classes are attached to the details element
215    if provided, and should be a string.
216    """
217    return (
218        '<details{}><summary>{}</summary>{}</details>'
219    ).format(
220        ' class="{}"'.format(classes) if classes is not None else "",
221        title,
222        content
223    )

Builds an HTML details element with the given title as its summary and content inside. The classes are attached to the details element if provided, and should be a string.

def build_html_tabs(tabs):
226def build_html_tabs(tabs):
227    """
228    Builds an HTML structure which uses several tab elements and a
229    scrollable region to implement a multiple-tabs structure. The given
230    tabs list should contain title, content pairs, where both titles and
231    contents are strings that may contain HTML code.
232
233    `resources/potluck.js` defines the Javascript code necessary for the
234    tabs to work correctly.
235    """
236    tab_pieces = [
237        (
238            """
239<li
240 class='tab{selected}'
241 tabindex="0"
242 aria-label="Activate to display {title} after this list."
243>
244{title}
245</li>
246"""         .format(
247                selected=" selected" if i == 0 else "",
248                title=title
249            ),
250            """
251<section
252 class='tab-content{selected}'
253 tabindex="0"
254>
255{content}
256</section>
257"""         .format(
258                selected=" selected" if i == 0 else "",
259                content=content
260            )
261        )
262        for i, (title, content) in enumerate(tabs)
263    ]
264    tabs = '\n\n'.join([piece[0] for piece in tab_pieces])
265    contents = '\n\n'.join([piece[1] for piece in tab_pieces])
266    return """
267<div class="tabs" role="presentation">
268  <ul class="tabs-top">
269{tabs}
270  </ul>
271  <div class="tabs-bot" aria-live="polite">
272{contents}
273  </div>
274</div>
275""" .format(
276        tabs=tabs,
277        contents=contents
278    )

Builds an HTML structure which uses several tab elements and a scrollable region to implement a multiple-tabs structure. The given tabs list should contain title, content pairs, where both titles and contents are strings that may contain HTML code.

resources/potluck.js defines the Javascript code necessary for the tabs to work correctly.

def fileslug(filename):
281def fileslug(filename):
282    """
283    Returns a safe-for-HTML-ID version of the given filename.
284    """
285    return filename\
286        .replace(os.path.sep, '-')\
287        .replace('.py', '')\
288        .replace('.', '-')

Returns a safe-for-HTML-ID version of the given filename.

def line_id(taskid, filename, lineno):
291def line_id(taskid, filename, lineno):
292    """
293    Generates an HTML ID for a code line, given the task ID, file name,
294    and line number.
295    """
296    return "{taskid}_{slug}_codeline_{lineno}".format(
297        taskid=taskid,
298        slug=fileslug(filename),
299        lineno=lineno
300    )

Generates an HTML ID for a code line, given the task ID, file name, and line number.

def block_id(taskid, filename):
303def block_id(taskid, filename):
304    """
305    Generates an HTML ID for a code block, given the task ID and file
306    name.
307    """
308    return "{taskid}_code_{slug}".format(
309        taskid=taskid,
310        slug=fileslug(filename)
311    )

Generates an HTML ID for a code block, given the task ID and file name.

def html_output_details(raw_output, title='Output'):
332def html_output_details(raw_output, title="Output"):
333    """
334    Takes raw program output and turns it into an expandable details tag
335    with pre formatting.
336    """
337    return (
338        '<details>\n'
339      + '<summary>{}</summary>\n'
340      + '<pre class="printed_output">{}</pre>\n'
341        '</details>'
342    ).format(
343        title,
344        raw_output
345    )

Takes raw program output and turns it into an expandable details tag with pre formatting.

def html_diff_table( output, reference, out_title='Actual output', ref_title='Expected output', joint_title=None, line_limit=300, trim_lines=1024):
348def html_diff_table(
349    output,
350    reference,
351    out_title='Actual output',
352    ref_title='Expected output',
353    joint_title=None,
354    line_limit=300,
355    trim_lines=1024
356):
357    """
358    Uses difflib to create and return an HTML string that encodes a table
359    comparing two (potentially multi-line) outputs. If joint_title is
360    given, the result is wrapped into a details tag with that title and
361    class diff_table.
362
363    If line_limit is not None (the default is 300) then only that many
364    lines of each output will be included, and a message about the
365    limitation will be added.
366
367    If the trim_lines value is None (the default is 1024) then the full
368    value of every line will be included no matter how long it is,
369    otherwise lines beyond that length will be trimmed to that length
370    (with three periods added to indicate this).
371    """
372    if isinstance(output, str):
373        output = output.split('\n')
374    if isinstance(reference, str):
375        reference = reference.split('\n')
376
377    if line_limit and len(output) > line_limit or len(reference) > line_limit:
378        result = "(comparing the first {})<br>\n".format(
379            phrasing.obj_num(line_limit, "line")
380        )
381        output = output[:line_limit]
382        reference = reference[:line_limit]
383    else:
384        result = ""
385
386    if (
387        trim_lines
388    and (
389            any(len(line) > trim_lines for line in output)
390         or any(len(line) > trim_lines for line in reference)
391        )
392    ):
393        result += (
394            "(comparing the first {} on each line)<br>\n"
395        ).format(phrasing.obj_num(trim_lines, "character"))
396        output = [
397            line[:trim_lines] + ('...' if len(line) > trim_lines else '')
398            for line in output
399        ]
400        reference = [
401            line[:trim_lines] + ('...' if len(line) > trim_lines else '')
402            for line in reference
403        ]
404
405    result += difflib.HtmlDiff().make_table(
406        output,
407        reference,
408        fromdesc=out_title,
409        todesc=ref_title,
410    )
411    if joint_title is None:
412        return result
413    else:
414        return build_html_details(joint_title, result, classes="diff_table")

Uses difflib to create and return an HTML string that encodes a table comparing two (potentially multi-line) outputs. If joint_title is given, the result is wrapped into a details tag with that title and class diff_table.

If line_limit is not None (the default is 300) then only that many lines of each output will be included, and a message about the limitation will be added.

If the trim_lines value is None (the default is 1024) then the full value of every line will be included no matter how long it is, otherwise lines beyond that length will be trimmed to that length (with three periods added to indicate this).

def html_image(image, alt_text, classes=None):
421def html_image(image, alt_text, classes=None):
422    """
423    Given a PIL image and associated alt text, builds an HTML image tag
424    that uses a data URL to display the provided image. A list of CSS
425    class strings may be provided to include in the tag.
426    """
427    src_bytes = io.BytesIO()
428    image.save(src_bytes, format="PNG")
429    data = base64.standard_b64encode(src_bytes.getvalue()).decode("utf-8")
430    if classes is not None:
431        class_attr = 'class="' + ' '.join(classes) + '" '
432    else:
433        class_attr = ''
434    return (
435        '<img {}src="data:image/png;base64,{}" alt="{}">'
436    ).format(class_attr, data, alt_text)

Given a PIL image and associated alt text, builds an HTML image tag that uses a data URL to display the provided image. A list of CSS class strings may be provided to include in the tag.

def html_animation(frames, alt_text, classes=None, delays=10, loop=True):
439def html_animation(frames, alt_text, classes=None, delays=10, loop=True):
440    """
441    Given a list of PIL images and associated alt text, builds an HTML
442    image tag that uses a data URL to display the provided images as
443    frames of an animation. A list of CSS class strings may be provided
444    to include in the tag.
445
446    If delays is provided, it specifies the delay in milliseconds between
447    each frame of the animation. A list of numbers with length equal to
448    the number of frames may be provided to set separate delays for each
449    frame, or a single number will set the same delay for each frame.
450
451    If loop is set to false, the animation will play only once (not
452    recommended).
453    """
454    src_bytes = io.BytesIO()
455    frames[0].save(
456        src_bytes,
457        format="GIF",
458        append_images=frames[1:],
459        save_all=True,
460        duration=delays,
461        loop=0 if loop else None # TODO: Does this work?
462    )
463    data = base64.standard_b64encode(src_bytes.getvalue()).decode("utf-8")
464    if classes is not None:
465        class_attr = 'class="' + ' '.join(classes) + '" '
466    else:
467        class_attr = ''
468    return (
469        '<img {}src="data:image/gif;base64,{}" alt="{}">'
470    ).format(class_attr, data, alt_text)

Given a list of PIL images and associated alt text, builds an HTML image tag that uses a data URL to display the provided images as frames of an animation. A list of CSS class strings may be provided to include in the tag.

If delays is provided, it specifies the delay in milliseconds between each frame of the animation. A list of numbers with length equal to the number of frames may be provided to set separate delays for each frame, or a single number will set the same delay for each frame.

If loop is set to false, the animation will play only once (not recommended).

def html_audio(raw_data, mimetype, label=None):
473def html_audio(raw_data, mimetype, label=None):
474    """
475    Returns an HTML audio tag that will play the provided audio data,
476    using a data URL. For reasons, the data URL is included twice, so the
477    data's size will be more than doubled in the HTML output (TODO: Don't
478    do that?!). If a string is specified as the label, an aria-label
479    value will be attached to the audio element, although this will
480    usually only be accessible to those using screen readers.
481    """
482    # Construct label attribute
483    label_attr = ''
484    if label is not None:
485        label_attr = ' aria-label="{}"'.format(label)
486
487    # Encode data and construct data URL
488    data = base64.standard_b64encode(raw_data).decode("utf-8")
489    data_url = 'data:{};base64,{}'.format(mimetype, data)
490
491    # Construct and return tag
492    return '''\
493<audio controls{label}><source type="{mime}" src="{data_url}">
494(Your browser does not support the audio element. Use this link to download the file and play it using a media player: <a href="{data_url}">download audio</a>)
495</audio>'''.format( # noqa E501
496        label=label_attr,
497        mime=mimetype,
498        data_url=data_url
499    )

Returns an HTML audio tag that will play the provided audio data, using a data URL. For reasons, the data URL is included twice, so the data's size will be more than doubled in the HTML output (TODO: Don't do that?!). If a string is specified as the label, an aria-label value will be attached to the audio element, although this will usually only be accessible to those using screen readers.

def status_css_class(status):
506def status_css_class(status):
507    """
508    Returns the CSS class used to indicate the given status.
509    """
510    return "status_" + status.replace(' ', '_')

Returns the CSS class used to indicate the given status.

def build_status_indicator(status):
513def build_status_indicator(status):
514    """
515    Builds a single div that indicates the given goal accomplishment
516    status.
517    """
518    status_class = status_css_class(status)
519    status_symbol = STATUS_SYMBOLS.get(status, "!")
520    return '<div class="goal_status {stc}">{sy}</div>'.format(
521        stc=status_class,
522        sy=status_symbol
523    )

Builds a single div that indicates the given goal accomplishment status.

def big_repr(thing):
531def big_repr(thing):
532    """
533    A function that works like repr, but with some extra formatting for
534    special known cases when the results would be too large.
535    """
536    base = repr(thing)
537    if len(base) > SHORT_REPR_LIMIT:
538        if type(thing) in (tuple, list, set): # NOT isinstance
539            # left and right delimeters
540            ld, rd = repr(type(thing)())
541            stuff = ',\n'.join(big_repr(elem) for elem in thing)
542            return ld + '\n' + indent(stuff, 2) + '\n' + rd
543        elif type(thing) is dict: # NOT isinstance
544            stuff = ',\n'.join(
545                big_repr(key) + ": " + big_repr(value)
546                for key, value in thing.items()
547            )
548            return '{\n' + indent(stuff, 2) + '\n}'
549        elif type(thing) is str and '\n' in thing:
550            return "'''\\\n{thing}'''".format(thing=thing)
551        # else we fall out and return base
552
553    return base

A function that works like repr, but with some extra formatting for special known cases when the results would be too large.

def build_display_box(text):
556def build_display_box(text):
557    """
558    Creates a disabled textarea element holding the given text. This
559    allows for a finite-size element to contain (potentially a lot) of
560    text that the user can scroll through or even search through.
561    """
562    # TODO: A disabled textarea is such a hack! Instead, use an inner pre
563    # inside an outer div with CSS to make it scrollable.
564    return '<textarea class="display_box" disabled>{text}</textarea>'.format(
565        text=text
566    )

Creates a disabled textarea element holding the given text. This allows for a finite-size element to contain (potentially a lot) of text that the user can scroll through or even search through.

def dynamic_html_repr(thing, reasonable=200, limit=10000):
569def dynamic_html_repr(thing, reasonable=200, limit=10000):
570    """
571    A dynamic representation of an object which is simply a pre-formatted
572    string for objects whose representations are reasonably small, and
573    which turns into a display box for representations which are larger,
574    with text being truncated after a (very large) limit.
575
576    The threshold for reasonably small representations as well as the
577    hard limit may be customized; use None for the hard limit to disable
578    truncation entirely (but don't complain about file sizes if you do).
579    """
580    rep = big_repr(thing)
581    if len(rep) <= reasonable:
582        return '<pre class="short_repr">{rep}</pre>'.format(
583            rep=escape(rep)
584        )
585    else:
586        return build_display_box(escape(truncate(rep, limit)))

A dynamic representation of an object which is simply a pre-formatted string for objects whose representations are reasonably small, and which turns into a display box for representations which are larger, with text being truncated after a (very large) limit.

The threshold for reasonably small representations as well as the hard limit may be customized; use None for the hard limit to disable truncation entirely (but don't complain about file sizes if you do).

def html_traceback(exc=None, title=None, linkable=None):
593def html_traceback(exc=None, title=None, linkable=None):
594    """
595    In an exception handler, returns an HTML string that includes the
596    exception type, message, and traceback. Must be called from an except
597    clause, unless an exception object with a __traceback__ value is
598    provided.
599
600    If title is given and not None, a details tag will be returned
601    using that title, which can be expanded to show the traceback,
602    otherwise just a pre tag is returned containing the traceback.
603
604    If linkable is given, it must be a dictionary mapping filenames to
605    task IDs, and line numbers of those files which appear in the
606    traceback will be turned into links to those lines.
607    """
608    result = escape(string_traceback(exc))
609    if linkable:
610        for fn in linkable:
611            taskid = linkable[fn]
612
613            def replacer(match):
614                link = html_link_to_line(taskid, fn, int(match.group(1)))
615                return '{}&quot;, line {},'.format(fn, link)
616
617            pat = r'{}&quot;, line ([0-9]+),'.format(fn)
618            result = re.sub(pat, replacer, result)
619
620    pre = '<pre class="traceback">\n{}\n</pre>'.format(result)
621    if title is not None:
622        return build_html_details(title, pre, "error")
623    else:
624        return pre

In an exception handler, returns an HTML string that includes the exception type, message, and traceback. Must be called from an except clause, unless an exception object with a __traceback__ value is provided.

If title is given and not None, a details tag will be returned using that title, which can be expanded to show the traceback, otherwise just a pre tag is returned containing the traceback.

If linkable is given, it must be a dictionary mapping filenames to task IDs, and line numbers of those files which appear in the traceback will be turned into links to those lines.

REWRITES = {'/home/pmwh/projects/potluck/potluck': '<potluck>'}

The mapping from filenames to replacements to be used for rewriting filenames in tracebacks.

def set_tb_rewrite(filename, rewrite_as):
636def set_tb_rewrite(filename, rewrite_as):
637    """
638    Sets up a rewriting rule that will be applied to string and HTML
639    tracebacks. You must provide the filename that should be rewritten,
640    and the string to replace it with. Set rewrite_as to None to remove a
641    previously-established rewrite rule.
642    """
643    global REWRITES
644    REWRITES[filename] = rewrite_as

Sets up a rewriting rule that will be applied to string and HTML tracebacks. You must provide the filename that should be rewritten, and the string to replace it with. Set rewrite_as to None to remove a previously-established rewrite rule.

def string_traceback(exc=None):
647def string_traceback(exc=None):
648    """
649    When called in an exception handler, returns a multi-line string
650    including what Python would normally print: the exception type,
651    message, and a traceback. You can also call it anywhere if you can
652    provide an exception object which has a __traceback__ value.
653
654    The traceback gets obfuscated by replacing full file paths that start
655    with the potluck directory with just the package directory and
656    filename, and by replacing full file paths that start with the spec
657    directory with just the task ID and then the file path from the spec
658    directory (usually starter/, soln/, or the submitted file itself).
659    """
660    sfd = os.path.split(file_utils.get_spec_file_name())[0]
661    rewrites = {}
662    rewrites.update(REWRITES)
663    if sfd:
664        rewrites[sfd] = "<task>"
665
666    if exc is None:
667        raw = traceback.format_exc()
668    else:
669        raw = ''.join(traceback.format_tb(exc.__traceback__) + [ str(exc) ])
670
671    return rewrite_traceback_filenames(raw, rewrites)

When called in an exception handler, returns a multi-line string including what Python would normally print: the exception type, message, and a traceback. You can also call it anywhere if you can provide an exception object which has a __traceback__ value.

The traceback gets obfuscated by replacing full file paths that start with the potluck directory with just the package directory and filename, and by replacing full file paths that start with the spec directory with just the task ID and then the file path from the spec directory (usually starter/, soln/, or the submitted file itself).

def rewrite_traceback_filenames(raw_traceback, prefix_map=None):
674def rewrite_traceback_filenames(raw_traceback, prefix_map=None):
675    """
676    Accepts a traceback as a string, and returns a modified string where
677    filenames have been altered by replacing the keys of the given
678    prefix_map with their values. In addition, filenames which include a
679    directory that ends in "__tmp" will have all directory entries up to
680    and including that one stripped from their path.
681
682    If no prefix map is given, the value of REWRITES will be used.
683    """
684    prefix_map = prefix_map or REWRITES
685    result = raw_traceback
686    for prefix in prefix_map:
687        replace = prefix_map[prefix]
688        result = result.replace(
689            'File "{prefix}'.format(prefix=prefix),
690            'File "{replace}'.format(replace=replace)
691        )
692
693    result = re.sub(
694        'File ".*__tmp' + os.path.sep,
695        'File "<submission>/',
696        result
697    )
698
699    return result

Accepts a traceback as a string, and returns a modified string where filenames have been altered by replacing the keys of the given prefix_map with their values. In addition, filenames which include a directory that ends in "__tmp" will have all directory entries up to and including that one stripped from their path.

If no prefix map is given, the value of REWRITES will be used.

def function_def_code_tags(fn_name, params_pattern, announce=None):
706def function_def_code_tags(fn_name, params_pattern, announce=None):
707    """
708    Returns a tuple containing two strings of HTML code used to represent
709    the given function definition in both short and long formats. The
710    short format just lists the first acceptable definition, while the
711    long format lists all of them. Note that both fn_name and
712    params_pattern may be lists of strings instead of strings; see
713    function_def_patterns.
714    """
715    if isinstance(fn_name, str):
716        names = [fn_name]
717    else:
718        names = list(fn_name)
719
720    # If there are specific parameters we can give users more info about
721    # what they need to do.
722    if isinstance(params_pattern, str):
723        specific_names = [
724            "{}({})".format(name, params_pattern) for name in names
725        ]
726    else:
727        specific_names = names
728
729    # Figure out what we're announcing as:
730    if announce is None:
731        announce = specific_names[0]
732
733    # Make code tag and detailed code tag:
734    code_tag = "<code>{}</code>".format(announce)
735    details_code = phrasing.comma_list(
736        ["<code>{}</code>".format(n) for n in specific_names],
737        junction="or"
738    )
739
740    # Add a comment about the number of parameters required
741    if isinstance(params_pattern, int):
742        with_n = " with {} {}".format(
743            params_pattern,
744            phrasing.plural(params_pattern, "parameter")
745        )
746        code_tag += with_n
747        details_code += with_n
748
749    return code_tag, details_code

Returns a tuple containing two strings of HTML code used to represent the given function definition in both short and long formats. The short format just lists the first acceptable definition, while the long format lists all of them. Note that both fn_name and params_pattern may be lists of strings instead of strings; see function_def_patterns.

def function_call_code_tags(fn_name, args_pattern, is_method=False):
752def function_call_code_tags(fn_name, args_pattern, is_method=False):
753    """
754    Works like `potluck.patterns.function_call_patterns`, but generates a
755    pair of HTML strings with summary and detailed descriptions of the
756    function call. In that sense it's also similar to
757    `function_def_code_tags`, except that it works for a function call
758    instead of a function definition.
759
760    If the args_pattern is "-any arguments-", the parentheses for the
761    function call will be omitted entirely.
762    """
763    if isinstance(fn_name, str):
764        names = [fn_name]
765    else:
766        names = list(fn_name)
767
768    # If there are specific args we can give users more info about what
769    # they need to do.
770    if args_pattern == "-any arguments-":
771        specific_names = names
772    elif isinstance(args_pattern, str):
773        if is_method:
774            specific_names = [
775                ".{}({})".format(name, args_pattern)
776                for name in names
777            ]
778        else:
779            specific_names = [
780                "{}({})".format(name, args_pattern)
781                for name in names
782            ]
783    else:
784        specific_names = names
785
786    # Make code tag and detailed code tag:
787    code_tag = "<code>{}</code>".format(
788        escape(specific_names[0])
789    )
790
791    details_code = phrasing.comma_list(
792        [
793            "<code>{}</code>".format(escape(name))
794            for name in specific_names
795        ],
796        junction="or"
797    )
798
799    return code_tag, details_code

Works like potluck.patterns.function_call_patterns, but generates a pair of HTML strings with summary and detailed descriptions of the function call. In that sense it's also similar to function_def_code_tags, except that it works for a function call instead of a function definition.

If the args_pattern is "-any arguments-", the parentheses for the function call will be omitted entirely.

def args_repr_list(args, kwargs):
802def args_repr_list(args, kwargs):
803    """
804    Creates an HTML string representation of the given positional and
805    keyword arguments, as a bulleted list.
806    """
807    arg_items = []
808    for arg in args:
809        arg_items.append(dynamic_html_repr(arg))
810
811    for kw in kwargs:
812        key_repr = dynamic_html_repr(kw)
813        val_repr = dynamic_html_repr(kwargs[kw])
814        arg_items.append(key_repr + "=" + val_repr)
815
816    return build_list(arg_items)

Creates an HTML string representation of the given positional and keyword arguments, as a bulleted list.