potluck.snippets
Code for defining examples that should be shown as part of instructions.
snippets.py
This module can be used in specifications files to define named examples, which can then be compiled to HTML files using the --snippets option. These examples will be evaluated within the context of the solution code, and their output + return values will be formatted for display to the students. By using snippets this way, as long as your solution file is up-to-date, your example output will never be out-of-sync with what the problem set is actually asking for.
Example usage:
from potluck import snippets as sn
EXAMPLE = [
{
"key": "value",
"key2": "value2",
},
{
"key": "valueB",
"key2": "value2B"
}
]
sn.Variables(
"vars", # snippet ID
"<code>EXAMPLE</code> variable", # snippet displayed title
(
"A simple example of what the input data might look like, and"
" the slightly more complex <code>DATA</code> variable provided"
" in the starter code."
), # displayed snippet caption
[ "EXAMPLE", "DATA" ] # list of variable names to display definitions of
).provide({ "EXAMPLE": EXAMPLE })
# provide overrides (or provides missing) solution module values
sn.FunctionCalls(
"examples", # snippet ID
"Example results", # title
(
"Some examples of what <code>processData</code> should return"
" for various inputs, using the <code>EXAMPLE</code> and"
" <code>DATA</code> variables shown above."
),
# caption (note we're assuming the 'vars' snippet will be included
# first, otherwise this caption doesn't make sense).
[
("processData", (EXAMPLE, 1)),
("processData", (EXAMPLE, 2)),
("processData", (EXAMPLE, 3))",
("processData", (cu.SolnValue("DATA"), 3)),
], # list of function name, arguments tuples to evaluate
)
sn.RunModule(
"run", # ID
"Full output example", # title
(
"An example of what the output should look like when your code"
" is run. Note that the blue text shows what inputs were"
" provided in this example."
), # caption
).provide_inputs(["A", "B"]) # we're providing some inputs during the run
The various snippet classes like Variable
, Expressions
, and
RunModule
each inherit from potluck.specifications.TestGroup
meaning that you can use the various modification methods from that
module (such as provide_inputs
as shown above) to control exactly how
the code is run.
1""" 2Code for defining examples that should be shown as part of instructions. 3 4snippets.py 5 6This module can be used in specifications files to define named examples, 7which can then be compiled to HTML files using the --snippets option. 8These examples will be evaluated within the context of the solution code, 9and their output + return values will be formatted for display to the 10students. By using snippets this way, as long as your solution file is 11up-to-date, your example output will never be out-of-sync with what the 12problem set is actually asking for. 13 14Example usage: 15 16```py 17from potluck import snippets as sn 18 19EXAMPLE = [ 20 { 21 "key": "value", 22 "key2": "value2", 23 }, 24 { 25 "key": "valueB", 26 "key2": "value2B" 27 } 28] 29 30sn.Variables( 31 "vars", # snippet ID 32 "<code>EXAMPLE</code> variable", # snippet displayed title 33 ( 34 "A simple example of what the input data might look like, and" 35 " the slightly more complex <code>DATA</code> variable provided" 36 " in the starter code." 37 ), # displayed snippet caption 38 [ "EXAMPLE", "DATA" ] # list of variable names to display definitions of 39).provide({ "EXAMPLE": EXAMPLE }) 40# provide overrides (or provides missing) solution module values 41 42sn.FunctionCalls( 43 "examples", # snippet ID 44 "Example results", # title 45 ( 46 "Some examples of what <code>processData</code> should return" 47 " for various inputs, using the <code>EXAMPLE</code> and" 48 " <code>DATA</code> variables shown above." 49 ), 50 # caption (note we're assuming the 'vars' snippet will be included 51 # first, otherwise this caption doesn't make sense). 52 [ 53 ("processData", (EXAMPLE, 1)), 54 ("processData", (EXAMPLE, 2)), 55 ("processData", (EXAMPLE, 3))", 56 ("processData", (cu.SolnValue("DATA"), 3)), 57 ], # list of function name, arguments tuples to evaluate 58) 59 60sn.RunModule( 61 "run", # ID 62 "Full output example", # title 63 ( 64 "An example of what the output should look like when your code" 65 " is run. Note that the blue text shows what inputs were" 66 " provided in this example." 67 ), # caption 68).provide_inputs(["A", "B"]) # we're providing some inputs during the run 69``` 70 71The various snippet classes like `Variable`, `Expressions`, and 72`RunModule` each inherit from `potluck.specifications.TestGroup` 73meaning that you can use the various modification methods from that 74module (such as `provide_inputs` as shown above) to control exactly how 75the code is run. 76""" 77 78import textwrap 79import re 80import os 81 82import pygments 83 84from . import logging 85from . import specifications 86from . import file_utils 87from . import html_tools 88from . import harness 89from . import render 90from . import rubrics 91from . import contexts 92from . import context_utils 93 94 95#---------# 96# Helpers # 97#---------# 98 99FMT_WIDTH = 80 100""" 101Line width we'll attempt to hit for formatting output nicely. 102""" 103 104 105def wrap_str(string, width): 106 """ 107 Returns a list of strings, which when combined form the given 108 string, where none is longer than the given width. Lines are broken 109 at spaces whenever possible, but may be broken in the middle of 110 words when a single word is longer than the target width. This 111 function uses a greedy algorithm that doesn't optimize the evenness 112 of the right margin. \\n and \\r are represented by those character 113 pairs, but other characters are as-is. 114 """ 115 escaped = string.replace('\n', '\\n') 116 escaped = escaped.replace('\r', '\\r') 117 words = escaped.split(' ') 118 result = [] 119 while len(words) > 0: # until we've used up the words 120 # Figure out how many words would fit on the next line 121 i = 0 122 # + i accounts for spaces we'll need 123 while len(words[:i + 1]) + (i + 1) < width: 124 i += 1 125 126 # Back off to the number that fits 127 i -= 1 128 129 # first word doesn't fit! 130 if i == -1: 131 first = words[0] 132 # Chop up the first word and put the pieces into the result 133 pieces = len(first) // width 134 for j in range(pieces): 135 result.append(first[width * j:width * (j + 1)]) 136 137 # Handle leftovers 138 rest = first[width * pieces:] 139 # Note that even empty leftovers serve as a placeholder to 140 # ensure that a space gets added. 141 words[0] = rest # put rest back into words, replacing old 142 else: 143 # at least one word fits 144 line = ' '.join(words[:i + 1]) + ' ' 145 result.append(line) 146 words = words[i + 1:] 147 148 # Remove the extra space introduced after the end of the last word 149 # Works even for an empty string input 150 result[-1] = result[-1][:-1] 151 152 return result 153 154 155def wrapped_repr(obj, width=FMT_WIDTH, indentation=1, prefix='', memo=None): 156 """ 157 Returns a string representing the given object, whose lines are not 158 longer than the given width, and which uses the given number of 159 spaces to indent sub-objects when necessary. Replacement for 160 pprint.pformat, which does NOT reorder dictionary keys. May violate 161 the width restriction when given a complex object that it doesn't 162 know how to wrap whose repr alone is wider than the specified width. 163 164 A prefix may be supplied, which will be added to the first line of 165 the given representation as-is. The prefix itself will not be subject 166 to wrapping, but the first line of the result will in some cases wrap 167 earlier to compensate for the prefix. The prefix should not include 168 any newlines or carriage returns. 169 170 The memo helps avoid problems with recursion, and doesn't need to be 171 provided explicitly. 172 """ 173 # Create a memo if we need to 174 if memo is None: 175 memo = set() 176 177 # Check the memo 178 if id(obj) in memo: 179 return '...' 180 181 # Add ourselves to the memo 182 memo.add(id(obj)) 183 184 simple = prefix + repr(obj) 185 186 if len(simple) <= width: # no need to do anything fancy 187 return simple 188 189 indent = ' ' * indentation 190 between = '\n' + indent 191 192 broken = repr(obj) 193 if len(broken) < width - indentation: 194 return prefix + '\\' + between + broken 195 196 elif type(obj) == str: # need to wrap this string 197 lines = wrap_str(obj, width - (indentation + 2)) 198 return ( 199 prefix + '(' + between 200 + between.join('"' + line + '"' for line in lines) 201 + '\n)' 202 ) 203 204 elif type(obj) == bytes: # need to wrap these bytes 205 lines = wrap_str(obj, width - (indentation + 3)) 206 return ( 207 prefix + '(' + between 208 + between.join('b"' + line + '"' for line in lines) 209 + '\n)' 210 ) 211 212 elif type(obj) in (list, tuple, set): # need to wrap each item 213 if type(obj) in (list, tuple): 214 ldelim, rdelim = str(type(obj)()) 215 elif len(obj) == 0: 216 return prefix + 'set()' # too bad if width is < 5 217 else: 218 ldelim = '{' 219 rdelim = '}' 220 221 result = prefix + ldelim + between 222 for item in obj: 223 result += textwrap.indent( 224 wrapped_repr(item, width - indentation, indentation), 225 indent 226 ) + ',' + between 227 228 return result[:-len(between)] + '\n' + rdelim 229 230 elif type(obj) == dict: # need to wrap keys and values 231 indent = ' ' * indentation 232 between = '\n' + indent 233 234 result = prefix + '{' + between 235 for key in obj: 236 key_repr = wrapped_repr( 237 key, 238 width - (indentation + 1), 239 indentation 240 ) 241 val = obj[key] 242 val_repr = wrapped_repr( 243 val, 244 width - indentation * 2, 245 indentation 246 ) 247 248 # Can we fit on one line? 249 if len(indent) + len(key_repr) + len(val_repr) + 3 <= width: 250 result += key_repr + ': ' + val_repr + ',' + between 251 elif val_repr[1] == '\n': # ldelim/newline start 252 result += ( 253 key_repr + ':' + val_repr[0] + '\n' 254 + textwrap.indent(val_repr[2:], indent) 255 + ',' + between 256 ) 257 else: 258 result += key_repr + ':\n' + textwrap.indent( 259 val_repr, 260 indent * 2 261 ) + ',' + between 262 263 # Chop off final between and add closing brace after a newline 264 result = result[:-len(between)] + '\n}' 265 266 return result 267 268 else: # Not sure how to wrap this, so we give up... 269 return prefix + repr(obj) 270 271 272def wrapped_with_prefix(obj, prefix, indentation_level=0): 273 """ 274 Returns a string representing the given object, as `wrapped_repr` 275 would, except that: 276 277 1. The first line includes the given prefix (as a string without 278 repr applied to it; after indentation) and the wrapping of object 279 values takes this into account. 280 2. The entire representation is indented by the given indentation 281 level (one space per level). 282 """ 283 rep = wrapped_repr( 284 obj, 285 width=FMT_WIDTH - indentation_level, 286 indentation=1, 287 prefix=prefix 288 ) 289 290 # remove extra indentation from tuple 291 rep = textwrap.dedent(rep) 292 293 # restore indent and add prefix 294 return textwrap.indent(rep, ' ' * indentation_level) 295 296 297def extract(context, key): 298 """ 299 Gets value from a context but raises a ValueError showing the 300 context's error log if it can't. 301 """ 302 if key in context: 303 return context[key] 304 else: 305 # Note: we use a ValueError here instead of a KeyError because 306 # KeyError uses repr on its message... 307 if "ref_error_log" in context: 308 raise ValueError( 309 ( 310 "Error: context is missing the '{}' key. Error log" 311 " is:\n{}" 312 ).format(key, context["ref_error_log"]), 313 ) 314 else: 315 raise ValueError( 316 "Error: context is missing the '{}' key. No error log" 317 " available." 318 ) 319 320 321#--------------# 322# Registration # 323#--------------# 324 325SNIPPET_REGISTRY = {} 326""" 327All registered snippets, by defining module and then snippet ID. 328""" 329 330 331#-----------------------# 332# Snippet setup/cleanup # 333#-----------------------# 334 335REMEMBER = {} 336""" 337Things to remember so we can undo setup changes. 338""" 339 340 341def snippet_setup(context): 342 """ 343 If the wavesynth module is available, disables the playTrack and 344 saveTrack functions from that module. If the optimism module is 345 available, disables colors for that module. 346 """ 347 try: 348 import wavesynth 349 REMEMBER["playTrack"] = wavesynth.playTrack 350 wavesynth.playTrack = lambda _=None: None 351 REMEMBER["saveTrack"] = wavesynth.saveTrack 352 wavesynth.saveTrack = lambda _: None 353 except ModuleNotFoundError: 354 pass 355 356 try: 357 import optimism 358 REMEMBER["opt_colors"] = optimism.COLORS 359 optimism.colors(False) 360 except ModuleNotFoundError: 361 pass 362 363 return context 364 365 366def snippet_cleanup(context): 367 """ 368 Puts things back how they were before `snippet_setup`. 369 """ 370 try: 371 import wavesynth 372 wavesynth.playTrack = REMEMBER["playTrack"] 373 wavesynth.saveTrack = REMEMBER["saveTrack"] 374 except ModuleNotFoundError: 375 pass 376 377 try: 378 import optimism 379 optimism.colors(REMEMBER["opt_colors"]) 380 except ModuleNotFoundError: 381 pass 382 383 return context 384 385 386#----------------------------# 387# Snippet class & subclasses # 388#----------------------------# 389 390class Snippet(specifications.TestGroup): 391 """ 392 Common functionality for snippets. Don't instantiate this class; 393 instead use one of the subclasses `Variables`, `Expressions`, or 394 `RunModule`. 395 396 Note that a `Snippet` is a `potluck.specifications.TestGroup`, from 397 which it inherits a powerful interface for customizing behavior. Some 398 default modifications are applied to every snippet: 399 400 1. Output, including stderr and error tracebacks, is captured. 401 2. A setup function is run which disables turtle tracing, wavesynth 402 track playing/saving, and optimism colors (only when the relevant 403 module(s) are available). A cleanup function that reverses all of 404 these changes except the turtle speed is also run afterwards. 405 """ 406 @staticmethod 407 def base_context(task_info): 408 """ 409 Creates a base context object for a snippet, given a task info 410 object. 411 """ 412 return { 413 "task_info": task_info, 414 "username": "__soln__", 415 "submission_root": task_info["specification"].soln_path, 416 "default_file": task_info["target"], 417 "actual_file": task_info["target"] 418 } 419 420 def __init__( 421 self, 422 sid, 423 title, 424 caption, 425 tests, 426 postscript='', 427 glamers=None, 428 ignore_errors=False 429 ): 430 """ 431 All `Snippet`s have a snippet ID, a title, a caption, and one or 432 more tests. `tests` must be an iterable of unregistered 433 `potluck.specifications.TestCase` objects. 434 435 An optional postscript is markdown source to be placed after the 436 code block. 437 438 An optional list of glamer functions may be provided. These will 439 be given individual pieces of HTML code and the associated 440 context those were rendered from and their result value will 441 replace the HTML code to be used; they are applied in-order. 442 443 Unless ignore_errors is set to False (default is True), errors 444 generated by the base payload and captured along with output will 445 be logged under the assumption that in most cases an exception in 446 a snippet is a bug with the snippet code rather than intentional. 447 """ 448 # Set ourselves up as a TestGroup... 449 super().__init__(sid, '_') 450 451 # We store our arguments 452 self.title = title 453 self.caption = caption 454 self.postscript = postscript 455 self.glamers = glamers or [] 456 self.ignore_errors = ignore_errors 457 458 # Final edit function to manipulate the context to be rendered 459 self.editor = None 460 461 # What's the name of our spec file? 462 self.spec_file = file_utils.get_spec_file_name() 463 464 # Add our tests to ourself 465 for t in tests: 466 self.add(t) 467 468 # Fetch the defining-module-specific registry 469 reg = SNIPPET_REGISTRY.setdefault( 470 file_utils.get_spec_module_name(), 471 {} 472 ) 473 474 # Prevent ID collision 475 if sid in reg: 476 raise ValueError( 477 "Multiple snippets cannot be registered with the same" 478 " ID ('{sid}')." 479 ) 480 481 # Register ourself 482 reg[sid] = self 483 484 # A place to cache our compiled result 485 self.cached_for = None 486 self.cached_value = None 487 488 # Set up default augmentations 489 self.capture_output(capture_errors=True, capture_stderr=True) 490 self.do_setup(snippet_setup) 491 self.do_cleanup(snippet_cleanup) 492 493 def provide(self, vars_map): 494 """ 495 A convenience function for providing the snippet with values not 496 defined in the solution module. Under the hood, this uses 497 `specifications.HasPayload.use_decorations`, so using that 498 alongside this will not work (one will overwrite the other). This 499 also means that you cannot call `provide` multiple times on the 500 same instance to accumulate provided values: each call discards 501 previous provided values. 502 """ 503 self.use_decorations( 504 { 505 k: (lambda _: v) 506 for (k, v) in vars_map.items() 507 }, 508 ignore_missing=True 509 ) 510 511 # TODO: define replacements so that formatting of calls can use 512 # varnames? 513 514 def compile(self, base_context): 515 """ 516 Runs the snippet, collects its results, and formats them as HTML. 517 Returns a string containing HTML code for displaying the snippet 518 (which assumes that the potluck.css stylesheet will be loaded). 519 520 This method will always re-run the compilation process, 521 regardless of whether a cached result is available, and will 522 update the cached result. Use `Snippet.get_html` to recompile 523 only as needed. 524 525 Requires a base context object (see `potluck.contexts.Context` 526 for the required structure). 527 528 The returned HTML's outermost tag is a <section> tag with 529 an id that starts with 'snippet:' and then ends with the 530 snippet's ID value. 531 """ 532 # Set up traceback-rewriting for the specifications module we 533 # were defined in 534 html_tools.set_tb_rewrite( 535 base_context["task_info"]["specification"].__file__, 536 "<task specification>" 537 ) 538 html_tools.set_tb_rewrite( 539 base_context["submission_root"], 540 "<solution>" 541 ) 542 543 # Tell the run_for_base_and_ref_values augmentation to run only 544 # for ref values. 545 self.augmentations.setdefault( 546 "run_for_base_and_ref_values", 547 {} 548 )["ref_only"] = True 549 550 for case in self.tests: 551 case.augmentations.setdefault( 552 "run_for_base_and_ref_values", 553 {} 554 )["ref_only"] = True 555 556 # Create our Goal object 557 goal = self.provide_goal() 558 559 # Reset and evaluate our goal: 560 goal.reset_network() 561 goal.evaluate(base_context) 562 563 # Grab the contexts that were used for each test 564 if goal.test_in and len(goal.test_in["contexts"]) > 0: 565 contexts = goal.test_in["contexts"] 566 else: 567 # This shouldn't be possible unless an empty list of 568 # variables or functions was provided... 569 raise ValueError( 570 "A Snippet must have at least one context to compile." 571 " (Did you pass an empty list to Variables or" 572 " FunctionCalls?)" 573 ) 574 575 # Translate from markdown 576 title = render.render_markdown(self.title) 577 caption = render.render_markdown(self.caption) 578 post = render.render_markdown(self.postscript) 579 580 context_dicts = [] 581 for ctx in contexts: 582 try: 583 cdict = ctx.create(base_context) 584 except context_utils.ContextCreationError: 585 cdict = { 586 # for RunModule snippets (note no ref_) 587 "filename": "UNKNOWN", 588 # In case it's a Variables snippet 589 "ref_variable": "UNKNOWN", 590 # for FunctionCalls snippets 591 "ref_function": "UNKNOWN", 592 "ref_args": (), 593 "ref_kwargs": {}, 594 # for Blocks snippets 595 "ref_block": "UNKNOWN", 596 # For all kinds of snippets 597 "ref_value": "There was an error compiling this snippet.", 598 "ref_output": "There was an error compiling this snippet.", 599 "ref_error_log": html_tools.string_traceback() 600 } 601 context_dicts.append(cdict) 602 603 # Strip out solution file paths from output, and perform final 604 # edits if we have an editor function 605 soln_path = os.path.abspath( 606 base_context["task_info"]["specification"].soln_path 607 ) 608 for context in context_dicts: 609 if ( 610 "ref_output" in context 611 and soln_path in context["ref_output"] 612 ): 613 context["ref_output"] = context["ref_output"].replace( 614 soln_path, 615 '<soln>' 616 ) 617 618 if ( 619 "ref_error_log" in context 620 and soln_path in context["ref_error_log"] 621 ): 622 context["ref_error_log"] = context["ref_error_log"].replace( 623 soln_path, 624 '<soln>' 625 ) 626 627 if self.editor: 628 self.editor(context) 629 630 # Include title & caption, and render each context to produce our 631 # result 632 result = ( 633 '<section class="snippet" id="snippet:{sid}">' 634 '<div class="snippet_title">{title}</div>\n' 635 '{caption}\n<pre>{snippet}</pre>\n{post}' 636 '</section>' 637 ).format( 638 sid=self.base_name, 639 title=title, 640 caption=caption, 641 post=post, 642 snippet=''.join( 643 self.compile_context(cd) 644 for cd in context_dicts 645 ) 646 ) 647 648 self.cached_for = base_context["task_info"]["id"] 649 self.cached_value = result 650 651 return result 652 653 def compile_context(self, context): 654 """ 655 Turns a context dictionary resulting from an individual snippet 656 case into HTML code for displaying that result, using 657 `render_context` but also potentially adding additional HTML for 658 extra context slots (see e.g., `show_image` and `play_audio`). 659 """ 660 if not self.ignore_errors and "ref_error" in context: 661 logging.log( 662 "Error captured by context creation:\n" 663 + context["ref_error"] 664 + "\nFull log is:\n" 665 + context.get("ref_error_log", '<missing>') 666 ) 667 result = self.render_context(context) 668 for glamer in self.glamers: 669 result = glamer(result, context) 670 671 return result 672 673 def render_context(self, context): 674 """ 675 Override this to define how a specific Snippet sub-class should 676 render a context dictionary into HTML code. 677 """ 678 raise NotImplementedError( 679 "Snippet is abstract and context rendering is only available" 680 " via overrides in sub-classes." 681 ) 682 683 def get_html(self, task_info): 684 """ 685 Returns the snippet value, either cached or newly-compiled 686 depending on the presence of an appropriate cached value. 687 """ 688 if self.cached_for == task_info["id"]: 689 return self.cached_value 690 else: 691 return self.compile(Snippet.base_context(task_info)) 692 693 def final_edit(self, editor): 694 """ 695 Sets up a function to be run on each created context just before 696 that context gets rendered. The editor may mutate the context it 697 is given in order to change what gets rendered. Multiple calls to 698 this function each simply replace the previous editor function. 699 700 Note that the ref_* values from the context are used to create 701 the snippet. 702 """ 703 self.editor = editor 704 705 def show_image(self): 706 """ 707 Adds a glamer that augments output by appending a "Image " 708 prompt followed by an image element which shows the contents of 709 the "ref_image" context slot. To establish a "ref_image" slot, 710 call a method like 711 `potluck.specifications.HasPayload.capture_turtle_image`. 712 713 Returns self for chaining. 714 """ 715 self.glamers.append(append_image_result) 716 return self 717 718 def play_audio(self): 719 """ 720 Adds a glamer that augments output by appending an "Audio " 721 prompt followed by an audio element which plays the contents of 722 the "ref_audio" context slot. To make an "ref_audio" slot, call a 723 method like 724 `potluck.specifications.HasPayload.capture_wavesynth`. 725 726 Returns self for chaining. 727 """ 728 self.glamers.append(append_audio_result) 729 return self 730 731 def show_memory_report(self): 732 """ 733 Adds a glamer that augments output by appending a "Memory Report" 734 prompt followed by a memory report showing the structure of the 735 "ref_value" context slot. 736 737 Returns self for chaining. 738 """ 739 self.glamers.append(append_memory_report) 740 return self 741 742 def show_file_contents(self): 743 """ 744 Adds a glamer that augments output by appending a "File" 745 prompt followed by a text box showing the filename and the 746 contents of that file. Requires 'ref_output_filename' and 747 'ref_output_file_contents' slots, which could be established 748 using a method like 749 `potluck.specifications.HasPayload.capture_file_contents` 750 751 Returns self for chaining. 752 """ 753 self.glamers.append(append_file_contents) 754 return self 755 756 757def highlight_code(code): 758 """ 759 Runs pygments highlighting but converts from a div/pre/code setup 760 back to just a code tag with the 'highlight' class. 761 """ 762 markup = pygments.highlight( 763 code, 764 pygments.lexers.PythonLexer(), 765 pygments.formatters.HtmlFormatter() 766 ) 767 # Note: markup will start a div and a pre we want to get rid of 768 start = '<div class="highlight"><pre>' 769 end = '</pre></div>\n' 770 if markup.startswith(start): 771 markup = markup[len(start):] 772 if markup.endswith(end): 773 markup = markup[:-len(end)] 774 775 return '<code class="highlight">{}</code>'.format(markup) 776 777 778class Variables(Snippet): 779 """ 780 A snippet which shows the definition of one or more variables. Use 781 this to display examples of input data, especially when you want to 782 use shorter expressions in examples of running code. If you want to 783 use variables that aren't defined in the solution module, use the 784 `Snippet.provide` method (but note that that method is incompatible 785 with using `specifications.HasPayload.use_decorations`). 786 """ 787 def __init__(self, sid, title, caption, varnames, postscript=''): 788 """ 789 A snippet ID, a title, and a caption are required, as is a list 790 of strings indicating the names of variables to show definitions 791 of. Use `Snippet.provide` to supply values for variables not in 792 the solution module. 793 794 A postscript is optional (see `Snippet`). 795 """ 796 cases = [ 797 specifications.TestValue( 798 varname, 799 register=False 800 ) 801 for varname in varnames 802 ] 803 super().__init__(sid, title, caption, cases, postscript) 804 805 def render_context(self, context): 806 """ 807 Renders a context created for goal evaluation as HTML markup for 808 the definition of a variable, including Jupyter-notebook-style 809 prompts. 810 """ 811 varname = extract(context, "ref_variable") 812 value = extract(context, "ref_value") 813 814 # format value's repr using wrapped_repr, but leaving room for 815 # varname = at beginning of first line 816 rep = wrapped_with_prefix(value, varname + ' = ') 817 818 # Use pygments to generate HTML markup for our assignment 819 markup = highlight_code(rep) 820 return ( 821 '<span class="prompt input">In []:</span>' 822 '<div class="snippet-input">{}</div>\n' 823 ).format(markup) 824 825 826class RunModule(Snippet): 827 """ 828 A snippet which shows the output produced by running a module. 829 Functions like `potluck.specifications.HasPayload.provide_inputs` 830 can be used to control exactly what happens. 831 832 The module to import is specified by the currently active file 833 context (see `potluck.contexts.FileContext`). 834 """ 835 def __init__(self, sid, title, caption, postscript=''): 836 """ 837 A snippet ID, title, and caption are required. 838 839 A postscript is optional (see `Snippet`). 840 """ 841 cases = [ specifications.TestImport(register=False) ] 842 super().__init__(sid, title, caption, cases, postscript) 843 844 def render_context(self, context): 845 """ 846 Renders a context created for goal evaluation as HTML markup for 847 running a module. Includes a Jupyter-style prompt with %run magic 848 syntax to show which file was run. 849 """ 850 filename = extract(context, "filename") # Note: no ref_ here 851 captured = context.get("ref_output", '') 852 captured_errs = context.get("ref_error_log", '') 853 854 # Wrap faked inputs with spans so we can color them blue 855 captured = re.sub( 856 harness.FAKE_INPUT_PATTERN, 857 r'<span class="input">\1</span>', 858 captured 859 ) 860 861 result = ( 862 '<span class="prompt input">In []:</span>' 863 '<div class="snippet-input">' 864 '<code class="magic">%run {filename}</code>' 865 '</div>\n' 866 ).format(filename=filename) 867 if captured: 868 result += ( 869 '<span class="prompt printed">Prints</span>' 870 '<div class="snippet-printed">{captured}</div>\n' 871 ).format(captured=captured) 872 if captured_errs: 873 result += ( 874 '<span class="prompt stderr">Logs</span>' 875 '<div class="snippet-stderr">{log}</div>\n' 876 ).format(log=captured_errs) 877 878 return result 879 880 881class FunctionCalls(Snippet): 882 """ 883 A snippet which shows the results (printed output and return values) 884 of calling one or more functions. To control what happens in detail, 885 use specialization methods from `potluck.specifications.HasPayload` 886 and `potluck.specifications.HasContext`. 887 """ 888 def __init__(self, sid, title, caption, calls, postscript=''): 889 """ 890 A snippet ID, title, and caption are required, along with a list 891 of function calls. Each entry in the list must be a tuple 892 containing a function name followed by a tuple of arguments, 893 and optionally, a dictionary of keyword arguments. 894 895 A postscript is optional (see `Snippet`). 896 """ 897 cases = [ 898 specifications.TestCase( 899 fname, 900 args, 901 kwargs or {}, 902 register=False 903 ) 904 for fname, args, kwargs in ( 905 map(lambda case: (case + (None,))[:3], calls) 906 ) 907 ] 908 super().__init__(sid, title, caption, cases, postscript) 909 910 def render_context(self, context): 911 """ 912 Renders a context created for goal evaluation as HTML markup for 913 calling a function. Includes a Jupyter-style prompt for input as 914 well as the return value. 915 """ 916 if "ref_error" in context: 917 print( 918 "Error during context creation:" 919 + context["ref_error"] 920 + "\nFull log is:\n" 921 + context.get("ref_error_log", '<missing>') 922 ) 923 fname = extract(context, "ref_function") 924 value = extract(context, "ref_value") 925 args = extract(context, "ref_args") 926 kwargs = extract(context, "ref_kwargs") 927 captured = context.get("ref_output", '') 928 captured_errs = context.get("ref_error_log", '') 929 930 # Figure out representations of each argument 931 argreps = [] 932 for arg in args: 933 argreps.append(wrapped_with_prefix(arg, '', 1)) 934 935 for kw in kwargs: 936 argreps.append(wrapped_with_prefix(kwargs[kw], kw + "=", 1)) 937 938 # Figure out full function call representation 939 oneline = "{}({})".format( 940 fname, 941 ', '.join(rep.strip() for rep in argreps) 942 ) 943 if '\n' not in oneline and len(oneline) <= FMT_WIDTH: 944 callrep = oneline 945 else: 946 callrep = "{}(\n{}\n)".format(fname, ',\n'.join(argreps)) 947 948 # Wrap faked inputs with spans so we can color them blue 949 captured = re.sub( 950 harness.FAKE_INPUT_PATTERN, 951 r'<span class="input">\1</span>', 952 captured 953 ) 954 955 # Highlight the function call 956 callrep = highlight_code(callrep) 957 958 result = ( 959 '<span class="prompt input">In []:</span>' 960 '<div class="snippet-input">{}</div>' 961 ).format(callrep) 962 963 if captured: 964 result += ( 965 '<span class="prompt printed">Prints</span>' 966 '<div class="snippet-printed">{}</div>\n' 967 ).format(captured) 968 969 if captured_errs: 970 result += ( 971 '<span class="prompt stderr">Logs</span>' 972 '<div class="snippet-stderr">{log}</div>\n' 973 ).format(log=captured_errs) 974 975 # Highlight the return value 976 if value is not None: 977 value = highlight_code(wrapped_with_prefix(value, "")) 978 979 result += ( 980 '<span class="prompt output">Out[]:</span>' 981 '<div class="snippet-output">{}</div>\n' 982 ).format(value) 983 984 return result 985 986 987class Blocks(Snippet): 988 """ 989 A snippet which shows the results (printed output and result value of 990 final line) of one or more blocks of statements, just like Jupyter 991 Notebook cells. To control what happens in detail, use specialization 992 methods from `potluck.specifications.HasPayload` and 993 `potluck.specifications.HasContext`. 994 995 Note: Any direct side-effects of the blocks (like changing the value 996 of a variable or defining a function) won't persist in the module 997 used for evaluation. However, indirect effects (like appending to a 998 list) will persist, and thus should be avoided because they could 999 have unpredictable effects on other components of the system such as 1000 evaluation. 1001 1002 Along the same lines, each block of code to demonstrate is executed 1003 independently of the others. You cannot define a variable in one 1004 block and then use it in another (although you could fake this using 1005 the ability to define separate presentation and actual code). 1006 """ 1007 def __init__(self, sid, title, caption, blocks, postscript=''): 1008 """ 1009 A snippet ID, title, and caption are required, along with a list 1010 of code blocks. Each entry in the list should be either a 1011 multi-line string, or a tuple of two such strings. In the first 1012 case, the string is treated as the code to run, in the second, 1013 the first item in the tuple is the code to display, while the 1014 second is the code to actually run. 1015 1016 A postscript is optional (see `Snippet`). 1017 """ 1018 cases = [] 1019 for item in blocks: 1020 if isinstance(item, str): 1021 cases.append( 1022 specifications.TestBlock( 1023 sid, 1024 item, 1025 register=False 1026 ) 1027 ) 1028 else: 1029 display, actual = item 1030 cases.append( 1031 specifications.TestBlock( 1032 sid, 1033 display, 1034 actual, 1035 register=False 1036 ) 1037 ) 1038 1039 super().__init__(sid, title, caption, cases, postscript) 1040 1041 def render_context(self, context): 1042 """ 1043 Renders a context created for goal evaluation as HTML markup for 1044 executing a block of code. Includes a Jupyter-style prompt for 1045 input as well as the result value of the last line of the block 1046 as long as that's not None. 1047 """ 1048 src = extract(context, "ref_block") 1049 value = extract(context, "ref_value") 1050 captured = context.get("ref_output", '') 1051 captured_errs = context.get("ref_error_log", '') 1052 1053 # Wrap faked inputs with spans so we can color them blue 1054 captured = re.sub( 1055 harness.FAKE_INPUT_PATTERN, 1056 r'<span class="input">\1</span>', 1057 captured 1058 ) 1059 1060 # Highlight the code block 1061 blockrep = highlight_code(src) 1062 1063 result = ( 1064 '<span class="prompt input">In []:</span>' 1065 '<div class="snippet-input">{}</div>\n' 1066 ).format(blockrep) 1067 1068 if captured: 1069 result += ( 1070 '<span class="prompt printed">Prints</span>' 1071 '<div class="snippet-printed">{}</div>\n' 1072 ).format(captured) 1073 1074 if captured_errs: 1075 result += ( 1076 '<span class="prompt stderr">Logs</span>' 1077 '<div class="snippet-stderr">{log}</div>\n' 1078 ).format(log=captured_errs) 1079 1080 # Highlight the return value 1081 if value is not None: 1082 value = highlight_code(wrapped_with_prefix(value, "")) 1083 1084 result += ( 1085 '<span class="prompt output">Out[]:</span>' 1086 '<div class="snippet-output">{}</div>\n' 1087 ).format(value) 1088 1089 return result 1090 1091 1092class Fakes(Snippet): 1093 """ 1094 One or more fake snippets, which format code, printed output, stderr 1095 output, and a result value (and possibly glamer additions) but each 1096 of these things is simply specified ahead of time. Note that you want 1097 to avoid using this if at all possible, because it re-creates the 1098 problems that this module was trying to solve (namely, examples 1099 becoming out-of-sync with the solution code). The specialization 1100 methods of this kind of snippet are ignored and have no effect, 1101 because no code is actually run. 1102 """ 1103 def __init__(self, sid, title, caption, fake_contexts, postscript=''): 1104 """ 1105 A snippet ID, title, and caption are required, along with a list 1106 of fake context dictionary objects to be rendered as if they came 1107 from a real test. Each dictionary must contain a "code" slot, and 1108 may contain "ref_value", "ref_output", and/or "ref_error_log" 1109 keys. If using glamers, relevant keys should be added directly to 1110 these fake contexts. 1111 1112 If "ref_value" or "ref_output" slots are missing, defaults of 1113 None and '' will be added, since these slots are ultimately 1114 required. 1115 1116 A postscript is optional (see `Snippet`). 1117 """ 1118 self.fake_contexts = fake_contexts 1119 for ctx in self.fake_contexts: 1120 if 'code' not in ctx: 1121 raise ValueError( 1122 "Fake context dictionaries must contain a 'code'" 1123 " slot." 1124 ) 1125 if 'ref_value' not in ctx: 1126 ctx['ref_value'] = None 1127 if 'ref_output' not in ctx: 1128 ctx['ref_output'] = '' 1129 super().__init__(sid, title, caption, [], postscript) 1130 1131 def create_goal(self): 1132 """ 1133 We override TestGroup.create_goal to just create a dummy goal 1134 which depends on dummy contexts. 1135 """ 1136 ctx_list = [] 1137 for fake in self.fake_contexts: 1138 def make_builder(): 1139 """ 1140 We need to capture 'fake' on each iteration of the loop, 1141 which is why this extra layer of indirection is added. 1142 """ 1143 nonlocal fake 1144 return lambda _: fake 1145 ctx_list.append(contexts.Context(builder=make_builder())) 1146 1147 return rubrics.NoteGoal( 1148 self.taskid, 1149 "fakeSnippetGoal:" + self.base_name, 1150 ( 1151 "NoteGoal for fake snippet '{}'.".format(self.base_name), 1152 "A fake NoteGoal." 1153 ), 1154 test_in={ "contexts": ctx_list } 1155 ) 1156 1157 def render_context(self, context): 1158 """ 1159 Renders a context created for goal evaluation as HTML markup for 1160 executing a block of code. Includes a Jupyter-style prompt for 1161 input as well as the result value of the last line of the block 1162 as long as that's not None. 1163 """ 1164 src = extract(context, "code") 1165 value = extract(context, "ref_value") 1166 captured = context.get("ref_output", '') 1167 captured_errs = context.get("ref_error_log", '') 1168 1169 # Wrap faked inputs with spans so we can color them blue 1170 captured = re.sub( 1171 harness.FAKE_INPUT_PATTERN, 1172 r'<span class="input">\1</span>', 1173 captured 1174 ) 1175 1176 # Highlight the code block 1177 blockrep = highlight_code(src) 1178 1179 result = ( 1180 '<span class="prompt input">In []:</span>' 1181 '<div class="snippet-input">{}</div>\n' 1182 ).format(blockrep) 1183 1184 if captured: 1185 result += ( 1186 '<span class="prompt printed">Prints</span>' 1187 '<div class="snippet-printed">{}</div>\n' 1188 ).format(captured) 1189 1190 if captured_errs: 1191 result += ( 1192 '<span class="prompt stderr">Logs</span>' 1193 '<div class="snippet-stderr">{log}</div>\n' 1194 ).format(log=captured_errs) 1195 1196 # Highlight the return value 1197 if value is not None: 1198 value = highlight_code(wrapped_with_prefix(value, "")) 1199 1200 result += ( 1201 '<span class="prompt output">Out[]:</span>' 1202 '<div class="snippet-output">{}</div>\n' 1203 ).format(value) 1204 1205 return result 1206 1207 1208class Files(Snippet): 1209 """ 1210 A snippet which shows the contents of one or more files, without 1211 running any code, in the same Jupyter-like format that file contents 1212 are shown when `Snippet.show_file_contents` is used. Most 1213 specialization methods don't work properly on this kind of snippet. 1214 1215 By default, file paths are interpreted as relative to the solutions 1216 directory, but specifying a different base directory via the 1217 constructor can change that. 1218 """ 1219 def __init__( 1220 self, 1221 sid, 1222 title, 1223 caption, 1224 filepaths, 1225 base='__soln__', 1226 postscript='' 1227 ): 1228 """ 1229 A snippet ID, title, and caption are required, along with a list 1230 of file paths. Each entry in the list should be a string path to 1231 a starter file relative to the starter directory. The versions of 1232 files in the solution directory will be used. But only files 1233 present in both starter and solution directories will be 1234 available. 1235 1236 If `base` is specified, it should be either '__soln__' (the 1237 default), '__starter__', or a path string. If it's __soln__ paths 1238 will be interpreted relative to the solution directory; if it's 1239 __starter__ they'll be interpreted relative to the starter 1240 directory, and for any other path, paths are interpreted relative 1241 to that directory. In any case, absolute paths will not be 1242 modified. 1243 1244 A postscript is optional (see `Snippet`). 1245 """ 1246 if base == "__soln__": 1247 base = file_utils.current_solution_path() 1248 elif base == "__starter__": 1249 base = file_utils.current_starter_path() 1250 # else leave base as-is 1251 1252 self.filepaths = { 1253 path: os.path.join(base, path) 1254 for path in filepaths 1255 } 1256 1257 super().__init__(sid, title, caption, [], postscript) 1258 1259 def create_goal(self): 1260 """ 1261 We override TestGroup.create_goal to just create a dummy goal 1262 which depends on dummy contexts. 1263 """ 1264 ctx_list = [] 1265 for showpath in self.filepaths: 1266 filepath = self.filepaths[showpath] 1267 with open(filepath, 'r', encoding="utf-8") as fileInput: 1268 contents = fileInput.read() 1269 1270 def make_builder(): 1271 """ 1272 We need to capture 'filepath' on each iteration of the 1273 loop, which is why this extra layer of indirection is 1274 added. 1275 """ 1276 nonlocal showpath, filepath, contents 1277 sp = showpath 1278 fp = filepath 1279 ct = contents 1280 return lambda _: { 1281 "path": sp, 1282 "real_path": fp, 1283 "contents": ct 1284 } 1285 1286 ctx_list.append(contexts.Context(builder=make_builder())) 1287 1288 return rubrics.NoteGoal( 1289 self.taskid, 1290 "fileSnippetGoal:" + self.base_name, 1291 ( 1292 "NoteGoal for file snippet '{}'.".format(self.base_name), 1293 "A file-displaying NoteGoal." 1294 ), 1295 test_in={ "contexts": ctx_list } 1296 ) 1297 1298 def render_context(self, context): 1299 """ 1300 Renders a context with just file information as HTML markup for 1301 displaying a file with %more magic. Includes a Jupyter-style 1302 prompt for input as well as the contents of the file. 1303 """ 1304 path = extract(context, "path") 1305 contents = extract(context, "contents") 1306 1307 result = ( 1308 '<span class="prompt input">In []:</span>' 1309 '<div class="snippet-input">' 1310 '<code class="magic">%more {filename}</code>' 1311 '</div>\n' 1312 '<span class="prompt special">File</span>' 1313 '<div class="snippet-filename">{filename}</div>\n' 1314 '<div class="snippet-file-contents">{contents}</div>\n' 1315 ).format(filename=path, contents=contents) 1316 1317 return result 1318 1319 1320#---------# 1321# Glamers # 1322#---------# 1323 1324def append_image_result(markup, context): 1325 """ 1326 Given some HTML markup and a context dictionary, turns the 1327 "ref_image" context slot into an HTML img tag and returns the given 1328 markup with that appended, prefixed by an "Image " prompt. If the 1329 context has no "image" value, the original markup is returned 1330 unmodified. 1331 1332 The "ref_image_alt" slot of the context is used as alt text for the 1333 image, with the "ref_output" slot being used as backup (under a 1334 hopeful assumption about turtleBeads being imported for printed 1335 descriptions). If neither is present, "no alt text available" will be 1336 used. 1337 """ 1338 # Short-circuit unless we've got an image 1339 if "ref_image" not in context: 1340 return markup 1341 1342 image = extract(context, "ref_image") 1343 alt = context.get( 1344 "ref_image_alt", 1345 context.get("ref_output", "no alt text available") 1346 ) 1347 1348 img_tag = html_tools.html_image(image, alt, ["example"]) 1349 return ( 1350 markup 1351 + '<span class="prompt special">Image </span>' 1352 + img_tag 1353 ) 1354 1355 1356def append_audio_result(markup, context): 1357 """ 1358 Given some HTML markup and a context dictionary, turns the 1359 "ref_audio" context slot into an HTML audio tag and returns the given 1360 markup with that appended, prefixed by an "Audio " prompt. 1361 1362 Note that this can result in a pretty large string if the WAV format 1363 is used, since the string needs to be base64-encoded and WAV is 1364 uncompressed (and we double the size by including the data URL twice) 1365 :( 1366 1367 TODO: Maybe use Ogg/Vorbis? 1368 1369 If there is no "ref_audio" context slot, the given markup is returned 1370 unmodified. 1371 1372 The "ref_audio" context value must be a dictionary with at least 1373 "mimetype" and "data" slots containing the MIME type for the data and 1374 the data itself (as a bytes object). It may also include a "label" 1375 slot which will be used invisibly as an aria-label property of the 1376 audio element; if absent no aria-label will be attached. 1377 """ 1378 # Short-circuit unless we've got audio 1379 if "ref_audio" not in context: 1380 return markup 1381 1382 audio = extract(context, "ref_audio") 1383 mime = audio["mimetype"] 1384 data = audio["data"] 1385 label = audio.get("label") 1386 1387 audio_tag = html_tools.html_audio(data, mime, label) 1388 return ( 1389 markup 1390 + '\n<span class="prompt special">Audio </span>' 1391 + audio_tag 1392 ) 1393 1394 1395def append_memory_report(markup, context): 1396 """ 1397 Given some HTML markup and a context dictionary, creates a memory 1398 report showing the structure of the object in the "ref_value" context 1399 slot, and appends that to the markup, using a 'Memory Report' tag. 1400 """ 1401 # Do nothing if no ref_value is available 1402 if "ref_value" not in context: 1403 return markup 1404 1405 obj = extract(context, "ref_value") 1406 1407 report = html_tools.escape(specifications.memory_report(obj)) 1408 formatted = "<pre>{report}</pre>".format(report=report) 1409 return ( 1410 markup 1411 + '\n<span class="prompt special">Memory\nReport</span>' 1412 + formatted 1413 ) 1414 1415 1416def append_file_contents(markup, context): 1417 """ 1418 Given some HTML markup and a context dictionary, turns the 1419 "ref_output_filename" and "ref_output_file_contents" context slots 1420 into HTML markup displaying the contents of that file, which gets 1421 appended to the given markup and returned. 1422 1423 Note that this is intended to display the contents of text files. 1424 1425 If either of the "ref_output_filename" or "ref_output_file_contents" 1426 context slots are missing, the given markup is returned unmodified. 1427 """ 1428 # Short-circuit unless we've got output file contents 1429 if ( 1430 "ref_output_filename" not in context 1431 or "ref_output_file_contents" not in context 1432 ): 1433 return markup 1434 1435 filename = extract(context, "ref_output_filename") 1436 contents = extract(context, "ref_output_file_contents") 1437 1438 return ( 1439 markup 1440 + ( 1441 '\n<span class="prompt special">File</span>' 1442 ) 1443 + '<div class="snippet-filename">{}</div>\n'.format(filename) 1444 + '<div class="snippet-file-contents">{}</div>\n'.format(contents) 1445 ) 1446 1447 1448#--------# 1449# Lookup # 1450#--------# 1451 1452def list_snippets(task_info): 1453 """ 1454 Returns a list containing all snippet IDs (strings) for the given 1455 task (as a task info dictionary). 1456 """ 1457 reg = SNIPPET_REGISTRY.get(task_info["specification"].__name__, {}) 1458 return list(reg.keys()) 1459 1460 1461def get_html(task_info, sid): 1462 """ 1463 Retrieves the HTML code (a string) for the snippet with the given ID 1464 in the given task (as a task info dictionary). Returns None if there 1465 is no such snippet. 1466 """ 1467 reg = SNIPPET_REGISTRY.get(task_info["specification"].__name__, {}) 1468 if sid not in reg: 1469 return None 1470 return reg[sid].get_html(task_info) 1471 1472 1473def get_all_snippets(task_info): 1474 """ 1475 Returns a list of HTML strings containing each registered snippet for 1476 the given (as a task_info dictionary) task. 1477 """ 1478 return [ 1479 get_html(task_info, sid) 1480 for sid in list_snippets(task_info) 1481 ]
Line width we'll attempt to hit for formatting output nicely.
106def wrap_str(string, width): 107 """ 108 Returns a list of strings, which when combined form the given 109 string, where none is longer than the given width. Lines are broken 110 at spaces whenever possible, but may be broken in the middle of 111 words when a single word is longer than the target width. This 112 function uses a greedy algorithm that doesn't optimize the evenness 113 of the right margin. \\n and \\r are represented by those character 114 pairs, but other characters are as-is. 115 """ 116 escaped = string.replace('\n', '\\n') 117 escaped = escaped.replace('\r', '\\r') 118 words = escaped.split(' ') 119 result = [] 120 while len(words) > 0: # until we've used up the words 121 # Figure out how many words would fit on the next line 122 i = 0 123 # + i accounts for spaces we'll need 124 while len(words[:i + 1]) + (i + 1) < width: 125 i += 1 126 127 # Back off to the number that fits 128 i -= 1 129 130 # first word doesn't fit! 131 if i == -1: 132 first = words[0] 133 # Chop up the first word and put the pieces into the result 134 pieces = len(first) // width 135 for j in range(pieces): 136 result.append(first[width * j:width * (j + 1)]) 137 138 # Handle leftovers 139 rest = first[width * pieces:] 140 # Note that even empty leftovers serve as a placeholder to 141 # ensure that a space gets added. 142 words[0] = rest # put rest back into words, replacing old 143 else: 144 # at least one word fits 145 line = ' '.join(words[:i + 1]) + ' ' 146 result.append(line) 147 words = words[i + 1:] 148 149 # Remove the extra space introduced after the end of the last word 150 # Works even for an empty string input 151 result[-1] = result[-1][:-1] 152 153 return result
Returns a list of strings, which when combined form the given string, where none is longer than the given width. Lines are broken at spaces whenever possible, but may be broken in the middle of words when a single word is longer than the target width. This function uses a greedy algorithm that doesn't optimize the evenness of the right margin. \n and \r are represented by those character pairs, but other characters are as-is.
156def wrapped_repr(obj, width=FMT_WIDTH, indentation=1, prefix='', memo=None): 157 """ 158 Returns a string representing the given object, whose lines are not 159 longer than the given width, and which uses the given number of 160 spaces to indent sub-objects when necessary. Replacement for 161 pprint.pformat, which does NOT reorder dictionary keys. May violate 162 the width restriction when given a complex object that it doesn't 163 know how to wrap whose repr alone is wider than the specified width. 164 165 A prefix may be supplied, which will be added to the first line of 166 the given representation as-is. The prefix itself will not be subject 167 to wrapping, but the first line of the result will in some cases wrap 168 earlier to compensate for the prefix. The prefix should not include 169 any newlines or carriage returns. 170 171 The memo helps avoid problems with recursion, and doesn't need to be 172 provided explicitly. 173 """ 174 # Create a memo if we need to 175 if memo is None: 176 memo = set() 177 178 # Check the memo 179 if id(obj) in memo: 180 return '...' 181 182 # Add ourselves to the memo 183 memo.add(id(obj)) 184 185 simple = prefix + repr(obj) 186 187 if len(simple) <= width: # no need to do anything fancy 188 return simple 189 190 indent = ' ' * indentation 191 between = '\n' + indent 192 193 broken = repr(obj) 194 if len(broken) < width - indentation: 195 return prefix + '\\' + between + broken 196 197 elif type(obj) == str: # need to wrap this string 198 lines = wrap_str(obj, width - (indentation + 2)) 199 return ( 200 prefix + '(' + between 201 + between.join('"' + line + '"' for line in lines) 202 + '\n)' 203 ) 204 205 elif type(obj) == bytes: # need to wrap these bytes 206 lines = wrap_str(obj, width - (indentation + 3)) 207 return ( 208 prefix + '(' + between 209 + between.join('b"' + line + '"' for line in lines) 210 + '\n)' 211 ) 212 213 elif type(obj) in (list, tuple, set): # need to wrap each item 214 if type(obj) in (list, tuple): 215 ldelim, rdelim = str(type(obj)()) 216 elif len(obj) == 0: 217 return prefix + 'set()' # too bad if width is < 5 218 else: 219 ldelim = '{' 220 rdelim = '}' 221 222 result = prefix + ldelim + between 223 for item in obj: 224 result += textwrap.indent( 225 wrapped_repr(item, width - indentation, indentation), 226 indent 227 ) + ',' + between 228 229 return result[:-len(between)] + '\n' + rdelim 230 231 elif type(obj) == dict: # need to wrap keys and values 232 indent = ' ' * indentation 233 between = '\n' + indent 234 235 result = prefix + '{' + between 236 for key in obj: 237 key_repr = wrapped_repr( 238 key, 239 width - (indentation + 1), 240 indentation 241 ) 242 val = obj[key] 243 val_repr = wrapped_repr( 244 val, 245 width - indentation * 2, 246 indentation 247 ) 248 249 # Can we fit on one line? 250 if len(indent) + len(key_repr) + len(val_repr) + 3 <= width: 251 result += key_repr + ': ' + val_repr + ',' + between 252 elif val_repr[1] == '\n': # ldelim/newline start 253 result += ( 254 key_repr + ':' + val_repr[0] + '\n' 255 + textwrap.indent(val_repr[2:], indent) 256 + ',' + between 257 ) 258 else: 259 result += key_repr + ':\n' + textwrap.indent( 260 val_repr, 261 indent * 2 262 ) + ',' + between 263 264 # Chop off final between and add closing brace after a newline 265 result = result[:-len(between)] + '\n}' 266 267 return result 268 269 else: # Not sure how to wrap this, so we give up... 270 return prefix + repr(obj)
Returns a string representing the given object, whose lines are not longer than the given width, and which uses the given number of spaces to indent sub-objects when necessary. Replacement for pprint.pformat, which does NOT reorder dictionary keys. May violate the width restriction when given a complex object that it doesn't know how to wrap whose repr alone is wider than the specified width.
A prefix may be supplied, which will be added to the first line of the given representation as-is. The prefix itself will not be subject to wrapping, but the first line of the result will in some cases wrap earlier to compensate for the prefix. The prefix should not include any newlines or carriage returns.
The memo helps avoid problems with recursion, and doesn't need to be provided explicitly.
273def wrapped_with_prefix(obj, prefix, indentation_level=0): 274 """ 275 Returns a string representing the given object, as `wrapped_repr` 276 would, except that: 277 278 1. The first line includes the given prefix (as a string without 279 repr applied to it; after indentation) and the wrapping of object 280 values takes this into account. 281 2. The entire representation is indented by the given indentation 282 level (one space per level). 283 """ 284 rep = wrapped_repr( 285 obj, 286 width=FMT_WIDTH - indentation_level, 287 indentation=1, 288 prefix=prefix 289 ) 290 291 # remove extra indentation from tuple 292 rep = textwrap.dedent(rep) 293 294 # restore indent and add prefix 295 return textwrap.indent(rep, ' ' * indentation_level)
Returns a string representing the given object, as wrapped_repr
would, except that:
- The first line includes the given prefix (as a string without repr applied to it; after indentation) and the wrapping of object values takes this into account.
- The entire representation is indented by the given indentation level (one space per level).
298def extract(context, key): 299 """ 300 Gets value from a context but raises a ValueError showing the 301 context's error log if it can't. 302 """ 303 if key in context: 304 return context[key] 305 else: 306 # Note: we use a ValueError here instead of a KeyError because 307 # KeyError uses repr on its message... 308 if "ref_error_log" in context: 309 raise ValueError( 310 ( 311 "Error: context is missing the '{}' key. Error log" 312 " is:\n{}" 313 ).format(key, context["ref_error_log"]), 314 ) 315 else: 316 raise ValueError( 317 "Error: context is missing the '{}' key. No error log" 318 " available." 319 )
Gets value from a context but raises a ValueError showing the context's error log if it can't.
All registered snippets, by defining module and then snippet ID.
Things to remember so we can undo setup changes.
342def snippet_setup(context): 343 """ 344 If the wavesynth module is available, disables the playTrack and 345 saveTrack functions from that module. If the optimism module is 346 available, disables colors for that module. 347 """ 348 try: 349 import wavesynth 350 REMEMBER["playTrack"] = wavesynth.playTrack 351 wavesynth.playTrack = lambda _=None: None 352 REMEMBER["saveTrack"] = wavesynth.saveTrack 353 wavesynth.saveTrack = lambda _: None 354 except ModuleNotFoundError: 355 pass 356 357 try: 358 import optimism 359 REMEMBER["opt_colors"] = optimism.COLORS 360 optimism.colors(False) 361 except ModuleNotFoundError: 362 pass 363 364 return context
If the wavesynth module is available, disables the playTrack and saveTrack functions from that module. If the optimism module is available, disables colors for that module.
367def snippet_cleanup(context): 368 """ 369 Puts things back how they were before `snippet_setup`. 370 """ 371 try: 372 import wavesynth 373 wavesynth.playTrack = REMEMBER["playTrack"] 374 wavesynth.saveTrack = REMEMBER["saveTrack"] 375 except ModuleNotFoundError: 376 pass 377 378 try: 379 import optimism 380 optimism.colors(REMEMBER["opt_colors"]) 381 except ModuleNotFoundError: 382 pass 383 384 return context
Puts things back how they were before snippet_setup
.
391class Snippet(specifications.TestGroup): 392 """ 393 Common functionality for snippets. Don't instantiate this class; 394 instead use one of the subclasses `Variables`, `Expressions`, or 395 `RunModule`. 396 397 Note that a `Snippet` is a `potluck.specifications.TestGroup`, from 398 which it inherits a powerful interface for customizing behavior. Some 399 default modifications are applied to every snippet: 400 401 1. Output, including stderr and error tracebacks, is captured. 402 2. A setup function is run which disables turtle tracing, wavesynth 403 track playing/saving, and optimism colors (only when the relevant 404 module(s) are available). A cleanup function that reverses all of 405 these changes except the turtle speed is also run afterwards. 406 """ 407 @staticmethod 408 def base_context(task_info): 409 """ 410 Creates a base context object for a snippet, given a task info 411 object. 412 """ 413 return { 414 "task_info": task_info, 415 "username": "__soln__", 416 "submission_root": task_info["specification"].soln_path, 417 "default_file": task_info["target"], 418 "actual_file": task_info["target"] 419 } 420 421 def __init__( 422 self, 423 sid, 424 title, 425 caption, 426 tests, 427 postscript='', 428 glamers=None, 429 ignore_errors=False 430 ): 431 """ 432 All `Snippet`s have a snippet ID, a title, a caption, and one or 433 more tests. `tests` must be an iterable of unregistered 434 `potluck.specifications.TestCase` objects. 435 436 An optional postscript is markdown source to be placed after the 437 code block. 438 439 An optional list of glamer functions may be provided. These will 440 be given individual pieces of HTML code and the associated 441 context those were rendered from and their result value will 442 replace the HTML code to be used; they are applied in-order. 443 444 Unless ignore_errors is set to False (default is True), errors 445 generated by the base payload and captured along with output will 446 be logged under the assumption that in most cases an exception in 447 a snippet is a bug with the snippet code rather than intentional. 448 """ 449 # Set ourselves up as a TestGroup... 450 super().__init__(sid, '_') 451 452 # We store our arguments 453 self.title = title 454 self.caption = caption 455 self.postscript = postscript 456 self.glamers = glamers or [] 457 self.ignore_errors = ignore_errors 458 459 # Final edit function to manipulate the context to be rendered 460 self.editor = None 461 462 # What's the name of our spec file? 463 self.spec_file = file_utils.get_spec_file_name() 464 465 # Add our tests to ourself 466 for t in tests: 467 self.add(t) 468 469 # Fetch the defining-module-specific registry 470 reg = SNIPPET_REGISTRY.setdefault( 471 file_utils.get_spec_module_name(), 472 {} 473 ) 474 475 # Prevent ID collision 476 if sid in reg: 477 raise ValueError( 478 "Multiple snippets cannot be registered with the same" 479 " ID ('{sid}')." 480 ) 481 482 # Register ourself 483 reg[sid] = self 484 485 # A place to cache our compiled result 486 self.cached_for = None 487 self.cached_value = None 488 489 # Set up default augmentations 490 self.capture_output(capture_errors=True, capture_stderr=True) 491 self.do_setup(snippet_setup) 492 self.do_cleanup(snippet_cleanup) 493 494 def provide(self, vars_map): 495 """ 496 A convenience function for providing the snippet with values not 497 defined in the solution module. Under the hood, this uses 498 `specifications.HasPayload.use_decorations`, so using that 499 alongside this will not work (one will overwrite the other). This 500 also means that you cannot call `provide` multiple times on the 501 same instance to accumulate provided values: each call discards 502 previous provided values. 503 """ 504 self.use_decorations( 505 { 506 k: (lambda _: v) 507 for (k, v) in vars_map.items() 508 }, 509 ignore_missing=True 510 ) 511 512 # TODO: define replacements so that formatting of calls can use 513 # varnames? 514 515 def compile(self, base_context): 516 """ 517 Runs the snippet, collects its results, and formats them as HTML. 518 Returns a string containing HTML code for displaying the snippet 519 (which assumes that the potluck.css stylesheet will be loaded). 520 521 This method will always re-run the compilation process, 522 regardless of whether a cached result is available, and will 523 update the cached result. Use `Snippet.get_html` to recompile 524 only as needed. 525 526 Requires a base context object (see `potluck.contexts.Context` 527 for the required structure). 528 529 The returned HTML's outermost tag is a <section> tag with 530 an id that starts with 'snippet:' and then ends with the 531 snippet's ID value. 532 """ 533 # Set up traceback-rewriting for the specifications module we 534 # were defined in 535 html_tools.set_tb_rewrite( 536 base_context["task_info"]["specification"].__file__, 537 "<task specification>" 538 ) 539 html_tools.set_tb_rewrite( 540 base_context["submission_root"], 541 "<solution>" 542 ) 543 544 # Tell the run_for_base_and_ref_values augmentation to run only 545 # for ref values. 546 self.augmentations.setdefault( 547 "run_for_base_and_ref_values", 548 {} 549 )["ref_only"] = True 550 551 for case in self.tests: 552 case.augmentations.setdefault( 553 "run_for_base_and_ref_values", 554 {} 555 )["ref_only"] = True 556 557 # Create our Goal object 558 goal = self.provide_goal() 559 560 # Reset and evaluate our goal: 561 goal.reset_network() 562 goal.evaluate(base_context) 563 564 # Grab the contexts that were used for each test 565 if goal.test_in and len(goal.test_in["contexts"]) > 0: 566 contexts = goal.test_in["contexts"] 567 else: 568 # This shouldn't be possible unless an empty list of 569 # variables or functions was provided... 570 raise ValueError( 571 "A Snippet must have at least one context to compile." 572 " (Did you pass an empty list to Variables or" 573 " FunctionCalls?)" 574 ) 575 576 # Translate from markdown 577 title = render.render_markdown(self.title) 578 caption = render.render_markdown(self.caption) 579 post = render.render_markdown(self.postscript) 580 581 context_dicts = [] 582 for ctx in contexts: 583 try: 584 cdict = ctx.create(base_context) 585 except context_utils.ContextCreationError: 586 cdict = { 587 # for RunModule snippets (note no ref_) 588 "filename": "UNKNOWN", 589 # In case it's a Variables snippet 590 "ref_variable": "UNKNOWN", 591 # for FunctionCalls snippets 592 "ref_function": "UNKNOWN", 593 "ref_args": (), 594 "ref_kwargs": {}, 595 # for Blocks snippets 596 "ref_block": "UNKNOWN", 597 # For all kinds of snippets 598 "ref_value": "There was an error compiling this snippet.", 599 "ref_output": "There was an error compiling this snippet.", 600 "ref_error_log": html_tools.string_traceback() 601 } 602 context_dicts.append(cdict) 603 604 # Strip out solution file paths from output, and perform final 605 # edits if we have an editor function 606 soln_path = os.path.abspath( 607 base_context["task_info"]["specification"].soln_path 608 ) 609 for context in context_dicts: 610 if ( 611 "ref_output" in context 612 and soln_path in context["ref_output"] 613 ): 614 context["ref_output"] = context["ref_output"].replace( 615 soln_path, 616 '<soln>' 617 ) 618 619 if ( 620 "ref_error_log" in context 621 and soln_path in context["ref_error_log"] 622 ): 623 context["ref_error_log"] = context["ref_error_log"].replace( 624 soln_path, 625 '<soln>' 626 ) 627 628 if self.editor: 629 self.editor(context) 630 631 # Include title & caption, and render each context to produce our 632 # result 633 result = ( 634 '<section class="snippet" id="snippet:{sid}">' 635 '<div class="snippet_title">{title}</div>\n' 636 '{caption}\n<pre>{snippet}</pre>\n{post}' 637 '</section>' 638 ).format( 639 sid=self.base_name, 640 title=title, 641 caption=caption, 642 post=post, 643 snippet=''.join( 644 self.compile_context(cd) 645 for cd in context_dicts 646 ) 647 ) 648 649 self.cached_for = base_context["task_info"]["id"] 650 self.cached_value = result 651 652 return result 653 654 def compile_context(self, context): 655 """ 656 Turns a context dictionary resulting from an individual snippet 657 case into HTML code for displaying that result, using 658 `render_context` but also potentially adding additional HTML for 659 extra context slots (see e.g., `show_image` and `play_audio`). 660 """ 661 if not self.ignore_errors and "ref_error" in context: 662 logging.log( 663 "Error captured by context creation:\n" 664 + context["ref_error"] 665 + "\nFull log is:\n" 666 + context.get("ref_error_log", '<missing>') 667 ) 668 result = self.render_context(context) 669 for glamer in self.glamers: 670 result = glamer(result, context) 671 672 return result 673 674 def render_context(self, context): 675 """ 676 Override this to define how a specific Snippet sub-class should 677 render a context dictionary into HTML code. 678 """ 679 raise NotImplementedError( 680 "Snippet is abstract and context rendering is only available" 681 " via overrides in sub-classes." 682 ) 683 684 def get_html(self, task_info): 685 """ 686 Returns the snippet value, either cached or newly-compiled 687 depending on the presence of an appropriate cached value. 688 """ 689 if self.cached_for == task_info["id"]: 690 return self.cached_value 691 else: 692 return self.compile(Snippet.base_context(task_info)) 693 694 def final_edit(self, editor): 695 """ 696 Sets up a function to be run on each created context just before 697 that context gets rendered. The editor may mutate the context it 698 is given in order to change what gets rendered. Multiple calls to 699 this function each simply replace the previous editor function. 700 701 Note that the ref_* values from the context are used to create 702 the snippet. 703 """ 704 self.editor = editor 705 706 def show_image(self): 707 """ 708 Adds a glamer that augments output by appending a "Image " 709 prompt followed by an image element which shows the contents of 710 the "ref_image" context slot. To establish a "ref_image" slot, 711 call a method like 712 `potluck.specifications.HasPayload.capture_turtle_image`. 713 714 Returns self for chaining. 715 """ 716 self.glamers.append(append_image_result) 717 return self 718 719 def play_audio(self): 720 """ 721 Adds a glamer that augments output by appending an "Audio " 722 prompt followed by an audio element which plays the contents of 723 the "ref_audio" context slot. To make an "ref_audio" slot, call a 724 method like 725 `potluck.specifications.HasPayload.capture_wavesynth`. 726 727 Returns self for chaining. 728 """ 729 self.glamers.append(append_audio_result) 730 return self 731 732 def show_memory_report(self): 733 """ 734 Adds a glamer that augments output by appending a "Memory Report" 735 prompt followed by a memory report showing the structure of the 736 "ref_value" context slot. 737 738 Returns self for chaining. 739 """ 740 self.glamers.append(append_memory_report) 741 return self 742 743 def show_file_contents(self): 744 """ 745 Adds a glamer that augments output by appending a "File" 746 prompt followed by a text box showing the filename and the 747 contents of that file. Requires 'ref_output_filename' and 748 'ref_output_file_contents' slots, which could be established 749 using a method like 750 `potluck.specifications.HasPayload.capture_file_contents` 751 752 Returns self for chaining. 753 """ 754 self.glamers.append(append_file_contents) 755 return self
Common functionality for snippets. Don't instantiate this class;
instead use one of the subclasses Variables
, Expressions
, or
RunModule
.
Note that a Snippet
is a potluck.specifications.TestGroup
, from
which it inherits a powerful interface for customizing behavior. Some
default modifications are applied to every snippet:
- Output, including stderr and error tracebacks, is captured.
- A setup function is run which disables turtle tracing, wavesynth track playing/saving, and optimism colors (only when the relevant module(s) are available). A cleanup function that reverses all of these changes except the turtle speed is also run afterwards.
421 def __init__( 422 self, 423 sid, 424 title, 425 caption, 426 tests, 427 postscript='', 428 glamers=None, 429 ignore_errors=False 430 ): 431 """ 432 All `Snippet`s have a snippet ID, a title, a caption, and one or 433 more tests. `tests` must be an iterable of unregistered 434 `potluck.specifications.TestCase` objects. 435 436 An optional postscript is markdown source to be placed after the 437 code block. 438 439 An optional list of glamer functions may be provided. These will 440 be given individual pieces of HTML code and the associated 441 context those were rendered from and their result value will 442 replace the HTML code to be used; they are applied in-order. 443 444 Unless ignore_errors is set to False (default is True), errors 445 generated by the base payload and captured along with output will 446 be logged under the assumption that in most cases an exception in 447 a snippet is a bug with the snippet code rather than intentional. 448 """ 449 # Set ourselves up as a TestGroup... 450 super().__init__(sid, '_') 451 452 # We store our arguments 453 self.title = title 454 self.caption = caption 455 self.postscript = postscript 456 self.glamers = glamers or [] 457 self.ignore_errors = ignore_errors 458 459 # Final edit function to manipulate the context to be rendered 460 self.editor = None 461 462 # What's the name of our spec file? 463 self.spec_file = file_utils.get_spec_file_name() 464 465 # Add our tests to ourself 466 for t in tests: 467 self.add(t) 468 469 # Fetch the defining-module-specific registry 470 reg = SNIPPET_REGISTRY.setdefault( 471 file_utils.get_spec_module_name(), 472 {} 473 ) 474 475 # Prevent ID collision 476 if sid in reg: 477 raise ValueError( 478 "Multiple snippets cannot be registered with the same" 479 " ID ('{sid}')." 480 ) 481 482 # Register ourself 483 reg[sid] = self 484 485 # A place to cache our compiled result 486 self.cached_for = None 487 self.cached_value = None 488 489 # Set up default augmentations 490 self.capture_output(capture_errors=True, capture_stderr=True) 491 self.do_setup(snippet_setup) 492 self.do_cleanup(snippet_cleanup)
All Snippet
s have a snippet ID, a title, a caption, and one or
more tests. tests
must be an iterable of unregistered
potluck.specifications.TestCase
objects.
An optional postscript is markdown source to be placed after the code block.
An optional list of glamer functions may be provided. These will be given individual pieces of HTML code and the associated context those were rendered from and their result value will replace the HTML code to be used; they are applied in-order.
Unless ignore_errors is set to False (default is True), errors generated by the base payload and captured along with output will be logged under the assumption that in most cases an exception in a snippet is a bug with the snippet code rather than intentional.
407 @staticmethod 408 def base_context(task_info): 409 """ 410 Creates a base context object for a snippet, given a task info 411 object. 412 """ 413 return { 414 "task_info": task_info, 415 "username": "__soln__", 416 "submission_root": task_info["specification"].soln_path, 417 "default_file": task_info["target"], 418 "actual_file": task_info["target"] 419 }
Creates a base context object for a snippet, given a task info object.
494 def provide(self, vars_map): 495 """ 496 A convenience function for providing the snippet with values not 497 defined in the solution module. Under the hood, this uses 498 `specifications.HasPayload.use_decorations`, so using that 499 alongside this will not work (one will overwrite the other). This 500 also means that you cannot call `provide` multiple times on the 501 same instance to accumulate provided values: each call discards 502 previous provided values. 503 """ 504 self.use_decorations( 505 { 506 k: (lambda _: v) 507 for (k, v) in vars_map.items() 508 }, 509 ignore_missing=True 510 )
A convenience function for providing the snippet with values not
defined in the solution module. Under the hood, this uses
specifications.HasPayload.use_decorations
, so using that
alongside this will not work (one will overwrite the other). This
also means that you cannot call provide
multiple times on the
same instance to accumulate provided values: each call discards
previous provided values.
515 def compile(self, base_context): 516 """ 517 Runs the snippet, collects its results, and formats them as HTML. 518 Returns a string containing HTML code for displaying the snippet 519 (which assumes that the potluck.css stylesheet will be loaded). 520 521 This method will always re-run the compilation process, 522 regardless of whether a cached result is available, and will 523 update the cached result. Use `Snippet.get_html` to recompile 524 only as needed. 525 526 Requires a base context object (see `potluck.contexts.Context` 527 for the required structure). 528 529 The returned HTML's outermost tag is a <section> tag with 530 an id that starts with 'snippet:' and then ends with the 531 snippet's ID value. 532 """ 533 # Set up traceback-rewriting for the specifications module we 534 # were defined in 535 html_tools.set_tb_rewrite( 536 base_context["task_info"]["specification"].__file__, 537 "<task specification>" 538 ) 539 html_tools.set_tb_rewrite( 540 base_context["submission_root"], 541 "<solution>" 542 ) 543 544 # Tell the run_for_base_and_ref_values augmentation to run only 545 # for ref values. 546 self.augmentations.setdefault( 547 "run_for_base_and_ref_values", 548 {} 549 )["ref_only"] = True 550 551 for case in self.tests: 552 case.augmentations.setdefault( 553 "run_for_base_and_ref_values", 554 {} 555 )["ref_only"] = True 556 557 # Create our Goal object 558 goal = self.provide_goal() 559 560 # Reset and evaluate our goal: 561 goal.reset_network() 562 goal.evaluate(base_context) 563 564 # Grab the contexts that were used for each test 565 if goal.test_in and len(goal.test_in["contexts"]) > 0: 566 contexts = goal.test_in["contexts"] 567 else: 568 # This shouldn't be possible unless an empty list of 569 # variables or functions was provided... 570 raise ValueError( 571 "A Snippet must have at least one context to compile." 572 " (Did you pass an empty list to Variables or" 573 " FunctionCalls?)" 574 ) 575 576 # Translate from markdown 577 title = render.render_markdown(self.title) 578 caption = render.render_markdown(self.caption) 579 post = render.render_markdown(self.postscript) 580 581 context_dicts = [] 582 for ctx in contexts: 583 try: 584 cdict = ctx.create(base_context) 585 except context_utils.ContextCreationError: 586 cdict = { 587 # for RunModule snippets (note no ref_) 588 "filename": "UNKNOWN", 589 # In case it's a Variables snippet 590 "ref_variable": "UNKNOWN", 591 # for FunctionCalls snippets 592 "ref_function": "UNKNOWN", 593 "ref_args": (), 594 "ref_kwargs": {}, 595 # for Blocks snippets 596 "ref_block": "UNKNOWN", 597 # For all kinds of snippets 598 "ref_value": "There was an error compiling this snippet.", 599 "ref_output": "There was an error compiling this snippet.", 600 "ref_error_log": html_tools.string_traceback() 601 } 602 context_dicts.append(cdict) 603 604 # Strip out solution file paths from output, and perform final 605 # edits if we have an editor function 606 soln_path = os.path.abspath( 607 base_context["task_info"]["specification"].soln_path 608 ) 609 for context in context_dicts: 610 if ( 611 "ref_output" in context 612 and soln_path in context["ref_output"] 613 ): 614 context["ref_output"] = context["ref_output"].replace( 615 soln_path, 616 '<soln>' 617 ) 618 619 if ( 620 "ref_error_log" in context 621 and soln_path in context["ref_error_log"] 622 ): 623 context["ref_error_log"] = context["ref_error_log"].replace( 624 soln_path, 625 '<soln>' 626 ) 627 628 if self.editor: 629 self.editor(context) 630 631 # Include title & caption, and render each context to produce our 632 # result 633 result = ( 634 '<section class="snippet" id="snippet:{sid}">' 635 '<div class="snippet_title">{title}</div>\n' 636 '{caption}\n<pre>{snippet}</pre>\n{post}' 637 '</section>' 638 ).format( 639 sid=self.base_name, 640 title=title, 641 caption=caption, 642 post=post, 643 snippet=''.join( 644 self.compile_context(cd) 645 for cd in context_dicts 646 ) 647 ) 648 649 self.cached_for = base_context["task_info"]["id"] 650 self.cached_value = result 651 652 return result
Runs the snippet, collects its results, and formats them as HTML. Returns a string containing HTML code for displaying the snippet (which assumes that the potluck.css stylesheet will be loaded).
This method will always re-run the compilation process,
regardless of whether a cached result is available, and will
update the cached result. Use Snippet.get_html
to recompile
only as needed.
Requires a base context object (see potluck.contexts.Context
for the required structure).
The returned HTML's outermost tag is a <section> tag with an id that starts with 'snippet:' and then ends with the snippet's ID value.
654 def compile_context(self, context): 655 """ 656 Turns a context dictionary resulting from an individual snippet 657 case into HTML code for displaying that result, using 658 `render_context` but also potentially adding additional HTML for 659 extra context slots (see e.g., `show_image` and `play_audio`). 660 """ 661 if not self.ignore_errors and "ref_error" in context: 662 logging.log( 663 "Error captured by context creation:\n" 664 + context["ref_error"] 665 + "\nFull log is:\n" 666 + context.get("ref_error_log", '<missing>') 667 ) 668 result = self.render_context(context) 669 for glamer in self.glamers: 670 result = glamer(result, context) 671 672 return result
Turns a context dictionary resulting from an individual snippet
case into HTML code for displaying that result, using
render_context
but also potentially adding additional HTML for
extra context slots (see e.g., show_image
and play_audio
).
674 def render_context(self, context): 675 """ 676 Override this to define how a specific Snippet sub-class should 677 render a context dictionary into HTML code. 678 """ 679 raise NotImplementedError( 680 "Snippet is abstract and context rendering is only available" 681 " via overrides in sub-classes." 682 )
Override this to define how a specific Snippet sub-class should render a context dictionary into HTML code.
684 def get_html(self, task_info): 685 """ 686 Returns the snippet value, either cached or newly-compiled 687 depending on the presence of an appropriate cached value. 688 """ 689 if self.cached_for == task_info["id"]: 690 return self.cached_value 691 else: 692 return self.compile(Snippet.base_context(task_info))
Returns the snippet value, either cached or newly-compiled depending on the presence of an appropriate cached value.
694 def final_edit(self, editor): 695 """ 696 Sets up a function to be run on each created context just before 697 that context gets rendered. The editor may mutate the context it 698 is given in order to change what gets rendered. Multiple calls to 699 this function each simply replace the previous editor function. 700 701 Note that the ref_* values from the context are used to create 702 the snippet. 703 """ 704 self.editor = editor
Sets up a function to be run on each created context just before that context gets rendered. The editor may mutate the context it is given in order to change what gets rendered. Multiple calls to this function each simply replace the previous editor function.
Note that the ref_* values from the context are used to create the snippet.
706 def show_image(self): 707 """ 708 Adds a glamer that augments output by appending a "Image " 709 prompt followed by an image element which shows the contents of 710 the "ref_image" context slot. To establish a "ref_image" slot, 711 call a method like 712 `potluck.specifications.HasPayload.capture_turtle_image`. 713 714 Returns self for chaining. 715 """ 716 self.glamers.append(append_image_result) 717 return self
Adds a glamer that augments output by appending a "Image "
prompt followed by an image element which shows the contents of
the "ref_image" context slot. To establish a "ref_image" slot,
call a method like
potluck.specifications.HasPayload.capture_turtle_image
.
Returns self for chaining.
719 def play_audio(self): 720 """ 721 Adds a glamer that augments output by appending an "Audio " 722 prompt followed by an audio element which plays the contents of 723 the "ref_audio" context slot. To make an "ref_audio" slot, call a 724 method like 725 `potluck.specifications.HasPayload.capture_wavesynth`. 726 727 Returns self for chaining. 728 """ 729 self.glamers.append(append_audio_result) 730 return self
Adds a glamer that augments output by appending an "Audio "
prompt followed by an audio element which plays the contents of
the "ref_audio" context slot. To make an "ref_audio" slot, call a
method like
potluck.specifications.HasPayload.capture_wavesynth
.
Returns self for chaining.
732 def show_memory_report(self): 733 """ 734 Adds a glamer that augments output by appending a "Memory Report" 735 prompt followed by a memory report showing the structure of the 736 "ref_value" context slot. 737 738 Returns self for chaining. 739 """ 740 self.glamers.append(append_memory_report) 741 return self
Adds a glamer that augments output by appending a "Memory Report" prompt followed by a memory report showing the structure of the "ref_value" context slot.
Returns self for chaining.
743 def show_file_contents(self): 744 """ 745 Adds a glamer that augments output by appending a "File" 746 prompt followed by a text box showing the filename and the 747 contents of that file. Requires 'ref_output_filename' and 748 'ref_output_file_contents' slots, which could be established 749 using a method like 750 `potluck.specifications.HasPayload.capture_file_contents` 751 752 Returns self for chaining. 753 """ 754 self.glamers.append(append_file_contents) 755 return self
Adds a glamer that augments output by appending a "File"
prompt followed by a text box showing the filename and the
contents of that file. Requires 'ref_output_filename' and
'ref_output_file_contents' slots, which could be established
using a method like
potluck.specifications.HasPayload.capture_file_contents
Returns self for chaining.
Inherited Members
- potluck.specifications.HasPayload
- synthesize_payload_info
- construct_payload
- describe_payload
- ensure_payload_constructor_arg
- prepare_source
- wrap_module
- ignore_output
- copy_args
- use_harness
- set_timeout
- do_setup
- do_cleanup
- capture_output
- provide_inputs
- use_decorations
- capture_trace
- sample_result_distribution
- capture_turtle_image
- capture_wavesynth
- capture_file_contents
- potluck.specifications.HasContext
- synthesize_context_info
- create_context
- set_context_description
- set_context_displayer
- describe_module_slot
- potluck.specifications.HasGoal
- provide_goal
- synthesize_goal_info
- create_goal_from_contexts
- ensure_goal_constructor_arg
- goal
- validate
- set_identifier
- set_goal_type
- set_goal_description
- test_module
- test_output
- test_with_harness
- test_trace
- check_trace_state
- check_invariant
- check_trace_count
- test_wavesynth_notes
- test_wavesynth_audio
- test_turtle_image
- test_file_contents
- compare_using
- succeed_unless_crashed
- compare_exactly
- compare_reports
- compare_strings_gently
- compare_strings_semi_strict
- compare_strings_firmly
- refine
758def highlight_code(code): 759 """ 760 Runs pygments highlighting but converts from a div/pre/code setup 761 back to just a code tag with the 'highlight' class. 762 """ 763 markup = pygments.highlight( 764 code, 765 pygments.lexers.PythonLexer(), 766 pygments.formatters.HtmlFormatter() 767 ) 768 # Note: markup will start a div and a pre we want to get rid of 769 start = '<div class="highlight"><pre>' 770 end = '</pre></div>\n' 771 if markup.startswith(start): 772 markup = markup[len(start):] 773 if markup.endswith(end): 774 markup = markup[:-len(end)] 775 776 return '<code class="highlight">{}</code>'.format(markup)
Runs pygments highlighting but converts from a div/pre/code setup back to just a code tag with the 'highlight' class.
779class Variables(Snippet): 780 """ 781 A snippet which shows the definition of one or more variables. Use 782 this to display examples of input data, especially when you want to 783 use shorter expressions in examples of running code. If you want to 784 use variables that aren't defined in the solution module, use the 785 `Snippet.provide` method (but note that that method is incompatible 786 with using `specifications.HasPayload.use_decorations`). 787 """ 788 def __init__(self, sid, title, caption, varnames, postscript=''): 789 """ 790 A snippet ID, a title, and a caption are required, as is a list 791 of strings indicating the names of variables to show definitions 792 of. Use `Snippet.provide` to supply values for variables not in 793 the solution module. 794 795 A postscript is optional (see `Snippet`). 796 """ 797 cases = [ 798 specifications.TestValue( 799 varname, 800 register=False 801 ) 802 for varname in varnames 803 ] 804 super().__init__(sid, title, caption, cases, postscript) 805 806 def render_context(self, context): 807 """ 808 Renders a context created for goal evaluation as HTML markup for 809 the definition of a variable, including Jupyter-notebook-style 810 prompts. 811 """ 812 varname = extract(context, "ref_variable") 813 value = extract(context, "ref_value") 814 815 # format value's repr using wrapped_repr, but leaving room for 816 # varname = at beginning of first line 817 rep = wrapped_with_prefix(value, varname + ' = ') 818 819 # Use pygments to generate HTML markup for our assignment 820 markup = highlight_code(rep) 821 return ( 822 '<span class="prompt input">In []:</span>' 823 '<div class="snippet-input">{}</div>\n' 824 ).format(markup)
A snippet which shows the definition of one or more variables. Use
this to display examples of input data, especially when you want to
use shorter expressions in examples of running code. If you want to
use variables that aren't defined in the solution module, use the
Snippet.provide
method (but note that that method is incompatible
with using specifications.HasPayload.use_decorations
).
788 def __init__(self, sid, title, caption, varnames, postscript=''): 789 """ 790 A snippet ID, a title, and a caption are required, as is a list 791 of strings indicating the names of variables to show definitions 792 of. Use `Snippet.provide` to supply values for variables not in 793 the solution module. 794 795 A postscript is optional (see `Snippet`). 796 """ 797 cases = [ 798 specifications.TestValue( 799 varname, 800 register=False 801 ) 802 for varname in varnames 803 ] 804 super().__init__(sid, title, caption, cases, postscript)
A snippet ID, a title, and a caption are required, as is a list
of strings indicating the names of variables to show definitions
of. Use Snippet.provide
to supply values for variables not in
the solution module.
A postscript is optional (see Snippet
).
806 def render_context(self, context): 807 """ 808 Renders a context created for goal evaluation as HTML markup for 809 the definition of a variable, including Jupyter-notebook-style 810 prompts. 811 """ 812 varname = extract(context, "ref_variable") 813 value = extract(context, "ref_value") 814 815 # format value's repr using wrapped_repr, but leaving room for 816 # varname = at beginning of first line 817 rep = wrapped_with_prefix(value, varname + ' = ') 818 819 # Use pygments to generate HTML markup for our assignment 820 markup = highlight_code(rep) 821 return ( 822 '<span class="prompt input">In []:</span>' 823 '<div class="snippet-input">{}</div>\n' 824 ).format(markup)
Renders a context created for goal evaluation as HTML markup for the definition of a variable, including Jupyter-notebook-style prompts.
Inherited Members
- Snippet
- base_context
- provide
- compile
- compile_context
- get_html
- final_edit
- show_image
- play_audio
- show_memory_report
- show_file_contents
- potluck.specifications.HasPayload
- synthesize_payload_info
- construct_payload
- describe_payload
- ensure_payload_constructor_arg
- prepare_source
- wrap_module
- ignore_output
- copy_args
- use_harness
- set_timeout
- do_setup
- do_cleanup
- capture_output
- provide_inputs
- use_decorations
- capture_trace
- sample_result_distribution
- capture_turtle_image
- capture_wavesynth
- capture_file_contents
- potluck.specifications.HasContext
- synthesize_context_info
- create_context
- set_context_description
- set_context_displayer
- describe_module_slot
- potluck.specifications.HasGoal
- provide_goal
- synthesize_goal_info
- create_goal_from_contexts
- ensure_goal_constructor_arg
- goal
- validate
- set_identifier
- set_goal_type
- set_goal_description
- test_module
- test_output
- test_with_harness
- test_trace
- check_trace_state
- check_invariant
- check_trace_count
- test_wavesynth_notes
- test_wavesynth_audio
- test_turtle_image
- test_file_contents
- compare_using
- succeed_unless_crashed
- compare_exactly
- compare_reports
- compare_strings_gently
- compare_strings_semi_strict
- compare_strings_firmly
- refine
827class RunModule(Snippet): 828 """ 829 A snippet which shows the output produced by running a module. 830 Functions like `potluck.specifications.HasPayload.provide_inputs` 831 can be used to control exactly what happens. 832 833 The module to import is specified by the currently active file 834 context (see `potluck.contexts.FileContext`). 835 """ 836 def __init__(self, sid, title, caption, postscript=''): 837 """ 838 A snippet ID, title, and caption are required. 839 840 A postscript is optional (see `Snippet`). 841 """ 842 cases = [ specifications.TestImport(register=False) ] 843 super().__init__(sid, title, caption, cases, postscript) 844 845 def render_context(self, context): 846 """ 847 Renders a context created for goal evaluation as HTML markup for 848 running a module. Includes a Jupyter-style prompt with %run magic 849 syntax to show which file was run. 850 """ 851 filename = extract(context, "filename") # Note: no ref_ here 852 captured = context.get("ref_output", '') 853 captured_errs = context.get("ref_error_log", '') 854 855 # Wrap faked inputs with spans so we can color them blue 856 captured = re.sub( 857 harness.FAKE_INPUT_PATTERN, 858 r'<span class="input">\1</span>', 859 captured 860 ) 861 862 result = ( 863 '<span class="prompt input">In []:</span>' 864 '<div class="snippet-input">' 865 '<code class="magic">%run {filename}</code>' 866 '</div>\n' 867 ).format(filename=filename) 868 if captured: 869 result += ( 870 '<span class="prompt printed">Prints</span>' 871 '<div class="snippet-printed">{captured}</div>\n' 872 ).format(captured=captured) 873 if captured_errs: 874 result += ( 875 '<span class="prompt stderr">Logs</span>' 876 '<div class="snippet-stderr">{log}</div>\n' 877 ).format(log=captured_errs) 878 879 return result
A snippet which shows the output produced by running a module.
Functions like potluck.specifications.HasPayload.provide_inputs
can be used to control exactly what happens.
The module to import is specified by the currently active file
context (see potluck.contexts.FileContext
).
836 def __init__(self, sid, title, caption, postscript=''): 837 """ 838 A snippet ID, title, and caption are required. 839 840 A postscript is optional (see `Snippet`). 841 """ 842 cases = [ specifications.TestImport(register=False) ] 843 super().__init__(sid, title, caption, cases, postscript)
A snippet ID, title, and caption are required.
A postscript is optional (see Snippet
).
845 def render_context(self, context): 846 """ 847 Renders a context created for goal evaluation as HTML markup for 848 running a module. Includes a Jupyter-style prompt with %run magic 849 syntax to show which file was run. 850 """ 851 filename = extract(context, "filename") # Note: no ref_ here 852 captured = context.get("ref_output", '') 853 captured_errs = context.get("ref_error_log", '') 854 855 # Wrap faked inputs with spans so we can color them blue 856 captured = re.sub( 857 harness.FAKE_INPUT_PATTERN, 858 r'<span class="input">\1</span>', 859 captured 860 ) 861 862 result = ( 863 '<span class="prompt input">In []:</span>' 864 '<div class="snippet-input">' 865 '<code class="magic">%run {filename}</code>' 866 '</div>\n' 867 ).format(filename=filename) 868 if captured: 869 result += ( 870 '<span class="prompt printed">Prints</span>' 871 '<div class="snippet-printed">{captured}</div>\n' 872 ).format(captured=captured) 873 if captured_errs: 874 result += ( 875 '<span class="prompt stderr">Logs</span>' 876 '<div class="snippet-stderr">{log}</div>\n' 877 ).format(log=captured_errs) 878 879 return result
Renders a context created for goal evaluation as HTML markup for running a module. Includes a Jupyter-style prompt with %run magic syntax to show which file was run.
Inherited Members
- Snippet
- base_context
- provide
- compile
- compile_context
- get_html
- final_edit
- show_image
- play_audio
- show_memory_report
- show_file_contents
- potluck.specifications.HasPayload
- synthesize_payload_info
- construct_payload
- describe_payload
- ensure_payload_constructor_arg
- prepare_source
- wrap_module
- ignore_output
- copy_args
- use_harness
- set_timeout
- do_setup
- do_cleanup
- capture_output
- provide_inputs
- use_decorations
- capture_trace
- sample_result_distribution
- capture_turtle_image
- capture_wavesynth
- capture_file_contents
- potluck.specifications.HasContext
- synthesize_context_info
- create_context
- set_context_description
- set_context_displayer
- describe_module_slot
- potluck.specifications.HasGoal
- provide_goal
- synthesize_goal_info
- create_goal_from_contexts
- ensure_goal_constructor_arg
- goal
- validate
- set_identifier
- set_goal_type
- set_goal_description
- test_module
- test_output
- test_with_harness
- test_trace
- check_trace_state
- check_invariant
- check_trace_count
- test_wavesynth_notes
- test_wavesynth_audio
- test_turtle_image
- test_file_contents
- compare_using
- succeed_unless_crashed
- compare_exactly
- compare_reports
- compare_strings_gently
- compare_strings_semi_strict
- compare_strings_firmly
- refine
882class FunctionCalls(Snippet): 883 """ 884 A snippet which shows the results (printed output and return values) 885 of calling one or more functions. To control what happens in detail, 886 use specialization methods from `potluck.specifications.HasPayload` 887 and `potluck.specifications.HasContext`. 888 """ 889 def __init__(self, sid, title, caption, calls, postscript=''): 890 """ 891 A snippet ID, title, and caption are required, along with a list 892 of function calls. Each entry in the list must be a tuple 893 containing a function name followed by a tuple of arguments, 894 and optionally, a dictionary of keyword arguments. 895 896 A postscript is optional (see `Snippet`). 897 """ 898 cases = [ 899 specifications.TestCase( 900 fname, 901 args, 902 kwargs or {}, 903 register=False 904 ) 905 for fname, args, kwargs in ( 906 map(lambda case: (case + (None,))[:3], calls) 907 ) 908 ] 909 super().__init__(sid, title, caption, cases, postscript) 910 911 def render_context(self, context): 912 """ 913 Renders a context created for goal evaluation as HTML markup for 914 calling a function. Includes a Jupyter-style prompt for input as 915 well as the return value. 916 """ 917 if "ref_error" in context: 918 print( 919 "Error during context creation:" 920 + context["ref_error"] 921 + "\nFull log is:\n" 922 + context.get("ref_error_log", '<missing>') 923 ) 924 fname = extract(context, "ref_function") 925 value = extract(context, "ref_value") 926 args = extract(context, "ref_args") 927 kwargs = extract(context, "ref_kwargs") 928 captured = context.get("ref_output", '') 929 captured_errs = context.get("ref_error_log", '') 930 931 # Figure out representations of each argument 932 argreps = [] 933 for arg in args: 934 argreps.append(wrapped_with_prefix(arg, '', 1)) 935 936 for kw in kwargs: 937 argreps.append(wrapped_with_prefix(kwargs[kw], kw + "=", 1)) 938 939 # Figure out full function call representation 940 oneline = "{}({})".format( 941 fname, 942 ', '.join(rep.strip() for rep in argreps) 943 ) 944 if '\n' not in oneline and len(oneline) <= FMT_WIDTH: 945 callrep = oneline 946 else: 947 callrep = "{}(\n{}\n)".format(fname, ',\n'.join(argreps)) 948 949 # Wrap faked inputs with spans so we can color them blue 950 captured = re.sub( 951 harness.FAKE_INPUT_PATTERN, 952 r'<span class="input">\1</span>', 953 captured 954 ) 955 956 # Highlight the function call 957 callrep = highlight_code(callrep) 958 959 result = ( 960 '<span class="prompt input">In []:</span>' 961 '<div class="snippet-input">{}</div>' 962 ).format(callrep) 963 964 if captured: 965 result += ( 966 '<span class="prompt printed">Prints</span>' 967 '<div class="snippet-printed">{}</div>\n' 968 ).format(captured) 969 970 if captured_errs: 971 result += ( 972 '<span class="prompt stderr">Logs</span>' 973 '<div class="snippet-stderr">{log}</div>\n' 974 ).format(log=captured_errs) 975 976 # Highlight the return value 977 if value is not None: 978 value = highlight_code(wrapped_with_prefix(value, "")) 979 980 result += ( 981 '<span class="prompt output">Out[]:</span>' 982 '<div class="snippet-output">{}</div>\n' 983 ).format(value) 984 985 return result
A snippet which shows the results (printed output and return values)
of calling one or more functions. To control what happens in detail,
use specialization methods from potluck.specifications.HasPayload
and potluck.specifications.HasContext
.
889 def __init__(self, sid, title, caption, calls, postscript=''): 890 """ 891 A snippet ID, title, and caption are required, along with a list 892 of function calls. Each entry in the list must be a tuple 893 containing a function name followed by a tuple of arguments, 894 and optionally, a dictionary of keyword arguments. 895 896 A postscript is optional (see `Snippet`). 897 """ 898 cases = [ 899 specifications.TestCase( 900 fname, 901 args, 902 kwargs or {}, 903 register=False 904 ) 905 for fname, args, kwargs in ( 906 map(lambda case: (case + (None,))[:3], calls) 907 ) 908 ] 909 super().__init__(sid, title, caption, cases, postscript)
A snippet ID, title, and caption are required, along with a list of function calls. Each entry in the list must be a tuple containing a function name followed by a tuple of arguments, and optionally, a dictionary of keyword arguments.
A postscript is optional (see Snippet
).
911 def render_context(self, context): 912 """ 913 Renders a context created for goal evaluation as HTML markup for 914 calling a function. Includes a Jupyter-style prompt for input as 915 well as the return value. 916 """ 917 if "ref_error" in context: 918 print( 919 "Error during context creation:" 920 + context["ref_error"] 921 + "\nFull log is:\n" 922 + context.get("ref_error_log", '<missing>') 923 ) 924 fname = extract(context, "ref_function") 925 value = extract(context, "ref_value") 926 args = extract(context, "ref_args") 927 kwargs = extract(context, "ref_kwargs") 928 captured = context.get("ref_output", '') 929 captured_errs = context.get("ref_error_log", '') 930 931 # Figure out representations of each argument 932 argreps = [] 933 for arg in args: 934 argreps.append(wrapped_with_prefix(arg, '', 1)) 935 936 for kw in kwargs: 937 argreps.append(wrapped_with_prefix(kwargs[kw], kw + "=", 1)) 938 939 # Figure out full function call representation 940 oneline = "{}({})".format( 941 fname, 942 ', '.join(rep.strip() for rep in argreps) 943 ) 944 if '\n' not in oneline and len(oneline) <= FMT_WIDTH: 945 callrep = oneline 946 else: 947 callrep = "{}(\n{}\n)".format(fname, ',\n'.join(argreps)) 948 949 # Wrap faked inputs with spans so we can color them blue 950 captured = re.sub( 951 harness.FAKE_INPUT_PATTERN, 952 r'<span class="input">\1</span>', 953 captured 954 ) 955 956 # Highlight the function call 957 callrep = highlight_code(callrep) 958 959 result = ( 960 '<span class="prompt input">In []:</span>' 961 '<div class="snippet-input">{}</div>' 962 ).format(callrep) 963 964 if captured: 965 result += ( 966 '<span class="prompt printed">Prints</span>' 967 '<div class="snippet-printed">{}</div>\n' 968 ).format(captured) 969 970 if captured_errs: 971 result += ( 972 '<span class="prompt stderr">Logs</span>' 973 '<div class="snippet-stderr">{log}</div>\n' 974 ).format(log=captured_errs) 975 976 # Highlight the return value 977 if value is not None: 978 value = highlight_code(wrapped_with_prefix(value, "")) 979 980 result += ( 981 '<span class="prompt output">Out[]:</span>' 982 '<div class="snippet-output">{}</div>\n' 983 ).format(value) 984 985 return result
Renders a context created for goal evaluation as HTML markup for calling a function. Includes a Jupyter-style prompt for input as well as the return value.
Inherited Members
- Snippet
- base_context
- provide
- compile
- compile_context
- get_html
- final_edit
- show_image
- play_audio
- show_memory_report
- show_file_contents
- potluck.specifications.HasPayload
- synthesize_payload_info
- construct_payload
- describe_payload
- ensure_payload_constructor_arg
- prepare_source
- wrap_module
- ignore_output
- copy_args
- use_harness
- set_timeout
- do_setup
- do_cleanup
- capture_output
- provide_inputs
- use_decorations
- capture_trace
- sample_result_distribution
- capture_turtle_image
- capture_wavesynth
- capture_file_contents
- potluck.specifications.HasContext
- synthesize_context_info
- create_context
- set_context_description
- set_context_displayer
- describe_module_slot
- potluck.specifications.HasGoal
- provide_goal
- synthesize_goal_info
- create_goal_from_contexts
- ensure_goal_constructor_arg
- goal
- validate
- set_identifier
- set_goal_type
- set_goal_description
- test_module
- test_output
- test_with_harness
- test_trace
- check_trace_state
- check_invariant
- check_trace_count
- test_wavesynth_notes
- test_wavesynth_audio
- test_turtle_image
- test_file_contents
- compare_using
- succeed_unless_crashed
- compare_exactly
- compare_reports
- compare_strings_gently
- compare_strings_semi_strict
- compare_strings_firmly
- refine
988class Blocks(Snippet): 989 """ 990 A snippet which shows the results (printed output and result value of 991 final line) of one or more blocks of statements, just like Jupyter 992 Notebook cells. To control what happens in detail, use specialization 993 methods from `potluck.specifications.HasPayload` and 994 `potluck.specifications.HasContext`. 995 996 Note: Any direct side-effects of the blocks (like changing the value 997 of a variable or defining a function) won't persist in the module 998 used for evaluation. However, indirect effects (like appending to a 999 list) will persist, and thus should be avoided because they could 1000 have unpredictable effects on other components of the system such as 1001 evaluation. 1002 1003 Along the same lines, each block of code to demonstrate is executed 1004 independently of the others. You cannot define a variable in one 1005 block and then use it in another (although you could fake this using 1006 the ability to define separate presentation and actual code). 1007 """ 1008 def __init__(self, sid, title, caption, blocks, postscript=''): 1009 """ 1010 A snippet ID, title, and caption are required, along with a list 1011 of code blocks. Each entry in the list should be either a 1012 multi-line string, or a tuple of two such strings. In the first 1013 case, the string is treated as the code to run, in the second, 1014 the first item in the tuple is the code to display, while the 1015 second is the code to actually run. 1016 1017 A postscript is optional (see `Snippet`). 1018 """ 1019 cases = [] 1020 for item in blocks: 1021 if isinstance(item, str): 1022 cases.append( 1023 specifications.TestBlock( 1024 sid, 1025 item, 1026 register=False 1027 ) 1028 ) 1029 else: 1030 display, actual = item 1031 cases.append( 1032 specifications.TestBlock( 1033 sid, 1034 display, 1035 actual, 1036 register=False 1037 ) 1038 ) 1039 1040 super().__init__(sid, title, caption, cases, postscript) 1041 1042 def render_context(self, context): 1043 """ 1044 Renders a context created for goal evaluation as HTML markup for 1045 executing a block of code. Includes a Jupyter-style prompt for 1046 input as well as the result value of the last line of the block 1047 as long as that's not None. 1048 """ 1049 src = extract(context, "ref_block") 1050 value = extract(context, "ref_value") 1051 captured = context.get("ref_output", '') 1052 captured_errs = context.get("ref_error_log", '') 1053 1054 # Wrap faked inputs with spans so we can color them blue 1055 captured = re.sub( 1056 harness.FAKE_INPUT_PATTERN, 1057 r'<span class="input">\1</span>', 1058 captured 1059 ) 1060 1061 # Highlight the code block 1062 blockrep = highlight_code(src) 1063 1064 result = ( 1065 '<span class="prompt input">In []:</span>' 1066 '<div class="snippet-input">{}</div>\n' 1067 ).format(blockrep) 1068 1069 if captured: 1070 result += ( 1071 '<span class="prompt printed">Prints</span>' 1072 '<div class="snippet-printed">{}</div>\n' 1073 ).format(captured) 1074 1075 if captured_errs: 1076 result += ( 1077 '<span class="prompt stderr">Logs</span>' 1078 '<div class="snippet-stderr">{log}</div>\n' 1079 ).format(log=captured_errs) 1080 1081 # Highlight the return value 1082 if value is not None: 1083 value = highlight_code(wrapped_with_prefix(value, "")) 1084 1085 result += ( 1086 '<span class="prompt output">Out[]:</span>' 1087 '<div class="snippet-output">{}</div>\n' 1088 ).format(value) 1089 1090 return result
A snippet which shows the results (printed output and result value of
final line) of one or more blocks of statements, just like Jupyter
Notebook cells. To control what happens in detail, use specialization
methods from potluck.specifications.HasPayload
and
potluck.specifications.HasContext
.
Note: Any direct side-effects of the blocks (like changing the value of a variable or defining a function) won't persist in the module used for evaluation. However, indirect effects (like appending to a list) will persist, and thus should be avoided because they could have unpredictable effects on other components of the system such as evaluation.
Along the same lines, each block of code to demonstrate is executed independently of the others. You cannot define a variable in one block and then use it in another (although you could fake this using the ability to define separate presentation and actual code).
1008 def __init__(self, sid, title, caption, blocks, postscript=''): 1009 """ 1010 A snippet ID, title, and caption are required, along with a list 1011 of code blocks. Each entry in the list should be either a 1012 multi-line string, or a tuple of two such strings. In the first 1013 case, the string is treated as the code to run, in the second, 1014 the first item in the tuple is the code to display, while the 1015 second is the code to actually run. 1016 1017 A postscript is optional (see `Snippet`). 1018 """ 1019 cases = [] 1020 for item in blocks: 1021 if isinstance(item, str): 1022 cases.append( 1023 specifications.TestBlock( 1024 sid, 1025 item, 1026 register=False 1027 ) 1028 ) 1029 else: 1030 display, actual = item 1031 cases.append( 1032 specifications.TestBlock( 1033 sid, 1034 display, 1035 actual, 1036 register=False 1037 ) 1038 ) 1039 1040 super().__init__(sid, title, caption, cases, postscript)
A snippet ID, title, and caption are required, along with a list of code blocks. Each entry in the list should be either a multi-line string, or a tuple of two such strings. In the first case, the string is treated as the code to run, in the second, the first item in the tuple is the code to display, while the second is the code to actually run.
A postscript is optional (see Snippet
).
1042 def render_context(self, context): 1043 """ 1044 Renders a context created for goal evaluation as HTML markup for 1045 executing a block of code. Includes a Jupyter-style prompt for 1046 input as well as the result value of the last line of the block 1047 as long as that's not None. 1048 """ 1049 src = extract(context, "ref_block") 1050 value = extract(context, "ref_value") 1051 captured = context.get("ref_output", '') 1052 captured_errs = context.get("ref_error_log", '') 1053 1054 # Wrap faked inputs with spans so we can color them blue 1055 captured = re.sub( 1056 harness.FAKE_INPUT_PATTERN, 1057 r'<span class="input">\1</span>', 1058 captured 1059 ) 1060 1061 # Highlight the code block 1062 blockrep = highlight_code(src) 1063 1064 result = ( 1065 '<span class="prompt input">In []:</span>' 1066 '<div class="snippet-input">{}</div>\n' 1067 ).format(blockrep) 1068 1069 if captured: 1070 result += ( 1071 '<span class="prompt printed">Prints</span>' 1072 '<div class="snippet-printed">{}</div>\n' 1073 ).format(captured) 1074 1075 if captured_errs: 1076 result += ( 1077 '<span class="prompt stderr">Logs</span>' 1078 '<div class="snippet-stderr">{log}</div>\n' 1079 ).format(log=captured_errs) 1080 1081 # Highlight the return value 1082 if value is not None: 1083 value = highlight_code(wrapped_with_prefix(value, "")) 1084 1085 result += ( 1086 '<span class="prompt output">Out[]:</span>' 1087 '<div class="snippet-output">{}</div>\n' 1088 ).format(value) 1089 1090 return result
Renders a context created for goal evaluation as HTML markup for executing a block of code. Includes a Jupyter-style prompt for input as well as the result value of the last line of the block as long as that's not None.
Inherited Members
- Snippet
- base_context
- provide
- compile
- compile_context
- get_html
- final_edit
- show_image
- play_audio
- show_memory_report
- show_file_contents
- potluck.specifications.HasPayload
- synthesize_payload_info
- construct_payload
- describe_payload
- ensure_payload_constructor_arg
- prepare_source
- wrap_module
- ignore_output
- copy_args
- use_harness
- set_timeout
- do_setup
- do_cleanup
- capture_output
- provide_inputs
- use_decorations
- capture_trace
- sample_result_distribution
- capture_turtle_image
- capture_wavesynth
- capture_file_contents
- potluck.specifications.HasContext
- synthesize_context_info
- create_context
- set_context_description
- set_context_displayer
- describe_module_slot
- potluck.specifications.HasGoal
- provide_goal
- synthesize_goal_info
- create_goal_from_contexts
- ensure_goal_constructor_arg
- goal
- validate
- set_identifier
- set_goal_type
- set_goal_description
- test_module
- test_output
- test_with_harness
- test_trace
- check_trace_state
- check_invariant
- check_trace_count
- test_wavesynth_notes
- test_wavesynth_audio
- test_turtle_image
- test_file_contents
- compare_using
- succeed_unless_crashed
- compare_exactly
- compare_reports
- compare_strings_gently
- compare_strings_semi_strict
- compare_strings_firmly
- refine
1093class Fakes(Snippet): 1094 """ 1095 One or more fake snippets, which format code, printed output, stderr 1096 output, and a result value (and possibly glamer additions) but each 1097 of these things is simply specified ahead of time. Note that you want 1098 to avoid using this if at all possible, because it re-creates the 1099 problems that this module was trying to solve (namely, examples 1100 becoming out-of-sync with the solution code). The specialization 1101 methods of this kind of snippet are ignored and have no effect, 1102 because no code is actually run. 1103 """ 1104 def __init__(self, sid, title, caption, fake_contexts, postscript=''): 1105 """ 1106 A snippet ID, title, and caption are required, along with a list 1107 of fake context dictionary objects to be rendered as if they came 1108 from a real test. Each dictionary must contain a "code" slot, and 1109 may contain "ref_value", "ref_output", and/or "ref_error_log" 1110 keys. If using glamers, relevant keys should be added directly to 1111 these fake contexts. 1112 1113 If "ref_value" or "ref_output" slots are missing, defaults of 1114 None and '' will be added, since these slots are ultimately 1115 required. 1116 1117 A postscript is optional (see `Snippet`). 1118 """ 1119 self.fake_contexts = fake_contexts 1120 for ctx in self.fake_contexts: 1121 if 'code' not in ctx: 1122 raise ValueError( 1123 "Fake context dictionaries must contain a 'code'" 1124 " slot." 1125 ) 1126 if 'ref_value' not in ctx: 1127 ctx['ref_value'] = None 1128 if 'ref_output' not in ctx: 1129 ctx['ref_output'] = '' 1130 super().__init__(sid, title, caption, [], postscript) 1131 1132 def create_goal(self): 1133 """ 1134 We override TestGroup.create_goal to just create a dummy goal 1135 which depends on dummy contexts. 1136 """ 1137 ctx_list = [] 1138 for fake in self.fake_contexts: 1139 def make_builder(): 1140 """ 1141 We need to capture 'fake' on each iteration of the loop, 1142 which is why this extra layer of indirection is added. 1143 """ 1144 nonlocal fake 1145 return lambda _: fake 1146 ctx_list.append(contexts.Context(builder=make_builder())) 1147 1148 return rubrics.NoteGoal( 1149 self.taskid, 1150 "fakeSnippetGoal:" + self.base_name, 1151 ( 1152 "NoteGoal for fake snippet '{}'.".format(self.base_name), 1153 "A fake NoteGoal." 1154 ), 1155 test_in={ "contexts": ctx_list } 1156 ) 1157 1158 def render_context(self, context): 1159 """ 1160 Renders a context created for goal evaluation as HTML markup for 1161 executing a block of code. Includes a Jupyter-style prompt for 1162 input as well as the result value of the last line of the block 1163 as long as that's not None. 1164 """ 1165 src = extract(context, "code") 1166 value = extract(context, "ref_value") 1167 captured = context.get("ref_output", '') 1168 captured_errs = context.get("ref_error_log", '') 1169 1170 # Wrap faked inputs with spans so we can color them blue 1171 captured = re.sub( 1172 harness.FAKE_INPUT_PATTERN, 1173 r'<span class="input">\1</span>', 1174 captured 1175 ) 1176 1177 # Highlight the code block 1178 blockrep = highlight_code(src) 1179 1180 result = ( 1181 '<span class="prompt input">In []:</span>' 1182 '<div class="snippet-input">{}</div>\n' 1183 ).format(blockrep) 1184 1185 if captured: 1186 result += ( 1187 '<span class="prompt printed">Prints</span>' 1188 '<div class="snippet-printed">{}</div>\n' 1189 ).format(captured) 1190 1191 if captured_errs: 1192 result += ( 1193 '<span class="prompt stderr">Logs</span>' 1194 '<div class="snippet-stderr">{log}</div>\n' 1195 ).format(log=captured_errs) 1196 1197 # Highlight the return value 1198 if value is not None: 1199 value = highlight_code(wrapped_with_prefix(value, "")) 1200 1201 result += ( 1202 '<span class="prompt output">Out[]:</span>' 1203 '<div class="snippet-output">{}</div>\n' 1204 ).format(value) 1205 1206 return result
One or more fake snippets, which format code, printed output, stderr output, and a result value (and possibly glamer additions) but each of these things is simply specified ahead of time. Note that you want to avoid using this if at all possible, because it re-creates the problems that this module was trying to solve (namely, examples becoming out-of-sync with the solution code). The specialization methods of this kind of snippet are ignored and have no effect, because no code is actually run.
1104 def __init__(self, sid, title, caption, fake_contexts, postscript=''): 1105 """ 1106 A snippet ID, title, and caption are required, along with a list 1107 of fake context dictionary objects to be rendered as if they came 1108 from a real test. Each dictionary must contain a "code" slot, and 1109 may contain "ref_value", "ref_output", and/or "ref_error_log" 1110 keys. If using glamers, relevant keys should be added directly to 1111 these fake contexts. 1112 1113 If "ref_value" or "ref_output" slots are missing, defaults of 1114 None and '' will be added, since these slots are ultimately 1115 required. 1116 1117 A postscript is optional (see `Snippet`). 1118 """ 1119 self.fake_contexts = fake_contexts 1120 for ctx in self.fake_contexts: 1121 if 'code' not in ctx: 1122 raise ValueError( 1123 "Fake context dictionaries must contain a 'code'" 1124 " slot." 1125 ) 1126 if 'ref_value' not in ctx: 1127 ctx['ref_value'] = None 1128 if 'ref_output' not in ctx: 1129 ctx['ref_output'] = '' 1130 super().__init__(sid, title, caption, [], postscript)
A snippet ID, title, and caption are required, along with a list of fake context dictionary objects to be rendered as if they came from a real test. Each dictionary must contain a "code" slot, and may contain "ref_value", "ref_output", and/or "ref_error_log" keys. If using glamers, relevant keys should be added directly to these fake contexts.
If "ref_value" or "ref_output" slots are missing, defaults of None and '' will be added, since these slots are ultimately required.
A postscript is optional (see Snippet
).
1132 def create_goal(self): 1133 """ 1134 We override TestGroup.create_goal to just create a dummy goal 1135 which depends on dummy contexts. 1136 """ 1137 ctx_list = [] 1138 for fake in self.fake_contexts: 1139 def make_builder(): 1140 """ 1141 We need to capture 'fake' on each iteration of the loop, 1142 which is why this extra layer of indirection is added. 1143 """ 1144 nonlocal fake 1145 return lambda _: fake 1146 ctx_list.append(contexts.Context(builder=make_builder())) 1147 1148 return rubrics.NoteGoal( 1149 self.taskid, 1150 "fakeSnippetGoal:" + self.base_name, 1151 ( 1152 "NoteGoal for fake snippet '{}'.".format(self.base_name), 1153 "A fake NoteGoal." 1154 ), 1155 test_in={ "contexts": ctx_list } 1156 )
We override TestGroup.create_goal to just create a dummy goal which depends on dummy contexts.
1158 def render_context(self, context): 1159 """ 1160 Renders a context created for goal evaluation as HTML markup for 1161 executing a block of code. Includes a Jupyter-style prompt for 1162 input as well as the result value of the last line of the block 1163 as long as that's not None. 1164 """ 1165 src = extract(context, "code") 1166 value = extract(context, "ref_value") 1167 captured = context.get("ref_output", '') 1168 captured_errs = context.get("ref_error_log", '') 1169 1170 # Wrap faked inputs with spans so we can color them blue 1171 captured = re.sub( 1172 harness.FAKE_INPUT_PATTERN, 1173 r'<span class="input">\1</span>', 1174 captured 1175 ) 1176 1177 # Highlight the code block 1178 blockrep = highlight_code(src) 1179 1180 result = ( 1181 '<span class="prompt input">In []:</span>' 1182 '<div class="snippet-input">{}</div>\n' 1183 ).format(blockrep) 1184 1185 if captured: 1186 result += ( 1187 '<span class="prompt printed">Prints</span>' 1188 '<div class="snippet-printed">{}</div>\n' 1189 ).format(captured) 1190 1191 if captured_errs: 1192 result += ( 1193 '<span class="prompt stderr">Logs</span>' 1194 '<div class="snippet-stderr">{log}</div>\n' 1195 ).format(log=captured_errs) 1196 1197 # Highlight the return value 1198 if value is not None: 1199 value = highlight_code(wrapped_with_prefix(value, "")) 1200 1201 result += ( 1202 '<span class="prompt output">Out[]:</span>' 1203 '<div class="snippet-output">{}</div>\n' 1204 ).format(value) 1205 1206 return result
Renders a context created for goal evaluation as HTML markup for executing a block of code. Includes a Jupyter-style prompt for input as well as the result value of the last line of the block as long as that's not None.
Inherited Members
- Snippet
- base_context
- provide
- compile
- compile_context
- get_html
- final_edit
- show_image
- play_audio
- show_memory_report
- show_file_contents
- potluck.specifications.HasPayload
- synthesize_payload_info
- construct_payload
- describe_payload
- ensure_payload_constructor_arg
- prepare_source
- wrap_module
- ignore_output
- copy_args
- use_harness
- set_timeout
- do_setup
- do_cleanup
- capture_output
- provide_inputs
- use_decorations
- capture_trace
- sample_result_distribution
- capture_turtle_image
- capture_wavesynth
- capture_file_contents
- potluck.specifications.HasContext
- synthesize_context_info
- create_context
- set_context_description
- set_context_displayer
- describe_module_slot
- potluck.specifications.HasGoal
- provide_goal
- synthesize_goal_info
- create_goal_from_contexts
- ensure_goal_constructor_arg
- goal
- validate
- set_identifier
- set_goal_type
- set_goal_description
- test_module
- test_output
- test_with_harness
- test_trace
- check_trace_state
- check_invariant
- check_trace_count
- test_wavesynth_notes
- test_wavesynth_audio
- test_turtle_image
- test_file_contents
- compare_using
- succeed_unless_crashed
- compare_exactly
- compare_reports
- compare_strings_gently
- compare_strings_semi_strict
- compare_strings_firmly
- refine
1209class Files(Snippet): 1210 """ 1211 A snippet which shows the contents of one or more files, without 1212 running any code, in the same Jupyter-like format that file contents 1213 are shown when `Snippet.show_file_contents` is used. Most 1214 specialization methods don't work properly on this kind of snippet. 1215 1216 By default, file paths are interpreted as relative to the solutions 1217 directory, but specifying a different base directory via the 1218 constructor can change that. 1219 """ 1220 def __init__( 1221 self, 1222 sid, 1223 title, 1224 caption, 1225 filepaths, 1226 base='__soln__', 1227 postscript='' 1228 ): 1229 """ 1230 A snippet ID, title, and caption are required, along with a list 1231 of file paths. Each entry in the list should be a string path to 1232 a starter file relative to the starter directory. The versions of 1233 files in the solution directory will be used. But only files 1234 present in both starter and solution directories will be 1235 available. 1236 1237 If `base` is specified, it should be either '__soln__' (the 1238 default), '__starter__', or a path string. If it's __soln__ paths 1239 will be interpreted relative to the solution directory; if it's 1240 __starter__ they'll be interpreted relative to the starter 1241 directory, and for any other path, paths are interpreted relative 1242 to that directory. In any case, absolute paths will not be 1243 modified. 1244 1245 A postscript is optional (see `Snippet`). 1246 """ 1247 if base == "__soln__": 1248 base = file_utils.current_solution_path() 1249 elif base == "__starter__": 1250 base = file_utils.current_starter_path() 1251 # else leave base as-is 1252 1253 self.filepaths = { 1254 path: os.path.join(base, path) 1255 for path in filepaths 1256 } 1257 1258 super().__init__(sid, title, caption, [], postscript) 1259 1260 def create_goal(self): 1261 """ 1262 We override TestGroup.create_goal to just create a dummy goal 1263 which depends on dummy contexts. 1264 """ 1265 ctx_list = [] 1266 for showpath in self.filepaths: 1267 filepath = self.filepaths[showpath] 1268 with open(filepath, 'r', encoding="utf-8") as fileInput: 1269 contents = fileInput.read() 1270 1271 def make_builder(): 1272 """ 1273 We need to capture 'filepath' on each iteration of the 1274 loop, which is why this extra layer of indirection is 1275 added. 1276 """ 1277 nonlocal showpath, filepath, contents 1278 sp = showpath 1279 fp = filepath 1280 ct = contents 1281 return lambda _: { 1282 "path": sp, 1283 "real_path": fp, 1284 "contents": ct 1285 } 1286 1287 ctx_list.append(contexts.Context(builder=make_builder())) 1288 1289 return rubrics.NoteGoal( 1290 self.taskid, 1291 "fileSnippetGoal:" + self.base_name, 1292 ( 1293 "NoteGoal for file snippet '{}'.".format(self.base_name), 1294 "A file-displaying NoteGoal." 1295 ), 1296 test_in={ "contexts": ctx_list } 1297 ) 1298 1299 def render_context(self, context): 1300 """ 1301 Renders a context with just file information as HTML markup for 1302 displaying a file with %more magic. Includes a Jupyter-style 1303 prompt for input as well as the contents of the file. 1304 """ 1305 path = extract(context, "path") 1306 contents = extract(context, "contents") 1307 1308 result = ( 1309 '<span class="prompt input">In []:</span>' 1310 '<div class="snippet-input">' 1311 '<code class="magic">%more {filename}</code>' 1312 '</div>\n' 1313 '<span class="prompt special">File</span>' 1314 '<div class="snippet-filename">{filename}</div>\n' 1315 '<div class="snippet-file-contents">{contents}</div>\n' 1316 ).format(filename=path, contents=contents) 1317 1318 return result
A snippet which shows the contents of one or more files, without
running any code, in the same Jupyter-like format that file contents
are shown when Snippet.show_file_contents
is used. Most
specialization methods don't work properly on this kind of snippet.
By default, file paths are interpreted as relative to the solutions directory, but specifying a different base directory via the constructor can change that.
1220 def __init__( 1221 self, 1222 sid, 1223 title, 1224 caption, 1225 filepaths, 1226 base='__soln__', 1227 postscript='' 1228 ): 1229 """ 1230 A snippet ID, title, and caption are required, along with a list 1231 of file paths. Each entry in the list should be a string path to 1232 a starter file relative to the starter directory. The versions of 1233 files in the solution directory will be used. But only files 1234 present in both starter and solution directories will be 1235 available. 1236 1237 If `base` is specified, it should be either '__soln__' (the 1238 default), '__starter__', or a path string. If it's __soln__ paths 1239 will be interpreted relative to the solution directory; if it's 1240 __starter__ they'll be interpreted relative to the starter 1241 directory, and for any other path, paths are interpreted relative 1242 to that directory. In any case, absolute paths will not be 1243 modified. 1244 1245 A postscript is optional (see `Snippet`). 1246 """ 1247 if base == "__soln__": 1248 base = file_utils.current_solution_path() 1249 elif base == "__starter__": 1250 base = file_utils.current_starter_path() 1251 # else leave base as-is 1252 1253 self.filepaths = { 1254 path: os.path.join(base, path) 1255 for path in filepaths 1256 } 1257 1258 super().__init__(sid, title, caption, [], postscript)
A snippet ID, title, and caption are required, along with a list of file paths. Each entry in the list should be a string path to a starter file relative to the starter directory. The versions of files in the solution directory will be used. But only files present in both starter and solution directories will be available.
If base
is specified, it should be either '__soln__' (the
default), '__starter__', or a path string. If it's __soln__ paths
will be interpreted relative to the solution directory; if it's
__starter__ they'll be interpreted relative to the starter
directory, and for any other path, paths are interpreted relative
to that directory. In any case, absolute paths will not be
modified.
A postscript is optional (see Snippet
).
1260 def create_goal(self): 1261 """ 1262 We override TestGroup.create_goal to just create a dummy goal 1263 which depends on dummy contexts. 1264 """ 1265 ctx_list = [] 1266 for showpath in self.filepaths: 1267 filepath = self.filepaths[showpath] 1268 with open(filepath, 'r', encoding="utf-8") as fileInput: 1269 contents = fileInput.read() 1270 1271 def make_builder(): 1272 """ 1273 We need to capture 'filepath' on each iteration of the 1274 loop, which is why this extra layer of indirection is 1275 added. 1276 """ 1277 nonlocal showpath, filepath, contents 1278 sp = showpath 1279 fp = filepath 1280 ct = contents 1281 return lambda _: { 1282 "path": sp, 1283 "real_path": fp, 1284 "contents": ct 1285 } 1286 1287 ctx_list.append(contexts.Context(builder=make_builder())) 1288 1289 return rubrics.NoteGoal( 1290 self.taskid, 1291 "fileSnippetGoal:" + self.base_name, 1292 ( 1293 "NoteGoal for file snippet '{}'.".format(self.base_name), 1294 "A file-displaying NoteGoal." 1295 ), 1296 test_in={ "contexts": ctx_list } 1297 )
We override TestGroup.create_goal to just create a dummy goal which depends on dummy contexts.
1299 def render_context(self, context): 1300 """ 1301 Renders a context with just file information as HTML markup for 1302 displaying a file with %more magic. Includes a Jupyter-style 1303 prompt for input as well as the contents of the file. 1304 """ 1305 path = extract(context, "path") 1306 contents = extract(context, "contents") 1307 1308 result = ( 1309 '<span class="prompt input">In []:</span>' 1310 '<div class="snippet-input">' 1311 '<code class="magic">%more {filename}</code>' 1312 '</div>\n' 1313 '<span class="prompt special">File</span>' 1314 '<div class="snippet-filename">{filename}</div>\n' 1315 '<div class="snippet-file-contents">{contents}</div>\n' 1316 ).format(filename=path, contents=contents) 1317 1318 return result
Renders a context with just file information as HTML markup for displaying a file with %more magic. Includes a Jupyter-style prompt for input as well as the contents of the file.
Inherited Members
- Snippet
- base_context
- provide
- compile
- compile_context
- get_html
- final_edit
- show_image
- play_audio
- show_memory_report
- show_file_contents
- potluck.specifications.HasPayload
- synthesize_payload_info
- construct_payload
- describe_payload
- ensure_payload_constructor_arg
- prepare_source
- wrap_module
- ignore_output
- copy_args
- use_harness
- set_timeout
- do_setup
- do_cleanup
- capture_output
- provide_inputs
- use_decorations
- capture_trace
- sample_result_distribution
- capture_turtle_image
- capture_wavesynth
- capture_file_contents
- potluck.specifications.HasContext
- synthesize_context_info
- create_context
- set_context_description
- set_context_displayer
- describe_module_slot
- potluck.specifications.HasGoal
- provide_goal
- synthesize_goal_info
- create_goal_from_contexts
- ensure_goal_constructor_arg
- goal
- validate
- set_identifier
- set_goal_type
- set_goal_description
- test_module
- test_output
- test_with_harness
- test_trace
- check_trace_state
- check_invariant
- check_trace_count
- test_wavesynth_notes
- test_wavesynth_audio
- test_turtle_image
- test_file_contents
- compare_using
- succeed_unless_crashed
- compare_exactly
- compare_reports
- compare_strings_gently
- compare_strings_semi_strict
- compare_strings_firmly
- refine
1325def append_image_result(markup, context): 1326 """ 1327 Given some HTML markup and a context dictionary, turns the 1328 "ref_image" context slot into an HTML img tag and returns the given 1329 markup with that appended, prefixed by an "Image " prompt. If the 1330 context has no "image" value, the original markup is returned 1331 unmodified. 1332 1333 The "ref_image_alt" slot of the context is used as alt text for the 1334 image, with the "ref_output" slot being used as backup (under a 1335 hopeful assumption about turtleBeads being imported for printed 1336 descriptions). If neither is present, "no alt text available" will be 1337 used. 1338 """ 1339 # Short-circuit unless we've got an image 1340 if "ref_image" not in context: 1341 return markup 1342 1343 image = extract(context, "ref_image") 1344 alt = context.get( 1345 "ref_image_alt", 1346 context.get("ref_output", "no alt text available") 1347 ) 1348 1349 img_tag = html_tools.html_image(image, alt, ["example"]) 1350 return ( 1351 markup 1352 + '<span class="prompt special">Image </span>' 1353 + img_tag 1354 )
Given some HTML markup and a context dictionary, turns the "ref_image" context slot into an HTML img tag and returns the given markup with that appended, prefixed by an "Image " prompt. If the context has no "image" value, the original markup is returned unmodified.
The "ref_image_alt" slot of the context is used as alt text for the image, with the "ref_output" slot being used as backup (under a hopeful assumption about turtleBeads being imported for printed descriptions). If neither is present, "no alt text available" will be used.
1357def append_audio_result(markup, context): 1358 """ 1359 Given some HTML markup and a context dictionary, turns the 1360 "ref_audio" context slot into an HTML audio tag and returns the given 1361 markup with that appended, prefixed by an "Audio " prompt. 1362 1363 Note that this can result in a pretty large string if the WAV format 1364 is used, since the string needs to be base64-encoded and WAV is 1365 uncompressed (and we double the size by including the data URL twice) 1366 :( 1367 1368 TODO: Maybe use Ogg/Vorbis? 1369 1370 If there is no "ref_audio" context slot, the given markup is returned 1371 unmodified. 1372 1373 The "ref_audio" context value must be a dictionary with at least 1374 "mimetype" and "data" slots containing the MIME type for the data and 1375 the data itself (as a bytes object). It may also include a "label" 1376 slot which will be used invisibly as an aria-label property of the 1377 audio element; if absent no aria-label will be attached. 1378 """ 1379 # Short-circuit unless we've got audio 1380 if "ref_audio" not in context: 1381 return markup 1382 1383 audio = extract(context, "ref_audio") 1384 mime = audio["mimetype"] 1385 data = audio["data"] 1386 label = audio.get("label") 1387 1388 audio_tag = html_tools.html_audio(data, mime, label) 1389 return ( 1390 markup 1391 + '\n<span class="prompt special">Audio </span>' 1392 + audio_tag 1393 )
Given some HTML markup and a context dictionary, turns the "ref_audio" context slot into an HTML audio tag and returns the given markup with that appended, prefixed by an "Audio " prompt.
Note that this can result in a pretty large string if the WAV format is used, since the string needs to be base64-encoded and WAV is uncompressed (and we double the size by including the data URL twice) :(
TODO: Maybe use Ogg/Vorbis?
If there is no "ref_audio" context slot, the given markup is returned unmodified.
The "ref_audio" context value must be a dictionary with at least "mimetype" and "data" slots containing the MIME type for the data and the data itself (as a bytes object). It may also include a "label" slot which will be used invisibly as an aria-label property of the audio element; if absent no aria-label will be attached.
1396def append_memory_report(markup, context): 1397 """ 1398 Given some HTML markup and a context dictionary, creates a memory 1399 report showing the structure of the object in the "ref_value" context 1400 slot, and appends that to the markup, using a 'Memory Report' tag. 1401 """ 1402 # Do nothing if no ref_value is available 1403 if "ref_value" not in context: 1404 return markup 1405 1406 obj = extract(context, "ref_value") 1407 1408 report = html_tools.escape(specifications.memory_report(obj)) 1409 formatted = "<pre>{report}</pre>".format(report=report) 1410 return ( 1411 markup 1412 + '\n<span class="prompt special">Memory\nReport</span>' 1413 + formatted 1414 )
Given some HTML markup and a context dictionary, creates a memory report showing the structure of the object in the "ref_value" context slot, and appends that to the markup, using a 'Memory Report' tag.
1417def append_file_contents(markup, context): 1418 """ 1419 Given some HTML markup and a context dictionary, turns the 1420 "ref_output_filename" and "ref_output_file_contents" context slots 1421 into HTML markup displaying the contents of that file, which gets 1422 appended to the given markup and returned. 1423 1424 Note that this is intended to display the contents of text files. 1425 1426 If either of the "ref_output_filename" or "ref_output_file_contents" 1427 context slots are missing, the given markup is returned unmodified. 1428 """ 1429 # Short-circuit unless we've got output file contents 1430 if ( 1431 "ref_output_filename" not in context 1432 or "ref_output_file_contents" not in context 1433 ): 1434 return markup 1435 1436 filename = extract(context, "ref_output_filename") 1437 contents = extract(context, "ref_output_file_contents") 1438 1439 return ( 1440 markup 1441 + ( 1442 '\n<span class="prompt special">File</span>' 1443 ) 1444 + '<div class="snippet-filename">{}</div>\n'.format(filename) 1445 + '<div class="snippet-file-contents">{}</div>\n'.format(contents) 1446 )
Given some HTML markup and a context dictionary, turns the "ref_output_filename" and "ref_output_file_contents" context slots into HTML markup displaying the contents of that file, which gets appended to the given markup and returned.
Note that this is intended to display the contents of text files.
If either of the "ref_output_filename" or "ref_output_file_contents" context slots are missing, the given markup is returned unmodified.
1453def list_snippets(task_info): 1454 """ 1455 Returns a list containing all snippet IDs (strings) for the given 1456 task (as a task info dictionary). 1457 """ 1458 reg = SNIPPET_REGISTRY.get(task_info["specification"].__name__, {}) 1459 return list(reg.keys())
Returns a list containing all snippet IDs (strings) for the given task (as a task info dictionary).
1462def get_html(task_info, sid): 1463 """ 1464 Retrieves the HTML code (a string) for the snippet with the given ID 1465 in the given task (as a task info dictionary). Returns None if there 1466 is no such snippet. 1467 """ 1468 reg = SNIPPET_REGISTRY.get(task_info["specification"].__name__, {}) 1469 if sid not in reg: 1470 return None 1471 return reg[sid].get_html(task_info)
Retrieves the HTML code (a string) for the snippet with the given ID in the given task (as a task info dictionary). Returns None if there is no such snippet.
1474def get_all_snippets(task_info): 1475 """ 1476 Returns a list of HTML strings containing each registered snippet for 1477 the given (as a task_info dictionary) task. 1478 """ 1479 return [ 1480 get_html(task_info, sid) 1481 for sid in list_snippets(task_info) 1482 ]
Returns a list of HTML strings containing each registered snippet for the given (as a task_info dictionary) task.