potluck.contexts
The Context
class and related functions which form the backbone of the
testing process.
contexts.py
A Context
represents a specific situation derived from
other contexts and ultimately submitted and/or solution code.
potluck.rubrics.Goal
objects actually evaluate things, but they depend
on one or more contexts to provide material to be evaluated.
For example, a typical unit test consists of a
potluck.rubrics.ComparisonTest
which runs a checker function on the
'value' and 'ref_value' context keys in a Context
which is created by
running the function to test with particular arguments (it will run the
submitted function to generate a 'value' entry and the solution function
to generate a ref_value
entry). This Context
will in turn depend on
another Context
which loads the submitted and solution modules so that
they are ready for testing, etc.
In general, Context
objects will form a directed acyclic graph of
dependencies, and because they cache their results, duplicate work is
avoided. Context
objects also have user-facing text to describe how
they are derived, as well as the ability to explain their results to the
user.
Although Context
objects represent the abstract idea of a certain test
result, the actual value of that result is stored in a context
dictionary, which is created and cached by the Context
and supplied to
Goal
objects on-demand.
1""" 2The `Context` class and related functions which form the backbone of the 3testing process. 4 5contexts.py 6 7A `Context` represents a specific situation derived from 8other contexts and ultimately submitted and/or solution code. 9`potluck.rubrics.Goal` objects actually evaluate things, but they depend 10on one or more contexts to provide material to be evaluated. 11 12For example, a typical unit test consists of a 13`potluck.rubrics.ComparisonTest` which runs a checker function on the 14'value' and 'ref_value' context keys in a `Context` which is created by 15running the function to test with particular arguments (it will run the 16submitted function to generate a 'value' entry and the solution function 17to generate a `ref_value` entry). This `Context` will in turn depend on 18another `Context` which loads the submitted and solution modules so that 19they are ready for testing, etc. 20 21In general, `Context` objects will form a directed acyclic graph of 22dependencies, and because they cache their results, duplicate work is 23avoided. `Context` objects also have user-facing text to describe how 24they are derived, as well as the ability to explain their results to the 25user. 26 27Although `Context` objects represent the abstract idea of a certain test 28result, the actual value of that result is stored in a context 29dictionary, which is created and cached by the `Context` and supplied to 30`Goal` objects on-demand. 31""" 32 33import copy 34import os 35import time 36import re 37import sys 38import io 39import tempfile 40import shutil 41 42try: 43 import optimism 44 OPTIMISTIC = True 45except Exception: 46 OPTIMISTIC = False 47 48from . import html_tools 49from . import phrasing 50from . import load 51from . import patterns 52from . import mast 53from . import context_utils 54from . import compare 55 56 57#---------# 58# Globals # 59#---------# 60 61RELEVANT_FILENAME = None 62""" 63A string (or None before it gets set) which holds the file name given to 64the most recent instantiation of a `FileContext`, which is thus also 65almost certainly the filename that will be used in any new contexts that 66are created (unless hijinks have ensued). 67""" 68 69RELEVANT_TESTS_FILENAME = None 70""" 71Like `RELEVANT_FILENAME`, but for a tests file to be validated rather 72than a submission file to be evaluated. 73""" 74 75VALUE_SIZE_LIMIT = 10000 76""" 77Limit (in terms of string length, not bytes) after which we truncate 78large values that would be displayed by `build_context_value_displayer`. 79""" 80 81 82#----------------------------------------# 83# Context class and associated functions # 84#----------------------------------------# 85 86def generic_context_displayer(context): 87 """ 88 The default display_product function for a Context, this just returns 89 the string "no details available." This may help cut down on report 90 sizes where displaying all context values would include highlighted 91 copies of the source code, etc. 92 """ 93 return "no details available" 94 95 96class Context: 97 """ 98 Represents some kind of product of submitted code, like an 99 AST tree, or produced output. Contexts can specialize each other, so 100 for example a regular expression could be used to transform a context 101 containing all output from a program into a context containing just a 102 single line of output. Context objects organize context_builder 103 functions into hierarchies, and manage caching of their results. 104 105 A `Context` object also needs to know how to render its results into 106 HTML to display to the user as part of the feedback process. 107 108 A `Context` object, via its context_builder function, knows how to 109 produce, on demand, a dictionary mapping context keys to values. 110 The base context (and therefore all derived contexts) will always 111 include the following `potluck.context_utils.BASE_CONTEXT_SLOTS`: 112 113 - "task_info": A task info dictionary for the current task, which 114 will have the following keys: 115 - "id": The task ID. 116 - "specification": the specification module for this task. 117 - "target": The default file or folder to grade in a submission. 118 - "title": The title of this task. 119 - "desc": The short description of this task. 120 - "reference_cache_file": The filename where reference values 121 may be cached. 122 - "ignore_cache": Whether or not to ignore the cache (might not 123 be present; ignore cache only if present and truthy). 124 - "username": The user who submitted the code we're evaluating. 125 - "submission_root": The root directory within which the 126 submission we're evaluating can be found. 127 - "default_file": The official name of the default file to evaluate. 128 - "actual_file": The actual name of the default file as submitted. 129 - "tests_submission_root": The root directory where the test's we're 130 validating can be found (present during validation). 131 - "default_tests_file": The official name of the default tests file 132 to validate. 133 - "actual_tests_file": The actual name of the tests file as 134 submitted. 135 136 When a context is associated with a specific goal, the context 137 dictionary will also always contain the following extra slots: 138 139 - "goal_id": The unique-per-task identifier string for the goal that 140 this context is associated with. 141 - "which_context": The index of this context within the goal's 142 testing contexts. Along with the task ID and goal ID, this can be 143 used to build a unique identifier for the context. 144 TODO: Context IDs? 145 146 The typical slots of derived context dictionaries include: 147 148 - "filename": The file name (without path) of the file we're 149 evaluating. 150 - "file_path": The full absolute path to the file we're evaluating. 151 - "source": A string containing the source code of the submitted 152 module. 153 - "parse_errors": A list of Exception objects which were 154 generated but ignored when parsing the submitted code. 155 - "defs": A mapping from function names to AST nodes covering 156 every `def` node in the submitted code (but note that 157 functions with identical names will shadow each other in this 158 map). 159 - "scope": an AST node to check within (e.g., for a function 160 definition check). See ImplementationCheck for an example of 161 how these contexts are created and used. 162 - "docstrings": A mapping from function names to documentation 163 strings for those functions. 164 - "top_scope": The top-level AST node for the submitted code. 165 - "module": a loaded (submitted or solution) module 166 (for a suite of tests that relies on the code). 167 - "value": arbitrary Python value returned from a function call. 168 - "output": a string containing output from tested code. This may be 169 the full output, or may be filtered by additional Context objects 170 to contain just part of the full output. 171 - "trace": a trace list recording certain function calls made 172 during a test, along with arbitrary state snapshots (see 173 `potluck.harness.tracing_function_calls`). 174 - "image": a Pillow Image object holding a captured image. 175 - "audio": a dictionary with "mimetype", "data", and optionally 176 "label" slots, holding captured audio data in binary form. The 177 mimetype should indicate a MIME type for the data, while the 178 "data" slot holds a bytes object containing the raw data in that 179 format. The label slot holds a string used to label the audio in 180 output to users. 181 - "output_filename": the filename for an output file written to by 182 testing code. 183 - "output_file_contents": a string containing the contents of a file 184 that was written to by tested code. 185 - "expectations": a list of dictionaries defining expectations 186 established using the `optimism` module (only present if that 187 module is available) Each has the following keys: 188 - "tag" A string indicating the file name and line number where 189 this expectation was established 190 - "case" The test case info. This is a dictionary with "result", 191 "output", and "context" slots, holding the result value, 192 printed output, and context details for a test case (see 193 `optimism.get_my_context` for details of the "context" 194 value). 195 - "type" The type of expectation: "result", "output", or 196 "custom". 197 - "value" The expected value (or output fragment, or the checker 198 function, depending on the expectation type). 199 - Other keys not defined here may be established and/or used by 200 certain Context or Goal classes. 201 - Versions of the keys above with "ref_" as a prefix hold 202 equivalent values derived from solution instead of submitted 203 code. 204 205 Note: For various reasons, the context building process creates 206 shallow copies of contexts, not deep copies (the context values, such 207 as modules, are often not deep-copyable). Accordingly, it is possible 208 for changes to some sub-component of a context to be seen by other 209 places where that context is being used, and thus YOU SHOULD NEVER 210 MUTATE A CONTEXT VALUE. Context builder functions may safely 211 synthesize new values based on old ones, and may freely update 212 context keys with new values, but should not mutate the values under 213 those keys. 214 """ 215 def __init__( 216 self, 217 description=( 218 "UNSPECIFIED TOPIC", 219 "UNSPECIFIED DETAILS", 220 "UNSPECIFIED FULL TOPIC", 221 "UNSPECIFIED FULL DETAILS" 222 ), 223 builder=None, 224 display_product=generic_context_displayer, 225 depends=None, 226 failure_explanation=None, 227 cache_values=True, 228 base=None, 229 hidden=False, 230 generate_warnings=False 231 ): 232 """ 233 You must supply a description pair (topic + details), triple 234 (topic + details + feedback topic), or quad (topic + details + 235 feedback topic + feedback details). Each item must be an HTML 236 string to be displayed to the students as part of the rubric; the 237 feedback versions if provided are used instead of the originals 238 when generating a feedback document as opposed to a blank rubric. 239 240 You must also supply a builder function, to be run when this 241 context is required. If depends is supplied, it should be a list 242 of Context objects, and before this context is established, each 243 of those contexts will be created and merged in turn, to 244 establish the prev_context argument to the builder function. The 245 builder function must accept one argument (a context dictionary) 246 and must return a dictionary of any new or modified keys that it 247 wants to update in that context dictionary. 248 249 You may supply a failure_explanation, which can be either a 250 string, or a function that will be given the context in which the 251 failure occurred and an Exception object (with an html_tb 252 attached) and expected to return a string. This will be used to 253 set the error message for a ContextCreationError thrown when the 254 context_builder crashes, and that can ultimately become part of 255 an explanation for a failed goal. 256 257 If cache_values is given as False, the context_builder function 258 will be re-run every time this context is requested, but by 259 default, the result will be run only when one of our dependencies 260 has a newer timestamp than our cached value. 261 262 To supply seed values to bootstrap context creation, a Context 263 may have a base value, which is used to start the 264 dependency-merging process to provide a context for its builder. 265 266 If hidden is given as True, this Context will not show up in the 267 Contexts list, and will only be visible as a context associated 268 with a goal when that goal is evaluated (use with care). 269 270 If generate_warnings is set to True (default is False) then 271 issues with context creation of this context (but not with 272 creation of dependencies) will be logged as warnings in 273 `list_and_render_contexts` output. 274 """ 275 self.description = description 276 if builder is None: 277 raise ValueError( 278 "Must specify a context builder when creating a Context." 279 ) 280 self.builder = builder 281 self.display_product = display_product 282 self.depends = depends or [] 283 self.failure_explanation = failure_explanation 284 self.cache_values = cache_values 285 self.base = base or {} 286 self.hidden = hidden 287 self.generate_warnings = generate_warnings 288 289 self.cached_value = None 290 self.cache_timestamp = None 291 292 self.working_from = None 293 # which base context we're currently working from, for better 294 # exception reporting 295 296 def __str__(self): 297 """ 298 Uses the first description entry to hint which context this is. 299 """ 300 return 'Context "' + self.feedback_topic() + '"' 301 302 def __repr__(self): 303 """ 304 Weaves in the description. 305 """ 306 return f'<{type(self).__name__} "{self.feedback_topic()}">' 307 308 def __copy__(self): 309 """ 310 Contexts may not be copied. They are entangled with each other, 311 and we also don't want to create multiple copies which will 312 duplicate the same work. 313 """ 314 raise NotImplementedError("Contexts may not be copied.") 315 316 def __deepcopy__(self, memo): 317 """ 318 Contexts may not be copied (see __copy__). 319 """ 320 raise NotImplementedError("Contexts may not be copied.") 321 322 def changed_at(self): 323 """ 324 Returns the timestamp of the most recent update to our cached 325 context dictionary, so that contexts that depend on this one can 326 know if they need to update themselves. May return None if no 327 cached value has been created yet. 328 """ 329 return self.cache_timestamp 330 331 def clear_cache(self): 332 """ 333 Removes cached info & resets cache timestamp. 334 """ 335 self.cached_value = None 336 self.cache_timestamp = None 337 338 def burn_cache(self): 339 """ 340 Clears our cache, and burns caches of our dependencies. 341 """ 342 self.clear_cache() 343 for dep in self.depends: 344 dep.burn_cache() 345 346 def create(self, base_context): 347 """ 348 Creates and returns a context dictionary, using our builder 349 function. If caching is enabled, a (shallow copy of a) cached 350 value will be returned if we think that it was created after any 351 changes in our dependencies. 352 353 A base context is required, and should be a dictionary; normally 354 it should have all the `potluck.context_utils.BASE_CONTEXT_SLOTS` 355 already populated. 356 357 The resulting context will have a special "__builder__" slot 358 which contains a reference to this `Context` object. Of course, 359 this slot will get overwritten by each context in a chain, so it 360 only stores the last builder to touch the context (but see below). 361 362 The resulting context will have a special "__unmerged__" slot 363 which contains a list of context dictionaries for each dependency 364 of this context, holding the context state created by just that 365 dependency before merging with later dependencies. This can be 366 used to retrieve separate values for the same slot from multiple 367 dependencies, where only the last of those values would normally 368 be retained. Those context dictionaries will of course have 369 "__builder__" and "__unmerged__" slots, so the full chain of 370 `Context` objects responsible for a given context can be 371 recursively retrieved if necessary. 372 """ 373 rerun = False 374 if not self.cache_values: 375 rerun = True 376 else: 377 # Check whether we even have a result yet: 378 if self.cache_timestamp is None: 379 rerun = True 380 else: 381 # Check timestamps of our dependencies 382 for dep in self.depends: 383 when = dep.changed_at() 384 if when is None or when > self.cache_timestamp: 385 rerun = True 386 break 387 388 if not rerun: 389 # Return a shallow copy of our cached value 390 result = copy.copy(base_context) 391 result.update(self.cached_value) 392 return result 393 else: 394 # Create new cached value and update our timestamp 395 self.working_from = base_context 396 prev_context = copy.copy(base_context) 397 prev_context.update(self.base) 398 unmerged = [] 399 for dep in self.depends: 400 try: 401 dep_context = dep.create(base_context) 402 prev_context.update(dep_context) 403 unmerged.append(dep_context) 404 except Exception as e: 405 e.html_tb = html_tools.html_traceback( 406 linkable=context_utils.linkmap(prev_context) 407 ) 408 raise context_utils.ContextCreationError( 409 self, 410 "Dependency failed.", 411 e 412 ) 413 prev_context["__unmerged__"] = unmerged 414 prev_context["__builder__"] = self 415 416 try: 417 our_results = self.builder(prev_context) 418 prev_context.update(our_results) 419 self.cached_value = prev_context 420 except Exception as e: 421 e.html_tb = html_tools.html_traceback( 422 linkable=context_utils.linkmap(prev_context) 423 ) 424 if isinstance(self.failure_explanation, str): 425 msg = self.failure_explanation 426 elif self.failure_explanation: 427 msg = self.failure_explanation(prev_context, e) 428 else: 429 msg = "Test setup failed." 430 raise context_utils.ContextCreationError(self, msg, e) 431 self.cache_timestamp = time.time() 432 # Return shallow copy of cached value 433 result = {} 434 result.update(self.cached_value) 435 return result 436 437 def deps_are_a_stick(self): 438 """ 439 Returns true if the transitive dependencies of this context form 440 a stick, not a real tree (i.e., each context, including this one, 441 has exactly 1 or 0 dependencies). 442 """ 443 return ( 444 len(self.depends) in (0, 1) 445 and all(dep.deps_are_a_stick for dep in self.depends) 446 ) 447 448 def rubric_topic(self): 449 """ 450 Gets the rubric version of this Context's topic. 451 """ 452 return self.description[0] 453 454 def rubric_details(self): 455 """ 456 Gets the rubric version of this Context's details. 457 """ 458 return self.description[1] 459 460 def feedback_topic(self): 461 """ 462 Gets the feedback version of this Context's topic, or just the 463 normal topic if there is no feedback version. 464 """ 465 return self.description[::2][-1] 466 467 def feedback_details(self): 468 """ 469 Gets the feedback version of this Context's details, or just the 470 normal details if there is no feedback version. 471 """ 472 return self.description[1::2][-1] 473 474 def html_topic(self, in_feedback=False): 475 """ 476 Returns an HTML string representing just this context object as a 477 div.topic, without including information about dependencies. If 478 in_feedback is given as True, the feedback version of the topic 479 and details is shown instead of the normal (rubric) version. 480 481 Details are included behind a help button. 482 """ 483 if in_feedback: 484 topic = self.feedback_topic() 485 details = self.feedback_details() 486 else: 487 topic = self.rubric_topic() 488 details = self.rubric_details() 489 return '<div class="topic">{}</div>'.format( 490 topic + ' ' + html_tools.create_help_button(details) 491 ) 492 493 def html_context_tree(self, in_feedback=False): 494 """ 495 Produces an HTML string which shows this context and those that 496 it depends on in a tree structure, with dependencies nested 497 inside the contexts that depend on them. In the special case of a 498 stick, a different format is used. 499 500 If in_feedback is given as true, the feedback topic and details 501 values from the description are used if present, instead of the 502 normal (rubric) topic and details. 503 """ 504 if self.deps_are_a_stick(): 505 if self.depends: 506 dep_chain = ( 507 '(depends on <div class="context_depends">{}</div>)' 508 ).format( 509 self.depends[0].html_context_tree(in_feedback) 510 # there is only one 511 ) 512 return ( 513 '<details class="context">\n<summary>{}</summary>\n' 514 + '{}\n</details>' 515 ).format( 516 self.html_topic(in_feedback), 517 dep_chain 518 ) 519 else: 520 return '<div class="context">\n{}\n</div>'.format( 521 self.html_topic(in_feedback) 522 ) 523 else: 524 # Dependencies are a tree... 525 dep_entries = '<br>\n'.join( 526 dep.html_context_tree(in_feedback) 527 for dep in self.depends 528 ) 529 depends_full = ( 530 '<div class="context_depends">\n' 531 + 'Depends on:<br>\n' 532 + '{}\n' 533 + '</div>' 534 ).format(dep_entries) if self.depends else '' 535 return ( 536 '<details class="context">\n<summary>{}</summary>\n' 537 + '{}\n</details>' 538 ).format( 539 self.html_topic(in_feedback), 540 depends_full 541 ) 542 543 def html_representation(self, base_context): 544 """ 545 Builds an HTML representation of this context's result using the 546 display_product function. A base context is required because 547 create is used to fetch the current value (or build a new one). 548 549 If context creation fails, the result will be a string describing 550 the error. 551 """ 552 try: 553 return self.display_product(self.create(base_context)) 554 except context_utils.ContextCreationError as e: 555 tb = html_tools.html_traceback(e) 556 return f"""\ 557<div class="context_error"> 558 <h3>An error was encountered while attempting to run this test.</h3> 559{tb} 560</div> 561""" 562 563 def warnings(self, base_context): 564 """ 565 Returns a list of HTML strings representing warnings generated by 566 this context (excluding warnings generated by contexts this one 567 depends on). A base context is required because we need to 568 generate the context value to see if there are warnings, although 569 a cached value will be used in most cases. Returns an empty list 570 when no warnings have been generated. 571 """ 572 try: 573 ctx = self.create(base_context) 574 return ctx.get("warnings", []) 575 except context_utils.ContextCreationError as e: 576 if isinstance(e.cause, context_utils.ContextCreationError): 577 # Error comes from a dependency, not directly from this 578 # context, so report nothing to avoid duplicating 579 # warnings 580 return [] 581 else: 582 # Error comes from this context, so report it 583 tb = html_tools.html_traceback(e) 584 return [ f"Error during context creation:<br>{tb}" ] 585 586 587def add_context_numbering(all_context_objs): 588 """ 589 Takes a list of Context objects and looks for objects with duplicated 590 short descriptions, adding numerical suffixes to these. 591 """ 592 # Map topics to lists of contexts 593 by_topic = {} 594 by_feedback_topic = {} 595 for ctx in all_context_objs: 596 topic = ctx.description[0] 597 by_topic.setdefault(topic, []).append(ctx) 598 599 if len(ctx.description) > 2: 600 by_feedback_topic.setdefault(ctx.description[2], []).append(ctx) 601 602 # Assign numbers to topics that are duplicated 603 for topic in by_topic: 604 contexts = by_topic[topic] 605 if len(contexts) > 1: 606 # these duplicates need numbering 607 for i, ctx in enumerate(contexts): 608 ctx.description = ( 609 ctx.description[0] + f" #{i+1}", 610 ) + ctx.description[1:] 611 612 # Repeat for feedback topics (numbering will hopefully be consistent 613 # because of consistent iteration order over all context objects, but 614 # if it's not, too bad. 615 for topic in by_feedback_topic: 616 contexts = by_feedback_topic[topic] 617 if len(contexts) > 1: 618 # these duplicates need numbering 619 for i, ctx in enumerate(contexts): 620 ctx.description = ctx.description[:2] + ( 621 ctx.description[2] + f" #{i+1}", 622 ) + ctx.description[3:] 623 624 625def build_context_graph(all_context_objs): 626 """ 627 Takes a list of Context objects and traces dependencies to build a 628 top-down rather than bottom-up graph. The graph consists of a list of 629 top-level node dictionaries with the following keys: 630 631 - context: The Context object for this node. 632 - children: A list containing pointers to the node dictionaries of 633 each Context that depends on this one. 634 - parents: A list containing pointers to the node dictionaries of 635 each Context that this one depends on. 636 - level: An integer indicating the longest chain of ancestors that 637 this node has (0 for nodes without dependencies). 638 639 The exact same node dictionary may appear as a child and/or parent of 640 multiple other nodes. 641 642 The ordering of children will be the same as the ordering of context 643 dependencies, while the ordering of the top-level nodes will match 644 the order that they occur within the given all_context_objs list. 645 """ 646 647 dict_cache = {} 648 649 def dict_for(cobj): 650 """ 651 Returns the dictionary for the given Context object, either by 652 creating it or remembering one already constructed during the 653 current construction process. For new encounters, the children 654 value will be an empty list. 655 """ 656 nonlocal dict_cache 657 if id(cobj) in dict_cache: 658 return dict_cache[id(cobj)] 659 else: 660 result = { "context": cobj, "children": [], "parents": [] } 661 dict_cache[id(cobj)] = result 662 return result 663 664 nodeps = [] 665 for ctx in all_context_objs: 666 d = dict_for(ctx) 667 if ctx.depends: 668 for dep in ctx.depends: 669 pd = dict_for(dep) 670 pd["children"].append(d) 671 d["parents"].append(pd) 672 else: 673 nodeps.append(d) 674 675 # Loop again and assign "level" values now that the graph is complete 676 for ctx in all_context_objs: 677 d = dict_for(ctx) 678 assign_cgraph_level(d) 679 680 return nodeps 681 682 683def assign_cgraph_level(node): 684 """ 685 Determines the level of a node in a contexts graph, which is 0 for 686 nodes without parents, and one plus the highest parent level for all 687 other nodes. 688 689 Store this value in the "level" slot of the node, and does the same 690 for any ancestor nodes encountered. 691 692 Returns an already-computed level if there is one, or else returns 693 the level value that it assigns to the node. 694 """ 695 if "level" in node: 696 return node["level"] 697 else: 698 if len(node["parents"]) == 0: 699 node["level"] = 0 700 else: 701 mp = 0 702 for pn in node["parents"]: 703 pl = assign_cgraph_level(pn) 704 if pl > mp: 705 mp = pl 706 node["level"] = mp + 1 707 708 return node["level"] 709 710 711def render_context_graph(cgraph, in_feedback=False): 712 """ 713 Renders a context graph (see `build_context_graph`) into HTML. Uses a 714 simple algorithm which includes duplicate entries for nodes with 715 multiple dependencies. 716 717 The in_feedback argument controls whether context items should show 718 their full or redacted versions. 719 """ 720 result = '<ol class="context_list">' 721 for cnode in cgraph: 722 result += '\n<li>{}</li>'.format( 723 html_tools.build_html_details( 724 cnode["context"].html_topic(in_feedback), 725 render_context_graph(cnode["children"], in_feedback) 726 ) 727 if cnode.get("children") 728 else cnode["context"].html_topic(in_feedback) 729 ) 730 731 result += '\n</ol>' # close .context_list ol 732 return result 733 734 735def list_and_render_contexts(cgraph, base_context=None): 736 """ 737 Transforms a context graph (see `build_context_graph`) into a list of 738 context summaries, which are dictionaries with the following keys: 739 740 - description: An HTML code string that describes the context item, 741 including a detailed description behind a help button. 742 - depends: A list containing integer indices into the entire context 743 list of the other contexts on which this context depends. Will be 744 empty for contexts without dependencies. 745 - level: An integer specifying how far this context should be 746 indented, which will be one greater than the maximum level of 747 contexts that this one depends on, starting with 0 for contexts 748 that have no dependencies. 749 - value: An HTML code string that displays or summarizes the value 750 produced by this context. This will be the result of the 751 context's display_product function run on the most recent result 752 of that context (or on a fresh result if caching is disabled for 753 the context). There may be a description of a context creation 754 error here if there is an error building the context value. 755 - warnings: A list of strings indicating context-based rather than 756 test-based warnings. In general contexts should only create 757 warnings in very specific circumstances, since tests are 758 responsible for most warnings, and might want to ignore issues 759 that a context could warn about. 760 761 The base_context argument is used to generate context results (will 762 only happen if cached results are not available or caching is 763 disabled for one or more contexts). If omitted or specified 764 explicitly as None, the result will have empty strings in each 765 "value" slot, and the descriptions will use redacted topic and 766 detail values; use this when generating a list for a rubric rather 767 than for a feedback document. 768 769 The contexts in the list will be ordered such that every context 770 comes later in the list than all contexts it depends on. 771 """ 772 result = [] 773 774 # Produce a flat list of nodes in an order that respects 775 # dependencies. 776 nodeslist = [] 777 778 # Processing queue and processed set 779 queue = cgraph[:] # place all top-level nodes in queue to start 780 processed = set() 781 782 while len(queue) > 0: 783 this = queue.pop(0) 784 785 depends = this["parents"] 786 if any(id(dep) not in processed for dep in depends): 787 continue 788 # we'll come back to this node later when its next 789 # unprocessed dependency queues it up 790 else: # no unprocessed dependencies 791 processed.add(id(this)) 792 nodeslist.append(this) 793 for child in reversed(this["children"]): 794 # Insert children at the front of the queue, doing so in 795 # reverse order so they end up in their natural order at 796 # the front 797 if id(child) not in processed: # should always be true? 798 queue.insert(0, child) 799 else: 800 raise NotImplementedError( 801 f"Unexpectedly encountered pre-processed child." 802 f"\n Parent is '{this['context'].html_topic()}'" 803 f"\n Child is '{child['context'].html_topic()}'" 804 ) 805 806 # A mapping from node IDs to their indices in the nodeslist 807 indexmap = { 808 id(cnode): idx 809 for idx, cnode in enumerate(nodeslist) 810 } 811 812 # Iterate over all nodes in order 813 for cnode in nodeslist: 814 ctx = cnode["context"] 815 result.append( 816 { 817 "description": ctx.html_topic( 818 base_context is not None 819 ), 820 "depends": [ 821 indexmap[id(child)] 822 for child in cnode["children"] 823 ], 824 "level": cnode["level"], 825 "value": ( 826 ctx.html_representation(base_context) 827 if base_context is not None 828 else "" 829 ), 830 "warnings": ( 831 ctx.warnings(base_context) 832 if base_context is not None and ctx.generate_warnings 833 else [] 834 ) 835 } 836 ) 837 838 return result 839 840 841#-----------------------------# 842# Automatic Context Mechanism # 843#-----------------------------# 844 845class AutoContext(Context): 846 """ 847 A `AutoContext` provides a way to automatically create dependencies 848 on common contexts without saving them in variables that have to get 849 passed around everywhere. When a `AutoContext` is created, it 850 registers itself as the current provider for certain context slots, 851 overwriting any previously-registered provider for those slots. 852 Then, another context may use the `auto` function to create a list 853 of `Context` objects to use as dependencies based on the slots it 854 needs; this list will be populated with the set of 855 most-recently-registered `AutoContext` objects for each slot 856 requested. 857 858 In addition to inheriting from `AutoContext`, `Context`s which 859 should be automatic must call their own `register` method and 860 provide it with one or more slot name strings as arguments. 861 """ 862 _registry = {} 863 _on_demand = {} 864 865 def reset(relevant_filename=None, relevant_tests_filename=None): 866 """ 867 This is a class method which resets the automatic contexts 868 registry, erasing all registered `Context` objects. Used prior 869 to loading a new task spec. 870 871 Note that this does not reset the on-demand factory functions 872 registry. 873 874 A `relevant_filename` may be provided to set the global 875 RELEVANT_FILENAME variable; setting this to the default filename 876 for the task we're about to load is helpful since it prevents the 877 spec from treating things created after an explicit-default file 878 context as different from things created before any file contexts 879 have been declared. If no `relevant_filename` is provided, the 880 global will be set back to its default of None. 881 882 A `relevant_tests_filename` may be provided and follows the same 883 logic as `relevant_filename`. 884 """ 885 global RELEVANT_FILENAME, RELEVANT_TESTS_FILENAME 886 RELEVANT_FILENAME = relevant_filename 887 RELEVANT_TESTS_FILENAME = relevant_tests_filename 888 AutoContext._registry = {} 889 890 def on_demand(factory, *slots): 891 """ 892 This class method registers a given factory function as the 893 on-demand provider of one or more slots. 894 895 The factory function must be able to run with no arguments and 896 must produce a `Context` object which can create the requested 897 slot(s). 898 899 Essentially, if `auto` is used to request an automatic context 900 for a slot, but no such context has been registered, an 901 on-demand factory function may be called to construct such a 902 context automatically in some simple cases. Calling this 903 function a second time re-using an old slot value will overwrite 904 the factory function for that slot. 905 """ 906 for slot in slots: 907 AutoContext._on_demand[slot] = factory 908 909 def register(self, *slots): 910 """ 911 Registers this auto context as the current provider for one or 912 more slots (named using strings). Subsequent calls to `auto` 913 that include one or more of those slots will include this object 914 in the resulting list. 915 916 The slots for which a context registers itself are stored in the 917 context's `_provides` attribute. 918 """ 919 for slot in slots: 920 AutoContext._registry[slot] = self 921 922 self._provides = slots 923 924 def refresh(self): 925 """ 926 Any `AutoContext` may call this method to re-register itself as 927 the current provider of all of its relevant slots. The slots to 928 register under are remembered from the initial call to 929 `register`. 930 """ 931 self.register(*self._provides) 932 933 934def auto(*slots): 935 """ 936 This function returns a list of `Context` objects, suitable for use 937 as all or part of a `depends` value for the creation of a new 938 `Context`, and/or as all or part of a `test_in` value for the 939 creation of a new `potluck.rubrics.Goal`. It does this by looking for 940 the most recent `AutoContext` objects that have registered themselves 941 as providers of the given slot or slots (these are just strings). 942 943 If no `AutoContext` has registered itself for a given slot, but 944 there is an on-demand factory registered for that slot, the factory 945 function will be called to generate a `Context` to use as a 946 dependency (which should also end up registering the resulting 947 `Context` under the appropriate slot(s)). 948 949 If multiple slots are given and one `Context` is registered under 950 several of them, that `Context` will only appear in the resulting 951 list once. Likewise, if an on-demand factory function creates a 952 `Context` which registers itself for several slots, and one or more 953 other slots in that list are also being requested, the factory 954 function will not be re-run for the subsequent slots, and the 955 resulting `Context` will only appear in the result list once. 956 957 If no slots are given, this function returns an empty list. 958 959 If a slot is requested for which there is no currently-registered 960 `AutoContext` and for which there is no registered on-demand 961 `Context` factory, an `ContextError` will be raised. 962 """ 963 result = [] 964 for slot in slots: 965 if slot in AutoContext._registry: 966 match = AutoContext._registry[slot] 967 if match not in result: 968 result.append(match) 969 elif slot in AutoContext._on_demand: 970 created = AutoContext._on_demand[slot]() 971 if created not in result: 972 result.append(created) 973 else: 974 raise context_utils.ContextError( 975 f"Automatic context request for slot '{slot}' could not" 976 f" be fulfilled because no AutoContext was registered" 977 f" for that slot, and no on-demand Context factory" 978 f" function was available for that slot either." 979 ) 980 981 return result 982 983 984#-------------------------# 985# Derived Context classes # 986#-------------------------# 987 988 989class FileContext(AutoContext): 990 """ 991 Establishes a 'filename' context slot that holds the name of a 992 specific file being evaluated. By default, the created `Context` has 993 no dependencies and is hidden. 994 995 The filename provided should be relative to the submission root, 996 which is either the directory where the target file exists, or the 997 target directory for multi-file submissions. If no target file is 998 identified, the default_file context slot value is used to target 999 the submission's default file. 1000 1001 Establishes 'ref_*' slots for each normal slot it establishes. 1002 """ 1003 def __init__(self, target_file=None, depends=None, hidden=True): 1004 """ 1005 A target_file string specifies the path to the target file that 1006 this context will focus evaluation on, relative to the 1007 submission root. If not provided, the "default_file" context 1008 slot value will be used. 1009 1010 Both 'filename' and 'file_path' context slots will be 1011 established by this builder; the former holds just the file (or 1012 directory) name of the target file, while the latter holds a 1013 full path to the file. 1014 1015 It's up to other `Context`s to make use of the slots established 1016 by this one. 1017 """ 1018 global RELEVANT_FILENAME 1019 # Set the relevant filename global, or fetch it 1020 if target_file is not None: 1021 RELEVANT_FILENAME = target_file 1022 elif RELEVANT_FILENAME is not None: 1023 target_file = RELEVANT_FILENAME 1024 1025 self.target_file = target_file 1026 1027 # First, create our context-builder function for this instance 1028 def establish_context(prev_context): 1029 """ 1030 Establishes 'filename' and 'file_path' slots based on the 1031 target_file value given to the `FileContext` that this is 1032 the context builder function for. Also establishes 1033 'ref_filename' and 'ref_file_path' slots pointing to the 1034 equivalent file in the solution code. 1035 """ 1036 task_info = context_utils.extract(prev_context, "task_info") 1037 soln_root = task_info["specification"].soln_path 1038 submission_root = context_utils.extract( 1039 prev_context, 1040 "submission_root" 1041 ) 1042 # Revise our description when we build our context if we're 1043 # forced to fetch the default file: 1044 file_target = self.target_file 1045 default_filename = context_utils.extract( 1046 prev_context, 1047 "default_file" 1048 ) 1049 if file_target is None: 1050 file_target = default_filename 1051 self.description = ( 1052 f"File '{file_target}'", 1053 f"We will evaluate your submitted file '{file_target}'.", 1054 ) 1055 if file_target == default_filename: 1056 actual_filename = context_utils.extract( 1057 prev_context, 1058 "actual_file" 1059 ) 1060 full_target = os.path.join( 1061 submission_root, 1062 actual_filename 1063 ) 1064 # Even if we're grading a custom-named file, the solution 1065 # won't have a custom name... 1066 ref_target = os.path.abspath( 1067 os.path.join(soln_root, default_filename) 1068 ) 1069 target_filename = default_filename 1070 else: 1071 # TODO: How can we properly handle ref targets for 1072 # custom-named files here? 1073 full_target = os.path.join(submission_root, file_target) 1074 ref_target = os.path.abspath( 1075 os.path.join(soln_root, file_target) 1076 ) 1077 _, target_filename = os.path.split(full_target) 1078 1079 if not os.path.exists(submission_root): 1080 # If the submission root directory is missing... 1081 raise context_utils.ContextCreationError( 1082 self, 1083 ( 1084 f"Submission root directory" 1085 f" '{submission_root}' does not exist." 1086 ) 1087 ) 1088 elif not os.path.exists(full_target): 1089 # If the target file is missing 1090 raise context_utils.ContextCreationError( 1091 self, 1092 ( 1093 f"Target submission file" 1094 f" '{full_target}' does not exist." 1095 ) 1096 ) 1097 elif not os.path.exists(ref_target): 1098 # If there is no equivalent solution file 1099 raise context_utils.ContextCreationError( 1100 self, 1101 f"No solution file '{ref_target}' is available." 1102 ) 1103 # else both submission root and full target exist 1104 1105 return { 1106 "filename": target_filename, 1107 "file_path": os.path.abspath(full_target), 1108 "ref_filename": target_filename, 1109 "ref_file_path": os.path.abspath(ref_target) 1110 } 1111 1112 # Now we can call the super-class init with the context builder 1113 # function we just created. 1114 super().__init__( 1115 description=( 1116 f"File '{target_file}'", 1117 f"We will evaluate your submitted file '{target_file}'." 1118 ), 1119 builder=establish_context, 1120 display_product=lambda context: ( 1121 f"Evaluating '{context['filename']}'" 1122 ), 1123 depends=depends, 1124 hidden=hidden 1125 ) 1126 1127 # Finally, we can register ourselves as an auto-context for the 1128 # "filename" and "file_path" slots. 1129 self.register( 1130 "filename", 1131 "file_path", 1132 "ref_filename", 1133 "ref_file_path" 1134 ) 1135 1136 1137# Register a factory for a default FileContext as an on-demand option 1138# for "filename", "file_path", and the associated ref_ slots: 1139AutoContext.on_demand( 1140 (lambda: FileContext()), 1141 "filename", "file_path", "ref_filename", "ref_file_path" 1142) 1143 1144 1145class TestsFileContext(AutoContext): 1146 """ 1147 Establishes a 'tests_filename' context slot that holds the name of a 1148 specific tests file being validated. By default, the created 1149 `Context` has no dependencies and is hidden. 1150 1151 The filename provided should be relative to the submission root, 1152 which is either the directory where the target tests file exists, or 1153 the target directory for multi-file submissions. If no target file is 1154 identified, the "default_tests_file" context slot value is used to 1155 target the submission's default tests file. 1156 1157 Establishes 'ref_*' slots for each normal slot it establishes. 1158 """ 1159 def __init__(self, target_tests_file=None, depends=None, hidden=True): 1160 """ 1161 A target_tests_file string specifies the path to the target tests 1162 file that this context will focus validation on, relative to the 1163 submission root. If not provided, the "default_tests_file" 1164 context slot value will be used. 1165 1166 Both 'tests_filename' and 'tests_file_path' context slots will be 1167 established by this builder; the former holds just the file (or 1168 directory) name of the target tests file, while the latter holds 1169 a full path to the file. 1170 1171 It's up to other `Context`s to make use of the slots established 1172 by this one. 1173 """ 1174 global RELEVANT_TESTS_FILENAME 1175 # Set the relevant filename global, or fetch it 1176 if target_tests_file is not None: 1177 RELEVANT_TESTS_FILENAME = target_tests_file 1178 elif RELEVANT_TESTS_FILENAME is not None: 1179 target_tests_file = RELEVANT_TESTS_FILENAME 1180 1181 self.target_tests_file = target_tests_file 1182 1183 # First, create our context-builder function for this instance 1184 def establish_context(prev_context): 1185 """ 1186 Establishes 'tests_filename' and 'tests_file_path' slots 1187 based on the target_tests_file value given to the 1188 `FileContext` that this is the context builder function for. 1189 Also establishes 'ref_tests_filename' and 1190 'ref_tests_file_path' slots pointing to the equivalent file 1191 in the solution code. 1192 """ 1193 task_info = context_utils.extract(prev_context, "task_info") 1194 soln_root = task_info["specification"].soln_path 1195 submission_root = context_utils.extract( 1196 prev_context, 1197 "tests_submission_root" 1198 ) 1199 # Revise our description when we build our context if we're 1200 # forced to fetch the default file: 1201 file_target = self.target_tests_file 1202 default_filename = context_utils.extract( 1203 prev_context, 1204 "default_tests_file" 1205 ) 1206 if file_target is None: 1207 file_target = default_filename 1208 self.description = ( 1209 f"Tests file '{file_target}'", 1210 ( 1211 f"We will validate your submitted tests file" 1212 f"'{file_target}'." 1213 ) 1214 ) 1215 if file_target == default_filename: 1216 actual_filename = context_utils.extract( 1217 prev_context, 1218 "actual_tests_file" 1219 ) 1220 full_target = os.path.join( 1221 submission_root, 1222 actual_filename 1223 ) 1224 # Even if we're grading a custom-named file, the solution 1225 # won't have a custom name... 1226 ref_target = os.path.abspath( 1227 os.path.join(soln_root, default_filename) 1228 ) 1229 target_filename = default_filename 1230 else: 1231 # TODO: How can we properly handle ref targets for 1232 # custom-named files here? 1233 full_target = os.path.join(submission_root, file_target) 1234 ref_target = os.path.abspath( 1235 os.path.join(soln_root, file_target) 1236 ) 1237 _, target_filename = os.path.split(full_target) 1238 1239 if not os.path.exists(submission_root): 1240 # If the submission root directory is missing... 1241 raise context_utils.ContextCreationError( 1242 self, 1243 ( 1244 f"Tests submission root directory" 1245 f" '{submission_root}' does not exist." 1246 ) 1247 ) 1248 elif not os.path.exists(full_target): 1249 # If the target file is missing 1250 raise context_utils.ContextCreationError( 1251 self, 1252 ( 1253 f"Target tests submission file" 1254 f" '{full_target}' does not exist." 1255 ) 1256 ) 1257 elif not os.path.exists(ref_target): 1258 # If there is no equivalent solution file 1259 raise context_utils.ContextCreationError( 1260 self, 1261 ( 1262 f"No solution tests file '{ref_target}' is" 1263 f" available." 1264 ) 1265 ) 1266 # else both submission root and full target exist 1267 1268 return { 1269 "tests_filename": target_filename, 1270 "tests_file_path": os.path.abspath(full_target), 1271 "ref_tests_filename": target_filename, 1272 "ref_tests_file_path": os.path.abspath(ref_target) 1273 } 1274 1275 # Now we can call the super-class init with the context builder 1276 # function we just created. 1277 super().__init__( 1278 description=( 1279 f"Tests file '{target_tests_file}'", 1280 ( 1281 f"We will validate your submitted tests file" 1282 f" '{target_tests_file}'." 1283 ) 1284 ), 1285 builder=establish_context, 1286 display_product=lambda context: ( 1287 f"Validating '{context['tests_filename']}'" 1288 ), 1289 depends=depends, 1290 hidden=hidden 1291 ) 1292 1293 # Finally, we can register ourselves as an auto-context for the 1294 # "filename" and "file_path" slots. 1295 self.register( 1296 "tests_filename", 1297 "tests_file_path", 1298 "ref_tests_filename", 1299 "ref_tests_file_path" 1300 ) 1301 1302 1303# Register a factory for a default TestsFileContext as an on-demand 1304# option for "tests_filename", "tests_file_path", and the associated ref_ 1305# slots: 1306AutoContext.on_demand( 1307 (lambda: TestsFileContext()), 1308 "tests_filename", "tests_file_path", "ref_tests_filename", 1309 "ref_tests_file_path" 1310) 1311 1312 1313class SandboxContext(AutoContext): 1314 """ 1315 Establishes two sandbox directories to be used for running all code 1316 being tested, including the initial loading of the module itself. One 1317 directory is for the submitted code and a second is for the solution 1318 code. 1319 """ 1320 def __init__(self, depends=None, hidden=True): 1321 """ 1322 Creates a context which establishes two new unique sandbox 1323 directories. The context creation function places the full paths 1324 to those directories in the "sandbox" and "ref_sandbox" context 1325 slots. A list of dependencies may be provided, and hidden can be 1326 set to False if desired. 1327 """ 1328 self.dir = None 1329 self.ref_dir = None 1330 # TODO: Clean up these temporary directories rather than letting 1331 # Python do that on shutdown... 1332 1333 def establish_context(prev_context): 1334 """ 1335 Creates a new temporary directory and puts its absolute path 1336 in the "sandbox" context slot. Creates a second temporary 1337 directory and puts its path in the "ref_sandbox" slot. 1338 Copies helper files into both directories, and copies the 1339 actual solution file(s) into the reference sandbox. 1340 """ 1341 sub_root = context_utils.extract(prev_context, "submission_root") 1342 sub_file = context_utils.extract(prev_context, "actual_file") 1343 tinfo = context_utils.extract(prev_context, "task_info") 1344 spec = tinfo["specification"] 1345 1346 self.dir = tempfile.TemporaryDirectory( 1347 suffix="__tmp", 1348 dir=load.SANDBOX_DIR 1349 ) 1350 self.ref_dir = tempfile.TemporaryDirectory( 1351 suffix="__ref_tmp", 1352 dir=load.SANDBOX_DIR 1353 ) 1354 1355 # Set up the sandboxes 1356 for sb in [self.dir, self.ref_dir]: 1357 # Copy helper files into the sandbox if there are any 1358 # Note that we don't use symlinks here, because we don't 1359 # want destructive student code to modify files outside 1360 # the sandbox... 1361 # TODO: Use symlinks in places where we feel safe, 1362 # especially for large starter files!!! 1363 helpers = context_utils.sandbox_filemap(spec) 1364 if helpers is not None: 1365 for filepath in helpers: 1366 to = os.path.join(sb.name, helpers[filepath]) 1367 if os.path.isdir(filepath): 1368 shutil.copytree(filepath, to) 1369 else: 1370 shutil.copy(filepath, to) 1371 1372 # Copy the submitted target file/directory into the sandbox 1373 subm_target = os.path.join(sub_root, sub_file) 1374 sandbox_target = os.path.join(self.dir.name, tinfo["target"]) 1375 if os.path.isdir(subm_target): 1376 shutil.copytree(subm_target, sandbox_target) 1377 else: 1378 shutil.copy(subm_target, sandbox_target) 1379 1380 # Copy the target file/directory from the solution dir into 1381 # the ref sandbox 1382 soln_target = os.path.join(spec.soln_path, tinfo["target"]) 1383 sandbox_target = os.path.join(self.ref_dir.name, tinfo["target"]) 1384 if os.path.isdir(soln_target): 1385 shutil.copytree(soln_target, sandbox_target) 1386 else: 1387 shutil.copy(soln_target, sandbox_target) 1388 1389 return { 1390 "sandbox": os.path.abspath(self.dir.name), 1391 "ref_sandbox": os.path.abspath(self.ref_dir.name) 1392 } 1393 1394 # Now we can call the super-class init with the context builder 1395 # function we just created. 1396 super().__init__( 1397 description=( 1398 "Sandbox directories", 1399 ( 1400 "We will create sandbox directories for running" 1401 " your submitted code and the solution code." 1402 ) 1403 ), 1404 builder=establish_context, 1405 display_product=lambda context: ( 1406 "Running in a sandbox" 1407 ), 1408 depends=depends, 1409 hidden=hidden 1410 ) 1411 1412 # Finally, we can register ourselves as an auto-context for the 1413 # "filename" and "file_path" slots. 1414 self.register( 1415 "sandbox", 1416 "ref_sandbox" 1417 ) 1418 1419 1420# Register a factory for a default SandboxContext as an on-demand option 1421# for the "sandbox" and "ref_sandbox" slots. 1422AutoContext.on_demand( 1423 (lambda: SandboxContext()), 1424 "sandbox", "ref_sandbox" 1425) 1426 1427 1428class TestsSandboxContext(AutoContext): 1429 """ 1430 Establishes a sandbox directory for validating tests. The process is 1431 largely the same as that used by SandboxContext, but a separate 1432 directory is used to prevent any possible interference between test 1433 validation and submission evaluation. 1434 """ 1435 def __init__(self, depends=None, hidden=True): 1436 """ 1437 Creates a context which establishes a new sandbox directory. The 1438 context creation function places the full path to this 1439 directory in the "tests_sandbox" context slot. A list of 1440 dependencies may be provided, and hidden can be set to False if 1441 desired. 1442 """ 1443 self.dir = None 1444 # TODO: Clean up this temporary directory rather than letting 1445 # Python do that on shutdown... 1446 1447 def establish_context(prev_context): 1448 """ 1449 Creates a new temporary directory and puts its absolute path 1450 in the "tests_sandbox" context slot. Copies helper files and 1451 the solution default target into this sandbox. 1452 """ 1453 tinfo = context_utils.extract(prev_context, "task_info") 1454 spec = tinfo["specification"] 1455 1456 self.dir = tempfile.TemporaryDirectory( 1457 suffix="__validation_tmp", 1458 dir=load.SANDBOX_DIR 1459 ) 1460 1461 # Copy helper files into the sandbox if there are any 1462 # Note that we don't use symlinks here, because we don't 1463 # want destructive student code to modify files outside 1464 # the sandbox... 1465 # TODO: Use symlinks in places where we feel safe, 1466 # especially for large starter files!!! 1467 helpers = context_utils.sandbox_filemap(spec) 1468 if helpers is not None: 1469 for filepath in helpers: 1470 to = os.path.join(self.dir.name, helpers[filepath]) 1471 if os.path.isdir(filepath): 1472 shutil.copytree(filepath, to) 1473 else: 1474 shutil.copy(filepath, to) 1475 1476 # Copy the target file/directory from the solution dir 1477 soln_target = os.path.join(spec.soln_path, tinfo["target"]) 1478 sandbox_target = os.path.join(self.dir.name, tinfo["target"]) 1479 if os.path.isdir(soln_target): 1480 shutil.copytree(soln_target, sandbox_target) 1481 else: 1482 shutil.copy(soln_target, sandbox_target) 1483 1484 return { "tests_sandbox": os.path.abspath(self.dir.name) } 1485 1486 # Now we can call the super-class init with the context builder 1487 # function we just created. 1488 super().__init__( 1489 description=( 1490 "Test validation sandbox", 1491 ( 1492 "We will create a sandbox directory for validating" 1493 " your submitted tests." 1494 ) 1495 ), 1496 builder=establish_context, 1497 display_product=lambda context: ( 1498 "Validating tests in a sandbox" 1499 ), 1500 depends=depends, 1501 hidden=hidden 1502 ) 1503 1504 # Finally, we can register ourselves as an auto-context for the 1505 # "filename" and "file_path" slots. 1506 self.register("tests_sandbox") 1507 1508 1509# Register a factory for a default TestsSandboxContext as an on-demand 1510# option for the "tests_sandbox" slot: 1511AutoContext.on_demand((lambda: TestsSandboxContext()), "tests_sandbox") 1512 1513 1514class CodeContext(AutoContext): 1515 """ 1516 Requires "filename" and "file_path" slots (see `FileContext`), and 1517 establishes a "source" slot which contains the raw text of the 1518 target file, along with a "scope" slot which contains the parsed AST 1519 from the code. 1520 1521 If the code cannot be parsed due to a `SyntaxError` or the like, a 1522 `ContextCreationError` will be generated naming the parsing error as 1523 its cause, although note that `potluck.load.fix_parse` is used which 1524 will attempt to steamroll some kinds of parsing errors while 1525 generating associated warnings. 1526 """ 1527 def __init__(self, depends=None, hidden=False, prep=None): 1528 """ 1529 Dependencies are optional; if not specified `auto` will be used 1530 to fill them in. `hidden` may be provided; by default this 1531 context is not hidden. A `prep` function may be provided; it will 1532 be applied to the source code string and its result will be used 1533 instead of the original source. 1534 """ 1535 # First, create our context builder 1536 def establish_context(prev_context): 1537 """ 1538 Establishes the following context slots based on the 1539 "file_path" slot, by reading the indicated file: 1540 1541 - original_source: The raw file contents. 1542 - source: Possibly-edited (to steamroll syntax errors or by a 1543 prep function) file contents. 1544 - scope: An AST module node resulting from parsing the 1545 modified file contents. 1546 - top_scope: Same as above (but not designed to be modified). 1547 - parse_errors: A list of Exception objects that were 1548 'successfully' steamrolled by editing the source code. 1549 """ 1550 filename = context_utils.extract(prev_context, "filename") 1551 target = context_utils.extract(prev_context, "file_path") 1552 with open(target, 'r', encoding="utf-8") as fin: 1553 original_source = fin.read() 1554 1555 if prep: 1556 source = prep(original_source) 1557 else: 1558 source = original_source 1559 1560 try: 1561 fixed, node, errors = load.fix_parse(source, filename) 1562 except Exception as e: 1563 raise context_utils.ContextCreationError( 1564 self, 1565 f"Unable to parse submitted file '{filename}'.", 1566 cause=e 1567 ) 1568 1569 if node is None: 1570 raise context_utils.ContextCreationError( 1571 self, 1572 f"Unable to parse submitted file '{filename}'.", 1573 cause=errors[0] 1574 ) 1575 1576 result = { 1577 "original_source": original_source, 1578 "source": fixed, 1579 "scope": node, 1580 "top_scope": node, 1581 "parse_errors": errors 1582 } 1583 1584 # Report parsing issues as warnings 1585 if errors: 1586 result["warnings"] = [ 1587 ( 1588 "The following errors were encountered when parsing" 1589 + " your code:<br>" 1590 + html_tools.build_list( 1591 html_tools.html_traceback(e) 1592 for e in errors 1593 ) 1594 ) 1595 ] 1596 1597 return result 1598 1599 # Figure out if we need to use automatic dependencies: 1600 if depends is None: 1601 depends = auto("filename", "file_path") 1602 1603 # Now we can call the super constructor 1604 super().__init__( 1605 description=( 1606 "Code in the target file", 1607 ( 1608 "We will parse the code in the target file and pay" 1609 "attention to how it was written." 1610 ), 1611 "Code in the target file", 1612 ( 1613 "We parsed the code in the target file and paid" 1614 "attention to how it was written." 1615 ), 1616 ), 1617 builder=establish_context, 1618 display_product=lambda context: ( 1619 f"The code for '{context['filename']}' (shown elsewhere)." 1620 ), 1621 depends=depends, 1622 hidden=hidden, 1623 # Errors at this level need to be reported! 1624 generate_warnings=True 1625 ) 1626 1627 # Finally, register ourselves as an auto provider for the slots 1628 # that we generate: 1629 self.register( 1630 "original_source", 1631 "source", 1632 "scope", 1633 "top_scope", 1634 "parse_errors" 1635 ) 1636 1637 1638# Register a factory for a default CodeContext as an on-demand option 1639# for the slots it can generate. 1640AutoContext.on_demand( 1641 (lambda: CodeContext()), 1642 "original_source", 1643 "source", 1644 "scope", 1645 "top_scope", 1646 "parse_errors" 1647) 1648 1649 1650class SolnCodeContext(AutoContext): 1651 """ 1652 Requires "ref_filename" and "ref_file_path" slots (see 1653 `FileContext`), and establishes a "ref_source" slot which contains 1654 the raw text of the equivalent file from the solution code, along 1655 with a "ref_scope" slot which contains the parsed AST from the 1656 solution code. Also establishes "ref_original_source" which may be 1657 different from "ref_source" when a prep function is used. 1658 1659 "task_info" and "submission_root" slots are also required, but those 1660 should always be present. 1661 1662 If the solution code cannot be parsed due to a `SyntaxError` or the 1663 like or because no equivalent solution file exists, a 1664 `ContextCreationError` will be generated naming the relevant error as 1665 its cause. 1666 """ 1667 def __init__(self, depends=None, hidden=False, prep=None): 1668 """ 1669 Dependencies are optional; if not specified `auto` will be used 1670 to fill them in. `hidden` may be provided; by default this 1671 context is not hidden. A `prep` function may be supplied, which 1672 will be given the source code and its return value will be used 1673 in place of the original source code. 1674 """ 1675 # First, create our context builder 1676 def establish_context(prev_context): 1677 """ 1678 Establishes the following context slots based on the 1679 "ref_file_path" slot, by reading the solution version of the 1680 indicated file: 1681 1682 ref_source: The source of the solution file. 1683 ref_scope: An AST module node resulting from parsing the 1684 solution file. 1685 ref_top_scope: As above, but won't be modified. 1686 """ 1687 soln_equivalent = context_utils.extract( 1688 prev_context, 1689 "ref_file_path" 1690 ) 1691 ref_filename = context_utils.extract( 1692 prev_context, 1693 "ref_filename" 1694 ) 1695 1696 if not os.path.isfile(soln_equivalent): 1697 raise context_utils.ContextCreationError( 1698 self, 1699 f"Target file {soln_equivalent} does not exist in the" 1700 f" solution directory." 1701 ) 1702 1703 with open(soln_equivalent, 'r', encoding="utf-8") as fin: 1704 contents = fin.read() 1705 1706 if prep: 1707 source = prep(contents) 1708 else: 1709 source = contents 1710 1711 try: 1712 node = mast.parse(source, filename=ref_filename) 1713 except Exception as e: 1714 raise context_utils.ContextCreationError( 1715 self, 1716 f"Unable to parse solution file '{soln_equivalent}'.", 1717 cause=e 1718 ) 1719 1720 return { 1721 "ref_original_source": contents, 1722 "ref_source": source, 1723 "ref_scope": node, 1724 "ref_top_scope": node 1725 } 1726 1727 # Figure out if we need to use automatic dependencies: 1728 if depends is None: 1729 depends = auto("ref_filename", "ref_file_path") 1730 1731 # Now we can call the super constructor 1732 super().__init__( 1733 description=( 1734 "Code in the solution file", 1735 "We will parse the code in the solution file.", 1736 ), 1737 builder=establish_context, 1738 display_product=lambda context: ( 1739 f"The solution code for '{context['filename']}'" 1740 f" (available after the revision period is over)." 1741 ), 1742 depends=depends, 1743 hidden=hidden 1744 ) 1745 1746 # Finally, register ourselves as an auto provider for the slots 1747 # that we generate: 1748 self.register( 1749 "ref_original_source", 1750 "ref_source", 1751 "ref_scope", 1752 "ref_top_scope" 1753 ) 1754 1755 1756# Register a factory for a default CodeContext as an on-demand option 1757# for the slots it can generate. 1758AutoContext.on_demand( 1759 (lambda: SolnCodeContext()), 1760 "ref_source", "ref_scope", "ref_top_scope" 1761) 1762 1763 1764class TestsCodeContext(AutoContext): 1765 """ 1766 Requires "tests_filename" and "tests_file_path" slots (see 1767 `TestsFileContext`), and establishes a "tests_source" slot which 1768 contains the raw text of the target file, along with a "tests_scope" 1769 slot which contains the parsed AST from the code. 1770 1771 If the code cannot be parsed due to a `SyntaxError` or the like, a 1772 `ContextCreationError` will be generated naming the parsing error as 1773 its cause, although note that `potluck.load.fix_parse` is used which 1774 will attempt to steamroll some kinds of parsing errors while 1775 generating associated warnings. 1776 """ 1777 def __init__(self, depends=None, hidden=False, prep=None): 1778 """ 1779 Dependencies are optional; if not specified `auto` will be used 1780 to fill them in. `hidden` may be provided; by default this 1781 context is not hidden. A `prep` function may be provided; it will 1782 be applied to the source code string and its result will be used 1783 instead of the original source. 1784 """ 1785 # First, create our context builder 1786 def establish_context(prev_context): 1787 """ 1788 Establishes the following context slots based on the 1789 "tests_file_path" slot, by reading the indicated file: 1790 1791 - original_tests_source: The raw file contents. 1792 - tests_source: Possibly-edited (to steamroll syntax errors 1793 or by a prep function) file contents. 1794 - tests_scope: An AST module node resulting from parsing the 1795 modified file contents. 1796 - top_tests_scope: Same as above (but not designed to be 1797 modified). 1798 - tests_parse_errors: A list of Exception objects that were 1799 'successfully' steamrolled by editing the source code. 1800 """ 1801 filename = context_utils.extract(prev_context, "tests_filename") 1802 target = context_utils.extract(prev_context, "tests_file_path") 1803 with open(target, 'r', encoding="utf-8") as fin: 1804 original_tests_source = fin.read() 1805 1806 if prep: 1807 tests_source = prep(original_tests_source) 1808 else: 1809 tests_source = original_tests_source 1810 1811 try: 1812 fixed, node, errors = load.fix_parse(tests_source, filename) 1813 except Exception as e: 1814 raise context_utils.ContextCreationError( 1815 self, 1816 f"Unable to parse submitted tests file '{filename}'.", 1817 cause=e 1818 ) 1819 1820 if node is None: 1821 raise context_utils.ContextCreationError( 1822 self, 1823 f"Unable to parse submitted tests file '{filename}'.", 1824 cause=errors[0] 1825 ) 1826 1827 result = { 1828 "original_tests_source": original_tests_source, 1829 "tests_source": fixed, 1830 "tests_scope": node, 1831 "top_tests_scope": node, 1832 "tests_parse_errors": errors 1833 } 1834 1835 # Report parsing issues as warnings 1836 if errors: 1837 result["warnings"] = [ 1838 ( 1839 "The following errors were encountered when parsing" 1840 + " your tests:<br>" 1841 + html_tools.build_list( 1842 html_tools.html_traceback(e) 1843 for e in errors 1844 ) 1845 ) 1846 ] 1847 1848 return result 1849 1850 # Figure out if we need to use automatic dependencies: 1851 if depends is None: 1852 depends = auto("tests_filename", "tests_file_path") 1853 1854 # Now we can call the super constructor 1855 super().__init__( 1856 description=( 1857 "Code in the tests file", 1858 ( 1859 "We will parse the code in the tests file and pay" 1860 " attention to how it was written." 1861 ), 1862 "Code in the tests file", 1863 ( 1864 "We parsed the code in the tests file and paid" 1865 "attention to how it was written." 1866 ), 1867 ), 1868 builder=establish_context, 1869 display_product=lambda context: ( 1870 f"The tests code in '{context['tests_filename']}'" 1871 f" (shown elsewhere)." 1872 ), 1873 depends=depends, 1874 hidden=hidden, 1875 # Errors at this level need to be reported! 1876 generate_warnings=True 1877 ) 1878 1879 # Finally, register ourselves as an auto provider for the slots 1880 # that we generate: 1881 self.register( 1882 "original_tests_source", 1883 "tests_source", 1884 "tests_scope", 1885 "top_tests_scope", 1886 "tests_parse_errors" 1887 ) 1888 1889 1890# Register a factory for a default CodeContext as an on-demand option 1891# for the slots it can generate. 1892AutoContext.on_demand( 1893 (lambda: TestsCodeContext()), 1894 "original_tests_source", 1895 "tests_source", 1896 "tests_scope", 1897 "top_tests_scope", 1898 "tests_parse_errors" 1899) 1900 1901 1902class ModuleContext(AutoContext): 1903 """ 1904 Requires a "top_scope" slot (see `CodeContext` which must hold an 1905 entire module's AST, and creates a "module" slot which holds the 1906 module object that results from running that code. 1907 1908 If `optimism` is available, any test cases established will be 1909 cleared when the module is loaded, and then any cases established by 1910 loading the module will be saved in a "test_cases" context slot. 1911 """ 1912 _filename = "filename" 1913 _src = "file_path" 1914 _from = "top_scope" 1915 _sandbox = "sandbox" 1916 _to = "module" 1917 _to_cases = "test_cases" 1918 _description = ( 1919 "The values defined by the code", 1920 ( 1921 "We will run your code so that we can run tests on the" 1922 " values it defines." 1923 ) 1924 ) 1925 1926 def display_result(self, context): 1927 """ 1928 Context result display function which lists names defined in the 1929 loaded module. 1930 """ 1931 loaded = context[self._to] 1932 defined = [ 1933 name 1934 for name in dir(loaded) 1935 if not name.startswith("__") or not name.endswith("__") 1936 ] 1937 if len(defined) == 0: 1938 result = "No values were defined in the file." 1939 else: 1940 result = ( 1941 "The following values were defined in the file:\n" 1942 + html_tools.build_list( 1943 "<code>{}</code>".format(name) 1944 for name in defined 1945 ) 1946 ) 1947 1948 if OPTIMISTIC: 1949 ndef = len(context[self._to_cases]) 1950 if ndef > 0: 1951 result += ( 1952 "<br>\nYour file defined {} test cases.".format(ndef) 1953 ) 1954 1955 return result 1956 1957 def __init__(self, depends=None, hidden=False, prep=None, wrap=None): 1958 """ 1959 Dependencies are optional; if not specified `auto` will be used 1960 to fill them in. `hidden` may be provided; by default this 1961 context is not hidden. 1962 1963 `prep` may be supplied; it is a function which receives the 1964 current context dictionary and will be run before the module is 1965 loaded. 1966 1967 `wrap` may be supplied; it is a function which will be given the 1968 module once it's loaded and its return value will be used instead 1969 of the original module. 1970 """ 1971 # First, create our context builder 1972 def establish_context(prev_context): 1973 """ 1974 Establishes the "module" context slot by executing the code 1975 in the "top_scope" slot. Actually, uses self._from and 1976 self._to to determine the slots to read/write, since it can 1977 also be used to create "ref_module" from "ref_top_scope". 1978 Also uses self._src if available to find the source file. 1979 """ 1980 # Fetch the AST node that we'd like to turn into a module 1981 node = context_utils.extract(prev_context, self._from) 1982 1983 # Prefix the file name so that submitted and solution 1984 # modules with the same name don't collide 1985 filename = context_utils.extract(prev_context, self._filename) 1986 if self._from == "top_scope": 1987 prefix = "subm_" 1988 elif self._from == "ref_top_scope": 1989 prefix = "soln_" 1990 elif self._from == "top_tests_scope": 1991 prefix = "tests_" 1992 else: 1993 prefix = "loaded_" 1994 full_name = prefix + filename 1995 1996 # Figure out our file source if we can 1997 src_path = prev_context.get(self._src) 1998 if src_path: 1999 src_path = os.path.abspath(src_path) 2000 2001 # Run the prep function if one was supplied 2002 if prep is not None: 2003 prep(prev_context) 2004 2005 # Set up phony stdin, so that stray inputs won't immediately 2006 # crash the program (if their results are used in a delicate 2007 # manner, they still will of course, but we supply '1' for 2008 # each input, which will survive conversion to an int or 2009 # float). 2010 old_stdin = sys.stdin 2011 sys.stdin = context_utils.AllOnes() 2012 # Note: we don't care about echoing inputs to stdout here... 2013 2014 # Set up phony stdout and stderr 2015 old_stdout = sys.stdout 2016 sys.stdout = io.StringIO() 2017 old_stderr = sys.stderr 2018 sys.stderr = io.StringIO() 2019 2020 # Prepare for capturing test cases 2021 test_cases = None 2022 2023 if OPTIMISTIC: 2024 # Ask for the default level of failure messages 2025 optimism.detailLevel(0) 2026 # Reset failure flag and ensure we don't skip checks 2027 optimism.clearFailure() 2028 optimism.skipChecksAfterFail(None) 2029 # Get rid of any previously-recorded cases 2030 optimism.deleteAllTestSuites() 2031 2032 # Actually load the module 2033 try: 2034 module = load.create_module_in_sandbox( 2035 node, 2036 full_name, 2037 sandbox_dir=context_utils.extract( 2038 prev_context, 2039 self._sandbox 2040 ), 2041 on_disk=src_path 2042 ) 2043 except Exception as e: 2044 raise context_utils.ContextCreationError( 2045 self, 2046 "Unable to run code.", 2047 e 2048 ) 2049 finally: # clean up input/output streams 2050 sys.stdin = old_stdin 2051 sys.stdout = old_stdout 2052 sys.stderr = old_stderr 2053 2054 if OPTIMISTIC: 2055 try: 2056 # Capture defined test cases 2057 test_cases = optimism.listAllTrials() 2058 except Exception as e: 2059 raise context_utils.ContextCreationError( 2060 self, 2061 "Error managing optimism tests.", 2062 e 2063 ) 2064 2065 # Wrap our module result if necessary 2066 if wrap is not None: 2067 module = wrap(module) 2068 2069 # Return our new slot 2070 result = { self._to: module } 2071 if test_cases is not None: 2072 result[self._to_cases] = test_cases 2073 2074 return result 2075 2076 # Figure out if we need to use automatic dependencies: 2077 if depends is None: 2078 depends = auto(self._filename, self._from, self._sandbox) 2079 2080 # Now we can call the super constructor 2081 super().__init__( 2082 description=self._description, 2083 builder=establish_context, 2084 display_product=self.display_result, 2085 depends=depends, 2086 hidden=hidden 2087 ) 2088 2089 # Finally, register ourselves as an auto provider for the slots 2090 # that we generate: 2091 if OPTIMISTIC: 2092 self.register(self._to, self._to_cases) 2093 else: 2094 self.register(self._to) 2095 2096 2097# Register a factory for a default ModuleContext as an on-demand option 2098# for the "module" slot, or for that slot plus the "test_cases" slot if 2099# optimism is available. 2100if OPTIMISTIC: 2101 AutoContext.on_demand( 2102 (lambda: ModuleContext()), 2103 "module", "test_cases" 2104 ) 2105else: 2106 AutoContext.on_demand((lambda: ModuleContext()), "module") 2107 2108 2109class SolnModuleContext(ModuleContext): 2110 """ 2111 Works like `ModuleContext`, but for the solution module: Requires a 2112 "ref_top_scope" slot (see `SolnCodeContext`) which must hold the 2113 solution module's AST, and creates a "ref_module" slot which holds 2114 the module object that results from running that code. 2115 2116 If the optimism module is available, also creates a 2117 "ref_expectations" slot. 2118 """ 2119 _filename = "ref_filename" 2120 _src = "ref_file_path" 2121 _from = "ref_top_scope" 2122 _sandbox = "ref_sandbox" 2123 _to = "ref_module" 2124 _to_cases = "ref_test_cases" 2125 _description = ( 2126 "The values defined by the solution code", 2127 ( 2128 "We will run the solution code so that we can compare" 2129 " its results to the results of your code." 2130 ) 2131 ) 2132 2133 # Note: just by overriding these fields we've done all that we need 2134 # to to change where we're reading from and where we're putting our 2135 # results. 2136 2137 2138# Register a factory for a default SolnModuleContext as an on-demand 2139# option for the "ref_module" slot, or for that slot plus the 2140# "ref_test_cases" slot if optimism is available. 2141if OPTIMISTIC: 2142 AutoContext.on_demand( 2143 (lambda: SolnModuleContext()), 2144 "ref_module", "ref_test_cases" 2145 ) 2146else: 2147 AutoContext.on_demand((lambda: SolnModuleContext()), "ref_module") 2148 2149 2150class TestsModuleContext(ModuleContext): 2151 """ 2152 Requires a "top_tests_scope" slot (see `TestsCodeContext` which must 2153 hold an entire module's AST, and creates a "tests_module" slot which 2154 holds the module object that results from running that code. 2155 """ 2156 _filename = "tests_filename" 2157 _src = "tests_file_path" 2158 _from = "top_tests_scope" 2159 _sandbox = "tests_sandbox" 2160 _to = "tests_module" 2161 _to_cases = "validation_test_cases" 2162 _description = ( 2163 "The values defined by the solution code", 2164 ( 2165 "We will run the solution code so that we can compare" 2166 " its results to the results of your code." 2167 ) 2168 ) 2169 2170 # Note: just by overriding these fields we've done all that we need 2171 # to to change where we're reading from and where we're putting our 2172 # results. 2173 2174 2175# Register a factory for a default TestsModuleContext as an on-demand 2176# option for the "tests_module" slot, or for "tests_module" and 2177# "validation_test_cases" if optimism is available. 2178if OPTIMISTIC: 2179 AutoContext.on_demand( 2180 (lambda: TestsModuleContext()), 2181 "tests_module", "validation_test_cases" 2182 ) 2183else: 2184 AutoContext.on_demand((lambda: TestsModuleContext()), "tests_module") 2185 2186 2187class DefinitionsContext(AutoContext): 2188 """ 2189 Creates a "defs" slot based on a "top_scope" slot which holds a 2190 mapping from function names to AST nodes covering every `def` which 2191 occurs in the provided scope, including nested and method 2192 definitions. 2193 2194 Note that nested definitions may shadow exterior definitions in the 2195 map if they have the same name. (TODO: Not that?) 2196 """ 2197 _from = "top_scope" 2198 _to = "defs" 2199 2200 def __init__(self, depends=None, hidden=False): 2201 """ 2202 Dependencies may be supplied and the context may be hidden. If no 2203 dependencies are given, an auto-dependency for the "top_scope" 2204 slot will be generated. 2205 """ 2206 def establish_context(prev_context): 2207 """ 2208 This context_builder function depends on a "scope" context 2209 holding an AST node, and adds a "defs" context item which 2210 contains a dicitonary of all AST nodes in that scope which 2211 are function definitions (includes interior defs). The keys 2212 of the dictionary are the names of the functions. Lambdas 2213 are not included in the list. 2214 """ 2215 within = context_utils.extract(prev_context, self._from) 2216 2217 # Find definition AST nodes 2218 alldefs = set() 2219 for pat in patterns.ALL_DEF_PATTERNS: 2220 alldefs |= set( 2221 node 2222 for node, bindings in mast.findall(within, pat) 2223 ) 2224 2225 # Create the mapping 2226 defmap = {} 2227 for defn in alldefs: 2228 defmap[defn.name] = defn 2229 2230 # Return our resulting slot 2231 return { self._to: defmap } 2232 2233 # Figure out if we need to use automatic dependencies: 2234 if depends is None: 2235 depends = auto(self._from) 2236 2237 # Now we can call the super constructor 2238 super().__init__( 2239 description=( 2240 "The function definitions in the code", 2241 ( 2242 "We will inspect the code and extract all of the" 2243 " function definitions." 2244 ) 2245 ), 2246 builder=establish_context, 2247 display_product=lambda context: ( 2248 "The following functions were defined:\n" 2249 + html_tools.build_list( 2250 f"<code>{name}</code>" 2251 for name in context[self._to] 2252 ) 2253 ), 2254 depends=depends, 2255 hidden=hidden 2256 ) 2257 2258 # Finally, register ourselves as an auto provider for the slot 2259 # that we generate: 2260 self.register(self._to) 2261 2262 2263# Register a factory for a default DefinitionsContext as an on-demand 2264# option for the "defs" slot. 2265AutoContext.on_demand((lambda: DefinitionsContext()), "defs") 2266 2267 2268class SolnDefinitionsContext(DefinitionsContext): 2269 """ 2270 Works like `DefinitionsContext` but extracts a "ref_defs" slot from 2271 the "ref_top_scope" slot. 2272 """ 2273 _from = "ref_top_scope" 2274 _to = "ref_defs" 2275 2276 2277# Register a factory for a default SolnDefinitionsContext as an on-demand 2278# option for the "ref_defs" slot. 2279AutoContext.on_demand((lambda: DefinitionsContext()), "ref_defs") 2280 2281 2282class DocstringsContext(AutoContext): 2283 """ 2284 Establishes a "docstrings" slot based on "defs" and "module" slots, 2285 which contains a mapping from function names to their docstrings. 2286 This mapping will only include functions defined at the top level of 2287 the module. 2288 """ 2289 2290 def __init__(self, depends=None, hidden=False): 2291 """ 2292 May have non-automatic dependencies and/or be hidden. If manual 2293 dependencies are provided, make sure they establish the "defs" 2294 and "module" slots. 2295 """ 2296 def establish_context(prev_context): 2297 """ 2298 This context_builder requires *both* a "defs" node (see 2299 `DefinitionsContext`) *and* a "module" node (see 2300 `ModuleContext`), because it makes use of both the AST and 2301 the actual imported module. 2302 2303 It uses the defs map to figure out what functions to look 2304 for, and then for every function defined *at the top level* 2305 of the submitted code, it looks up the docstring from the 2306 module object, returning a mapping from function names to 2307 docstrings. If there are functions defined inside of other 2308 functions, they will not show up in the resulting 2309 "docstrings" context item. 2310 """ 2311 defs = context_utils.extract(prev_context, "defs") 2312 submitted = context_utils.extract(prev_context, "module") 2313 2314 docsmap = {} 2315 2316 for fname in defs: 2317 fcn = getattr(submitted, fname, None) 2318 if fcn: 2319 # this function is defined at the module level 2320 doc = getattr(fcn, "__doc__", None) or "" 2321 doc = doc.strip() 2322 docsmap[fname] = doc 2323 2324 return { "docstrings": docsmap } 2325 2326 # Figure out if we need to use automatic dependencies: 2327 if depends is None: 2328 depends = auto("defs", "module") 2329 2330 super().__init__( 2331 description=( 2332 "The function docstrings", 2333 ( 2334 "We will extract the docstrings from each function" 2335 " defined by your code." 2336 ) 2337 ), 2338 builder=establish_context, 2339 display_product=lambda context: ( 2340 "The following docstrings were found:\n" 2341 + html_tools.build_list( 2342 ( 2343 f"Function <code>{name}</code>:<br>" 2344 f"<pre>{doc}</pre>" 2345 ) 2346 for name, doc in context["docstrings"].items() 2347 ) 2348 ), 2349 depends=depends, 2350 hidden=hidden 2351 ) 2352 2353 # Finally, register ourselves as an auto provider for the slot 2354 # that we generate: 2355 self.register("docstrings") 2356 2357 2358# Register a factory for a default DocstringsContext as an on-demand 2359# option for the "docstrings" slot. 2360AutoContext.on_demand((lambda: DocstringsContext()), "docstrings") 2361 2362 2363#--------------------------------------------------# 2364# Functions for displaying context builder results # 2365#--------------------------------------------------# 2366 2367def build_context_value_displayer( 2368 key, 2369 compare_ref=True, 2370 include_diff=True, 2371 labels=["Your value", "Solution value", "Comparison"] 2372): 2373 """ 2374 Creates a display_product function which will show the contents of a 2375 single specific context key, and by default, will include multiple 2376 tabs that show the value, the reference value, and a diff of the two 2377 values. String values are shown as-is; non-string values are 2378 converted to strings using html_tools.big_repr. 2379 2380 If the number of characters in a value's representation would exceed 2381 VALUE_SIZE_LIMIT, we will truncate it. 2382 2383 Set compare_ref to False to simply show the value for the specified 2384 key, and set include_diff to False when compare_ref is True to omit 2385 the difference tab in the comparison. 2386 2387 Custom labels for the two values and their difference (second and/or 2388 third labels may be ignored depending on other flags) may be given 2389 using the labels argument. 2390 2391 Returns a function suitable for use as the display_product argument 2392 to a Context. 2393 """ 2394 def display_context_value(context): 2395 """ 2396 A function that returns HTML code which displays the value of a 2397 single specific context key, possibly with tabs to view the value 2398 produced by submitted code, the reference value, and the 2399 difference between the two (as a diff). 2400 2401 See build_context_value_displayer, which created this function. 2402 """ 2403 if not compare_ref: 2404 if key in context: 2405 # simply return a single <pre> containing a representation of 2406 # the value 2407 if isinstance(context[key], str): 2408 rep = context[key] 2409 else: 2410 try: 2411 rep = html_tools.big_repr(context[key]) 2412 except TypeError: 2413 rep = repr(context[key]) 2414 rep = html_tools.escape( 2415 html_tools.truncate(rep, VALUE_SIZE_LIMIT) 2416 ) 2417 return f"<pre class='context-value'>{rep}</pre>" 2418 else: 2419 # failed to create the context key we're looking for! 2420 return ( 2421 f"<div class='context-missing'>Failed to establish" 2422 f" context '{key}'!</div>" 2423 ) 2424 else: 2425 if key in context: 2426 if isinstance(context[key], str): 2427 rep = context[key] 2428 else: 2429 try: 2430 rep = html_tools.big_repr(context[key]) 2431 except TypeError: 2432 rep = repr(context[key]) 2433 rep = html_tools.truncate(rep, VALUE_SIZE_LIMIT) 2434 erep = html_tools.escape(rep) 2435 rep_html = f"<pre class='context-value'>{erep}</pre>" 2436 else: 2437 rep = "" 2438 rep_html = ( 2439 f"<div class='context-missing'>Failed to establish" 2440 f" context '{key}'!</div>" 2441 ) 2442 2443 if "ref_" + key in context: 2444 if isinstance(context["ref_" + key], str): 2445 ref_rep = context["ref_" + key] 2446 else: 2447 try: 2448 ref_rep = html_tools.big_repr(context["ref_" + key]) 2449 except TypeError: 2450 ref_rep = repr(context["ref_" + key]) 2451 ref_rep = html_tools.truncate(ref_rep, VALUE_SIZE_LIMIT) 2452 ref_erep = html_tools.escape(ref_rep) 2453 ref_rep_html = f"<pre class='context-value'>{ref_erep}</pre>" 2454 else: 2455 ref_rep = "" 2456 ref_rep_html = ( 2457 f"<div class='context-missing'>Failed to establish" 2458 f" context 'ref_{key}'!</div>" 2459 ) 2460 2461 if include_diff: 2462 # Include a tab for the differences 2463 diff = html_tools.html_diff_table( 2464 rep, 2465 ref_rep, 2466 out_title=labels[0], 2467 ref_title=labels[1] 2468 ) 2469 return html_tools.build_html_tabs( 2470 [ 2471 (labels[0], rep_html), 2472 (labels[1], ref_rep_html), 2473 (labels[2], diff), 2474 ] 2475 ) 2476 else: 2477 # No tab for the differences 2478 return html_tools.build_html_tabs( 2479 [ 2480 (labels[0], rep_html), 2481 (labels[1], ref_rep_html), 2482 ] 2483 ) 2484 2485 return display_context_value 2486 2487 2488def build_simple_context_value_displayer( 2489 key, 2490 compare_ref=True, 2491 labels=["Your value", "Solution value"] 2492): 2493 """ 2494 Creates a display_product function similar to the 2495 `build_context_value_displayer` result, but for simple values which 2496 don't need a pre wrapper and which can be displayed side-by-side 2497 (e.g., numbers). No diff is included, as it's presumed that any 2498 differences will be obvious, and values are converted to strings 2499 using str() instead of html_tools.big_repr. Representations that end 2500 up longer than VALUE_SIZE_LIMIT are still truncated. 2501 2502 Set compare_ref to False to include only the main value. 2503 2504 Custom labels for the two values may be given using the labels 2505 argument. These are not used if compare_ref is False. 2506 2507 Returns a function suitable for use as the display_product argument 2508 to a Context. 2509 """ 2510 def display_context_value(context): 2511 """ 2512 A function that returns HTML code which displays the value of a 2513 single specific context key, possibly side-by-side with the 2514 corresponding reference value. 2515 2516 See build_simple_context_value_displayer, which created this function. 2517 """ 2518 if not compare_ref: 2519 if key in context: 2520 return str(context[key]) 2521 else: 2522 return ( 2523 f"<div class='context-missing'>Failed to establish" 2524 f" context '{key}'!</div>" 2525 ) 2526 else: 2527 if key in context: 2528 rep = html_tools.truncate( 2529 repr(context[key]), 2530 VALUE_SIZE_LIMIT 2531 ) 2532 erep = html_tools.escape(rep) 2533 rep_html = "<code>{}</code>".format(erep) 2534 else: 2535 rep_html = ( 2536 f"<div class='context-missing'>Failed to establish" 2537 f" context '{key}'!</div>" 2538 ) 2539 2540 if "ref_" + key in context: 2541 ref_rep = html_tools.truncate( 2542 repr(context["ref_" + key]), 2543 VALUE_SIZE_LIMIT 2544 ) 2545 ref_erep = html_tools.escape(ref_rep) 2546 ref_rep_html = "<code>{}</code>".format(ref_erep) 2547 else: 2548 ref_rep_html = ( 2549 f"<div class='context-missing'>Failed to establish" 2550 f" context 'ref_{key}'!</div>" 2551 ) 2552 2553 return f""" 2554<table class='context-values'> 2555 <tbody> 2556 <tr> <th>{labels[0]}</th> <td>{rep_html}</td> </tr> 2557 <tr> <th>{labels[1]}</th> <td>{ref_rep_html}</td> </tr> 2558 </tbody> 2559</table> 2560""" 2561 2562 return display_context_value 2563 2564 2565def create_distribution_result_displayer(context_key="distribution"): 2566 """ 2567 Creates a distribution results display function, which will read 2568 values from the given context key ("distribution" by default). Also 2569 reads a value from the matching "ref_" key. 2570 """ 2571 def display_distribution_results(context): 2572 """ 2573 Displays the 'distribution' and 'ref_distribution' context keys 2574 side-by-side. 2575 """ 2576 sub_dist = context[context_key]["results"] 2577 ref_dist = context["ref_" + context_key]["results"] 2578 2579 all_results = set(sub_dist) | set(ref_dist) 2580 2581 n_samples = context[context_key]["trials"] 2582 2583 rows = [] 2584 for result in sorted(all_results): 2585 rows.append( 2586 ( 2587 "<tr> <td>{result}</td> <td>{n}</td>" 2588 + " <td>{ref_n}</td> </tr>" 2589 ).format( 2590 result=html_tools.dynamic_html_repr( 2591 result, 2592 limit=VALUE_SIZE_LIMIT 2593 ), 2594 n=repr(sub_dist.get(result, 0)), 2595 ref_n=repr(ref_dist.get(result, 0)) 2596 ) 2597 ) 2598 2599 return """ 2600The distribution of results from your function and the solution function 2601after {n_samples} trials (note: distributions may differ somewhat due to 2602random chance.): 2603<table class='result_distribution'> 2604 <thead> 2605 <tr> 2606 <th>Result value</th> 2607 <th>Observed count</th> 2608 <th>Solution count</th> 2609 </tr> 2610 </thead> 2611 <tbody> 2612 {rows} 2613 </tbody> 2614</table> 2615""".format(n_samples=n_samples, rows='\n '.join(rows)) 2616 2617 return display_distribution_results 2618 2619 2620def create_image_result_displayer(context_key="image", alt_key="output"): 2621 """ 2622 Creates a context value display function which shows the "image" slot 2623 (or an image from another slot) with alt text from the "output" slot 2624 (assuming turtleBeads descriptions are used). 2625 2626 If a ref_ slot and an alt ref_ slot are available, a comparison will 2627 be included. 2628 """ 2629 def image_displayer(context): 2630 """ 2631 Returns HTML code for displaying an image from the given context, 2632 with alt text from a different slot. 2633 """ 2634 img = context[context_key] 2635 alt = context[alt_key] 2636 if "ref_" + context_key in context and "ref_" + alt_key in context: 2637 ref_img = context["ref_" + context_key] 2638 ref_alt = context["ref_" + alt_key] 2639 return html_tools.build_html_tabs( 2640 [ 2641 ( 2642 "Your image:", 2643 html_tools.html_image(img, alt) 2644 ), 2645 ( 2646 "Solution image:", 2647 html_tools.html_image(ref_img, ref_alt) 2648 ), 2649 ( 2650 "Animation:", 2651 html_tools.html_animation( 2652 compare.diff_anim_frames(img, ref_img, 10), 2653 ( 2654 # TODO: Diff of alt texts here? 2655 "An animation between your image and the" 2656 " solution image." 2657 ), 2658 delays=[500] + [100] * 10 + [500] 2659 ) 2660 ) 2661 ] 2662 ) 2663 else: 2664 # No ref values available for a comparison 2665 return html_tools.html_image(img, alt) 2666 2667 return image_displayer 2668 2669 2670#---------------# 2671# SiftedContext # 2672#---------------# 2673 2674class SiftedContext(Context): 2675 """ 2676 Working from the "output" and "ref_output" slots (or some other 2677 custom list of slots), this `Context` creates "sifted" and 2678 "ref_sifted" slots which hold the results of matching a regular 2679 expression against the input value. 2680 """ 2681 def __init__( 2682 self, 2683 pattern, 2684 depends, 2685 description=None, 2686 slot_map={"output": "sifted", "ref_output": "ref_sifted"}, 2687 first_match=False, 2688 require_match=True, 2689 use_matchobjs=False 2690 ): 2691 """ 2692 Dependencies must be supplied. A custom description may be 2693 supplied (and is often useful). A custom slot map may be supplied 2694 to specify which incoming slots to process, and for each incoming 2695 slot, which new slot to create to store the result from that 2696 slot. 2697 """ 2698 if isinstance(pattern, str): 2699 pattern = re.compile(pattern) 2700 2701 def establish_context(prev_context): 2702 """ 2703 This context_builder function processes a custom list of 2704 slots by applying a regular expression to them. 2705 """ 2706 result = {} 2707 # Process each requested slot 2708 for from_slot in slot_map: 2709 to_slot = slot_map[from_slot] 2710 2711 # Grab our input value 2712 value = context_utils.extract(prev_context, from_slot) 2713 if not isinstance(value, str): 2714 raise TypeError( 2715 f"SiftedContext can only refine string values," 2716 f" but was asked to refine value of slot" 2717 f" {from_slot} which was a {type(value)}." 2718 ) 2719 2720 # Apply our regular expression 2721 matches = pattern.finditer(value) 2722 2723 # Grab the first match if that's what we need 2724 if first_match: 2725 try: 2726 first = next(matches) 2727 except StopIteration: 2728 raise ValueError( 2729 f"While refining '{from_slot}' context," 2730 f" found no matches for pattern." 2731 ) 2732 # Add either the match object or matching string 2733 if use_matchobjs: 2734 result[to_slot] = first 2735 else: 2736 result[to_slot] = first.group() 2737 else: # Grab all matches 2738 if use_matchobjs: 2739 objs = [m for m in matches] 2740 else: 2741 objs = [m.group() for m in matches] 2742 2743 # list might be empty... 2744 if require_match and len(objs) == 0: 2745 raise ValueError( 2746 f"While refining '{from_slot}' context," 2747 f" found no matches for pattern." 2748 ) 2749 2750 result[to_slot] = objs 2751 2752 # Return our results 2753 return result 2754 2755 def display_result(context): 2756 """ 2757 Displays the results of slot sifting as a tabbed HTML 2758 structure with one tab per input slot. 2759 """ 2760 tablist = [] 2761 for from_slot in slot_map: 2762 to_slot = slot_map[from_slot] 2763 result = context_utils.extract(context, to_slot) 2764 2765 if isinstance(result, re.Match): 2766 display = f"<pre>{result.group(0)}</pre>" 2767 elif isinstance(result, list): 2768 if len(result) == 0: 2769 display = "<no matches>" 2770 elif isinstance(result[0], str): 2771 display = html_tools.build_list( 2772 [ 2773 f"<pre>{entry}</pre>" 2774 for entry in result 2775 ] 2776 ) 2777 else: # results are Match objects 2778 display = html_tools.build_list( 2779 [ 2780 f"<pre>{match.group(0)}</pre>" 2781 for match in result 2782 ] 2783 ) 2784 else: # results should be strings 2785 display = f"<pre>{result}</pre>" 2786 2787 tablist.append((from_slot, display)) 2788 2789 return ( 2790 "Results for expression <pre><code>{expr}</code></pre>:<br>" 2791 + html_tools.build_html_tabs(tablist) 2792 ) 2793 2794 # Create a default description if necessary 2795 if description is None: 2796 stuff = phrasing.comma_list( 2797 slot 2798 for slot in slot_map 2799 if not slot.startswith("ref_") 2800 ) 2801 description = ( 2802 f"Certain parts of the {stuff}", 2803 ( 2804 f"We will search for the pattern" 2805 f"<pre><code>{pattern.pattern}</code></pre> within" 2806 f" the {stuff} and inspect the results." 2807 ) 2808 ) 2809 2810 # Now we can call the super constructor 2811 super().__init__( 2812 description=description, 2813 builder=establish_context, 2814 display_product=display_result, 2815 depends=depends 2816 )
A string (or None before it gets set) which holds the file name given to
the most recent instantiation of a FileContext
, which is thus also
almost certainly the filename that will be used in any new contexts that
are created (unless hijinks have ensued).
Like RELEVANT_FILENAME
, but for a tests file to be validated rather
than a submission file to be evaluated.
Limit (in terms of string length, not bytes) after which we truncate
large values that would be displayed by build_context_value_displayer
.
87def generic_context_displayer(context): 88 """ 89 The default display_product function for a Context, this just returns 90 the string "no details available." This may help cut down on report 91 sizes where displaying all context values would include highlighted 92 copies of the source code, etc. 93 """ 94 return "no details available"
The default display_product function for a Context, this just returns the string "no details available." This may help cut down on report sizes where displaying all context values would include highlighted copies of the source code, etc.
97class Context: 98 """ 99 Represents some kind of product of submitted code, like an 100 AST tree, or produced output. Contexts can specialize each other, so 101 for example a regular expression could be used to transform a context 102 containing all output from a program into a context containing just a 103 single line of output. Context objects organize context_builder 104 functions into hierarchies, and manage caching of their results. 105 106 A `Context` object also needs to know how to render its results into 107 HTML to display to the user as part of the feedback process. 108 109 A `Context` object, via its context_builder function, knows how to 110 produce, on demand, a dictionary mapping context keys to values. 111 The base context (and therefore all derived contexts) will always 112 include the following `potluck.context_utils.BASE_CONTEXT_SLOTS`: 113 114 - "task_info": A task info dictionary for the current task, which 115 will have the following keys: 116 - "id": The task ID. 117 - "specification": the specification module for this task. 118 - "target": The default file or folder to grade in a submission. 119 - "title": The title of this task. 120 - "desc": The short description of this task. 121 - "reference_cache_file": The filename where reference values 122 may be cached. 123 - "ignore_cache": Whether or not to ignore the cache (might not 124 be present; ignore cache only if present and truthy). 125 - "username": The user who submitted the code we're evaluating. 126 - "submission_root": The root directory within which the 127 submission we're evaluating can be found. 128 - "default_file": The official name of the default file to evaluate. 129 - "actual_file": The actual name of the default file as submitted. 130 - "tests_submission_root": The root directory where the test's we're 131 validating can be found (present during validation). 132 - "default_tests_file": The official name of the default tests file 133 to validate. 134 - "actual_tests_file": The actual name of the tests file as 135 submitted. 136 137 When a context is associated with a specific goal, the context 138 dictionary will also always contain the following extra slots: 139 140 - "goal_id": The unique-per-task identifier string for the goal that 141 this context is associated with. 142 - "which_context": The index of this context within the goal's 143 testing contexts. Along with the task ID and goal ID, this can be 144 used to build a unique identifier for the context. 145 TODO: Context IDs? 146 147 The typical slots of derived context dictionaries include: 148 149 - "filename": The file name (without path) of the file we're 150 evaluating. 151 - "file_path": The full absolute path to the file we're evaluating. 152 - "source": A string containing the source code of the submitted 153 module. 154 - "parse_errors": A list of Exception objects which were 155 generated but ignored when parsing the submitted code. 156 - "defs": A mapping from function names to AST nodes covering 157 every `def` node in the submitted code (but note that 158 functions with identical names will shadow each other in this 159 map). 160 - "scope": an AST node to check within (e.g., for a function 161 definition check). See ImplementationCheck for an example of 162 how these contexts are created and used. 163 - "docstrings": A mapping from function names to documentation 164 strings for those functions. 165 - "top_scope": The top-level AST node for the submitted code. 166 - "module": a loaded (submitted or solution) module 167 (for a suite of tests that relies on the code). 168 - "value": arbitrary Python value returned from a function call. 169 - "output": a string containing output from tested code. This may be 170 the full output, or may be filtered by additional Context objects 171 to contain just part of the full output. 172 - "trace": a trace list recording certain function calls made 173 during a test, along with arbitrary state snapshots (see 174 `potluck.harness.tracing_function_calls`). 175 - "image": a Pillow Image object holding a captured image. 176 - "audio": a dictionary with "mimetype", "data", and optionally 177 "label" slots, holding captured audio data in binary form. The 178 mimetype should indicate a MIME type for the data, while the 179 "data" slot holds a bytes object containing the raw data in that 180 format. The label slot holds a string used to label the audio in 181 output to users. 182 - "output_filename": the filename for an output file written to by 183 testing code. 184 - "output_file_contents": a string containing the contents of a file 185 that was written to by tested code. 186 - "expectations": a list of dictionaries defining expectations 187 established using the `optimism` module (only present if that 188 module is available) Each has the following keys: 189 - "tag" A string indicating the file name and line number where 190 this expectation was established 191 - "case" The test case info. This is a dictionary with "result", 192 "output", and "context" slots, holding the result value, 193 printed output, and context details for a test case (see 194 `optimism.get_my_context` for details of the "context" 195 value). 196 - "type" The type of expectation: "result", "output", or 197 "custom". 198 - "value" The expected value (or output fragment, or the checker 199 function, depending on the expectation type). 200 - Other keys not defined here may be established and/or used by 201 certain Context or Goal classes. 202 - Versions of the keys above with "ref_" as a prefix hold 203 equivalent values derived from solution instead of submitted 204 code. 205 206 Note: For various reasons, the context building process creates 207 shallow copies of contexts, not deep copies (the context values, such 208 as modules, are often not deep-copyable). Accordingly, it is possible 209 for changes to some sub-component of a context to be seen by other 210 places where that context is being used, and thus YOU SHOULD NEVER 211 MUTATE A CONTEXT VALUE. Context builder functions may safely 212 synthesize new values based on old ones, and may freely update 213 context keys with new values, but should not mutate the values under 214 those keys. 215 """ 216 def __init__( 217 self, 218 description=( 219 "UNSPECIFIED TOPIC", 220 "UNSPECIFIED DETAILS", 221 "UNSPECIFIED FULL TOPIC", 222 "UNSPECIFIED FULL DETAILS" 223 ), 224 builder=None, 225 display_product=generic_context_displayer, 226 depends=None, 227 failure_explanation=None, 228 cache_values=True, 229 base=None, 230 hidden=False, 231 generate_warnings=False 232 ): 233 """ 234 You must supply a description pair (topic + details), triple 235 (topic + details + feedback topic), or quad (topic + details + 236 feedback topic + feedback details). Each item must be an HTML 237 string to be displayed to the students as part of the rubric; the 238 feedback versions if provided are used instead of the originals 239 when generating a feedback document as opposed to a blank rubric. 240 241 You must also supply a builder function, to be run when this 242 context is required. If depends is supplied, it should be a list 243 of Context objects, and before this context is established, each 244 of those contexts will be created and merged in turn, to 245 establish the prev_context argument to the builder function. The 246 builder function must accept one argument (a context dictionary) 247 and must return a dictionary of any new or modified keys that it 248 wants to update in that context dictionary. 249 250 You may supply a failure_explanation, which can be either a 251 string, or a function that will be given the context in which the 252 failure occurred and an Exception object (with an html_tb 253 attached) and expected to return a string. This will be used to 254 set the error message for a ContextCreationError thrown when the 255 context_builder crashes, and that can ultimately become part of 256 an explanation for a failed goal. 257 258 If cache_values is given as False, the context_builder function 259 will be re-run every time this context is requested, but by 260 default, the result will be run only when one of our dependencies 261 has a newer timestamp than our cached value. 262 263 To supply seed values to bootstrap context creation, a Context 264 may have a base value, which is used to start the 265 dependency-merging process to provide a context for its builder. 266 267 If hidden is given as True, this Context will not show up in the 268 Contexts list, and will only be visible as a context associated 269 with a goal when that goal is evaluated (use with care). 270 271 If generate_warnings is set to True (default is False) then 272 issues with context creation of this context (but not with 273 creation of dependencies) will be logged as warnings in 274 `list_and_render_contexts` output. 275 """ 276 self.description = description 277 if builder is None: 278 raise ValueError( 279 "Must specify a context builder when creating a Context." 280 ) 281 self.builder = builder 282 self.display_product = display_product 283 self.depends = depends or [] 284 self.failure_explanation = failure_explanation 285 self.cache_values = cache_values 286 self.base = base or {} 287 self.hidden = hidden 288 self.generate_warnings = generate_warnings 289 290 self.cached_value = None 291 self.cache_timestamp = None 292 293 self.working_from = None 294 # which base context we're currently working from, for better 295 # exception reporting 296 297 def __str__(self): 298 """ 299 Uses the first description entry to hint which context this is. 300 """ 301 return 'Context "' + self.feedback_topic() + '"' 302 303 def __repr__(self): 304 """ 305 Weaves in the description. 306 """ 307 return f'<{type(self).__name__} "{self.feedback_topic()}">' 308 309 def __copy__(self): 310 """ 311 Contexts may not be copied. They are entangled with each other, 312 and we also don't want to create multiple copies which will 313 duplicate the same work. 314 """ 315 raise NotImplementedError("Contexts may not be copied.") 316 317 def __deepcopy__(self, memo): 318 """ 319 Contexts may not be copied (see __copy__). 320 """ 321 raise NotImplementedError("Contexts may not be copied.") 322 323 def changed_at(self): 324 """ 325 Returns the timestamp of the most recent update to our cached 326 context dictionary, so that contexts that depend on this one can 327 know if they need to update themselves. May return None if no 328 cached value has been created yet. 329 """ 330 return self.cache_timestamp 331 332 def clear_cache(self): 333 """ 334 Removes cached info & resets cache timestamp. 335 """ 336 self.cached_value = None 337 self.cache_timestamp = None 338 339 def burn_cache(self): 340 """ 341 Clears our cache, and burns caches of our dependencies. 342 """ 343 self.clear_cache() 344 for dep in self.depends: 345 dep.burn_cache() 346 347 def create(self, base_context): 348 """ 349 Creates and returns a context dictionary, using our builder 350 function. If caching is enabled, a (shallow copy of a) cached 351 value will be returned if we think that it was created after any 352 changes in our dependencies. 353 354 A base context is required, and should be a dictionary; normally 355 it should have all the `potluck.context_utils.BASE_CONTEXT_SLOTS` 356 already populated. 357 358 The resulting context will have a special "__builder__" slot 359 which contains a reference to this `Context` object. Of course, 360 this slot will get overwritten by each context in a chain, so it 361 only stores the last builder to touch the context (but see below). 362 363 The resulting context will have a special "__unmerged__" slot 364 which contains a list of context dictionaries for each dependency 365 of this context, holding the context state created by just that 366 dependency before merging with later dependencies. This can be 367 used to retrieve separate values for the same slot from multiple 368 dependencies, where only the last of those values would normally 369 be retained. Those context dictionaries will of course have 370 "__builder__" and "__unmerged__" slots, so the full chain of 371 `Context` objects responsible for a given context can be 372 recursively retrieved if necessary. 373 """ 374 rerun = False 375 if not self.cache_values: 376 rerun = True 377 else: 378 # Check whether we even have a result yet: 379 if self.cache_timestamp is None: 380 rerun = True 381 else: 382 # Check timestamps of our dependencies 383 for dep in self.depends: 384 when = dep.changed_at() 385 if when is None or when > self.cache_timestamp: 386 rerun = True 387 break 388 389 if not rerun: 390 # Return a shallow copy of our cached value 391 result = copy.copy(base_context) 392 result.update(self.cached_value) 393 return result 394 else: 395 # Create new cached value and update our timestamp 396 self.working_from = base_context 397 prev_context = copy.copy(base_context) 398 prev_context.update(self.base) 399 unmerged = [] 400 for dep in self.depends: 401 try: 402 dep_context = dep.create(base_context) 403 prev_context.update(dep_context) 404 unmerged.append(dep_context) 405 except Exception as e: 406 e.html_tb = html_tools.html_traceback( 407 linkable=context_utils.linkmap(prev_context) 408 ) 409 raise context_utils.ContextCreationError( 410 self, 411 "Dependency failed.", 412 e 413 ) 414 prev_context["__unmerged__"] = unmerged 415 prev_context["__builder__"] = self 416 417 try: 418 our_results = self.builder(prev_context) 419 prev_context.update(our_results) 420 self.cached_value = prev_context 421 except Exception as e: 422 e.html_tb = html_tools.html_traceback( 423 linkable=context_utils.linkmap(prev_context) 424 ) 425 if isinstance(self.failure_explanation, str): 426 msg = self.failure_explanation 427 elif self.failure_explanation: 428 msg = self.failure_explanation(prev_context, e) 429 else: 430 msg = "Test setup failed." 431 raise context_utils.ContextCreationError(self, msg, e) 432 self.cache_timestamp = time.time() 433 # Return shallow copy of cached value 434 result = {} 435 result.update(self.cached_value) 436 return result 437 438 def deps_are_a_stick(self): 439 """ 440 Returns true if the transitive dependencies of this context form 441 a stick, not a real tree (i.e., each context, including this one, 442 has exactly 1 or 0 dependencies). 443 """ 444 return ( 445 len(self.depends) in (0, 1) 446 and all(dep.deps_are_a_stick for dep in self.depends) 447 ) 448 449 def rubric_topic(self): 450 """ 451 Gets the rubric version of this Context's topic. 452 """ 453 return self.description[0] 454 455 def rubric_details(self): 456 """ 457 Gets the rubric version of this Context's details. 458 """ 459 return self.description[1] 460 461 def feedback_topic(self): 462 """ 463 Gets the feedback version of this Context's topic, or just the 464 normal topic if there is no feedback version. 465 """ 466 return self.description[::2][-1] 467 468 def feedback_details(self): 469 """ 470 Gets the feedback version of this Context's details, or just the 471 normal details if there is no feedback version. 472 """ 473 return self.description[1::2][-1] 474 475 def html_topic(self, in_feedback=False): 476 """ 477 Returns an HTML string representing just this context object as a 478 div.topic, without including information about dependencies. If 479 in_feedback is given as True, the feedback version of the topic 480 and details is shown instead of the normal (rubric) version. 481 482 Details are included behind a help button. 483 """ 484 if in_feedback: 485 topic = self.feedback_topic() 486 details = self.feedback_details() 487 else: 488 topic = self.rubric_topic() 489 details = self.rubric_details() 490 return '<div class="topic">{}</div>'.format( 491 topic + ' ' + html_tools.create_help_button(details) 492 ) 493 494 def html_context_tree(self, in_feedback=False): 495 """ 496 Produces an HTML string which shows this context and those that 497 it depends on in a tree structure, with dependencies nested 498 inside the contexts that depend on them. In the special case of a 499 stick, a different format is used. 500 501 If in_feedback is given as true, the feedback topic and details 502 values from the description are used if present, instead of the 503 normal (rubric) topic and details. 504 """ 505 if self.deps_are_a_stick(): 506 if self.depends: 507 dep_chain = ( 508 '(depends on <div class="context_depends">{}</div>)' 509 ).format( 510 self.depends[0].html_context_tree(in_feedback) 511 # there is only one 512 ) 513 return ( 514 '<details class="context">\n<summary>{}</summary>\n' 515 + '{}\n</details>' 516 ).format( 517 self.html_topic(in_feedback), 518 dep_chain 519 ) 520 else: 521 return '<div class="context">\n{}\n</div>'.format( 522 self.html_topic(in_feedback) 523 ) 524 else: 525 # Dependencies are a tree... 526 dep_entries = '<br>\n'.join( 527 dep.html_context_tree(in_feedback) 528 for dep in self.depends 529 ) 530 depends_full = ( 531 '<div class="context_depends">\n' 532 + 'Depends on:<br>\n' 533 + '{}\n' 534 + '</div>' 535 ).format(dep_entries) if self.depends else '' 536 return ( 537 '<details class="context">\n<summary>{}</summary>\n' 538 + '{}\n</details>' 539 ).format( 540 self.html_topic(in_feedback), 541 depends_full 542 ) 543 544 def html_representation(self, base_context): 545 """ 546 Builds an HTML representation of this context's result using the 547 display_product function. A base context is required because 548 create is used to fetch the current value (or build a new one). 549 550 If context creation fails, the result will be a string describing 551 the error. 552 """ 553 try: 554 return self.display_product(self.create(base_context)) 555 except context_utils.ContextCreationError as e: 556 tb = html_tools.html_traceback(e) 557 return f"""\ 558<div class="context_error"> 559 <h3>An error was encountered while attempting to run this test.</h3> 560{tb} 561</div> 562""" 563 564 def warnings(self, base_context): 565 """ 566 Returns a list of HTML strings representing warnings generated by 567 this context (excluding warnings generated by contexts this one 568 depends on). A base context is required because we need to 569 generate the context value to see if there are warnings, although 570 a cached value will be used in most cases. Returns an empty list 571 when no warnings have been generated. 572 """ 573 try: 574 ctx = self.create(base_context) 575 return ctx.get("warnings", []) 576 except context_utils.ContextCreationError as e: 577 if isinstance(e.cause, context_utils.ContextCreationError): 578 # Error comes from a dependency, not directly from this 579 # context, so report nothing to avoid duplicating 580 # warnings 581 return [] 582 else: 583 # Error comes from this context, so report it 584 tb = html_tools.html_traceback(e) 585 return [ f"Error during context creation:<br>{tb}" ]
Represents some kind of product of submitted code, like an AST tree, or produced output. Contexts can specialize each other, so for example a regular expression could be used to transform a context containing all output from a program into a context containing just a single line of output. Context objects organize context_builder functions into hierarchies, and manage caching of their results.
A Context
object also needs to know how to render its results into
HTML to display to the user as part of the feedback process.
A Context
object, via its context_builder function, knows how to
produce, on demand, a dictionary mapping context keys to values.
The base context (and therefore all derived contexts) will always
include the following potluck.context_utils.BASE_CONTEXT_SLOTS
:
- "task_info": A task info dictionary for the current task, which
will have the following keys:
- "id": The task ID.
- "specification": the specification module for this task.
- "target": The default file or folder to grade in a submission.
- "title": The title of this task.
- "desc": The short description of this task.
- "reference_cache_file": The filename where reference values may be cached.
- "ignore_cache": Whether or not to ignore the cache (might not be present; ignore cache only if present and truthy).
- "username": The user who submitted the code we're evaluating.
- "submission_root": The root directory within which the submission we're evaluating can be found.
- "default_file": The official name of the default file to evaluate.
- "actual_file": The actual name of the default file as submitted.
- "tests_submission_root": The root directory where the test's we're validating can be found (present during validation).
- "default_tests_file": The official name of the default tests file to validate.
- "actual_tests_file": The actual name of the tests file as submitted.
When a context is associated with a specific goal, the context dictionary will also always contain the following extra slots:
- "goal_id": The unique-per-task identifier string for the goal that this context is associated with.
- "which_context": The index of this context within the goal's testing contexts. Along with the task ID and goal ID, this can be used to build a unique identifier for the context. TODO: Context IDs?
The typical slots of derived context dictionaries include:
- "filename": The file name (without path) of the file we're evaluating.
- "file_path": The full absolute path to the file we're evaluating.
- "source": A string containing the source code of the submitted module.
- "parse_errors": A list of Exception objects which were generated but ignored when parsing the submitted code.
- "defs": A mapping from function names to AST nodes covering
every
def
node in the submitted code (but note that functions with identical names will shadow each other in this map). - "scope": an AST node to check within (e.g., for a function definition check). See ImplementationCheck for an example of how these contexts are created and used.
- "docstrings": A mapping from function names to documentation strings for those functions.
- "top_scope": The top-level AST node for the submitted code.
- "module": a loaded (submitted or solution) module (for a suite of tests that relies on the code).
- "value": arbitrary Python value returned from a function call.
- "output": a string containing output from tested code. This may be the full output, or may be filtered by additional Context objects to contain just part of the full output.
- "trace": a trace list recording certain function calls made
during a test, along with arbitrary state snapshots (see
potluck.harness.tracing_function_calls
). - "image": a Pillow Image object holding a captured image.
- "audio": a dictionary with "mimetype", "data", and optionally "label" slots, holding captured audio data in binary form. The mimetype should indicate a MIME type for the data, while the "data" slot holds a bytes object containing the raw data in that format. The label slot holds a string used to label the audio in output to users.
- "output_filename": the filename for an output file written to by testing code.
- "output_file_contents": a string containing the contents of a file that was written to by tested code.
- "expectations": a list of dictionaries defining expectations
established using the
optimism
module (only present if that module is available) Each has the following keys:- "tag" A string indicating the file name and line number where this expectation was established
- "case" The test case info. This is a dictionary with "result",
"output", and "context" slots, holding the result value,
printed output, and context details for a test case (see
optimism.get_my_context
for details of the "context" value). - "type" The type of expectation: "result", "output", or "custom".
- "value" The expected value (or output fragment, or the checker function, depending on the expectation type).
- Other keys not defined here may be established and/or used by certain Context or Goal classes.
- Versions of the keys above with "ref_" as a prefix hold equivalent values derived from solution instead of submitted code.
Note: For various reasons, the context building process creates shallow copies of contexts, not deep copies (the context values, such as modules, are often not deep-copyable). Accordingly, it is possible for changes to some sub-component of a context to be seen by other places where that context is being used, and thus YOU SHOULD NEVER MUTATE A CONTEXT VALUE. Context builder functions may safely synthesize new values based on old ones, and may freely update context keys with new values, but should not mutate the values under those keys.
216 def __init__( 217 self, 218 description=( 219 "UNSPECIFIED TOPIC", 220 "UNSPECIFIED DETAILS", 221 "UNSPECIFIED FULL TOPIC", 222 "UNSPECIFIED FULL DETAILS" 223 ), 224 builder=None, 225 display_product=generic_context_displayer, 226 depends=None, 227 failure_explanation=None, 228 cache_values=True, 229 base=None, 230 hidden=False, 231 generate_warnings=False 232 ): 233 """ 234 You must supply a description pair (topic + details), triple 235 (topic + details + feedback topic), or quad (topic + details + 236 feedback topic + feedback details). Each item must be an HTML 237 string to be displayed to the students as part of the rubric; the 238 feedback versions if provided are used instead of the originals 239 when generating a feedback document as opposed to a blank rubric. 240 241 You must also supply a builder function, to be run when this 242 context is required. If depends is supplied, it should be a list 243 of Context objects, and before this context is established, each 244 of those contexts will be created and merged in turn, to 245 establish the prev_context argument to the builder function. The 246 builder function must accept one argument (a context dictionary) 247 and must return a dictionary of any new or modified keys that it 248 wants to update in that context dictionary. 249 250 You may supply a failure_explanation, which can be either a 251 string, or a function that will be given the context in which the 252 failure occurred and an Exception object (with an html_tb 253 attached) and expected to return a string. This will be used to 254 set the error message for a ContextCreationError thrown when the 255 context_builder crashes, and that can ultimately become part of 256 an explanation for a failed goal. 257 258 If cache_values is given as False, the context_builder function 259 will be re-run every time this context is requested, but by 260 default, the result will be run only when one of our dependencies 261 has a newer timestamp than our cached value. 262 263 To supply seed values to bootstrap context creation, a Context 264 may have a base value, which is used to start the 265 dependency-merging process to provide a context for its builder. 266 267 If hidden is given as True, this Context will not show up in the 268 Contexts list, and will only be visible as a context associated 269 with a goal when that goal is evaluated (use with care). 270 271 If generate_warnings is set to True (default is False) then 272 issues with context creation of this context (but not with 273 creation of dependencies) will be logged as warnings in 274 `list_and_render_contexts` output. 275 """ 276 self.description = description 277 if builder is None: 278 raise ValueError( 279 "Must specify a context builder when creating a Context." 280 ) 281 self.builder = builder 282 self.display_product = display_product 283 self.depends = depends or [] 284 self.failure_explanation = failure_explanation 285 self.cache_values = cache_values 286 self.base = base or {} 287 self.hidden = hidden 288 self.generate_warnings = generate_warnings 289 290 self.cached_value = None 291 self.cache_timestamp = None 292 293 self.working_from = None 294 # which base context we're currently working from, for better 295 # exception reporting
You must supply a description pair (topic + details), triple (topic + details + feedback topic), or quad (topic + details + feedback topic + feedback details). Each item must be an HTML string to be displayed to the students as part of the rubric; the feedback versions if provided are used instead of the originals when generating a feedback document as opposed to a blank rubric.
You must also supply a builder function, to be run when this context is required. If depends is supplied, it should be a list of Context objects, and before this context is established, each of those contexts will be created and merged in turn, to establish the prev_context argument to the builder function. The builder function must accept one argument (a context dictionary) and must return a dictionary of any new or modified keys that it wants to update in that context dictionary.
You may supply a failure_explanation, which can be either a string, or a function that will be given the context in which the failure occurred and an Exception object (with an html_tb attached) and expected to return a string. This will be used to set the error message for a ContextCreationError thrown when the context_builder crashes, and that can ultimately become part of an explanation for a failed goal.
If cache_values is given as False, the context_builder function will be re-run every time this context is requested, but by default, the result will be run only when one of our dependencies has a newer timestamp than our cached value.
To supply seed values to bootstrap context creation, a Context may have a base value, which is used to start the dependency-merging process to provide a context for its builder.
If hidden is given as True, this Context will not show up in the Contexts list, and will only be visible as a context associated with a goal when that goal is evaluated (use with care).
If generate_warnings is set to True (default is False) then
issues with context creation of this context (but not with
creation of dependencies) will be logged as warnings in
list_and_render_contexts
output.
323 def changed_at(self): 324 """ 325 Returns the timestamp of the most recent update to our cached 326 context dictionary, so that contexts that depend on this one can 327 know if they need to update themselves. May return None if no 328 cached value has been created yet. 329 """ 330 return self.cache_timestamp
Returns the timestamp of the most recent update to our cached context dictionary, so that contexts that depend on this one can know if they need to update themselves. May return None if no cached value has been created yet.
332 def clear_cache(self): 333 """ 334 Removes cached info & resets cache timestamp. 335 """ 336 self.cached_value = None 337 self.cache_timestamp = None
Removes cached info & resets cache timestamp.
339 def burn_cache(self): 340 """ 341 Clears our cache, and burns caches of our dependencies. 342 """ 343 self.clear_cache() 344 for dep in self.depends: 345 dep.burn_cache()
Clears our cache, and burns caches of our dependencies.
347 def create(self, base_context): 348 """ 349 Creates and returns a context dictionary, using our builder 350 function. If caching is enabled, a (shallow copy of a) cached 351 value will be returned if we think that it was created after any 352 changes in our dependencies. 353 354 A base context is required, and should be a dictionary; normally 355 it should have all the `potluck.context_utils.BASE_CONTEXT_SLOTS` 356 already populated. 357 358 The resulting context will have a special "__builder__" slot 359 which contains a reference to this `Context` object. Of course, 360 this slot will get overwritten by each context in a chain, so it 361 only stores the last builder to touch the context (but see below). 362 363 The resulting context will have a special "__unmerged__" slot 364 which contains a list of context dictionaries for each dependency 365 of this context, holding the context state created by just that 366 dependency before merging with later dependencies. This can be 367 used to retrieve separate values for the same slot from multiple 368 dependencies, where only the last of those values would normally 369 be retained. Those context dictionaries will of course have 370 "__builder__" and "__unmerged__" slots, so the full chain of 371 `Context` objects responsible for a given context can be 372 recursively retrieved if necessary. 373 """ 374 rerun = False 375 if not self.cache_values: 376 rerun = True 377 else: 378 # Check whether we even have a result yet: 379 if self.cache_timestamp is None: 380 rerun = True 381 else: 382 # Check timestamps of our dependencies 383 for dep in self.depends: 384 when = dep.changed_at() 385 if when is None or when > self.cache_timestamp: 386 rerun = True 387 break 388 389 if not rerun: 390 # Return a shallow copy of our cached value 391 result = copy.copy(base_context) 392 result.update(self.cached_value) 393 return result 394 else: 395 # Create new cached value and update our timestamp 396 self.working_from = base_context 397 prev_context = copy.copy(base_context) 398 prev_context.update(self.base) 399 unmerged = [] 400 for dep in self.depends: 401 try: 402 dep_context = dep.create(base_context) 403 prev_context.update(dep_context) 404 unmerged.append(dep_context) 405 except Exception as e: 406 e.html_tb = html_tools.html_traceback( 407 linkable=context_utils.linkmap(prev_context) 408 ) 409 raise context_utils.ContextCreationError( 410 self, 411 "Dependency failed.", 412 e 413 ) 414 prev_context["__unmerged__"] = unmerged 415 prev_context["__builder__"] = self 416 417 try: 418 our_results = self.builder(prev_context) 419 prev_context.update(our_results) 420 self.cached_value = prev_context 421 except Exception as e: 422 e.html_tb = html_tools.html_traceback( 423 linkable=context_utils.linkmap(prev_context) 424 ) 425 if isinstance(self.failure_explanation, str): 426 msg = self.failure_explanation 427 elif self.failure_explanation: 428 msg = self.failure_explanation(prev_context, e) 429 else: 430 msg = "Test setup failed." 431 raise context_utils.ContextCreationError(self, msg, e) 432 self.cache_timestamp = time.time() 433 # Return shallow copy of cached value 434 result = {} 435 result.update(self.cached_value) 436 return result
Creates and returns a context dictionary, using our builder function. If caching is enabled, a (shallow copy of a) cached value will be returned if we think that it was created after any changes in our dependencies.
A base context is required, and should be a dictionary; normally
it should have all the potluck.context_utils.BASE_CONTEXT_SLOTS
already populated.
The resulting context will have a special "__builder__" slot
which contains a reference to this Context
object. Of course,
this slot will get overwritten by each context in a chain, so it
only stores the last builder to touch the context (but see below).
The resulting context will have a special "__unmerged__" slot
which contains a list of context dictionaries for each dependency
of this context, holding the context state created by just that
dependency before merging with later dependencies. This can be
used to retrieve separate values for the same slot from multiple
dependencies, where only the last of those values would normally
be retained. Those context dictionaries will of course have
"__builder__" and "__unmerged__" slots, so the full chain of
Context
objects responsible for a given context can be
recursively retrieved if necessary.
438 def deps_are_a_stick(self): 439 """ 440 Returns true if the transitive dependencies of this context form 441 a stick, not a real tree (i.e., each context, including this one, 442 has exactly 1 or 0 dependencies). 443 """ 444 return ( 445 len(self.depends) in (0, 1) 446 and all(dep.deps_are_a_stick for dep in self.depends) 447 )
Returns true if the transitive dependencies of this context form a stick, not a real tree (i.e., each context, including this one, has exactly 1 or 0 dependencies).
449 def rubric_topic(self): 450 """ 451 Gets the rubric version of this Context's topic. 452 """ 453 return self.description[0]
Gets the rubric version of this Context's topic.
455 def rubric_details(self): 456 """ 457 Gets the rubric version of this Context's details. 458 """ 459 return self.description[1]
Gets the rubric version of this Context's details.
461 def feedback_topic(self): 462 """ 463 Gets the feedback version of this Context's topic, or just the 464 normal topic if there is no feedback version. 465 """ 466 return self.description[::2][-1]
Gets the feedback version of this Context's topic, or just the normal topic if there is no feedback version.
468 def feedback_details(self): 469 """ 470 Gets the feedback version of this Context's details, or just the 471 normal details if there is no feedback version. 472 """ 473 return self.description[1::2][-1]
Gets the feedback version of this Context's details, or just the normal details if there is no feedback version.
475 def html_topic(self, in_feedback=False): 476 """ 477 Returns an HTML string representing just this context object as a 478 div.topic, without including information about dependencies. If 479 in_feedback is given as True, the feedback version of the topic 480 and details is shown instead of the normal (rubric) version. 481 482 Details are included behind a help button. 483 """ 484 if in_feedback: 485 topic = self.feedback_topic() 486 details = self.feedback_details() 487 else: 488 topic = self.rubric_topic() 489 details = self.rubric_details() 490 return '<div class="topic">{}</div>'.format( 491 topic + ' ' + html_tools.create_help_button(details) 492 )
Returns an HTML string representing just this context object as a div.topic, without including information about dependencies. If in_feedback is given as True, the feedback version of the topic and details is shown instead of the normal (rubric) version.
Details are included behind a help button.
494 def html_context_tree(self, in_feedback=False): 495 """ 496 Produces an HTML string which shows this context and those that 497 it depends on in a tree structure, with dependencies nested 498 inside the contexts that depend on them. In the special case of a 499 stick, a different format is used. 500 501 If in_feedback is given as true, the feedback topic and details 502 values from the description are used if present, instead of the 503 normal (rubric) topic and details. 504 """ 505 if self.deps_are_a_stick(): 506 if self.depends: 507 dep_chain = ( 508 '(depends on <div class="context_depends">{}</div>)' 509 ).format( 510 self.depends[0].html_context_tree(in_feedback) 511 # there is only one 512 ) 513 return ( 514 '<details class="context">\n<summary>{}</summary>\n' 515 + '{}\n</details>' 516 ).format( 517 self.html_topic(in_feedback), 518 dep_chain 519 ) 520 else: 521 return '<div class="context">\n{}\n</div>'.format( 522 self.html_topic(in_feedback) 523 ) 524 else: 525 # Dependencies are a tree... 526 dep_entries = '<br>\n'.join( 527 dep.html_context_tree(in_feedback) 528 for dep in self.depends 529 ) 530 depends_full = ( 531 '<div class="context_depends">\n' 532 + 'Depends on:<br>\n' 533 + '{}\n' 534 + '</div>' 535 ).format(dep_entries) if self.depends else '' 536 return ( 537 '<details class="context">\n<summary>{}</summary>\n' 538 + '{}\n</details>' 539 ).format( 540 self.html_topic(in_feedback), 541 depends_full 542 )
Produces an HTML string which shows this context and those that it depends on in a tree structure, with dependencies nested inside the contexts that depend on them. In the special case of a stick, a different format is used.
If in_feedback is given as true, the feedback topic and details values from the description are used if present, instead of the normal (rubric) topic and details.
544 def html_representation(self, base_context): 545 """ 546 Builds an HTML representation of this context's result using the 547 display_product function. A base context is required because 548 create is used to fetch the current value (or build a new one). 549 550 If context creation fails, the result will be a string describing 551 the error. 552 """ 553 try: 554 return self.display_product(self.create(base_context)) 555 except context_utils.ContextCreationError as e: 556 tb = html_tools.html_traceback(e) 557 return f"""\ 558<div class="context_error"> 559 <h3>An error was encountered while attempting to run this test.</h3> 560{tb} 561</div> 562"""
Builds an HTML representation of this context's result using the display_product function. A base context is required because create is used to fetch the current value (or build a new one).
If context creation fails, the result will be a string describing the error.
564 def warnings(self, base_context): 565 """ 566 Returns a list of HTML strings representing warnings generated by 567 this context (excluding warnings generated by contexts this one 568 depends on). A base context is required because we need to 569 generate the context value to see if there are warnings, although 570 a cached value will be used in most cases. Returns an empty list 571 when no warnings have been generated. 572 """ 573 try: 574 ctx = self.create(base_context) 575 return ctx.get("warnings", []) 576 except context_utils.ContextCreationError as e: 577 if isinstance(e.cause, context_utils.ContextCreationError): 578 # Error comes from a dependency, not directly from this 579 # context, so report nothing to avoid duplicating 580 # warnings 581 return [] 582 else: 583 # Error comes from this context, so report it 584 tb = html_tools.html_traceback(e) 585 return [ f"Error during context creation:<br>{tb}" ]
Returns a list of HTML strings representing warnings generated by this context (excluding warnings generated by contexts this one depends on). A base context is required because we need to generate the context value to see if there are warnings, although a cached value will be used in most cases. Returns an empty list when no warnings have been generated.
588def add_context_numbering(all_context_objs): 589 """ 590 Takes a list of Context objects and looks for objects with duplicated 591 short descriptions, adding numerical suffixes to these. 592 """ 593 # Map topics to lists of contexts 594 by_topic = {} 595 by_feedback_topic = {} 596 for ctx in all_context_objs: 597 topic = ctx.description[0] 598 by_topic.setdefault(topic, []).append(ctx) 599 600 if len(ctx.description) > 2: 601 by_feedback_topic.setdefault(ctx.description[2], []).append(ctx) 602 603 # Assign numbers to topics that are duplicated 604 for topic in by_topic: 605 contexts = by_topic[topic] 606 if len(contexts) > 1: 607 # these duplicates need numbering 608 for i, ctx in enumerate(contexts): 609 ctx.description = ( 610 ctx.description[0] + f" #{i+1}", 611 ) + ctx.description[1:] 612 613 # Repeat for feedback topics (numbering will hopefully be consistent 614 # because of consistent iteration order over all context objects, but 615 # if it's not, too bad. 616 for topic in by_feedback_topic: 617 contexts = by_feedback_topic[topic] 618 if len(contexts) > 1: 619 # these duplicates need numbering 620 for i, ctx in enumerate(contexts): 621 ctx.description = ctx.description[:2] + ( 622 ctx.description[2] + f" #{i+1}", 623 ) + ctx.description[3:]
Takes a list of Context objects and looks for objects with duplicated short descriptions, adding numerical suffixes to these.
626def build_context_graph(all_context_objs): 627 """ 628 Takes a list of Context objects and traces dependencies to build a 629 top-down rather than bottom-up graph. The graph consists of a list of 630 top-level node dictionaries with the following keys: 631 632 - context: The Context object for this node. 633 - children: A list containing pointers to the node dictionaries of 634 each Context that depends on this one. 635 - parents: A list containing pointers to the node dictionaries of 636 each Context that this one depends on. 637 - level: An integer indicating the longest chain of ancestors that 638 this node has (0 for nodes without dependencies). 639 640 The exact same node dictionary may appear as a child and/or parent of 641 multiple other nodes. 642 643 The ordering of children will be the same as the ordering of context 644 dependencies, while the ordering of the top-level nodes will match 645 the order that they occur within the given all_context_objs list. 646 """ 647 648 dict_cache = {} 649 650 def dict_for(cobj): 651 """ 652 Returns the dictionary for the given Context object, either by 653 creating it or remembering one already constructed during the 654 current construction process. For new encounters, the children 655 value will be an empty list. 656 """ 657 nonlocal dict_cache 658 if id(cobj) in dict_cache: 659 return dict_cache[id(cobj)] 660 else: 661 result = { "context": cobj, "children": [], "parents": [] } 662 dict_cache[id(cobj)] = result 663 return result 664 665 nodeps = [] 666 for ctx in all_context_objs: 667 d = dict_for(ctx) 668 if ctx.depends: 669 for dep in ctx.depends: 670 pd = dict_for(dep) 671 pd["children"].append(d) 672 d["parents"].append(pd) 673 else: 674 nodeps.append(d) 675 676 # Loop again and assign "level" values now that the graph is complete 677 for ctx in all_context_objs: 678 d = dict_for(ctx) 679 assign_cgraph_level(d) 680 681 return nodeps
Takes a list of Context objects and traces dependencies to build a top-down rather than bottom-up graph. The graph consists of a list of top-level node dictionaries with the following keys:
- context: The Context object for this node.
- children: A list containing pointers to the node dictionaries of each Context that depends on this one.
- parents: A list containing pointers to the node dictionaries of each Context that this one depends on.
- level: An integer indicating the longest chain of ancestors that this node has (0 for nodes without dependencies).
The exact same node dictionary may appear as a child and/or parent of multiple other nodes.
The ordering of children will be the same as the ordering of context dependencies, while the ordering of the top-level nodes will match the order that they occur within the given all_context_objs list.
684def assign_cgraph_level(node): 685 """ 686 Determines the level of a node in a contexts graph, which is 0 for 687 nodes without parents, and one plus the highest parent level for all 688 other nodes. 689 690 Store this value in the "level" slot of the node, and does the same 691 for any ancestor nodes encountered. 692 693 Returns an already-computed level if there is one, or else returns 694 the level value that it assigns to the node. 695 """ 696 if "level" in node: 697 return node["level"] 698 else: 699 if len(node["parents"]) == 0: 700 node["level"] = 0 701 else: 702 mp = 0 703 for pn in node["parents"]: 704 pl = assign_cgraph_level(pn) 705 if pl > mp: 706 mp = pl 707 node["level"] = mp + 1 708 709 return node["level"]
Determines the level of a node in a contexts graph, which is 0 for nodes without parents, and one plus the highest parent level for all other nodes.
Store this value in the "level" slot of the node, and does the same for any ancestor nodes encountered.
Returns an already-computed level if there is one, or else returns the level value that it assigns to the node.
712def render_context_graph(cgraph, in_feedback=False): 713 """ 714 Renders a context graph (see `build_context_graph`) into HTML. Uses a 715 simple algorithm which includes duplicate entries for nodes with 716 multiple dependencies. 717 718 The in_feedback argument controls whether context items should show 719 their full or redacted versions. 720 """ 721 result = '<ol class="context_list">' 722 for cnode in cgraph: 723 result += '\n<li>{}</li>'.format( 724 html_tools.build_html_details( 725 cnode["context"].html_topic(in_feedback), 726 render_context_graph(cnode["children"], in_feedback) 727 ) 728 if cnode.get("children") 729 else cnode["context"].html_topic(in_feedback) 730 ) 731 732 result += '\n</ol>' # close .context_list ol 733 return result
Renders a context graph (see build_context_graph
) into HTML. Uses a
simple algorithm which includes duplicate entries for nodes with
multiple dependencies.
The in_feedback argument controls whether context items should show their full or redacted versions.
736def list_and_render_contexts(cgraph, base_context=None): 737 """ 738 Transforms a context graph (see `build_context_graph`) into a list of 739 context summaries, which are dictionaries with the following keys: 740 741 - description: An HTML code string that describes the context item, 742 including a detailed description behind a help button. 743 - depends: A list containing integer indices into the entire context 744 list of the other contexts on which this context depends. Will be 745 empty for contexts without dependencies. 746 - level: An integer specifying how far this context should be 747 indented, which will be one greater than the maximum level of 748 contexts that this one depends on, starting with 0 for contexts 749 that have no dependencies. 750 - value: An HTML code string that displays or summarizes the value 751 produced by this context. This will be the result of the 752 context's display_product function run on the most recent result 753 of that context (or on a fresh result if caching is disabled for 754 the context). There may be a description of a context creation 755 error here if there is an error building the context value. 756 - warnings: A list of strings indicating context-based rather than 757 test-based warnings. In general contexts should only create 758 warnings in very specific circumstances, since tests are 759 responsible for most warnings, and might want to ignore issues 760 that a context could warn about. 761 762 The base_context argument is used to generate context results (will 763 only happen if cached results are not available or caching is 764 disabled for one or more contexts). If omitted or specified 765 explicitly as None, the result will have empty strings in each 766 "value" slot, and the descriptions will use redacted topic and 767 detail values; use this when generating a list for a rubric rather 768 than for a feedback document. 769 770 The contexts in the list will be ordered such that every context 771 comes later in the list than all contexts it depends on. 772 """ 773 result = [] 774 775 # Produce a flat list of nodes in an order that respects 776 # dependencies. 777 nodeslist = [] 778 779 # Processing queue and processed set 780 queue = cgraph[:] # place all top-level nodes in queue to start 781 processed = set() 782 783 while len(queue) > 0: 784 this = queue.pop(0) 785 786 depends = this["parents"] 787 if any(id(dep) not in processed for dep in depends): 788 continue 789 # we'll come back to this node later when its next 790 # unprocessed dependency queues it up 791 else: # no unprocessed dependencies 792 processed.add(id(this)) 793 nodeslist.append(this) 794 for child in reversed(this["children"]): 795 # Insert children at the front of the queue, doing so in 796 # reverse order so they end up in their natural order at 797 # the front 798 if id(child) not in processed: # should always be true? 799 queue.insert(0, child) 800 else: 801 raise NotImplementedError( 802 f"Unexpectedly encountered pre-processed child." 803 f"\n Parent is '{this['context'].html_topic()}'" 804 f"\n Child is '{child['context'].html_topic()}'" 805 ) 806 807 # A mapping from node IDs to their indices in the nodeslist 808 indexmap = { 809 id(cnode): idx 810 for idx, cnode in enumerate(nodeslist) 811 } 812 813 # Iterate over all nodes in order 814 for cnode in nodeslist: 815 ctx = cnode["context"] 816 result.append( 817 { 818 "description": ctx.html_topic( 819 base_context is not None 820 ), 821 "depends": [ 822 indexmap[id(child)] 823 for child in cnode["children"] 824 ], 825 "level": cnode["level"], 826 "value": ( 827 ctx.html_representation(base_context) 828 if base_context is not None 829 else "" 830 ), 831 "warnings": ( 832 ctx.warnings(base_context) 833 if base_context is not None and ctx.generate_warnings 834 else [] 835 ) 836 } 837 ) 838 839 return result
Transforms a context graph (see build_context_graph
) into a list of
context summaries, which are dictionaries with the following keys:
- description: An HTML code string that describes the context item, including a detailed description behind a help button.
- depends: A list containing integer indices into the entire context list of the other contexts on which this context depends. Will be empty for contexts without dependencies.
- level: An integer specifying how far this context should be indented, which will be one greater than the maximum level of contexts that this one depends on, starting with 0 for contexts that have no dependencies.
- value: An HTML code string that displays or summarizes the value produced by this context. This will be the result of the context's display_product function run on the most recent result of that context (or on a fresh result if caching is disabled for the context). There may be a description of a context creation error here if there is an error building the context value.
- warnings: A list of strings indicating context-based rather than test-based warnings. In general contexts should only create warnings in very specific circumstances, since tests are responsible for most warnings, and might want to ignore issues that a context could warn about.
The base_context argument is used to generate context results (will only happen if cached results are not available or caching is disabled for one or more contexts). If omitted or specified explicitly as None, the result will have empty strings in each "value" slot, and the descriptions will use redacted topic and detail values; use this when generating a list for a rubric rather than for a feedback document.
The contexts in the list will be ordered such that every context comes later in the list than all contexts it depends on.
846class AutoContext(Context): 847 """ 848 A `AutoContext` provides a way to automatically create dependencies 849 on common contexts without saving them in variables that have to get 850 passed around everywhere. When a `AutoContext` is created, it 851 registers itself as the current provider for certain context slots, 852 overwriting any previously-registered provider for those slots. 853 Then, another context may use the `auto` function to create a list 854 of `Context` objects to use as dependencies based on the slots it 855 needs; this list will be populated with the set of 856 most-recently-registered `AutoContext` objects for each slot 857 requested. 858 859 In addition to inheriting from `AutoContext`, `Context`s which 860 should be automatic must call their own `register` method and 861 provide it with one or more slot name strings as arguments. 862 """ 863 _registry = {} 864 _on_demand = {} 865 866 def reset(relevant_filename=None, relevant_tests_filename=None): 867 """ 868 This is a class method which resets the automatic contexts 869 registry, erasing all registered `Context` objects. Used prior 870 to loading a new task spec. 871 872 Note that this does not reset the on-demand factory functions 873 registry. 874 875 A `relevant_filename` may be provided to set the global 876 RELEVANT_FILENAME variable; setting this to the default filename 877 for the task we're about to load is helpful since it prevents the 878 spec from treating things created after an explicit-default file 879 context as different from things created before any file contexts 880 have been declared. If no `relevant_filename` is provided, the 881 global will be set back to its default of None. 882 883 A `relevant_tests_filename` may be provided and follows the same 884 logic as `relevant_filename`. 885 """ 886 global RELEVANT_FILENAME, RELEVANT_TESTS_FILENAME 887 RELEVANT_FILENAME = relevant_filename 888 RELEVANT_TESTS_FILENAME = relevant_tests_filename 889 AutoContext._registry = {} 890 891 def on_demand(factory, *slots): 892 """ 893 This class method registers a given factory function as the 894 on-demand provider of one or more slots. 895 896 The factory function must be able to run with no arguments and 897 must produce a `Context` object which can create the requested 898 slot(s). 899 900 Essentially, if `auto` is used to request an automatic context 901 for a slot, but no such context has been registered, an 902 on-demand factory function may be called to construct such a 903 context automatically in some simple cases. Calling this 904 function a second time re-using an old slot value will overwrite 905 the factory function for that slot. 906 """ 907 for slot in slots: 908 AutoContext._on_demand[slot] = factory 909 910 def register(self, *slots): 911 """ 912 Registers this auto context as the current provider for one or 913 more slots (named using strings). Subsequent calls to `auto` 914 that include one or more of those slots will include this object 915 in the resulting list. 916 917 The slots for which a context registers itself are stored in the 918 context's `_provides` attribute. 919 """ 920 for slot in slots: 921 AutoContext._registry[slot] = self 922 923 self._provides = slots 924 925 def refresh(self): 926 """ 927 Any `AutoContext` may call this method to re-register itself as 928 the current provider of all of its relevant slots. The slots to 929 register under are remembered from the initial call to 930 `register`. 931 """ 932 self.register(*self._provides)
A AutoContext
provides a way to automatically create dependencies
on common contexts without saving them in variables that have to get
passed around everywhere. When a AutoContext
is created, it
registers itself as the current provider for certain context slots,
overwriting any previously-registered provider for those slots.
Then, another context may use the auto
function to create a list
of Context
objects to use as dependencies based on the slots it
needs; this list will be populated with the set of
most-recently-registered AutoContext
objects for each slot
requested.
In addition to inheriting from AutoContext
, Context
s which
should be automatic must call their own register
method and
provide it with one or more slot name strings as arguments.
866 def reset(relevant_filename=None, relevant_tests_filename=None): 867 """ 868 This is a class method which resets the automatic contexts 869 registry, erasing all registered `Context` objects. Used prior 870 to loading a new task spec. 871 872 Note that this does not reset the on-demand factory functions 873 registry. 874 875 A `relevant_filename` may be provided to set the global 876 RELEVANT_FILENAME variable; setting this to the default filename 877 for the task we're about to load is helpful since it prevents the 878 spec from treating things created after an explicit-default file 879 context as different from things created before any file contexts 880 have been declared. If no `relevant_filename` is provided, the 881 global will be set back to its default of None. 882 883 A `relevant_tests_filename` may be provided and follows the same 884 logic as `relevant_filename`. 885 """ 886 global RELEVANT_FILENAME, RELEVANT_TESTS_FILENAME 887 RELEVANT_FILENAME = relevant_filename 888 RELEVANT_TESTS_FILENAME = relevant_tests_filename 889 AutoContext._registry = {}
This is a class method which resets the automatic contexts
registry, erasing all registered Context
objects. Used prior
to loading a new task spec.
Note that this does not reset the on-demand factory functions registry.
A relevant_filename
may be provided to set the global
RELEVANT_FILENAME variable; setting this to the default filename
for the task we're about to load is helpful since it prevents the
spec from treating things created after an explicit-default file
context as different from things created before any file contexts
have been declared. If no relevant_filename
is provided, the
global will be set back to its default of None.
A relevant_tests_filename
may be provided and follows the same
logic as relevant_filename
.
891 def on_demand(factory, *slots): 892 """ 893 This class method registers a given factory function as the 894 on-demand provider of one or more slots. 895 896 The factory function must be able to run with no arguments and 897 must produce a `Context` object which can create the requested 898 slot(s). 899 900 Essentially, if `auto` is used to request an automatic context 901 for a slot, but no such context has been registered, an 902 on-demand factory function may be called to construct such a 903 context automatically in some simple cases. Calling this 904 function a second time re-using an old slot value will overwrite 905 the factory function for that slot. 906 """ 907 for slot in slots: 908 AutoContext._on_demand[slot] = factory
This class method registers a given factory function as the on-demand provider of one or more slots.
The factory function must be able to run with no arguments and
must produce a Context
object which can create the requested
slot(s).
Essentially, if auto
is used to request an automatic context
for a slot, but no such context has been registered, an
on-demand factory function may be called to construct such a
context automatically in some simple cases. Calling this
function a second time re-using an old slot value will overwrite
the factory function for that slot.
910 def register(self, *slots): 911 """ 912 Registers this auto context as the current provider for one or 913 more slots (named using strings). Subsequent calls to `auto` 914 that include one or more of those slots will include this object 915 in the resulting list. 916 917 The slots for which a context registers itself are stored in the 918 context's `_provides` attribute. 919 """ 920 for slot in slots: 921 AutoContext._registry[slot] = self 922 923 self._provides = slots
Registers this auto context as the current provider for one or
more slots (named using strings). Subsequent calls to auto
that include one or more of those slots will include this object
in the resulting list.
The slots for which a context registers itself are stored in the
context's _provides
attribute.
925 def refresh(self): 926 """ 927 Any `AutoContext` may call this method to re-register itself as 928 the current provider of all of its relevant slots. The slots to 929 register under are remembered from the initial call to 930 `register`. 931 """ 932 self.register(*self._provides)
Any AutoContext
may call this method to re-register itself as
the current provider of all of its relevant slots. The slots to
register under are remembered from the initial call to
register
.
935def auto(*slots): 936 """ 937 This function returns a list of `Context` objects, suitable for use 938 as all or part of a `depends` value for the creation of a new 939 `Context`, and/or as all or part of a `test_in` value for the 940 creation of a new `potluck.rubrics.Goal`. It does this by looking for 941 the most recent `AutoContext` objects that have registered themselves 942 as providers of the given slot or slots (these are just strings). 943 944 If no `AutoContext` has registered itself for a given slot, but 945 there is an on-demand factory registered for that slot, the factory 946 function will be called to generate a `Context` to use as a 947 dependency (which should also end up registering the resulting 948 `Context` under the appropriate slot(s)). 949 950 If multiple slots are given and one `Context` is registered under 951 several of them, that `Context` will only appear in the resulting 952 list once. Likewise, if an on-demand factory function creates a 953 `Context` which registers itself for several slots, and one or more 954 other slots in that list are also being requested, the factory 955 function will not be re-run for the subsequent slots, and the 956 resulting `Context` will only appear in the result list once. 957 958 If no slots are given, this function returns an empty list. 959 960 If a slot is requested for which there is no currently-registered 961 `AutoContext` and for which there is no registered on-demand 962 `Context` factory, an `ContextError` will be raised. 963 """ 964 result = [] 965 for slot in slots: 966 if slot in AutoContext._registry: 967 match = AutoContext._registry[slot] 968 if match not in result: 969 result.append(match) 970 elif slot in AutoContext._on_demand: 971 created = AutoContext._on_demand[slot]() 972 if created not in result: 973 result.append(created) 974 else: 975 raise context_utils.ContextError( 976 f"Automatic context request for slot '{slot}' could not" 977 f" be fulfilled because no AutoContext was registered" 978 f" for that slot, and no on-demand Context factory" 979 f" function was available for that slot either." 980 ) 981 982 return result
This function returns a list of Context
objects, suitable for use
as all or part of a depends
value for the creation of a new
Context
, and/or as all or part of a test_in
value for the
creation of a new potluck.rubrics.Goal
. It does this by looking for
the most recent AutoContext
objects that have registered themselves
as providers of the given slot or slots (these are just strings).
If no AutoContext
has registered itself for a given slot, but
there is an on-demand factory registered for that slot, the factory
function will be called to generate a Context
to use as a
dependency (which should also end up registering the resulting
Context
under the appropriate slot(s)).
If multiple slots are given and one Context
is registered under
several of them, that Context
will only appear in the resulting
list once. Likewise, if an on-demand factory function creates a
Context
which registers itself for several slots, and one or more
other slots in that list are also being requested, the factory
function will not be re-run for the subsequent slots, and the
resulting Context
will only appear in the result list once.
If no slots are given, this function returns an empty list.
If a slot is requested for which there is no currently-registered
AutoContext
and for which there is no registered on-demand
Context
factory, an ContextError
will be raised.
990class FileContext(AutoContext): 991 """ 992 Establishes a 'filename' context slot that holds the name of a 993 specific file being evaluated. By default, the created `Context` has 994 no dependencies and is hidden. 995 996 The filename provided should be relative to the submission root, 997 which is either the directory where the target file exists, or the 998 target directory for multi-file submissions. If no target file is 999 identified, the default_file context slot value is used to target 1000 the submission's default file. 1001 1002 Establishes 'ref_*' slots for each normal slot it establishes. 1003 """ 1004 def __init__(self, target_file=None, depends=None, hidden=True): 1005 """ 1006 A target_file string specifies the path to the target file that 1007 this context will focus evaluation on, relative to the 1008 submission root. If not provided, the "default_file" context 1009 slot value will be used. 1010 1011 Both 'filename' and 'file_path' context slots will be 1012 established by this builder; the former holds just the file (or 1013 directory) name of the target file, while the latter holds a 1014 full path to the file. 1015 1016 It's up to other `Context`s to make use of the slots established 1017 by this one. 1018 """ 1019 global RELEVANT_FILENAME 1020 # Set the relevant filename global, or fetch it 1021 if target_file is not None: 1022 RELEVANT_FILENAME = target_file 1023 elif RELEVANT_FILENAME is not None: 1024 target_file = RELEVANT_FILENAME 1025 1026 self.target_file = target_file 1027 1028 # First, create our context-builder function for this instance 1029 def establish_context(prev_context): 1030 """ 1031 Establishes 'filename' and 'file_path' slots based on the 1032 target_file value given to the `FileContext` that this is 1033 the context builder function for. Also establishes 1034 'ref_filename' and 'ref_file_path' slots pointing to the 1035 equivalent file in the solution code. 1036 """ 1037 task_info = context_utils.extract(prev_context, "task_info") 1038 soln_root = task_info["specification"].soln_path 1039 submission_root = context_utils.extract( 1040 prev_context, 1041 "submission_root" 1042 ) 1043 # Revise our description when we build our context if we're 1044 # forced to fetch the default file: 1045 file_target = self.target_file 1046 default_filename = context_utils.extract( 1047 prev_context, 1048 "default_file" 1049 ) 1050 if file_target is None: 1051 file_target = default_filename 1052 self.description = ( 1053 f"File '{file_target}'", 1054 f"We will evaluate your submitted file '{file_target}'.", 1055 ) 1056 if file_target == default_filename: 1057 actual_filename = context_utils.extract( 1058 prev_context, 1059 "actual_file" 1060 ) 1061 full_target = os.path.join( 1062 submission_root, 1063 actual_filename 1064 ) 1065 # Even if we're grading a custom-named file, the solution 1066 # won't have a custom name... 1067 ref_target = os.path.abspath( 1068 os.path.join(soln_root, default_filename) 1069 ) 1070 target_filename = default_filename 1071 else: 1072 # TODO: How can we properly handle ref targets for 1073 # custom-named files here? 1074 full_target = os.path.join(submission_root, file_target) 1075 ref_target = os.path.abspath( 1076 os.path.join(soln_root, file_target) 1077 ) 1078 _, target_filename = os.path.split(full_target) 1079 1080 if not os.path.exists(submission_root): 1081 # If the submission root directory is missing... 1082 raise context_utils.ContextCreationError( 1083 self, 1084 ( 1085 f"Submission root directory" 1086 f" '{submission_root}' does not exist." 1087 ) 1088 ) 1089 elif not os.path.exists(full_target): 1090 # If the target file is missing 1091 raise context_utils.ContextCreationError( 1092 self, 1093 ( 1094 f"Target submission file" 1095 f" '{full_target}' does not exist." 1096 ) 1097 ) 1098 elif not os.path.exists(ref_target): 1099 # If there is no equivalent solution file 1100 raise context_utils.ContextCreationError( 1101 self, 1102 f"No solution file '{ref_target}' is available." 1103 ) 1104 # else both submission root and full target exist 1105 1106 return { 1107 "filename": target_filename, 1108 "file_path": os.path.abspath(full_target), 1109 "ref_filename": target_filename, 1110 "ref_file_path": os.path.abspath(ref_target) 1111 } 1112 1113 # Now we can call the super-class init with the context builder 1114 # function we just created. 1115 super().__init__( 1116 description=( 1117 f"File '{target_file}'", 1118 f"We will evaluate your submitted file '{target_file}'." 1119 ), 1120 builder=establish_context, 1121 display_product=lambda context: ( 1122 f"Evaluating '{context['filename']}'" 1123 ), 1124 depends=depends, 1125 hidden=hidden 1126 ) 1127 1128 # Finally, we can register ourselves as an auto-context for the 1129 # "filename" and "file_path" slots. 1130 self.register( 1131 "filename", 1132 "file_path", 1133 "ref_filename", 1134 "ref_file_path" 1135 )
Establishes a 'filename' context slot that holds the name of a
specific file being evaluated. By default, the created Context
has
no dependencies and is hidden.
The filename provided should be relative to the submission root, which is either the directory where the target file exists, or the target directory for multi-file submissions. If no target file is identified, the default_file context slot value is used to target the submission's default file.
Establishes 'ref_*' slots for each normal slot it establishes.
1004 def __init__(self, target_file=None, depends=None, hidden=True): 1005 """ 1006 A target_file string specifies the path to the target file that 1007 this context will focus evaluation on, relative to the 1008 submission root. If not provided, the "default_file" context 1009 slot value will be used. 1010 1011 Both 'filename' and 'file_path' context slots will be 1012 established by this builder; the former holds just the file (or 1013 directory) name of the target file, while the latter holds a 1014 full path to the file. 1015 1016 It's up to other `Context`s to make use of the slots established 1017 by this one. 1018 """ 1019 global RELEVANT_FILENAME 1020 # Set the relevant filename global, or fetch it 1021 if target_file is not None: 1022 RELEVANT_FILENAME = target_file 1023 elif RELEVANT_FILENAME is not None: 1024 target_file = RELEVANT_FILENAME 1025 1026 self.target_file = target_file 1027 1028 # First, create our context-builder function for this instance 1029 def establish_context(prev_context): 1030 """ 1031 Establishes 'filename' and 'file_path' slots based on the 1032 target_file value given to the `FileContext` that this is 1033 the context builder function for. Also establishes 1034 'ref_filename' and 'ref_file_path' slots pointing to the 1035 equivalent file in the solution code. 1036 """ 1037 task_info = context_utils.extract(prev_context, "task_info") 1038 soln_root = task_info["specification"].soln_path 1039 submission_root = context_utils.extract( 1040 prev_context, 1041 "submission_root" 1042 ) 1043 # Revise our description when we build our context if we're 1044 # forced to fetch the default file: 1045 file_target = self.target_file 1046 default_filename = context_utils.extract( 1047 prev_context, 1048 "default_file" 1049 ) 1050 if file_target is None: 1051 file_target = default_filename 1052 self.description = ( 1053 f"File '{file_target}'", 1054 f"We will evaluate your submitted file '{file_target}'.", 1055 ) 1056 if file_target == default_filename: 1057 actual_filename = context_utils.extract( 1058 prev_context, 1059 "actual_file" 1060 ) 1061 full_target = os.path.join( 1062 submission_root, 1063 actual_filename 1064 ) 1065 # Even if we're grading a custom-named file, the solution 1066 # won't have a custom name... 1067 ref_target = os.path.abspath( 1068 os.path.join(soln_root, default_filename) 1069 ) 1070 target_filename = default_filename 1071 else: 1072 # TODO: How can we properly handle ref targets for 1073 # custom-named files here? 1074 full_target = os.path.join(submission_root, file_target) 1075 ref_target = os.path.abspath( 1076 os.path.join(soln_root, file_target) 1077 ) 1078 _, target_filename = os.path.split(full_target) 1079 1080 if not os.path.exists(submission_root): 1081 # If the submission root directory is missing... 1082 raise context_utils.ContextCreationError( 1083 self, 1084 ( 1085 f"Submission root directory" 1086 f" '{submission_root}' does not exist." 1087 ) 1088 ) 1089 elif not os.path.exists(full_target): 1090 # If the target file is missing 1091 raise context_utils.ContextCreationError( 1092 self, 1093 ( 1094 f"Target submission file" 1095 f" '{full_target}' does not exist." 1096 ) 1097 ) 1098 elif not os.path.exists(ref_target): 1099 # If there is no equivalent solution file 1100 raise context_utils.ContextCreationError( 1101 self, 1102 f"No solution file '{ref_target}' is available." 1103 ) 1104 # else both submission root and full target exist 1105 1106 return { 1107 "filename": target_filename, 1108 "file_path": os.path.abspath(full_target), 1109 "ref_filename": target_filename, 1110 "ref_file_path": os.path.abspath(ref_target) 1111 } 1112 1113 # Now we can call the super-class init with the context builder 1114 # function we just created. 1115 super().__init__( 1116 description=( 1117 f"File '{target_file}'", 1118 f"We will evaluate your submitted file '{target_file}'." 1119 ), 1120 builder=establish_context, 1121 display_product=lambda context: ( 1122 f"Evaluating '{context['filename']}'" 1123 ), 1124 depends=depends, 1125 hidden=hidden 1126 ) 1127 1128 # Finally, we can register ourselves as an auto-context for the 1129 # "filename" and "file_path" slots. 1130 self.register( 1131 "filename", 1132 "file_path", 1133 "ref_filename", 1134 "ref_file_path" 1135 )
A target_file string specifies the path to the target file that this context will focus evaluation on, relative to the submission root. If not provided, the "default_file" context slot value will be used.
Both 'filename' and 'file_path' context slots will be established by this builder; the former holds just the file (or directory) name of the target file, while the latter holds a full path to the file.
It's up to other Context
s to make use of the slots established
by this one.
1146class TestsFileContext(AutoContext): 1147 """ 1148 Establishes a 'tests_filename' context slot that holds the name of a 1149 specific tests file being validated. By default, the created 1150 `Context` has no dependencies and is hidden. 1151 1152 The filename provided should be relative to the submission root, 1153 which is either the directory where the target tests file exists, or 1154 the target directory for multi-file submissions. If no target file is 1155 identified, the "default_tests_file" context slot value is used to 1156 target the submission's default tests file. 1157 1158 Establishes 'ref_*' slots for each normal slot it establishes. 1159 """ 1160 def __init__(self, target_tests_file=None, depends=None, hidden=True): 1161 """ 1162 A target_tests_file string specifies the path to the target tests 1163 file that this context will focus validation on, relative to the 1164 submission root. If not provided, the "default_tests_file" 1165 context slot value will be used. 1166 1167 Both 'tests_filename' and 'tests_file_path' context slots will be 1168 established by this builder; the former holds just the file (or 1169 directory) name of the target tests file, while the latter holds 1170 a full path to the file. 1171 1172 It's up to other `Context`s to make use of the slots established 1173 by this one. 1174 """ 1175 global RELEVANT_TESTS_FILENAME 1176 # Set the relevant filename global, or fetch it 1177 if target_tests_file is not None: 1178 RELEVANT_TESTS_FILENAME = target_tests_file 1179 elif RELEVANT_TESTS_FILENAME is not None: 1180 target_tests_file = RELEVANT_TESTS_FILENAME 1181 1182 self.target_tests_file = target_tests_file 1183 1184 # First, create our context-builder function for this instance 1185 def establish_context(prev_context): 1186 """ 1187 Establishes 'tests_filename' and 'tests_file_path' slots 1188 based on the target_tests_file value given to the 1189 `FileContext` that this is the context builder function for. 1190 Also establishes 'ref_tests_filename' and 1191 'ref_tests_file_path' slots pointing to the equivalent file 1192 in the solution code. 1193 """ 1194 task_info = context_utils.extract(prev_context, "task_info") 1195 soln_root = task_info["specification"].soln_path 1196 submission_root = context_utils.extract( 1197 prev_context, 1198 "tests_submission_root" 1199 ) 1200 # Revise our description when we build our context if we're 1201 # forced to fetch the default file: 1202 file_target = self.target_tests_file 1203 default_filename = context_utils.extract( 1204 prev_context, 1205 "default_tests_file" 1206 ) 1207 if file_target is None: 1208 file_target = default_filename 1209 self.description = ( 1210 f"Tests file '{file_target}'", 1211 ( 1212 f"We will validate your submitted tests file" 1213 f"'{file_target}'." 1214 ) 1215 ) 1216 if file_target == default_filename: 1217 actual_filename = context_utils.extract( 1218 prev_context, 1219 "actual_tests_file" 1220 ) 1221 full_target = os.path.join( 1222 submission_root, 1223 actual_filename 1224 ) 1225 # Even if we're grading a custom-named file, the solution 1226 # won't have a custom name... 1227 ref_target = os.path.abspath( 1228 os.path.join(soln_root, default_filename) 1229 ) 1230 target_filename = default_filename 1231 else: 1232 # TODO: How can we properly handle ref targets for 1233 # custom-named files here? 1234 full_target = os.path.join(submission_root, file_target) 1235 ref_target = os.path.abspath( 1236 os.path.join(soln_root, file_target) 1237 ) 1238 _, target_filename = os.path.split(full_target) 1239 1240 if not os.path.exists(submission_root): 1241 # If the submission root directory is missing... 1242 raise context_utils.ContextCreationError( 1243 self, 1244 ( 1245 f"Tests submission root directory" 1246 f" '{submission_root}' does not exist." 1247 ) 1248 ) 1249 elif not os.path.exists(full_target): 1250 # If the target file is missing 1251 raise context_utils.ContextCreationError( 1252 self, 1253 ( 1254 f"Target tests submission file" 1255 f" '{full_target}' does not exist." 1256 ) 1257 ) 1258 elif not os.path.exists(ref_target): 1259 # If there is no equivalent solution file 1260 raise context_utils.ContextCreationError( 1261 self, 1262 ( 1263 f"No solution tests file '{ref_target}' is" 1264 f" available." 1265 ) 1266 ) 1267 # else both submission root and full target exist 1268 1269 return { 1270 "tests_filename": target_filename, 1271 "tests_file_path": os.path.abspath(full_target), 1272 "ref_tests_filename": target_filename, 1273 "ref_tests_file_path": os.path.abspath(ref_target) 1274 } 1275 1276 # Now we can call the super-class init with the context builder 1277 # function we just created. 1278 super().__init__( 1279 description=( 1280 f"Tests file '{target_tests_file}'", 1281 ( 1282 f"We will validate your submitted tests file" 1283 f" '{target_tests_file}'." 1284 ) 1285 ), 1286 builder=establish_context, 1287 display_product=lambda context: ( 1288 f"Validating '{context['tests_filename']}'" 1289 ), 1290 depends=depends, 1291 hidden=hidden 1292 ) 1293 1294 # Finally, we can register ourselves as an auto-context for the 1295 # "filename" and "file_path" slots. 1296 self.register( 1297 "tests_filename", 1298 "tests_file_path", 1299 "ref_tests_filename", 1300 "ref_tests_file_path" 1301 )
Establishes a 'tests_filename' context slot that holds the name of a
specific tests file being validated. By default, the created
Context
has no dependencies and is hidden.
The filename provided should be relative to the submission root, which is either the directory where the target tests file exists, or the target directory for multi-file submissions. If no target file is identified, the "default_tests_file" context slot value is used to target the submission's default tests file.
Establishes 'ref_*' slots for each normal slot it establishes.
1160 def __init__(self, target_tests_file=None, depends=None, hidden=True): 1161 """ 1162 A target_tests_file string specifies the path to the target tests 1163 file that this context will focus validation on, relative to the 1164 submission root. If not provided, the "default_tests_file" 1165 context slot value will be used. 1166 1167 Both 'tests_filename' and 'tests_file_path' context slots will be 1168 established by this builder; the former holds just the file (or 1169 directory) name of the target tests file, while the latter holds 1170 a full path to the file. 1171 1172 It's up to other `Context`s to make use of the slots established 1173 by this one. 1174 """ 1175 global RELEVANT_TESTS_FILENAME 1176 # Set the relevant filename global, or fetch it 1177 if target_tests_file is not None: 1178 RELEVANT_TESTS_FILENAME = target_tests_file 1179 elif RELEVANT_TESTS_FILENAME is not None: 1180 target_tests_file = RELEVANT_TESTS_FILENAME 1181 1182 self.target_tests_file = target_tests_file 1183 1184 # First, create our context-builder function for this instance 1185 def establish_context(prev_context): 1186 """ 1187 Establishes 'tests_filename' and 'tests_file_path' slots 1188 based on the target_tests_file value given to the 1189 `FileContext` that this is the context builder function for. 1190 Also establishes 'ref_tests_filename' and 1191 'ref_tests_file_path' slots pointing to the equivalent file 1192 in the solution code. 1193 """ 1194 task_info = context_utils.extract(prev_context, "task_info") 1195 soln_root = task_info["specification"].soln_path 1196 submission_root = context_utils.extract( 1197 prev_context, 1198 "tests_submission_root" 1199 ) 1200 # Revise our description when we build our context if we're 1201 # forced to fetch the default file: 1202 file_target = self.target_tests_file 1203 default_filename = context_utils.extract( 1204 prev_context, 1205 "default_tests_file" 1206 ) 1207 if file_target is None: 1208 file_target = default_filename 1209 self.description = ( 1210 f"Tests file '{file_target}'", 1211 ( 1212 f"We will validate your submitted tests file" 1213 f"'{file_target}'." 1214 ) 1215 ) 1216 if file_target == default_filename: 1217 actual_filename = context_utils.extract( 1218 prev_context, 1219 "actual_tests_file" 1220 ) 1221 full_target = os.path.join( 1222 submission_root, 1223 actual_filename 1224 ) 1225 # Even if we're grading a custom-named file, the solution 1226 # won't have a custom name... 1227 ref_target = os.path.abspath( 1228 os.path.join(soln_root, default_filename) 1229 ) 1230 target_filename = default_filename 1231 else: 1232 # TODO: How can we properly handle ref targets for 1233 # custom-named files here? 1234 full_target = os.path.join(submission_root, file_target) 1235 ref_target = os.path.abspath( 1236 os.path.join(soln_root, file_target) 1237 ) 1238 _, target_filename = os.path.split(full_target) 1239 1240 if not os.path.exists(submission_root): 1241 # If the submission root directory is missing... 1242 raise context_utils.ContextCreationError( 1243 self, 1244 ( 1245 f"Tests submission root directory" 1246 f" '{submission_root}' does not exist." 1247 ) 1248 ) 1249 elif not os.path.exists(full_target): 1250 # If the target file is missing 1251 raise context_utils.ContextCreationError( 1252 self, 1253 ( 1254 f"Target tests submission file" 1255 f" '{full_target}' does not exist." 1256 ) 1257 ) 1258 elif not os.path.exists(ref_target): 1259 # If there is no equivalent solution file 1260 raise context_utils.ContextCreationError( 1261 self, 1262 ( 1263 f"No solution tests file '{ref_target}' is" 1264 f" available." 1265 ) 1266 ) 1267 # else both submission root and full target exist 1268 1269 return { 1270 "tests_filename": target_filename, 1271 "tests_file_path": os.path.abspath(full_target), 1272 "ref_tests_filename": target_filename, 1273 "ref_tests_file_path": os.path.abspath(ref_target) 1274 } 1275 1276 # Now we can call the super-class init with the context builder 1277 # function we just created. 1278 super().__init__( 1279 description=( 1280 f"Tests file '{target_tests_file}'", 1281 ( 1282 f"We will validate your submitted tests file" 1283 f" '{target_tests_file}'." 1284 ) 1285 ), 1286 builder=establish_context, 1287 display_product=lambda context: ( 1288 f"Validating '{context['tests_filename']}'" 1289 ), 1290 depends=depends, 1291 hidden=hidden 1292 ) 1293 1294 # Finally, we can register ourselves as an auto-context for the 1295 # "filename" and "file_path" slots. 1296 self.register( 1297 "tests_filename", 1298 "tests_file_path", 1299 "ref_tests_filename", 1300 "ref_tests_file_path" 1301 )
A target_tests_file string specifies the path to the target tests file that this context will focus validation on, relative to the submission root. If not provided, the "default_tests_file" context slot value will be used.
Both 'tests_filename' and 'tests_file_path' context slots will be established by this builder; the former holds just the file (or directory) name of the target tests file, while the latter holds a full path to the file.
It's up to other Context
s to make use of the slots established
by this one.
1314class SandboxContext(AutoContext): 1315 """ 1316 Establishes two sandbox directories to be used for running all code 1317 being tested, including the initial loading of the module itself. One 1318 directory is for the submitted code and a second is for the solution 1319 code. 1320 """ 1321 def __init__(self, depends=None, hidden=True): 1322 """ 1323 Creates a context which establishes two new unique sandbox 1324 directories. The context creation function places the full paths 1325 to those directories in the "sandbox" and "ref_sandbox" context 1326 slots. A list of dependencies may be provided, and hidden can be 1327 set to False if desired. 1328 """ 1329 self.dir = None 1330 self.ref_dir = None 1331 # TODO: Clean up these temporary directories rather than letting 1332 # Python do that on shutdown... 1333 1334 def establish_context(prev_context): 1335 """ 1336 Creates a new temporary directory and puts its absolute path 1337 in the "sandbox" context slot. Creates a second temporary 1338 directory and puts its path in the "ref_sandbox" slot. 1339 Copies helper files into both directories, and copies the 1340 actual solution file(s) into the reference sandbox. 1341 """ 1342 sub_root = context_utils.extract(prev_context, "submission_root") 1343 sub_file = context_utils.extract(prev_context, "actual_file") 1344 tinfo = context_utils.extract(prev_context, "task_info") 1345 spec = tinfo["specification"] 1346 1347 self.dir = tempfile.TemporaryDirectory( 1348 suffix="__tmp", 1349 dir=load.SANDBOX_DIR 1350 ) 1351 self.ref_dir = tempfile.TemporaryDirectory( 1352 suffix="__ref_tmp", 1353 dir=load.SANDBOX_DIR 1354 ) 1355 1356 # Set up the sandboxes 1357 for sb in [self.dir, self.ref_dir]: 1358 # Copy helper files into the sandbox if there are any 1359 # Note that we don't use symlinks here, because we don't 1360 # want destructive student code to modify files outside 1361 # the sandbox... 1362 # TODO: Use symlinks in places where we feel safe, 1363 # especially for large starter files!!! 1364 helpers = context_utils.sandbox_filemap(spec) 1365 if helpers is not None: 1366 for filepath in helpers: 1367 to = os.path.join(sb.name, helpers[filepath]) 1368 if os.path.isdir(filepath): 1369 shutil.copytree(filepath, to) 1370 else: 1371 shutil.copy(filepath, to) 1372 1373 # Copy the submitted target file/directory into the sandbox 1374 subm_target = os.path.join(sub_root, sub_file) 1375 sandbox_target = os.path.join(self.dir.name, tinfo["target"]) 1376 if os.path.isdir(subm_target): 1377 shutil.copytree(subm_target, sandbox_target) 1378 else: 1379 shutil.copy(subm_target, sandbox_target) 1380 1381 # Copy the target file/directory from the solution dir into 1382 # the ref sandbox 1383 soln_target = os.path.join(spec.soln_path, tinfo["target"]) 1384 sandbox_target = os.path.join(self.ref_dir.name, tinfo["target"]) 1385 if os.path.isdir(soln_target): 1386 shutil.copytree(soln_target, sandbox_target) 1387 else: 1388 shutil.copy(soln_target, sandbox_target) 1389 1390 return { 1391 "sandbox": os.path.abspath(self.dir.name), 1392 "ref_sandbox": os.path.abspath(self.ref_dir.name) 1393 } 1394 1395 # Now we can call the super-class init with the context builder 1396 # function we just created. 1397 super().__init__( 1398 description=( 1399 "Sandbox directories", 1400 ( 1401 "We will create sandbox directories for running" 1402 " your submitted code and the solution code." 1403 ) 1404 ), 1405 builder=establish_context, 1406 display_product=lambda context: ( 1407 "Running in a sandbox" 1408 ), 1409 depends=depends, 1410 hidden=hidden 1411 ) 1412 1413 # Finally, we can register ourselves as an auto-context for the 1414 # "filename" and "file_path" slots. 1415 self.register( 1416 "sandbox", 1417 "ref_sandbox" 1418 )
Establishes two sandbox directories to be used for running all code being tested, including the initial loading of the module itself. One directory is for the submitted code and a second is for the solution code.
1321 def __init__(self, depends=None, hidden=True): 1322 """ 1323 Creates a context which establishes two new unique sandbox 1324 directories. The context creation function places the full paths 1325 to those directories in the "sandbox" and "ref_sandbox" context 1326 slots. A list of dependencies may be provided, and hidden can be 1327 set to False if desired. 1328 """ 1329 self.dir = None 1330 self.ref_dir = None 1331 # TODO: Clean up these temporary directories rather than letting 1332 # Python do that on shutdown... 1333 1334 def establish_context(prev_context): 1335 """ 1336 Creates a new temporary directory and puts its absolute path 1337 in the "sandbox" context slot. Creates a second temporary 1338 directory and puts its path in the "ref_sandbox" slot. 1339 Copies helper files into both directories, and copies the 1340 actual solution file(s) into the reference sandbox. 1341 """ 1342 sub_root = context_utils.extract(prev_context, "submission_root") 1343 sub_file = context_utils.extract(prev_context, "actual_file") 1344 tinfo = context_utils.extract(prev_context, "task_info") 1345 spec = tinfo["specification"] 1346 1347 self.dir = tempfile.TemporaryDirectory( 1348 suffix="__tmp", 1349 dir=load.SANDBOX_DIR 1350 ) 1351 self.ref_dir = tempfile.TemporaryDirectory( 1352 suffix="__ref_tmp", 1353 dir=load.SANDBOX_DIR 1354 ) 1355 1356 # Set up the sandboxes 1357 for sb in [self.dir, self.ref_dir]: 1358 # Copy helper files into the sandbox if there are any 1359 # Note that we don't use symlinks here, because we don't 1360 # want destructive student code to modify files outside 1361 # the sandbox... 1362 # TODO: Use symlinks in places where we feel safe, 1363 # especially for large starter files!!! 1364 helpers = context_utils.sandbox_filemap(spec) 1365 if helpers is not None: 1366 for filepath in helpers: 1367 to = os.path.join(sb.name, helpers[filepath]) 1368 if os.path.isdir(filepath): 1369 shutil.copytree(filepath, to) 1370 else: 1371 shutil.copy(filepath, to) 1372 1373 # Copy the submitted target file/directory into the sandbox 1374 subm_target = os.path.join(sub_root, sub_file) 1375 sandbox_target = os.path.join(self.dir.name, tinfo["target"]) 1376 if os.path.isdir(subm_target): 1377 shutil.copytree(subm_target, sandbox_target) 1378 else: 1379 shutil.copy(subm_target, sandbox_target) 1380 1381 # Copy the target file/directory from the solution dir into 1382 # the ref sandbox 1383 soln_target = os.path.join(spec.soln_path, tinfo["target"]) 1384 sandbox_target = os.path.join(self.ref_dir.name, tinfo["target"]) 1385 if os.path.isdir(soln_target): 1386 shutil.copytree(soln_target, sandbox_target) 1387 else: 1388 shutil.copy(soln_target, sandbox_target) 1389 1390 return { 1391 "sandbox": os.path.abspath(self.dir.name), 1392 "ref_sandbox": os.path.abspath(self.ref_dir.name) 1393 } 1394 1395 # Now we can call the super-class init with the context builder 1396 # function we just created. 1397 super().__init__( 1398 description=( 1399 "Sandbox directories", 1400 ( 1401 "We will create sandbox directories for running" 1402 " your submitted code and the solution code." 1403 ) 1404 ), 1405 builder=establish_context, 1406 display_product=lambda context: ( 1407 "Running in a sandbox" 1408 ), 1409 depends=depends, 1410 hidden=hidden 1411 ) 1412 1413 # Finally, we can register ourselves as an auto-context for the 1414 # "filename" and "file_path" slots. 1415 self.register( 1416 "sandbox", 1417 "ref_sandbox" 1418 )
Creates a context which establishes two new unique sandbox directories. The context creation function places the full paths to those directories in the "sandbox" and "ref_sandbox" context slots. A list of dependencies may be provided, and hidden can be set to False if desired.
1429class TestsSandboxContext(AutoContext): 1430 """ 1431 Establishes a sandbox directory for validating tests. The process is 1432 largely the same as that used by SandboxContext, but a separate 1433 directory is used to prevent any possible interference between test 1434 validation and submission evaluation. 1435 """ 1436 def __init__(self, depends=None, hidden=True): 1437 """ 1438 Creates a context which establishes a new sandbox directory. The 1439 context creation function places the full path to this 1440 directory in the "tests_sandbox" context slot. A list of 1441 dependencies may be provided, and hidden can be set to False if 1442 desired. 1443 """ 1444 self.dir = None 1445 # TODO: Clean up this temporary directory rather than letting 1446 # Python do that on shutdown... 1447 1448 def establish_context(prev_context): 1449 """ 1450 Creates a new temporary directory and puts its absolute path 1451 in the "tests_sandbox" context slot. Copies helper files and 1452 the solution default target into this sandbox. 1453 """ 1454 tinfo = context_utils.extract(prev_context, "task_info") 1455 spec = tinfo["specification"] 1456 1457 self.dir = tempfile.TemporaryDirectory( 1458 suffix="__validation_tmp", 1459 dir=load.SANDBOX_DIR 1460 ) 1461 1462 # Copy helper files into the sandbox if there are any 1463 # Note that we don't use symlinks here, because we don't 1464 # want destructive student code to modify files outside 1465 # the sandbox... 1466 # TODO: Use symlinks in places where we feel safe, 1467 # especially for large starter files!!! 1468 helpers = context_utils.sandbox_filemap(spec) 1469 if helpers is not None: 1470 for filepath in helpers: 1471 to = os.path.join(self.dir.name, helpers[filepath]) 1472 if os.path.isdir(filepath): 1473 shutil.copytree(filepath, to) 1474 else: 1475 shutil.copy(filepath, to) 1476 1477 # Copy the target file/directory from the solution dir 1478 soln_target = os.path.join(spec.soln_path, tinfo["target"]) 1479 sandbox_target = os.path.join(self.dir.name, tinfo["target"]) 1480 if os.path.isdir(soln_target): 1481 shutil.copytree(soln_target, sandbox_target) 1482 else: 1483 shutil.copy(soln_target, sandbox_target) 1484 1485 return { "tests_sandbox": os.path.abspath(self.dir.name) } 1486 1487 # Now we can call the super-class init with the context builder 1488 # function we just created. 1489 super().__init__( 1490 description=( 1491 "Test validation sandbox", 1492 ( 1493 "We will create a sandbox directory for validating" 1494 " your submitted tests." 1495 ) 1496 ), 1497 builder=establish_context, 1498 display_product=lambda context: ( 1499 "Validating tests in a sandbox" 1500 ), 1501 depends=depends, 1502 hidden=hidden 1503 ) 1504 1505 # Finally, we can register ourselves as an auto-context for the 1506 # "filename" and "file_path" slots. 1507 self.register("tests_sandbox")
Establishes a sandbox directory for validating tests. The process is largely the same as that used by SandboxContext, but a separate directory is used to prevent any possible interference between test validation and submission evaluation.
1436 def __init__(self, depends=None, hidden=True): 1437 """ 1438 Creates a context which establishes a new sandbox directory. The 1439 context creation function places the full path to this 1440 directory in the "tests_sandbox" context slot. A list of 1441 dependencies may be provided, and hidden can be set to False if 1442 desired. 1443 """ 1444 self.dir = None 1445 # TODO: Clean up this temporary directory rather than letting 1446 # Python do that on shutdown... 1447 1448 def establish_context(prev_context): 1449 """ 1450 Creates a new temporary directory and puts its absolute path 1451 in the "tests_sandbox" context slot. Copies helper files and 1452 the solution default target into this sandbox. 1453 """ 1454 tinfo = context_utils.extract(prev_context, "task_info") 1455 spec = tinfo["specification"] 1456 1457 self.dir = tempfile.TemporaryDirectory( 1458 suffix="__validation_tmp", 1459 dir=load.SANDBOX_DIR 1460 ) 1461 1462 # Copy helper files into the sandbox if there are any 1463 # Note that we don't use symlinks here, because we don't 1464 # want destructive student code to modify files outside 1465 # the sandbox... 1466 # TODO: Use symlinks in places where we feel safe, 1467 # especially for large starter files!!! 1468 helpers = context_utils.sandbox_filemap(spec) 1469 if helpers is not None: 1470 for filepath in helpers: 1471 to = os.path.join(self.dir.name, helpers[filepath]) 1472 if os.path.isdir(filepath): 1473 shutil.copytree(filepath, to) 1474 else: 1475 shutil.copy(filepath, to) 1476 1477 # Copy the target file/directory from the solution dir 1478 soln_target = os.path.join(spec.soln_path, tinfo["target"]) 1479 sandbox_target = os.path.join(self.dir.name, tinfo["target"]) 1480 if os.path.isdir(soln_target): 1481 shutil.copytree(soln_target, sandbox_target) 1482 else: 1483 shutil.copy(soln_target, sandbox_target) 1484 1485 return { "tests_sandbox": os.path.abspath(self.dir.name) } 1486 1487 # Now we can call the super-class init with the context builder 1488 # function we just created. 1489 super().__init__( 1490 description=( 1491 "Test validation sandbox", 1492 ( 1493 "We will create a sandbox directory for validating" 1494 " your submitted tests." 1495 ) 1496 ), 1497 builder=establish_context, 1498 display_product=lambda context: ( 1499 "Validating tests in a sandbox" 1500 ), 1501 depends=depends, 1502 hidden=hidden 1503 ) 1504 1505 # Finally, we can register ourselves as an auto-context for the 1506 # "filename" and "file_path" slots. 1507 self.register("tests_sandbox")
Creates a context which establishes a new sandbox directory. The context creation function places the full path to this directory in the "tests_sandbox" context slot. A list of dependencies may be provided, and hidden can be set to False if desired.
1515class CodeContext(AutoContext): 1516 """ 1517 Requires "filename" and "file_path" slots (see `FileContext`), and 1518 establishes a "source" slot which contains the raw text of the 1519 target file, along with a "scope" slot which contains the parsed AST 1520 from the code. 1521 1522 If the code cannot be parsed due to a `SyntaxError` or the like, a 1523 `ContextCreationError` will be generated naming the parsing error as 1524 its cause, although note that `potluck.load.fix_parse` is used which 1525 will attempt to steamroll some kinds of parsing errors while 1526 generating associated warnings. 1527 """ 1528 def __init__(self, depends=None, hidden=False, prep=None): 1529 """ 1530 Dependencies are optional; if not specified `auto` will be used 1531 to fill them in. `hidden` may be provided; by default this 1532 context is not hidden. A `prep` function may be provided; it will 1533 be applied to the source code string and its result will be used 1534 instead of the original source. 1535 """ 1536 # First, create our context builder 1537 def establish_context(prev_context): 1538 """ 1539 Establishes the following context slots based on the 1540 "file_path" slot, by reading the indicated file: 1541 1542 - original_source: The raw file contents. 1543 - source: Possibly-edited (to steamroll syntax errors or by a 1544 prep function) file contents. 1545 - scope: An AST module node resulting from parsing the 1546 modified file contents. 1547 - top_scope: Same as above (but not designed to be modified). 1548 - parse_errors: A list of Exception objects that were 1549 'successfully' steamrolled by editing the source code. 1550 """ 1551 filename = context_utils.extract(prev_context, "filename") 1552 target = context_utils.extract(prev_context, "file_path") 1553 with open(target, 'r', encoding="utf-8") as fin: 1554 original_source = fin.read() 1555 1556 if prep: 1557 source = prep(original_source) 1558 else: 1559 source = original_source 1560 1561 try: 1562 fixed, node, errors = load.fix_parse(source, filename) 1563 except Exception as e: 1564 raise context_utils.ContextCreationError( 1565 self, 1566 f"Unable to parse submitted file '{filename}'.", 1567 cause=e 1568 ) 1569 1570 if node is None: 1571 raise context_utils.ContextCreationError( 1572 self, 1573 f"Unable to parse submitted file '{filename}'.", 1574 cause=errors[0] 1575 ) 1576 1577 result = { 1578 "original_source": original_source, 1579 "source": fixed, 1580 "scope": node, 1581 "top_scope": node, 1582 "parse_errors": errors 1583 } 1584 1585 # Report parsing issues as warnings 1586 if errors: 1587 result["warnings"] = [ 1588 ( 1589 "The following errors were encountered when parsing" 1590 + " your code:<br>" 1591 + html_tools.build_list( 1592 html_tools.html_traceback(e) 1593 for e in errors 1594 ) 1595 ) 1596 ] 1597 1598 return result 1599 1600 # Figure out if we need to use automatic dependencies: 1601 if depends is None: 1602 depends = auto("filename", "file_path") 1603 1604 # Now we can call the super constructor 1605 super().__init__( 1606 description=( 1607 "Code in the target file", 1608 ( 1609 "We will parse the code in the target file and pay" 1610 "attention to how it was written." 1611 ), 1612 "Code in the target file", 1613 ( 1614 "We parsed the code in the target file and paid" 1615 "attention to how it was written." 1616 ), 1617 ), 1618 builder=establish_context, 1619 display_product=lambda context: ( 1620 f"The code for '{context['filename']}' (shown elsewhere)." 1621 ), 1622 depends=depends, 1623 hidden=hidden, 1624 # Errors at this level need to be reported! 1625 generate_warnings=True 1626 ) 1627 1628 # Finally, register ourselves as an auto provider for the slots 1629 # that we generate: 1630 self.register( 1631 "original_source", 1632 "source", 1633 "scope", 1634 "top_scope", 1635 "parse_errors" 1636 )
Requires "filename" and "file_path" slots (see FileContext
), and
establishes a "source" slot which contains the raw text of the
target file, along with a "scope" slot which contains the parsed AST
from the code.
If the code cannot be parsed due to a SyntaxError
or the like, a
ContextCreationError
will be generated naming the parsing error as
its cause, although note that potluck.load.fix_parse
is used which
will attempt to steamroll some kinds of parsing errors while
generating associated warnings.
1528 def __init__(self, depends=None, hidden=False, prep=None): 1529 """ 1530 Dependencies are optional; if not specified `auto` will be used 1531 to fill them in. `hidden` may be provided; by default this 1532 context is not hidden. A `prep` function may be provided; it will 1533 be applied to the source code string and its result will be used 1534 instead of the original source. 1535 """ 1536 # First, create our context builder 1537 def establish_context(prev_context): 1538 """ 1539 Establishes the following context slots based on the 1540 "file_path" slot, by reading the indicated file: 1541 1542 - original_source: The raw file contents. 1543 - source: Possibly-edited (to steamroll syntax errors or by a 1544 prep function) file contents. 1545 - scope: An AST module node resulting from parsing the 1546 modified file contents. 1547 - top_scope: Same as above (but not designed to be modified). 1548 - parse_errors: A list of Exception objects that were 1549 'successfully' steamrolled by editing the source code. 1550 """ 1551 filename = context_utils.extract(prev_context, "filename") 1552 target = context_utils.extract(prev_context, "file_path") 1553 with open(target, 'r', encoding="utf-8") as fin: 1554 original_source = fin.read() 1555 1556 if prep: 1557 source = prep(original_source) 1558 else: 1559 source = original_source 1560 1561 try: 1562 fixed, node, errors = load.fix_parse(source, filename) 1563 except Exception as e: 1564 raise context_utils.ContextCreationError( 1565 self, 1566 f"Unable to parse submitted file '{filename}'.", 1567 cause=e 1568 ) 1569 1570 if node is None: 1571 raise context_utils.ContextCreationError( 1572 self, 1573 f"Unable to parse submitted file '{filename}'.", 1574 cause=errors[0] 1575 ) 1576 1577 result = { 1578 "original_source": original_source, 1579 "source": fixed, 1580 "scope": node, 1581 "top_scope": node, 1582 "parse_errors": errors 1583 } 1584 1585 # Report parsing issues as warnings 1586 if errors: 1587 result["warnings"] = [ 1588 ( 1589 "The following errors were encountered when parsing" 1590 + " your code:<br>" 1591 + html_tools.build_list( 1592 html_tools.html_traceback(e) 1593 for e in errors 1594 ) 1595 ) 1596 ] 1597 1598 return result 1599 1600 # Figure out if we need to use automatic dependencies: 1601 if depends is None: 1602 depends = auto("filename", "file_path") 1603 1604 # Now we can call the super constructor 1605 super().__init__( 1606 description=( 1607 "Code in the target file", 1608 ( 1609 "We will parse the code in the target file and pay" 1610 "attention to how it was written." 1611 ), 1612 "Code in the target file", 1613 ( 1614 "We parsed the code in the target file and paid" 1615 "attention to how it was written." 1616 ), 1617 ), 1618 builder=establish_context, 1619 display_product=lambda context: ( 1620 f"The code for '{context['filename']}' (shown elsewhere)." 1621 ), 1622 depends=depends, 1623 hidden=hidden, 1624 # Errors at this level need to be reported! 1625 generate_warnings=True 1626 ) 1627 1628 # Finally, register ourselves as an auto provider for the slots 1629 # that we generate: 1630 self.register( 1631 "original_source", 1632 "source", 1633 "scope", 1634 "top_scope", 1635 "parse_errors" 1636 )
Dependencies are optional; if not specified auto
will be used
to fill them in. hidden
may be provided; by default this
context is not hidden. A prep
function may be provided; it will
be applied to the source code string and its result will be used
instead of the original source.
1651class SolnCodeContext(AutoContext): 1652 """ 1653 Requires "ref_filename" and "ref_file_path" slots (see 1654 `FileContext`), and establishes a "ref_source" slot which contains 1655 the raw text of the equivalent file from the solution code, along 1656 with a "ref_scope" slot which contains the parsed AST from the 1657 solution code. Also establishes "ref_original_source" which may be 1658 different from "ref_source" when a prep function is used. 1659 1660 "task_info" and "submission_root" slots are also required, but those 1661 should always be present. 1662 1663 If the solution code cannot be parsed due to a `SyntaxError` or the 1664 like or because no equivalent solution file exists, a 1665 `ContextCreationError` will be generated naming the relevant error as 1666 its cause. 1667 """ 1668 def __init__(self, depends=None, hidden=False, prep=None): 1669 """ 1670 Dependencies are optional; if not specified `auto` will be used 1671 to fill them in. `hidden` may be provided; by default this 1672 context is not hidden. A `prep` function may be supplied, which 1673 will be given the source code and its return value will be used 1674 in place of the original source code. 1675 """ 1676 # First, create our context builder 1677 def establish_context(prev_context): 1678 """ 1679 Establishes the following context slots based on the 1680 "ref_file_path" slot, by reading the solution version of the 1681 indicated file: 1682 1683 ref_source: The source of the solution file. 1684 ref_scope: An AST module node resulting from parsing the 1685 solution file. 1686 ref_top_scope: As above, but won't be modified. 1687 """ 1688 soln_equivalent = context_utils.extract( 1689 prev_context, 1690 "ref_file_path" 1691 ) 1692 ref_filename = context_utils.extract( 1693 prev_context, 1694 "ref_filename" 1695 ) 1696 1697 if not os.path.isfile(soln_equivalent): 1698 raise context_utils.ContextCreationError( 1699 self, 1700 f"Target file {soln_equivalent} does not exist in the" 1701 f" solution directory." 1702 ) 1703 1704 with open(soln_equivalent, 'r', encoding="utf-8") as fin: 1705 contents = fin.read() 1706 1707 if prep: 1708 source = prep(contents) 1709 else: 1710 source = contents 1711 1712 try: 1713 node = mast.parse(source, filename=ref_filename) 1714 except Exception as e: 1715 raise context_utils.ContextCreationError( 1716 self, 1717 f"Unable to parse solution file '{soln_equivalent}'.", 1718 cause=e 1719 ) 1720 1721 return { 1722 "ref_original_source": contents, 1723 "ref_source": source, 1724 "ref_scope": node, 1725 "ref_top_scope": node 1726 } 1727 1728 # Figure out if we need to use automatic dependencies: 1729 if depends is None: 1730 depends = auto("ref_filename", "ref_file_path") 1731 1732 # Now we can call the super constructor 1733 super().__init__( 1734 description=( 1735 "Code in the solution file", 1736 "We will parse the code in the solution file.", 1737 ), 1738 builder=establish_context, 1739 display_product=lambda context: ( 1740 f"The solution code for '{context['filename']}'" 1741 f" (available after the revision period is over)." 1742 ), 1743 depends=depends, 1744 hidden=hidden 1745 ) 1746 1747 # Finally, register ourselves as an auto provider for the slots 1748 # that we generate: 1749 self.register( 1750 "ref_original_source", 1751 "ref_source", 1752 "ref_scope", 1753 "ref_top_scope" 1754 )
Requires "ref_filename" and "ref_file_path" slots (see
FileContext
), and establishes a "ref_source" slot which contains
the raw text of the equivalent file from the solution code, along
with a "ref_scope" slot which contains the parsed AST from the
solution code. Also establishes "ref_original_source" which may be
different from "ref_source" when a prep function is used.
"task_info" and "submission_root" slots are also required, but those should always be present.
If the solution code cannot be parsed due to a SyntaxError
or the
like or because no equivalent solution file exists, a
ContextCreationError
will be generated naming the relevant error as
its cause.
1668 def __init__(self, depends=None, hidden=False, prep=None): 1669 """ 1670 Dependencies are optional; if not specified `auto` will be used 1671 to fill them in. `hidden` may be provided; by default this 1672 context is not hidden. A `prep` function may be supplied, which 1673 will be given the source code and its return value will be used 1674 in place of the original source code. 1675 """ 1676 # First, create our context builder 1677 def establish_context(prev_context): 1678 """ 1679 Establishes the following context slots based on the 1680 "ref_file_path" slot, by reading the solution version of the 1681 indicated file: 1682 1683 ref_source: The source of the solution file. 1684 ref_scope: An AST module node resulting from parsing the 1685 solution file. 1686 ref_top_scope: As above, but won't be modified. 1687 """ 1688 soln_equivalent = context_utils.extract( 1689 prev_context, 1690 "ref_file_path" 1691 ) 1692 ref_filename = context_utils.extract( 1693 prev_context, 1694 "ref_filename" 1695 ) 1696 1697 if not os.path.isfile(soln_equivalent): 1698 raise context_utils.ContextCreationError( 1699 self, 1700 f"Target file {soln_equivalent} does not exist in the" 1701 f" solution directory." 1702 ) 1703 1704 with open(soln_equivalent, 'r', encoding="utf-8") as fin: 1705 contents = fin.read() 1706 1707 if prep: 1708 source = prep(contents) 1709 else: 1710 source = contents 1711 1712 try: 1713 node = mast.parse(source, filename=ref_filename) 1714 except Exception as e: 1715 raise context_utils.ContextCreationError( 1716 self, 1717 f"Unable to parse solution file '{soln_equivalent}'.", 1718 cause=e 1719 ) 1720 1721 return { 1722 "ref_original_source": contents, 1723 "ref_source": source, 1724 "ref_scope": node, 1725 "ref_top_scope": node 1726 } 1727 1728 # Figure out if we need to use automatic dependencies: 1729 if depends is None: 1730 depends = auto("ref_filename", "ref_file_path") 1731 1732 # Now we can call the super constructor 1733 super().__init__( 1734 description=( 1735 "Code in the solution file", 1736 "We will parse the code in the solution file.", 1737 ), 1738 builder=establish_context, 1739 display_product=lambda context: ( 1740 f"The solution code for '{context['filename']}'" 1741 f" (available after the revision period is over)." 1742 ), 1743 depends=depends, 1744 hidden=hidden 1745 ) 1746 1747 # Finally, register ourselves as an auto provider for the slots 1748 # that we generate: 1749 self.register( 1750 "ref_original_source", 1751 "ref_source", 1752 "ref_scope", 1753 "ref_top_scope" 1754 )
Dependencies are optional; if not specified auto
will be used
to fill them in. hidden
may be provided; by default this
context is not hidden. A prep
function may be supplied, which
will be given the source code and its return value will be used
in place of the original source code.
1765class TestsCodeContext(AutoContext): 1766 """ 1767 Requires "tests_filename" and "tests_file_path" slots (see 1768 `TestsFileContext`), and establishes a "tests_source" slot which 1769 contains the raw text of the target file, along with a "tests_scope" 1770 slot which contains the parsed AST from the code. 1771 1772 If the code cannot be parsed due to a `SyntaxError` or the like, a 1773 `ContextCreationError` will be generated naming the parsing error as 1774 its cause, although note that `potluck.load.fix_parse` is used which 1775 will attempt to steamroll some kinds of parsing errors while 1776 generating associated warnings. 1777 """ 1778 def __init__(self, depends=None, hidden=False, prep=None): 1779 """ 1780 Dependencies are optional; if not specified `auto` will be used 1781 to fill them in. `hidden` may be provided; by default this 1782 context is not hidden. A `prep` function may be provided; it will 1783 be applied to the source code string and its result will be used 1784 instead of the original source. 1785 """ 1786 # First, create our context builder 1787 def establish_context(prev_context): 1788 """ 1789 Establishes the following context slots based on the 1790 "tests_file_path" slot, by reading the indicated file: 1791 1792 - original_tests_source: The raw file contents. 1793 - tests_source: Possibly-edited (to steamroll syntax errors 1794 or by a prep function) file contents. 1795 - tests_scope: An AST module node resulting from parsing the 1796 modified file contents. 1797 - top_tests_scope: Same as above (but not designed to be 1798 modified). 1799 - tests_parse_errors: A list of Exception objects that were 1800 'successfully' steamrolled by editing the source code. 1801 """ 1802 filename = context_utils.extract(prev_context, "tests_filename") 1803 target = context_utils.extract(prev_context, "tests_file_path") 1804 with open(target, 'r', encoding="utf-8") as fin: 1805 original_tests_source = fin.read() 1806 1807 if prep: 1808 tests_source = prep(original_tests_source) 1809 else: 1810 tests_source = original_tests_source 1811 1812 try: 1813 fixed, node, errors = load.fix_parse(tests_source, filename) 1814 except Exception as e: 1815 raise context_utils.ContextCreationError( 1816 self, 1817 f"Unable to parse submitted tests file '{filename}'.", 1818 cause=e 1819 ) 1820 1821 if node is None: 1822 raise context_utils.ContextCreationError( 1823 self, 1824 f"Unable to parse submitted tests file '{filename}'.", 1825 cause=errors[0] 1826 ) 1827 1828 result = { 1829 "original_tests_source": original_tests_source, 1830 "tests_source": fixed, 1831 "tests_scope": node, 1832 "top_tests_scope": node, 1833 "tests_parse_errors": errors 1834 } 1835 1836 # Report parsing issues as warnings 1837 if errors: 1838 result["warnings"] = [ 1839 ( 1840 "The following errors were encountered when parsing" 1841 + " your tests:<br>" 1842 + html_tools.build_list( 1843 html_tools.html_traceback(e) 1844 for e in errors 1845 ) 1846 ) 1847 ] 1848 1849 return result 1850 1851 # Figure out if we need to use automatic dependencies: 1852 if depends is None: 1853 depends = auto("tests_filename", "tests_file_path") 1854 1855 # Now we can call the super constructor 1856 super().__init__( 1857 description=( 1858 "Code in the tests file", 1859 ( 1860 "We will parse the code in the tests file and pay" 1861 " attention to how it was written." 1862 ), 1863 "Code in the tests file", 1864 ( 1865 "We parsed the code in the tests file and paid" 1866 "attention to how it was written." 1867 ), 1868 ), 1869 builder=establish_context, 1870 display_product=lambda context: ( 1871 f"The tests code in '{context['tests_filename']}'" 1872 f" (shown elsewhere)." 1873 ), 1874 depends=depends, 1875 hidden=hidden, 1876 # Errors at this level need to be reported! 1877 generate_warnings=True 1878 ) 1879 1880 # Finally, register ourselves as an auto provider for the slots 1881 # that we generate: 1882 self.register( 1883 "original_tests_source", 1884 "tests_source", 1885 "tests_scope", 1886 "top_tests_scope", 1887 "tests_parse_errors" 1888 )
Requires "tests_filename" and "tests_file_path" slots (see
TestsFileContext
), and establishes a "tests_source" slot which
contains the raw text of the target file, along with a "tests_scope"
slot which contains the parsed AST from the code.
If the code cannot be parsed due to a SyntaxError
or the like, a
ContextCreationError
will be generated naming the parsing error as
its cause, although note that potluck.load.fix_parse
is used which
will attempt to steamroll some kinds of parsing errors while
generating associated warnings.
1778 def __init__(self, depends=None, hidden=False, prep=None): 1779 """ 1780 Dependencies are optional; if not specified `auto` will be used 1781 to fill them in. `hidden` may be provided; by default this 1782 context is not hidden. A `prep` function may be provided; it will 1783 be applied to the source code string and its result will be used 1784 instead of the original source. 1785 """ 1786 # First, create our context builder 1787 def establish_context(prev_context): 1788 """ 1789 Establishes the following context slots based on the 1790 "tests_file_path" slot, by reading the indicated file: 1791 1792 - original_tests_source: The raw file contents. 1793 - tests_source: Possibly-edited (to steamroll syntax errors 1794 or by a prep function) file contents. 1795 - tests_scope: An AST module node resulting from parsing the 1796 modified file contents. 1797 - top_tests_scope: Same as above (but not designed to be 1798 modified). 1799 - tests_parse_errors: A list of Exception objects that were 1800 'successfully' steamrolled by editing the source code. 1801 """ 1802 filename = context_utils.extract(prev_context, "tests_filename") 1803 target = context_utils.extract(prev_context, "tests_file_path") 1804 with open(target, 'r', encoding="utf-8") as fin: 1805 original_tests_source = fin.read() 1806 1807 if prep: 1808 tests_source = prep(original_tests_source) 1809 else: 1810 tests_source = original_tests_source 1811 1812 try: 1813 fixed, node, errors = load.fix_parse(tests_source, filename) 1814 except Exception as e: 1815 raise context_utils.ContextCreationError( 1816 self, 1817 f"Unable to parse submitted tests file '{filename}'.", 1818 cause=e 1819 ) 1820 1821 if node is None: 1822 raise context_utils.ContextCreationError( 1823 self, 1824 f"Unable to parse submitted tests file '{filename}'.", 1825 cause=errors[0] 1826 ) 1827 1828 result = { 1829 "original_tests_source": original_tests_source, 1830 "tests_source": fixed, 1831 "tests_scope": node, 1832 "top_tests_scope": node, 1833 "tests_parse_errors": errors 1834 } 1835 1836 # Report parsing issues as warnings 1837 if errors: 1838 result["warnings"] = [ 1839 ( 1840 "The following errors were encountered when parsing" 1841 + " your tests:<br>" 1842 + html_tools.build_list( 1843 html_tools.html_traceback(e) 1844 for e in errors 1845 ) 1846 ) 1847 ] 1848 1849 return result 1850 1851 # Figure out if we need to use automatic dependencies: 1852 if depends is None: 1853 depends = auto("tests_filename", "tests_file_path") 1854 1855 # Now we can call the super constructor 1856 super().__init__( 1857 description=( 1858 "Code in the tests file", 1859 ( 1860 "We will parse the code in the tests file and pay" 1861 " attention to how it was written." 1862 ), 1863 "Code in the tests file", 1864 ( 1865 "We parsed the code in the tests file and paid" 1866 "attention to how it was written." 1867 ), 1868 ), 1869 builder=establish_context, 1870 display_product=lambda context: ( 1871 f"The tests code in '{context['tests_filename']}'" 1872 f" (shown elsewhere)." 1873 ), 1874 depends=depends, 1875 hidden=hidden, 1876 # Errors at this level need to be reported! 1877 generate_warnings=True 1878 ) 1879 1880 # Finally, register ourselves as an auto provider for the slots 1881 # that we generate: 1882 self.register( 1883 "original_tests_source", 1884 "tests_source", 1885 "tests_scope", 1886 "top_tests_scope", 1887 "tests_parse_errors" 1888 )
Dependencies are optional; if not specified auto
will be used
to fill them in. hidden
may be provided; by default this
context is not hidden. A prep
function may be provided; it will
be applied to the source code string and its result will be used
instead of the original source.
1903class ModuleContext(AutoContext): 1904 """ 1905 Requires a "top_scope" slot (see `CodeContext` which must hold an 1906 entire module's AST, and creates a "module" slot which holds the 1907 module object that results from running that code. 1908 1909 If `optimism` is available, any test cases established will be 1910 cleared when the module is loaded, and then any cases established by 1911 loading the module will be saved in a "test_cases" context slot. 1912 """ 1913 _filename = "filename" 1914 _src = "file_path" 1915 _from = "top_scope" 1916 _sandbox = "sandbox" 1917 _to = "module" 1918 _to_cases = "test_cases" 1919 _description = ( 1920 "The values defined by the code", 1921 ( 1922 "We will run your code so that we can run tests on the" 1923 " values it defines." 1924 ) 1925 ) 1926 1927 def display_result(self, context): 1928 """ 1929 Context result display function which lists names defined in the 1930 loaded module. 1931 """ 1932 loaded = context[self._to] 1933 defined = [ 1934 name 1935 for name in dir(loaded) 1936 if not name.startswith("__") or not name.endswith("__") 1937 ] 1938 if len(defined) == 0: 1939 result = "No values were defined in the file." 1940 else: 1941 result = ( 1942 "The following values were defined in the file:\n" 1943 + html_tools.build_list( 1944 "<code>{}</code>".format(name) 1945 for name in defined 1946 ) 1947 ) 1948 1949 if OPTIMISTIC: 1950 ndef = len(context[self._to_cases]) 1951 if ndef > 0: 1952 result += ( 1953 "<br>\nYour file defined {} test cases.".format(ndef) 1954 ) 1955 1956 return result 1957 1958 def __init__(self, depends=None, hidden=False, prep=None, wrap=None): 1959 """ 1960 Dependencies are optional; if not specified `auto` will be used 1961 to fill them in. `hidden` may be provided; by default this 1962 context is not hidden. 1963 1964 `prep` may be supplied; it is a function which receives the 1965 current context dictionary and will be run before the module is 1966 loaded. 1967 1968 `wrap` may be supplied; it is a function which will be given the 1969 module once it's loaded and its return value will be used instead 1970 of the original module. 1971 """ 1972 # First, create our context builder 1973 def establish_context(prev_context): 1974 """ 1975 Establishes the "module" context slot by executing the code 1976 in the "top_scope" slot. Actually, uses self._from and 1977 self._to to determine the slots to read/write, since it can 1978 also be used to create "ref_module" from "ref_top_scope". 1979 Also uses self._src if available to find the source file. 1980 """ 1981 # Fetch the AST node that we'd like to turn into a module 1982 node = context_utils.extract(prev_context, self._from) 1983 1984 # Prefix the file name so that submitted and solution 1985 # modules with the same name don't collide 1986 filename = context_utils.extract(prev_context, self._filename) 1987 if self._from == "top_scope": 1988 prefix = "subm_" 1989 elif self._from == "ref_top_scope": 1990 prefix = "soln_" 1991 elif self._from == "top_tests_scope": 1992 prefix = "tests_" 1993 else: 1994 prefix = "loaded_" 1995 full_name = prefix + filename 1996 1997 # Figure out our file source if we can 1998 src_path = prev_context.get(self._src) 1999 if src_path: 2000 src_path = os.path.abspath(src_path) 2001 2002 # Run the prep function if one was supplied 2003 if prep is not None: 2004 prep(prev_context) 2005 2006 # Set up phony stdin, so that stray inputs won't immediately 2007 # crash the program (if their results are used in a delicate 2008 # manner, they still will of course, but we supply '1' for 2009 # each input, which will survive conversion to an int or 2010 # float). 2011 old_stdin = sys.stdin 2012 sys.stdin = context_utils.AllOnes() 2013 # Note: we don't care about echoing inputs to stdout here... 2014 2015 # Set up phony stdout and stderr 2016 old_stdout = sys.stdout 2017 sys.stdout = io.StringIO() 2018 old_stderr = sys.stderr 2019 sys.stderr = io.StringIO() 2020 2021 # Prepare for capturing test cases 2022 test_cases = None 2023 2024 if OPTIMISTIC: 2025 # Ask for the default level of failure messages 2026 optimism.detailLevel(0) 2027 # Reset failure flag and ensure we don't skip checks 2028 optimism.clearFailure() 2029 optimism.skipChecksAfterFail(None) 2030 # Get rid of any previously-recorded cases 2031 optimism.deleteAllTestSuites() 2032 2033 # Actually load the module 2034 try: 2035 module = load.create_module_in_sandbox( 2036 node, 2037 full_name, 2038 sandbox_dir=context_utils.extract( 2039 prev_context, 2040 self._sandbox 2041 ), 2042 on_disk=src_path 2043 ) 2044 except Exception as e: 2045 raise context_utils.ContextCreationError( 2046 self, 2047 "Unable to run code.", 2048 e 2049 ) 2050 finally: # clean up input/output streams 2051 sys.stdin = old_stdin 2052 sys.stdout = old_stdout 2053 sys.stderr = old_stderr 2054 2055 if OPTIMISTIC: 2056 try: 2057 # Capture defined test cases 2058 test_cases = optimism.listAllTrials() 2059 except Exception as e: 2060 raise context_utils.ContextCreationError( 2061 self, 2062 "Error managing optimism tests.", 2063 e 2064 ) 2065 2066 # Wrap our module result if necessary 2067 if wrap is not None: 2068 module = wrap(module) 2069 2070 # Return our new slot 2071 result = { self._to: module } 2072 if test_cases is not None: 2073 result[self._to_cases] = test_cases 2074 2075 return result 2076 2077 # Figure out if we need to use automatic dependencies: 2078 if depends is None: 2079 depends = auto(self._filename, self._from, self._sandbox) 2080 2081 # Now we can call the super constructor 2082 super().__init__( 2083 description=self._description, 2084 builder=establish_context, 2085 display_product=self.display_result, 2086 depends=depends, 2087 hidden=hidden 2088 ) 2089 2090 # Finally, register ourselves as an auto provider for the slots 2091 # that we generate: 2092 if OPTIMISTIC: 2093 self.register(self._to, self._to_cases) 2094 else: 2095 self.register(self._to)
Requires a "top_scope" slot (see CodeContext
which must hold an
entire module's AST, and creates a "module" slot which holds the
module object that results from running that code.
If optimism
is available, any test cases established will be
cleared when the module is loaded, and then any cases established by
loading the module will be saved in a "test_cases" context slot.
1958 def __init__(self, depends=None, hidden=False, prep=None, wrap=None): 1959 """ 1960 Dependencies are optional; if not specified `auto` will be used 1961 to fill them in. `hidden` may be provided; by default this 1962 context is not hidden. 1963 1964 `prep` may be supplied; it is a function which receives the 1965 current context dictionary and will be run before the module is 1966 loaded. 1967 1968 `wrap` may be supplied; it is a function which will be given the 1969 module once it's loaded and its return value will be used instead 1970 of the original module. 1971 """ 1972 # First, create our context builder 1973 def establish_context(prev_context): 1974 """ 1975 Establishes the "module" context slot by executing the code 1976 in the "top_scope" slot. Actually, uses self._from and 1977 self._to to determine the slots to read/write, since it can 1978 also be used to create "ref_module" from "ref_top_scope". 1979 Also uses self._src if available to find the source file. 1980 """ 1981 # Fetch the AST node that we'd like to turn into a module 1982 node = context_utils.extract(prev_context, self._from) 1983 1984 # Prefix the file name so that submitted and solution 1985 # modules with the same name don't collide 1986 filename = context_utils.extract(prev_context, self._filename) 1987 if self._from == "top_scope": 1988 prefix = "subm_" 1989 elif self._from == "ref_top_scope": 1990 prefix = "soln_" 1991 elif self._from == "top_tests_scope": 1992 prefix = "tests_" 1993 else: 1994 prefix = "loaded_" 1995 full_name = prefix + filename 1996 1997 # Figure out our file source if we can 1998 src_path = prev_context.get(self._src) 1999 if src_path: 2000 src_path = os.path.abspath(src_path) 2001 2002 # Run the prep function if one was supplied 2003 if prep is not None: 2004 prep(prev_context) 2005 2006 # Set up phony stdin, so that stray inputs won't immediately 2007 # crash the program (if their results are used in a delicate 2008 # manner, they still will of course, but we supply '1' for 2009 # each input, which will survive conversion to an int or 2010 # float). 2011 old_stdin = sys.stdin 2012 sys.stdin = context_utils.AllOnes() 2013 # Note: we don't care about echoing inputs to stdout here... 2014 2015 # Set up phony stdout and stderr 2016 old_stdout = sys.stdout 2017 sys.stdout = io.StringIO() 2018 old_stderr = sys.stderr 2019 sys.stderr = io.StringIO() 2020 2021 # Prepare for capturing test cases 2022 test_cases = None 2023 2024 if OPTIMISTIC: 2025 # Ask for the default level of failure messages 2026 optimism.detailLevel(0) 2027 # Reset failure flag and ensure we don't skip checks 2028 optimism.clearFailure() 2029 optimism.skipChecksAfterFail(None) 2030 # Get rid of any previously-recorded cases 2031 optimism.deleteAllTestSuites() 2032 2033 # Actually load the module 2034 try: 2035 module = load.create_module_in_sandbox( 2036 node, 2037 full_name, 2038 sandbox_dir=context_utils.extract( 2039 prev_context, 2040 self._sandbox 2041 ), 2042 on_disk=src_path 2043 ) 2044 except Exception as e: 2045 raise context_utils.ContextCreationError( 2046 self, 2047 "Unable to run code.", 2048 e 2049 ) 2050 finally: # clean up input/output streams 2051 sys.stdin = old_stdin 2052 sys.stdout = old_stdout 2053 sys.stderr = old_stderr 2054 2055 if OPTIMISTIC: 2056 try: 2057 # Capture defined test cases 2058 test_cases = optimism.listAllTrials() 2059 except Exception as e: 2060 raise context_utils.ContextCreationError( 2061 self, 2062 "Error managing optimism tests.", 2063 e 2064 ) 2065 2066 # Wrap our module result if necessary 2067 if wrap is not None: 2068 module = wrap(module) 2069 2070 # Return our new slot 2071 result = { self._to: module } 2072 if test_cases is not None: 2073 result[self._to_cases] = test_cases 2074 2075 return result 2076 2077 # Figure out if we need to use automatic dependencies: 2078 if depends is None: 2079 depends = auto(self._filename, self._from, self._sandbox) 2080 2081 # Now we can call the super constructor 2082 super().__init__( 2083 description=self._description, 2084 builder=establish_context, 2085 display_product=self.display_result, 2086 depends=depends, 2087 hidden=hidden 2088 ) 2089 2090 # Finally, register ourselves as an auto provider for the slots 2091 # that we generate: 2092 if OPTIMISTIC: 2093 self.register(self._to, self._to_cases) 2094 else: 2095 self.register(self._to)
Dependencies are optional; if not specified auto
will be used
to fill them in. hidden
may be provided; by default this
context is not hidden.
prep
may be supplied; it is a function which receives the
current context dictionary and will be run before the module is
loaded.
wrap
may be supplied; it is a function which will be given the
module once it's loaded and its return value will be used instead
of the original module.
1927 def display_result(self, context): 1928 """ 1929 Context result display function which lists names defined in the 1930 loaded module. 1931 """ 1932 loaded = context[self._to] 1933 defined = [ 1934 name 1935 for name in dir(loaded) 1936 if not name.startswith("__") or not name.endswith("__") 1937 ] 1938 if len(defined) == 0: 1939 result = "No values were defined in the file." 1940 else: 1941 result = ( 1942 "The following values were defined in the file:\n" 1943 + html_tools.build_list( 1944 "<code>{}</code>".format(name) 1945 for name in defined 1946 ) 1947 ) 1948 1949 if OPTIMISTIC: 1950 ndef = len(context[self._to_cases]) 1951 if ndef > 0: 1952 result += ( 1953 "<br>\nYour file defined {} test cases.".format(ndef) 1954 ) 1955 1956 return result
Context result display function which lists names defined in the loaded module.
2110class SolnModuleContext(ModuleContext): 2111 """ 2112 Works like `ModuleContext`, but for the solution module: Requires a 2113 "ref_top_scope" slot (see `SolnCodeContext`) which must hold the 2114 solution module's AST, and creates a "ref_module" slot which holds 2115 the module object that results from running that code. 2116 2117 If the optimism module is available, also creates a 2118 "ref_expectations" slot. 2119 """ 2120 _filename = "ref_filename" 2121 _src = "ref_file_path" 2122 _from = "ref_top_scope" 2123 _sandbox = "ref_sandbox" 2124 _to = "ref_module" 2125 _to_cases = "ref_test_cases" 2126 _description = ( 2127 "The values defined by the solution code", 2128 ( 2129 "We will run the solution code so that we can compare" 2130 " its results to the results of your code." 2131 ) 2132 ) 2133 2134 # Note: just by overriding these fields we've done all that we need 2135 # to to change where we're reading from and where we're putting our 2136 # results.
Works like ModuleContext
, but for the solution module: Requires a
"ref_top_scope" slot (see SolnCodeContext
) which must hold the
solution module's AST, and creates a "ref_module" slot which holds
the module object that results from running that code.
If the optimism module is available, also creates a "ref_expectations" slot.
2151class TestsModuleContext(ModuleContext): 2152 """ 2153 Requires a "top_tests_scope" slot (see `TestsCodeContext` which must 2154 hold an entire module's AST, and creates a "tests_module" slot which 2155 holds the module object that results from running that code. 2156 """ 2157 _filename = "tests_filename" 2158 _src = "tests_file_path" 2159 _from = "top_tests_scope" 2160 _sandbox = "tests_sandbox" 2161 _to = "tests_module" 2162 _to_cases = "validation_test_cases" 2163 _description = ( 2164 "The values defined by the solution code", 2165 ( 2166 "We will run the solution code so that we can compare" 2167 " its results to the results of your code." 2168 ) 2169 ) 2170 2171 # Note: just by overriding these fields we've done all that we need 2172 # to to change where we're reading from and where we're putting our 2173 # results.
Requires a "top_tests_scope" slot (see TestsCodeContext
which must
hold an entire module's AST, and creates a "tests_module" slot which
holds the module object that results from running that code.
2188class DefinitionsContext(AutoContext): 2189 """ 2190 Creates a "defs" slot based on a "top_scope" slot which holds a 2191 mapping from function names to AST nodes covering every `def` which 2192 occurs in the provided scope, including nested and method 2193 definitions. 2194 2195 Note that nested definitions may shadow exterior definitions in the 2196 map if they have the same name. (TODO: Not that?) 2197 """ 2198 _from = "top_scope" 2199 _to = "defs" 2200 2201 def __init__(self, depends=None, hidden=False): 2202 """ 2203 Dependencies may be supplied and the context may be hidden. If no 2204 dependencies are given, an auto-dependency for the "top_scope" 2205 slot will be generated. 2206 """ 2207 def establish_context(prev_context): 2208 """ 2209 This context_builder function depends on a "scope" context 2210 holding an AST node, and adds a "defs" context item which 2211 contains a dicitonary of all AST nodes in that scope which 2212 are function definitions (includes interior defs). The keys 2213 of the dictionary are the names of the functions. Lambdas 2214 are not included in the list. 2215 """ 2216 within = context_utils.extract(prev_context, self._from) 2217 2218 # Find definition AST nodes 2219 alldefs = set() 2220 for pat in patterns.ALL_DEF_PATTERNS: 2221 alldefs |= set( 2222 node 2223 for node, bindings in mast.findall(within, pat) 2224 ) 2225 2226 # Create the mapping 2227 defmap = {} 2228 for defn in alldefs: 2229 defmap[defn.name] = defn 2230 2231 # Return our resulting slot 2232 return { self._to: defmap } 2233 2234 # Figure out if we need to use automatic dependencies: 2235 if depends is None: 2236 depends = auto(self._from) 2237 2238 # Now we can call the super constructor 2239 super().__init__( 2240 description=( 2241 "The function definitions in the code", 2242 ( 2243 "We will inspect the code and extract all of the" 2244 " function definitions." 2245 ) 2246 ), 2247 builder=establish_context, 2248 display_product=lambda context: ( 2249 "The following functions were defined:\n" 2250 + html_tools.build_list( 2251 f"<code>{name}</code>" 2252 for name in context[self._to] 2253 ) 2254 ), 2255 depends=depends, 2256 hidden=hidden 2257 ) 2258 2259 # Finally, register ourselves as an auto provider for the slot 2260 # that we generate: 2261 self.register(self._to)
Creates a "defs" slot based on a "top_scope" slot which holds a
mapping from function names to AST nodes covering every def
which
occurs in the provided scope, including nested and method
definitions.
Note that nested definitions may shadow exterior definitions in the map if they have the same name. (TODO: Not that?)
2201 def __init__(self, depends=None, hidden=False): 2202 """ 2203 Dependencies may be supplied and the context may be hidden. If no 2204 dependencies are given, an auto-dependency for the "top_scope" 2205 slot will be generated. 2206 """ 2207 def establish_context(prev_context): 2208 """ 2209 This context_builder function depends on a "scope" context 2210 holding an AST node, and adds a "defs" context item which 2211 contains a dicitonary of all AST nodes in that scope which 2212 are function definitions (includes interior defs). The keys 2213 of the dictionary are the names of the functions. Lambdas 2214 are not included in the list. 2215 """ 2216 within = context_utils.extract(prev_context, self._from) 2217 2218 # Find definition AST nodes 2219 alldefs = set() 2220 for pat in patterns.ALL_DEF_PATTERNS: 2221 alldefs |= set( 2222 node 2223 for node, bindings in mast.findall(within, pat) 2224 ) 2225 2226 # Create the mapping 2227 defmap = {} 2228 for defn in alldefs: 2229 defmap[defn.name] = defn 2230 2231 # Return our resulting slot 2232 return { self._to: defmap } 2233 2234 # Figure out if we need to use automatic dependencies: 2235 if depends is None: 2236 depends = auto(self._from) 2237 2238 # Now we can call the super constructor 2239 super().__init__( 2240 description=( 2241 "The function definitions in the code", 2242 ( 2243 "We will inspect the code and extract all of the" 2244 " function definitions." 2245 ) 2246 ), 2247 builder=establish_context, 2248 display_product=lambda context: ( 2249 "The following functions were defined:\n" 2250 + html_tools.build_list( 2251 f"<code>{name}</code>" 2252 for name in context[self._to] 2253 ) 2254 ), 2255 depends=depends, 2256 hidden=hidden 2257 ) 2258 2259 # Finally, register ourselves as an auto provider for the slot 2260 # that we generate: 2261 self.register(self._to)
Dependencies may be supplied and the context may be hidden. If no dependencies are given, an auto-dependency for the "top_scope" slot will be generated.
2269class SolnDefinitionsContext(DefinitionsContext): 2270 """ 2271 Works like `DefinitionsContext` but extracts a "ref_defs" slot from 2272 the "ref_top_scope" slot. 2273 """ 2274 _from = "ref_top_scope" 2275 _to = "ref_defs"
Works like DefinitionsContext
but extracts a "ref_defs" slot from
the "ref_top_scope" slot.
2283class DocstringsContext(AutoContext): 2284 """ 2285 Establishes a "docstrings" slot based on "defs" and "module" slots, 2286 which contains a mapping from function names to their docstrings. 2287 This mapping will only include functions defined at the top level of 2288 the module. 2289 """ 2290 2291 def __init__(self, depends=None, hidden=False): 2292 """ 2293 May have non-automatic dependencies and/or be hidden. If manual 2294 dependencies are provided, make sure they establish the "defs" 2295 and "module" slots. 2296 """ 2297 def establish_context(prev_context): 2298 """ 2299 This context_builder requires *both* a "defs" node (see 2300 `DefinitionsContext`) *and* a "module" node (see 2301 `ModuleContext`), because it makes use of both the AST and 2302 the actual imported module. 2303 2304 It uses the defs map to figure out what functions to look 2305 for, and then for every function defined *at the top level* 2306 of the submitted code, it looks up the docstring from the 2307 module object, returning a mapping from function names to 2308 docstrings. If there are functions defined inside of other 2309 functions, they will not show up in the resulting 2310 "docstrings" context item. 2311 """ 2312 defs = context_utils.extract(prev_context, "defs") 2313 submitted = context_utils.extract(prev_context, "module") 2314 2315 docsmap = {} 2316 2317 for fname in defs: 2318 fcn = getattr(submitted, fname, None) 2319 if fcn: 2320 # this function is defined at the module level 2321 doc = getattr(fcn, "__doc__", None) or "" 2322 doc = doc.strip() 2323 docsmap[fname] = doc 2324 2325 return { "docstrings": docsmap } 2326 2327 # Figure out if we need to use automatic dependencies: 2328 if depends is None: 2329 depends = auto("defs", "module") 2330 2331 super().__init__( 2332 description=( 2333 "The function docstrings", 2334 ( 2335 "We will extract the docstrings from each function" 2336 " defined by your code." 2337 ) 2338 ), 2339 builder=establish_context, 2340 display_product=lambda context: ( 2341 "The following docstrings were found:\n" 2342 + html_tools.build_list( 2343 ( 2344 f"Function <code>{name}</code>:<br>" 2345 f"<pre>{doc}</pre>" 2346 ) 2347 for name, doc in context["docstrings"].items() 2348 ) 2349 ), 2350 depends=depends, 2351 hidden=hidden 2352 ) 2353 2354 # Finally, register ourselves as an auto provider for the slot 2355 # that we generate: 2356 self.register("docstrings")
Establishes a "docstrings" slot based on "defs" and "module" slots, which contains a mapping from function names to their docstrings. This mapping will only include functions defined at the top level of the module.
2291 def __init__(self, depends=None, hidden=False): 2292 """ 2293 May have non-automatic dependencies and/or be hidden. If manual 2294 dependencies are provided, make sure they establish the "defs" 2295 and "module" slots. 2296 """ 2297 def establish_context(prev_context): 2298 """ 2299 This context_builder requires *both* a "defs" node (see 2300 `DefinitionsContext`) *and* a "module" node (see 2301 `ModuleContext`), because it makes use of both the AST and 2302 the actual imported module. 2303 2304 It uses the defs map to figure out what functions to look 2305 for, and then for every function defined *at the top level* 2306 of the submitted code, it looks up the docstring from the 2307 module object, returning a mapping from function names to 2308 docstrings. If there are functions defined inside of other 2309 functions, they will not show up in the resulting 2310 "docstrings" context item. 2311 """ 2312 defs = context_utils.extract(prev_context, "defs") 2313 submitted = context_utils.extract(prev_context, "module") 2314 2315 docsmap = {} 2316 2317 for fname in defs: 2318 fcn = getattr(submitted, fname, None) 2319 if fcn: 2320 # this function is defined at the module level 2321 doc = getattr(fcn, "__doc__", None) or "" 2322 doc = doc.strip() 2323 docsmap[fname] = doc 2324 2325 return { "docstrings": docsmap } 2326 2327 # Figure out if we need to use automatic dependencies: 2328 if depends is None: 2329 depends = auto("defs", "module") 2330 2331 super().__init__( 2332 description=( 2333 "The function docstrings", 2334 ( 2335 "We will extract the docstrings from each function" 2336 " defined by your code." 2337 ) 2338 ), 2339 builder=establish_context, 2340 display_product=lambda context: ( 2341 "The following docstrings were found:\n" 2342 + html_tools.build_list( 2343 ( 2344 f"Function <code>{name}</code>:<br>" 2345 f"<pre>{doc}</pre>" 2346 ) 2347 for name, doc in context["docstrings"].items() 2348 ) 2349 ), 2350 depends=depends, 2351 hidden=hidden 2352 ) 2353 2354 # Finally, register ourselves as an auto provider for the slot 2355 # that we generate: 2356 self.register("docstrings")
May have non-automatic dependencies and/or be hidden. If manual dependencies are provided, make sure they establish the "defs" and "module" slots.
2368def build_context_value_displayer( 2369 key, 2370 compare_ref=True, 2371 include_diff=True, 2372 labels=["Your value", "Solution value", "Comparison"] 2373): 2374 """ 2375 Creates a display_product function which will show the contents of a 2376 single specific context key, and by default, will include multiple 2377 tabs that show the value, the reference value, and a diff of the two 2378 values. String values are shown as-is; non-string values are 2379 converted to strings using html_tools.big_repr. 2380 2381 If the number of characters in a value's representation would exceed 2382 VALUE_SIZE_LIMIT, we will truncate it. 2383 2384 Set compare_ref to False to simply show the value for the specified 2385 key, and set include_diff to False when compare_ref is True to omit 2386 the difference tab in the comparison. 2387 2388 Custom labels for the two values and their difference (second and/or 2389 third labels may be ignored depending on other flags) may be given 2390 using the labels argument. 2391 2392 Returns a function suitable for use as the display_product argument 2393 to a Context. 2394 """ 2395 def display_context_value(context): 2396 """ 2397 A function that returns HTML code which displays the value of a 2398 single specific context key, possibly with tabs to view the value 2399 produced by submitted code, the reference value, and the 2400 difference between the two (as a diff). 2401 2402 See build_context_value_displayer, which created this function. 2403 """ 2404 if not compare_ref: 2405 if key in context: 2406 # simply return a single <pre> containing a representation of 2407 # the value 2408 if isinstance(context[key], str): 2409 rep = context[key] 2410 else: 2411 try: 2412 rep = html_tools.big_repr(context[key]) 2413 except TypeError: 2414 rep = repr(context[key]) 2415 rep = html_tools.escape( 2416 html_tools.truncate(rep, VALUE_SIZE_LIMIT) 2417 ) 2418 return f"<pre class='context-value'>{rep}</pre>" 2419 else: 2420 # failed to create the context key we're looking for! 2421 return ( 2422 f"<div class='context-missing'>Failed to establish" 2423 f" context '{key}'!</div>" 2424 ) 2425 else: 2426 if key in context: 2427 if isinstance(context[key], str): 2428 rep = context[key] 2429 else: 2430 try: 2431 rep = html_tools.big_repr(context[key]) 2432 except TypeError: 2433 rep = repr(context[key]) 2434 rep = html_tools.truncate(rep, VALUE_SIZE_LIMIT) 2435 erep = html_tools.escape(rep) 2436 rep_html = f"<pre class='context-value'>{erep}</pre>" 2437 else: 2438 rep = "" 2439 rep_html = ( 2440 f"<div class='context-missing'>Failed to establish" 2441 f" context '{key}'!</div>" 2442 ) 2443 2444 if "ref_" + key in context: 2445 if isinstance(context["ref_" + key], str): 2446 ref_rep = context["ref_" + key] 2447 else: 2448 try: 2449 ref_rep = html_tools.big_repr(context["ref_" + key]) 2450 except TypeError: 2451 ref_rep = repr(context["ref_" + key]) 2452 ref_rep = html_tools.truncate(ref_rep, VALUE_SIZE_LIMIT) 2453 ref_erep = html_tools.escape(ref_rep) 2454 ref_rep_html = f"<pre class='context-value'>{ref_erep}</pre>" 2455 else: 2456 ref_rep = "" 2457 ref_rep_html = ( 2458 f"<div class='context-missing'>Failed to establish" 2459 f" context 'ref_{key}'!</div>" 2460 ) 2461 2462 if include_diff: 2463 # Include a tab for the differences 2464 diff = html_tools.html_diff_table( 2465 rep, 2466 ref_rep, 2467 out_title=labels[0], 2468 ref_title=labels[1] 2469 ) 2470 return html_tools.build_html_tabs( 2471 [ 2472 (labels[0], rep_html), 2473 (labels[1], ref_rep_html), 2474 (labels[2], diff), 2475 ] 2476 ) 2477 else: 2478 # No tab for the differences 2479 return html_tools.build_html_tabs( 2480 [ 2481 (labels[0], rep_html), 2482 (labels[1], ref_rep_html), 2483 ] 2484 ) 2485 2486 return display_context_value
Creates a display_product function which will show the contents of a single specific context key, and by default, will include multiple tabs that show the value, the reference value, and a diff of the two values. String values are shown as-is; non-string values are converted to strings using html_tools.big_repr.
If the number of characters in a value's representation would exceed VALUE_SIZE_LIMIT, we will truncate it.
Set compare_ref to False to simply show the value for the specified key, and set include_diff to False when compare_ref is True to omit the difference tab in the comparison.
Custom labels for the two values and their difference (second and/or third labels may be ignored depending on other flags) may be given using the labels argument.
Returns a function suitable for use as the display_product argument to a Context.
2489def build_simple_context_value_displayer( 2490 key, 2491 compare_ref=True, 2492 labels=["Your value", "Solution value"] 2493): 2494 """ 2495 Creates a display_product function similar to the 2496 `build_context_value_displayer` result, but for simple values which 2497 don't need a pre wrapper and which can be displayed side-by-side 2498 (e.g., numbers). No diff is included, as it's presumed that any 2499 differences will be obvious, and values are converted to strings 2500 using str() instead of html_tools.big_repr. Representations that end 2501 up longer than VALUE_SIZE_LIMIT are still truncated. 2502 2503 Set compare_ref to False to include only the main value. 2504 2505 Custom labels for the two values may be given using the labels 2506 argument. These are not used if compare_ref is False. 2507 2508 Returns a function suitable for use as the display_product argument 2509 to a Context. 2510 """ 2511 def display_context_value(context): 2512 """ 2513 A function that returns HTML code which displays the value of a 2514 single specific context key, possibly side-by-side with the 2515 corresponding reference value. 2516 2517 See build_simple_context_value_displayer, which created this function. 2518 """ 2519 if not compare_ref: 2520 if key in context: 2521 return str(context[key]) 2522 else: 2523 return ( 2524 f"<div class='context-missing'>Failed to establish" 2525 f" context '{key}'!</div>" 2526 ) 2527 else: 2528 if key in context: 2529 rep = html_tools.truncate( 2530 repr(context[key]), 2531 VALUE_SIZE_LIMIT 2532 ) 2533 erep = html_tools.escape(rep) 2534 rep_html = "<code>{}</code>".format(erep) 2535 else: 2536 rep_html = ( 2537 f"<div class='context-missing'>Failed to establish" 2538 f" context '{key}'!</div>" 2539 ) 2540 2541 if "ref_" + key in context: 2542 ref_rep = html_tools.truncate( 2543 repr(context["ref_" + key]), 2544 VALUE_SIZE_LIMIT 2545 ) 2546 ref_erep = html_tools.escape(ref_rep) 2547 ref_rep_html = "<code>{}</code>".format(ref_erep) 2548 else: 2549 ref_rep_html = ( 2550 f"<div class='context-missing'>Failed to establish" 2551 f" context 'ref_{key}'!</div>" 2552 ) 2553 2554 return f""" 2555<table class='context-values'> 2556 <tbody> 2557 <tr> <th>{labels[0]}</th> <td>{rep_html}</td> </tr> 2558 <tr> <th>{labels[1]}</th> <td>{ref_rep_html}</td> </tr> 2559 </tbody> 2560</table> 2561""" 2562 2563 return display_context_value
Creates a display_product function similar to the
build_context_value_displayer
result, but for simple values which
don't need a pre wrapper and which can be displayed side-by-side
(e.g., numbers). No diff is included, as it's presumed that any
differences will be obvious, and values are converted to strings
using str() instead of html_tools.big_repr. Representations that end
up longer than VALUE_SIZE_LIMIT are still truncated.
Set compare_ref to False to include only the main value.
Custom labels for the two values may be given using the labels argument. These are not used if compare_ref is False.
Returns a function suitable for use as the display_product argument to a Context.
2566def create_distribution_result_displayer(context_key="distribution"): 2567 """ 2568 Creates a distribution results display function, which will read 2569 values from the given context key ("distribution" by default). Also 2570 reads a value from the matching "ref_" key. 2571 """ 2572 def display_distribution_results(context): 2573 """ 2574 Displays the 'distribution' and 'ref_distribution' context keys 2575 side-by-side. 2576 """ 2577 sub_dist = context[context_key]["results"] 2578 ref_dist = context["ref_" + context_key]["results"] 2579 2580 all_results = set(sub_dist) | set(ref_dist) 2581 2582 n_samples = context[context_key]["trials"] 2583 2584 rows = [] 2585 for result in sorted(all_results): 2586 rows.append( 2587 ( 2588 "<tr> <td>{result}</td> <td>{n}</td>" 2589 + " <td>{ref_n}</td> </tr>" 2590 ).format( 2591 result=html_tools.dynamic_html_repr( 2592 result, 2593 limit=VALUE_SIZE_LIMIT 2594 ), 2595 n=repr(sub_dist.get(result, 0)), 2596 ref_n=repr(ref_dist.get(result, 0)) 2597 ) 2598 ) 2599 2600 return """ 2601The distribution of results from your function and the solution function 2602after {n_samples} trials (note: distributions may differ somewhat due to 2603random chance.): 2604<table class='result_distribution'> 2605 <thead> 2606 <tr> 2607 <th>Result value</th> 2608 <th>Observed count</th> 2609 <th>Solution count</th> 2610 </tr> 2611 </thead> 2612 <tbody> 2613 {rows} 2614 </tbody> 2615</table> 2616""".format(n_samples=n_samples, rows='\n '.join(rows)) 2617 2618 return display_distribution_results
Creates a distribution results display function, which will read values from the given context key ("distribution" by default). Also reads a value from the matching "ref_" key.
2621def create_image_result_displayer(context_key="image", alt_key="output"): 2622 """ 2623 Creates a context value display function which shows the "image" slot 2624 (or an image from another slot) with alt text from the "output" slot 2625 (assuming turtleBeads descriptions are used). 2626 2627 If a ref_ slot and an alt ref_ slot are available, a comparison will 2628 be included. 2629 """ 2630 def image_displayer(context): 2631 """ 2632 Returns HTML code for displaying an image from the given context, 2633 with alt text from a different slot. 2634 """ 2635 img = context[context_key] 2636 alt = context[alt_key] 2637 if "ref_" + context_key in context and "ref_" + alt_key in context: 2638 ref_img = context["ref_" + context_key] 2639 ref_alt = context["ref_" + alt_key] 2640 return html_tools.build_html_tabs( 2641 [ 2642 ( 2643 "Your image:", 2644 html_tools.html_image(img, alt) 2645 ), 2646 ( 2647 "Solution image:", 2648 html_tools.html_image(ref_img, ref_alt) 2649 ), 2650 ( 2651 "Animation:", 2652 html_tools.html_animation( 2653 compare.diff_anim_frames(img, ref_img, 10), 2654 ( 2655 # TODO: Diff of alt texts here? 2656 "An animation between your image and the" 2657 " solution image." 2658 ), 2659 delays=[500] + [100] * 10 + [500] 2660 ) 2661 ) 2662 ] 2663 ) 2664 else: 2665 # No ref values available for a comparison 2666 return html_tools.html_image(img, alt) 2667 2668 return image_displayer
Creates a context value display function which shows the "image" slot (or an image from another slot) with alt text from the "output" slot (assuming turtleBeads descriptions are used).
If a ref_ slot and an alt ref_ slot are available, a comparison will be included.
2675class SiftedContext(Context): 2676 """ 2677 Working from the "output" and "ref_output" slots (or some other 2678 custom list of slots), this `Context` creates "sifted" and 2679 "ref_sifted" slots which hold the results of matching a regular 2680 expression against the input value. 2681 """ 2682 def __init__( 2683 self, 2684 pattern, 2685 depends, 2686 description=None, 2687 slot_map={"output": "sifted", "ref_output": "ref_sifted"}, 2688 first_match=False, 2689 require_match=True, 2690 use_matchobjs=False 2691 ): 2692 """ 2693 Dependencies must be supplied. A custom description may be 2694 supplied (and is often useful). A custom slot map may be supplied 2695 to specify which incoming slots to process, and for each incoming 2696 slot, which new slot to create to store the result from that 2697 slot. 2698 """ 2699 if isinstance(pattern, str): 2700 pattern = re.compile(pattern) 2701 2702 def establish_context(prev_context): 2703 """ 2704 This context_builder function processes a custom list of 2705 slots by applying a regular expression to them. 2706 """ 2707 result = {} 2708 # Process each requested slot 2709 for from_slot in slot_map: 2710 to_slot = slot_map[from_slot] 2711 2712 # Grab our input value 2713 value = context_utils.extract(prev_context, from_slot) 2714 if not isinstance(value, str): 2715 raise TypeError( 2716 f"SiftedContext can only refine string values," 2717 f" but was asked to refine value of slot" 2718 f" {from_slot} which was a {type(value)}." 2719 ) 2720 2721 # Apply our regular expression 2722 matches = pattern.finditer(value) 2723 2724 # Grab the first match if that's what we need 2725 if first_match: 2726 try: 2727 first = next(matches) 2728 except StopIteration: 2729 raise ValueError( 2730 f"While refining '{from_slot}' context," 2731 f" found no matches for pattern." 2732 ) 2733 # Add either the match object or matching string 2734 if use_matchobjs: 2735 result[to_slot] = first 2736 else: 2737 result[to_slot] = first.group() 2738 else: # Grab all matches 2739 if use_matchobjs: 2740 objs = [m for m in matches] 2741 else: 2742 objs = [m.group() for m in matches] 2743 2744 # list might be empty... 2745 if require_match and len(objs) == 0: 2746 raise ValueError( 2747 f"While refining '{from_slot}' context," 2748 f" found no matches for pattern." 2749 ) 2750 2751 result[to_slot] = objs 2752 2753 # Return our results 2754 return result 2755 2756 def display_result(context): 2757 """ 2758 Displays the results of slot sifting as a tabbed HTML 2759 structure with one tab per input slot. 2760 """ 2761 tablist = [] 2762 for from_slot in slot_map: 2763 to_slot = slot_map[from_slot] 2764 result = context_utils.extract(context, to_slot) 2765 2766 if isinstance(result, re.Match): 2767 display = f"<pre>{result.group(0)}</pre>" 2768 elif isinstance(result, list): 2769 if len(result) == 0: 2770 display = "<no matches>" 2771 elif isinstance(result[0], str): 2772 display = html_tools.build_list( 2773 [ 2774 f"<pre>{entry}</pre>" 2775 for entry in result 2776 ] 2777 ) 2778 else: # results are Match objects 2779 display = html_tools.build_list( 2780 [ 2781 f"<pre>{match.group(0)}</pre>" 2782 for match in result 2783 ] 2784 ) 2785 else: # results should be strings 2786 display = f"<pre>{result}</pre>" 2787 2788 tablist.append((from_slot, display)) 2789 2790 return ( 2791 "Results for expression <pre><code>{expr}</code></pre>:<br>" 2792 + html_tools.build_html_tabs(tablist) 2793 ) 2794 2795 # Create a default description if necessary 2796 if description is None: 2797 stuff = phrasing.comma_list( 2798 slot 2799 for slot in slot_map 2800 if not slot.startswith("ref_") 2801 ) 2802 description = ( 2803 f"Certain parts of the {stuff}", 2804 ( 2805 f"We will search for the pattern" 2806 f"<pre><code>{pattern.pattern}</code></pre> within" 2807 f" the {stuff} and inspect the results." 2808 ) 2809 ) 2810 2811 # Now we can call the super constructor 2812 super().__init__( 2813 description=description, 2814 builder=establish_context, 2815 display_product=display_result, 2816 depends=depends 2817 )
Working from the "output" and "ref_output" slots (or some other
custom list of slots), this Context
creates "sifted" and
"ref_sifted" slots which hold the results of matching a regular
expression against the input value.
2682 def __init__( 2683 self, 2684 pattern, 2685 depends, 2686 description=None, 2687 slot_map={"output": "sifted", "ref_output": "ref_sifted"}, 2688 first_match=False, 2689 require_match=True, 2690 use_matchobjs=False 2691 ): 2692 """ 2693 Dependencies must be supplied. A custom description may be 2694 supplied (and is often useful). A custom slot map may be supplied 2695 to specify which incoming slots to process, and for each incoming 2696 slot, which new slot to create to store the result from that 2697 slot. 2698 """ 2699 if isinstance(pattern, str): 2700 pattern = re.compile(pattern) 2701 2702 def establish_context(prev_context): 2703 """ 2704 This context_builder function processes a custom list of 2705 slots by applying a regular expression to them. 2706 """ 2707 result = {} 2708 # Process each requested slot 2709 for from_slot in slot_map: 2710 to_slot = slot_map[from_slot] 2711 2712 # Grab our input value 2713 value = context_utils.extract(prev_context, from_slot) 2714 if not isinstance(value, str): 2715 raise TypeError( 2716 f"SiftedContext can only refine string values," 2717 f" but was asked to refine value of slot" 2718 f" {from_slot} which was a {type(value)}." 2719 ) 2720 2721 # Apply our regular expression 2722 matches = pattern.finditer(value) 2723 2724 # Grab the first match if that's what we need 2725 if first_match: 2726 try: 2727 first = next(matches) 2728 except StopIteration: 2729 raise ValueError( 2730 f"While refining '{from_slot}' context," 2731 f" found no matches for pattern." 2732 ) 2733 # Add either the match object or matching string 2734 if use_matchobjs: 2735 result[to_slot] = first 2736 else: 2737 result[to_slot] = first.group() 2738 else: # Grab all matches 2739 if use_matchobjs: 2740 objs = [m for m in matches] 2741 else: 2742 objs = [m.group() for m in matches] 2743 2744 # list might be empty... 2745 if require_match and len(objs) == 0: 2746 raise ValueError( 2747 f"While refining '{from_slot}' context," 2748 f" found no matches for pattern." 2749 ) 2750 2751 result[to_slot] = objs 2752 2753 # Return our results 2754 return result 2755 2756 def display_result(context): 2757 """ 2758 Displays the results of slot sifting as a tabbed HTML 2759 structure with one tab per input slot. 2760 """ 2761 tablist = [] 2762 for from_slot in slot_map: 2763 to_slot = slot_map[from_slot] 2764 result = context_utils.extract(context, to_slot) 2765 2766 if isinstance(result, re.Match): 2767 display = f"<pre>{result.group(0)}</pre>" 2768 elif isinstance(result, list): 2769 if len(result) == 0: 2770 display = "<no matches>" 2771 elif isinstance(result[0], str): 2772 display = html_tools.build_list( 2773 [ 2774 f"<pre>{entry}</pre>" 2775 for entry in result 2776 ] 2777 ) 2778 else: # results are Match objects 2779 display = html_tools.build_list( 2780 [ 2781 f"<pre>{match.group(0)}</pre>" 2782 for match in result 2783 ] 2784 ) 2785 else: # results should be strings 2786 display = f"<pre>{result}</pre>" 2787 2788 tablist.append((from_slot, display)) 2789 2790 return ( 2791 "Results for expression <pre><code>{expr}</code></pre>:<br>" 2792 + html_tools.build_html_tabs(tablist) 2793 ) 2794 2795 # Create a default description if necessary 2796 if description is None: 2797 stuff = phrasing.comma_list( 2798 slot 2799 for slot in slot_map 2800 if not slot.startswith("ref_") 2801 ) 2802 description = ( 2803 f"Certain parts of the {stuff}", 2804 ( 2805 f"We will search for the pattern" 2806 f"<pre><code>{pattern.pattern}</code></pre> within" 2807 f" the {stuff} and inspect the results." 2808 ) 2809 ) 2810 2811 # Now we can call the super constructor 2812 super().__init__( 2813 description=description, 2814 builder=establish_context, 2815 display_product=display_result, 2816 depends=depends 2817 )
Dependencies must be supplied. A custom description may be supplied (and is often useful). A custom slot map may be supplied to specify which incoming slots to process, and for each incoming slot, which new slot to create to store the result from that slot.