def make_function(program, function_template): arg_names = inspect.signature(function_template).parameters tree = ast.parse(program) try: for node, arg_name in zip(tree.body, arg_names): assert isinstance(node, ast.Assign) target = only(node.targets) assert isinstance(target, ast.Name) assert target.id == arg_name except AssertionError: raise ExerciseError(f"""\ Your code should start like this: {indented_inputs_string(dict.fromkeys(arg_names, "..."))} """) assignments = tree.body[:len(arg_names)] exercise = tree.body[len(arg_names):] tree.body = assignments code = compile(tree, "<string>", "exec", dont_inherit=True) initial_names = {} try: exec(code, initial_names) except Exception as e: raise InvalidInitialCode from e del initial_names["__builtins__"] tree.body = exercise code = compile(tree, "<string>", "exec", dont_inherit=True) def func(**kwargs): exec(code, kwargs) return initial_names, func
def test_middle_iterations(self): @eye def f(): for i in range(20): for j in range(20): if i == 10 and j >= 12: str(i + 1) stuff = get_call_stuff(get_call_ids(f)[0]) iteration_list = only(stuff.call_data['loop_iterations'].values()) indexes = [i['index'] for i in iteration_list] self.assertEqual(indexes, [0, 1, 2, 10, 17, 18, 19]) iteration_list = only(iteration_list[3]['loops'].values()) indexes = [i['index'] for i in iteration_list] self.assertEqual(indexes, [0, 1, 2, 12, 13, 17, 18, 19])
def function_node(func, tree): function_name = t.get_code_bit(func.__name__) return only( node for node in tree.body if isinstance(node, ast.FunctionDef) if node.name == function_name )
def generate_for_type(typ): if isinstance(typ, typing._GenericAlias): if typ.__origin__ is list: return generate_list(only(typ.__args__)) return { str: generate_string(), bool: random.choice([True, False]), int: random.randrange(100), }[typ]
def input_messages(input_nodes): if not input_nodes: return [] message = t.Terms.q_wiz_input_message_start multi_nodes = [ node for node, group in input_nodes.items() if len(group) > 1 ] for node, group in input_nodes.items(): strings, exs = zip(*group) if len(strings) > 1: if len(multi_nodes) > 1: list_name = f"test_inputs_{multi_nodes.index(node) + 1}" else: list_name = f"test_inputs" list_line = f"{list_name} = {list(strings)}" replacement_text = f"{list_name}.pop(0)" else: list_line = None replacement_text = repr(only(strings)) source = only({ex.source for ex in exs}) text_range = only({ex.text_range() for ex in exs}) piece = only(piece for piece in source.pieces if node.lineno in piece and node.end_lineno in piece) def piece_lines(lines): return indent( dedent("\n".join(lines[lineno - 1] for lineno in piece)), ' ' * 4) replaced_text = asttokens.util.replace( source.text, [(*text_range, replacement_text)]) replaced_lines = piece_lines(replaced_text.splitlines()) original_lines = piece_lines(source.lines) message += f"\n\n{t.Terms.q_wiz_input_replace_with.format(**locals())}\n\n" if list_line: message += f"\n\n{t.Terms.q_wiz_input_and_add.format(**locals())}\n\n" return [message]
def _plain_call_at(self, frame, val) -> ast.Call: """ Returns the Call node currently being evaluated in this frame where the callable is just a variable name, not an attribute or some other expression, and that name resolves to `val`. """ return only([ node for node in self._plain_calls_in_stmt_at_line(frame.f_lineno) if resolve_var(frame, node.func.id) == val ])
def tester(frame_info, arg, returns=None): result = eval( compile(ast.Expression(only(frame_info.call.args)), '<>', 'eval'), frame_info.frame.f_globals, frame_info.frame.f_locals, ) assert result == arg, (result, arg) if returns is None: return arg return returns
def function_tree(self): # We define this here so MessageSteps implicitly inheriting from ExerciseStep don't complain it doesn't exist # noinspection PyUnresolvedReferences function_name = self.solution.__name__ if function_name == "solution": raise ValueError( "This exercise doesn't require defining a function") return only(node for node in ast.walk(self.tree) if isinstance(node, ast.FunctionDef) if node.name == function_name)
def step(loop, increment): selector = '.loop-navigator > .btn:%s-child' % ('first' if increment == -1 else 'last') buttons = driver.find_elements_by_css_selector(selector) self.assertEqual(len(buttons), 2) buttons[loop].click() vals['ij'[loop]] += increment for expr in expr_strings: ActionChains(driver).move_to_element(find_expr(expr)).perform() value = str(eval(expr, {}, vals)) self.assertEqual(expr_value.text, value) node = only(n for n in tree_nodes() if n.text.startswith(expr + ' =')) self.assertEqual(node.text, '%s = int: %s' % (expr, value))
def trace_function(self, func): # type: (FunctionType) -> FunctionType new_func = super(BirdsEye, self).trace_function(func) code_info = self._code_infos.get(new_func.__code__) if code_info: return new_func lines, start_lineno = inspect.getsourcelines( func) # type: List[Text], int end_lineno = start_lineno + len(lines) name = safe_qualname(func) source_file = inspect.getsourcefile(func) if source_file.startswith('<ipython-input'): filename = IPYTHON_FILE_PATH else: filename = os.path.abspath(source_file) nodes = list( self._nodes_of_interest(new_func.traced_file, start_lineno, end_lineno)) html_body = self._nodes_html(nodes, start_lineno, end_lineno, new_func.traced_file) tokens = new_func.traced_file.tokens func_node = only(node for node, _ in nodes if isinstance(node, ast.FunctionDef) and node.first_token.start[0] == start_lineno) func_startpos, raw_body = source_without_decorators(tokens, func_node) data_dict = dict( # These are for the PyCharm plugin node_ranges=list(self._node_ranges(nodes, tokens, func_startpos)), loop_ranges=list(self._loop_ranges(nodes, tokens, func_startpos)), # This maps each node to the loops enclosing that node node_loops={ node._tree_index: [n._tree_index for n in node._loops] for node, _ in nodes if node._loops }) data = json.dumps(data_dict, sort_keys=True) db_func = self._db_func(data, filename, html_body, name, start_lineno, raw_body) arg_info = inspect.getargs(new_func.__code__) arg_names = list(chain(flatten_list(arg_info[0]), arg_info[1:])) # type: List[str] self._code_infos[new_func.__code__] = CodeInfo(db_func, new_func.traced_file, arg_names) return new_func
def normalise_response(response, is_message, substep): response["result"] = response.pop("output_parts") for line in response["result"]: line["text"] = normalise_output(line["text"]) del response["birdseye_objects"] del response["awaiting_input"] del response["error"] del response["output"] if not response["prediction"]["choices"]: del response["prediction"] if is_message: response["message"] = only(response.pop("messages")) assert response["message"] == highlighted_markdown(substep.text) else: assert response.pop("messages") == [] response["message"] = ""
def _trace( self, name, filename, traced_file, code, typ, source="", start_lineno=1, end_lineno=None, arg_names=(), ): if not end_lineno: end_lineno = start_lineno + len(source.splitlines()) nodes = list(self._nodes_of_interest(traced_file, start_lineno, end_lineno)) html_body = self._nodes_html(nodes, start_lineno, end_lineno, traced_file) data_dict = dict( # This maps each node to the loops enclosing that node node_loops={ node._tree_index: [n._tree_index for n in node._loops] for node, _ in nodes if node._loops }, ) if typ == "function": tokens = traced_file.tokens func_node = only( node for node, _ in nodes if isinstance(node, ast.FunctionDef) and node.first_token.start[0] == start_lineno ) func_startpos, source = source_without_decorators(tokens, func_node) # These are for the PyCharm plugin data_dict.update( node_ranges=list(self._node_ranges(nodes, tokens, func_startpos)), loop_ranges=list(self._loop_ranges(nodes, tokens, func_startpos)), ) data = json.dumps(data_dict, sort_keys=True) db_func = self._db_func( data, filename, html_body, name, start_lineno, source, typ ) self._code_infos[code] = CodeInfo(db_func, traced_file, arg_names)
def _plain_calls_in_stmt_at_line(self, lineno: int) -> List[ast.Call]: """ Returns a list of the Call nodes in the statement containing this line which are 'plain', i.e. the callable is just a variable name, not an attribute or some other expression. Note that this can return Call nodes that aren't at the given line, as long as they are in the statement that contains the line. Because a statement is inferred from a line number, there must be no semicolons separating statements on this line. """ stmt = only({ statement_containing_node(node) for node in self.nodes_by_line[lineno] }) # finds only statement at line - no semicolons allowed return [ node for node in ast.walk(stmt) if isinstance(node, ast.Call) and isinstance(node.func, ast.Name) ]
def normalise_response(response, is_message, substep): response["result"] = response.pop("output_parts") for line in response["result"]: line["text"] = normalise_output(line["text"]) if line["type"] == "traceback": line["text"] = line["text"].splitlines() response.pop("birdseye_objects", None) del response["error"] del response["output"] response["prediction"] = get_predictions(substep) if not response["prediction"]["choices"]: del response["prediction"] if is_message: response["message"] = only(response.pop("messages")) assert response["message"] == highlighted_markdown(substep.text) else: assert response.pop("messages") == [] response["message"] = ""
def assigned_names(node, *, allow_one: bool, allow_loops: bool) -> Tuple[Tuple[str], ast.AST]: """ Finds the names being assigned to in the nearest ancestor of the given node that assigns names and satisfies the given conditions. If allow_loops is false, this only considers assignment statements, e.g. `x, y = ...`. If it's true, then for loops and comprehensions are also considered. If allow_one is false, nodes which assign only one name are ignored. Returns: 1. a tuple of strings containing the names of the nodes being assigned 2. The AST node where the assignment happens """ while hasattr(node, 'parent'): node = node.parent target = None if isinstance(node, ast.Assign): target = only(node.targets) elif isinstance(node, (ast.For, ast.comprehension)) and allow_loops: target = node.target if not target: continue names = node_names(target) if len(names) > 1 or allow_one: break else: raise TypeError('No assignment found') return names, node
def find_by_text(text, elements): return only(n for n in elements if n.text == text)
class API: def __init__(self, request): self.request = request @property def user(self) -> User: return self.request.user def run_code(self, code, source, page_index, step_index): page_slug = page_slugs_list[page_index] page = pages[page_slug] step_name = pages[page_slug].step_names[step_index] step = getattr(page, step_name) entry_dict = dict( input=code, source=source, page_slug=page_slug, step_name=step_name, user_id=self.user.id, ) entry = None if settings.SAVE_CODE_ENTRIES: entry = CodeEntry.objects.create(**entry_dict) result = worker_result(entry_dict) if settings.SAVE_CODE_ENTRIES: entry.output = result["output"] entry.save() if result["error"]: return dict(error=result["error"]) if passed := result["passed"]: self.move_step(page_index, step_index + 1) output_parts = result["output_parts"] if not result["awaiting_input"]: output_parts.append(dict(text=">>> ", color="white")) birdseye_url = None birdseye_objects = result["birdseye_objects"] if birdseye_objects: functions = birdseye_objects["functions"] top_old_function_id = only(f["id"] for f in functions if f["name"] == "<module>") function_ids = [d.pop('id') for d in functions] functions = [ eye.db.Function(**{ **d, 'hash': uuid4().hex }) for d in functions ] with eye.db.session_scope() as session: for func in functions: session.add(func) session.commit() function_ids = { old: func.id for old, func in zip(function_ids, functions) } call_id = None for call in birdseye_objects["calls"]: old_function_id = call["function_id"] is_top_call = old_function_id == top_old_function_id call["function_id"] = function_ids[old_function_id] call["start_time"] = datetime.fromisoformat( call["start_time"]) call = eye.db.Call(**call) session.add(call) if is_top_call: call_id = call.id birdseye_url = f"/birdseye/call/{call_id}" return dict( result=output_parts, messages=list(map(highlighted_markdown, result["messages"])), state=self.current_state(), birdseye_url=birdseye_url, passed=passed, prediction=dict( choices=getattr(step, "predicted_output_choices", None), answer=getattr(step, "correct_output", None), ) if passed else dict(choices=None, answer=None), )
def test_steps(api): transcript = [] for page_index, page in enumerate(pages.values()): for step_index, step_name in enumerate(page.step_names[:-1]): step = getattr(page, step_name) for substep in [*step.messages, step]: program = substep.program if "\n" in program: code_source = step.expected_code_source or "editor" else: code_source = "shell" response = api( "run_code", code=program, source=code_source, page_index=page_index, step_index=step_index, ) assert "state" in response state = response.pop("state") for line in response["result"]: line["text"] = normalise_output(line["text"]) del response["birdseye_url"] if not response["prediction"]["choices"]: del response["prediction"] transcript_item = dict( program=program.splitlines(), page=page.title, step=step_name, response=response, ) transcript.append(transcript_item) is_message = substep in step.messages if is_message: response["message"] = only(response.pop("messages")) assert response["message"] == substep.text else: assert response.pop("messages") == [] response["message"] = "" if step.get_solution: get_solution = "".join(step.get_solution["tokens"]) assert "def solution(" not in get_solution assert "returns_stdout" not in get_solution assert get_solution.strip() in program transcript_item[ "get_solution"] = get_solution.splitlines() if step.parsons_solution: is_function = transcript_item["get_solution"][ 0].startswith("def ") assert len( step.get_solution["lines"]) >= 4 + is_function assert response["passed"] == (not is_message) assert step_index + response["passed"] == state[ "pages_progress"][page_index] path = Path(__file__).parent / "test_transcript.json" if os.environ.get("FIX_TESTS", 0): dump = json.dumps(transcript, indent=4, sort_keys=True) path.write_text(dump) else: assert transcript == json.loads(path.read_text())
def select_from(frame_info, sql, params=(), cursor=None, where=None): """ Instead of: cursor.execute(''' SELECT foo, bar FROM my_table WHERE spam = ? AND thing = ? ''', [spam, thing]) for foo, bar in cursor: ... write: for foo, bar in select_from('my_table', where=[spam, thing]): ... Specifically: - the assigned names (similar to the assigned_names and unpack_keys spells) are placed in the SELECT clause - the first argument (usually just a table name but can be any SQL) goes after the FROM - if the where argument is supplied, it must be a list or tuple literal of values which are supplied as query parameters and whose names are used in a WHERE clause using the = and AND operators. If you use this argument, don't put a WHERE clause in the sql argument and don't supply params - a cursor object is automatically pulled from the calling frame, but if this doesn't work you can supply one with the cursor keyword argument - the params argument can be supplied for more custom cases than the where argument provides. - if this is used in a loop or list comprehension, all rows in the result will be iterated over. If it is used in an assignment statement, one row will be returned. - If there are multiple names being assigned (i.e. multiple columns being selected) then the row will be returned and thus unpacked. If there is only one name, it will automatically be unpacked so you don't have to add [0]. This spell is much more a fun rough idea than the others. It is expected that there are many use cases it will not fit into nicely. """ if cursor is None: frame = frame_info.frame cursor = only( c for c in chain(frame.f_locals.values(), frame.f_globals.values()) if 'cursor' in str(type(c).__mro__).lower() and callable(getattr(c, 'execute', None))) names, node = frame_info.assigned_names(allow_one=True, allow_loops=True) sql = 'SELECT %s FROM %s' % (', '.join(names), sql) if where: where_arg = only(kw.value for kw in frame_info.call.keywords if kw.arg == 'where') where_names = node_names(where_arg) assert len(where_names) == len(where) sql += ' WHERE ' + ' AND '.join('%s = ?' % name for name in where_names) assert params == () params = where cursor.execute(sql, params) def unpack(row): if len(row) == 1: return row[0] else: return row if isinstance(node, ast.Assign): return unpack(cursor.fetchone()) else: def vals(): for row in cursor: yield unpack(row) return vals()
def executing_piece(self): return only((start, end) for (start, end) in self.scope_pieces if start <= self.lineno < end)