Пример #1
0
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
Пример #2
0
    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])
Пример #3
0
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
    )
Пример #4
0
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]
Пример #5
0
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]
Пример #6
0
 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
     ])
Пример #7
0
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
Пример #8
0
    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)
Пример #9
0
        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))
Пример #10
0
    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
Пример #11
0
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"] = ""
Пример #12
0
    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)
Пример #13
0
    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)
        ]
Пример #14
0
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"] = ""
Пример #15
0
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
Пример #16
0
 def find_by_text(text, elements):
     return only(n for n in elements if n.text == text)
Пример #17
0
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),
        )
Пример #18
0
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())
Пример #19
0
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()
Пример #20
0
 def executing_piece(self):
     return only((start, end) for (start, end) in self.scope_pieces
                 if start <= self.lineno < end)