From 180531c53d785fdb0f5a42a06139ec6cead30357 Mon Sep 17 00:00:00 2001 From: Filip Schouwenaars Date: Wed, 22 Aug 2018 17:47:28 +0200 Subject: [PATCH 001/209] fix typo --- docs/articles/checking_compound_statements.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/articles/checking_compound_statements.rst b/docs/articles/checking_compound_statements.rst index 305b8051..e03159bc 100644 --- a/docs/articles/checking_compound_statements.rst +++ b/docs/articles/checking_compound_statements.rst @@ -186,9 +186,9 @@ The following example checks whether students correctly defined their own functi # sct Ex().check_function_def('shout_echo').check_correct( multi( - check_call("f('hey', 3)".has_equal_value(), - check_call("f('hi', 2)".has_equal_value(), - check_call("f('hi')".has_equal_value() + check_call("f('hey', 3)").has_equal_value(), + check_call("f('hi', 2)").has_equal_value(), + check_call("f('hi')").has_equal_value() ), check_body().set_context('test', 1).multi( has_equal_value(name = 'echo_word'), From 74ffdefb6ac4401cfb69b173de58f086a3150f8e Mon Sep 17 00:00:00 2001 From: Filip Schouwenaars Date: Thu, 30 Aug 2018 13:13:38 +0200 Subject: [PATCH 002/209] Allow overriding - has_equal_value/error/output can now take an 'override' value to avoid the solution from running. - check_object does not check whether an object is defined in the solution process if the student and solution processes are identical - add documentation article accordingly --- docs/articles/single_process_exercise.rst | 106 ++++++++++++++++++++++ docs/index.rst | 1 + pythonwhat/State.py | 8 ++ pythonwhat/check_object.py | 4 +- pythonwhat/has_funcs.py | 54 +++++++---- pythonwhat/local.py | 11 ++- pythonwhat/tasks.py | 10 +- tests/test_check_function.py | 1 - tests/test_docs.py | 1 - tests/test_has_expr.py | 15 +++ tests/test_messaging.py | 18 +++- tests/test_test_object.py | 9 ++ 12 files changed, 205 insertions(+), 33 deletions(-) create mode 100644 docs/articles/single_process_exercise.rst create mode 100644 tests/test_has_expr.py diff --git a/docs/articles/single_process_exercise.rst b/docs/articles/single_process_exercise.rst new file mode 100644 index 00000000..e9a65a43 --- /dev/null +++ b/docs/articles/single_process_exercise.rst @@ -0,0 +1,106 @@ +SingleProcessExercise +--------------------- + +Introduction +============ + +Typical interactive exercises on DataCamp will be of the type ``NormalExercise`` or something similar. + +For these normal exercises, the pythonbackend (the Python package responsible +for running Python code that the student submitted) will execute: + +- the solution code in a solution process + (once, at exercise initialization), +- the student's submission in a student process + (every time the student hits submit, after which the process is restarted from scratch) +- the student's experimentation commands in the console in a console process + (every time the user executes a command, without restarting afterwards) + +These completely separate processes make sure that: + +- the different commands do not interfere with one another; + if you import a package in one process, + the package will not become available in the other process. +- pythonwhat has access to a 'target solution process' to easily do comparisons; + to compare an object ``x``, you simply have to use ``Ex().check_object('x').has_equal_value()`` and + pythonwhat will figure out the value ``x`` should have from the solution process. + +To learn more about how the backend works, you can visit +`this wiki article `_. + +Why does this exercise type exist? +================================== + +There are Python courses that make extensive use of programs running outside of Python. +Sometimes, these programs cannot handle it well when different +Python process are trying to interface with it. +An example of this is PySpark, where on container startup, +a Spark cluster is started up, that you can then interface with. +Things go horribly wrong if you try to access this PySpark cluster from different Python processes. + +To solve for this, a new exercise type was built, +that does not create three separate Python processes (solution, student, console). +Instead, only one process is created: + +- the solution code is not executed in this process. +- the student's sumission is executed in this process, but the process is not restarted afterwards +- the student's experimentation commands in the console are executed in the same process. + +From a user perspective, this shouldn't pose too much difficulties, +with the exception that the code execution is now stateful. + +So, what's the problem then? +============================ + +As mentioned earlier, pythonwhat depends heavily on the existence of +two separate process: a 'target' solution process, and a student process. +Functions such as ```has_equal_value()`` compare values and the results of expression in these processes. +In the ``SingleProcessExercise``, the student process and +the solution process are identical, it's one and the same process, +so these comparisons don't make any sense. + +Therefore, the 'process-based checks' in pythonwhat have to be used with care when +writing SCTs for a ``SingleProcessExercise``. More specifically: + +- ``check_object()`` should work okay, as there is some magic happening behind the scenes. +- ``has_equal_value()`` and ``has_equal_output()`` should be used with the ``override`` argument. + When this argument is specified, the expression that is 'zoomed in on' in the solution code will not be executed in the solution proces. + Instead, it will just take the value you pass to ``override`` to compare the result/output of the expression that is zoomed in on in the student code to. + +Example +======= + +As an example, suppose we want to check whether a student correctly created a list ``x``: + +.. code:: + + # solution + x = [1, 2, 3, 4, 5] + +If this solution were part of a traditional ``NormalExercise``, your SCT would be simple: + +.. code:: + + # SCT + Ex().check_object('x').has_equal_value() + +However, if this solution were part of a ``SingleProcessExercise``, the above SCT would not work. +Instead, you'll want to do the following: + +.. code:: + + # SCT + Ex().check_object('x').has_equal_value(override = [1, 2, 3, 4, 5]) + +Here, we use ``override`` to tell pythonwhat not to go look for the value of ``x`` in the solution process. +Instead, it uses the manually specified value in ``override`` to compare to. + +You can use ``override`` in combination with other arguments in ``has_equal_x()``, such as ``expr_code``. +Suppose you're only interested in the element at index 2 of the list ``x``: + +.. code:: + + # SCT + Ex().check_object('x').has_equal_value(expr_code = 'x[2]', override = 3) + +Tricky stuff, but it works! \ No newline at end of file diff --git a/docs/index.rst b/docs/index.rst index fc7297b2..501766eb 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -44,6 +44,7 @@ The reference docs become useful when you grasp all concepts and want to look up articles/checking_compound_statements.rst articles/expression_tests.rst articles/processes.rst + articles/single_process_exercise.rst articles/electives.rst articles/test_to_check.rst diff --git a/pythonwhat/State.py b/pythonwhat/State.py index 9cf4510d..c891e835 100644 --- a/pythonwhat/State.py +++ b/pythonwhat/State.py @@ -211,6 +211,14 @@ def update(self, **kwargs): setattr(child, k, v) return child + def has_different_processes(self): + # process classes have an _identity field that is a tuple + try: + return self.student_process._identity[0] != self.solution_process._identity[0] + except: + # play it safe (most common) + return True + @staticmethod def parse_external(x): rep = Reporter.active_reporter diff --git a/pythonwhat/check_object.py b/pythonwhat/check_object.py index ecb6a6d0..bf477c98 100644 --- a/pythonwhat/check_object.py +++ b/pythonwhat/check_object.py @@ -17,7 +17,7 @@ def check_object(index, missing_msg=None, expand_msg=None, state=None, typestr=" Args: index (str): the name of the object which value has to be checked. missing_msg (str): feedback message when the object is not defined in the student's environment. - expect_msg (str): prepending message to put in front. + expand_msg (str): prepending message to put in front. :Example: @@ -49,7 +49,7 @@ def check_object(index, missing_msg=None, expand_msg=None, state=None, typestr=" rep = Reporter.active_reporter - if not isDefinedInProcess(index, state.solution_process): + if not isDefinedInProcess(index, state.solution_process) and state.has_different_processes(): raise NameError("%r not in solution environment " % index) append_message = {'msg': expand_msg, 'kwargs': {'index': index, 'typestr': typestr}} diff --git a/pythonwhat/has_funcs.py b/pythonwhat/has_funcs.py index 10239e29..47f5fefc 100644 --- a/pythonwhat/has_funcs.py +++ b/pythonwhat/has_funcs.py @@ -167,6 +167,7 @@ def parse_tree(tree): DEFAULT_ERROR_MSG_INV="__JINJA__:Running {{'it' if parent['part'] else 'the higlighted expression'}} didn't generate an error, but it should!" DEFAULT_UNDEFINED_NAME_MSG="__JINJA__:Running {{'it' if parent['part'] else 'the higlighted expression'}} should define a variable `{{name}}` without errors, but it doesn't." DEFAULT_INCORRECT_NAME_MSG="__JINJA__:Are you sure you assigned the correct value to `{{name}}`?" +DEFAULT_INCORRECT_EXPR_CODE_MSG="__JINJA__:Running the expression `{{expr_code}}` didn't generate the expected result." def has_expr(incorrect_msg=None, error_msg=None, undefined_msg=None, @@ -178,17 +179,26 @@ def has_expr(incorrect_msg=None, name=None, copy=True, func=None, + override=None, state=None, test=None): if append is None: # if not specified, set to False if incorrect_msg was manually specified append = incorrect_msg is None if incorrect_msg is None: - incorrect_msg = DEFAULT_INCORRECT_MSG if name is None else DEFAULT_INCORRECT_NAME_MSG + if name: + incorrect_msg = DEFAULT_INCORRECT_NAME_MSG + elif expr_code: + incorrect_msg = DEFAULT_INCORRECT_EXPR_CODE_MSG + else: + incorrect_msg = DEFAULT_INCORRECT_MSG if undefined_msg is None: undefined_msg = DEFAULT_UNDEFINED_NAME_MSG if error_msg is None: - error_msg = DEFAULT_ERROR_MSG_INV if test == 'error' else DEFAULT_ERROR_MSG + if test == 'error': + error_msg = DEFAULT_ERROR_MSG_INV + else: + error_msg = DEFAULT_ERROR_MSG rep = Reporter.active_reporter @@ -200,16 +210,21 @@ def has_expr(incorrect_msg=None, name=name, copy=copy) - eval_sol, str_sol = get_func(tree=state.solution_tree, - process=state.solution_process, - context=state.solution_context, - env=state.solution_env) + if override is not None: + # don't bother with running expression and fetching output/value + # eval_sol, str_sol = eval + eval_sol, str_sol = override, str(override) + else: + eval_sol, str_sol = get_func(tree=state.solution_tree, + process=state.solution_process, + context=state.solution_context, + env=state.solution_env) - if (test == 'error') ^ isinstance(eval_sol, Exception): - raise ValueError("Evaluating expression raised error in solution process (or not an error if testing for one). " - "Error: {} - {}".format(type(eval_sol), str_sol)) - if isinstance(eval_sol, ReprFail): - raise ValueError("Couldn't figure out the value of a default argument: " + eval_sol.info) + if (test == 'error') ^ isinstance(eval_sol, Exception): + raise ValueError("Evaluating expression raised error in solution process (or not an error if testing for one). " + "Error: {} - {}".format(type(eval_sol), str_sol)) + if isinstance(eval_sol, ReprFail): + raise ValueError("Couldn't extract the value for the highlighted expression from the solution process: " + eval_sol.info) eval_stu, str_stu = get_func(tree=state.student_tree, process=state.student_process, @@ -221,7 +236,8 @@ def has_expr(incorrect_msg=None, 'stu_part': state.student_parts, 'sol_part': state.solution_parts, 'name': name, 'test': test, - 'test_desc': '' if test == 'value' else 'the %s ' % test + 'test_desc': '' if test == 'value' else 'the %s ' % test, + 'expr_code': expr_code } fmt_kwargs['stu_eval'] = utils.shorten_str(str(eval_stu)) @@ -276,12 +292,16 @@ def has_expr(incorrect_msg=None, expr_code (str): if this argument is set, the expression in the student/solution code will not be ran. Instead, the given piece of code will be ran in the student as well as the solution environment and the result will be compared. - name (str): If this is specified, the {1} of running this expression after running the focused expression - is returned, instead of the {1} of the focussed expression in itself. This is typically used to inspect the - {1} of an object after executing the body of e.g. a ``for`` loop. + name (str): If this is specified, the {0} of running this expression after running the focused expression + is returned, instead of the {0} of the focussed expression in itself. This is typically used to inspect the + {0} of an object after executing the body of e.g. a ``for`` loop. copy (bool): whether to try to deep copy objects in the environment, such as lists, that could accidentally be mutated. Disable to speed up SCTs. Disabling may lead to cryptic mutation issues. func: custom binary function of form f(stu_result, sol_result), for equality testing. + override: If specified, this avoids the execution of the targeted code in the solution process. Instead, it + will compare the {0} of the expression in the student process with the value specified in ``override``. + Typically used in a ``SingleProcessExercise`` or if you want to allow for different solutions other than + the one coded up in the solution. """ has_equal_value = partial(has_expr, test = 'value') @@ -317,14 +337,14 @@ def has_expr(incorrect_msg=None, When called on an SCT chain, ``has_equal_output()`` will execute the student and solution code that is 'zoomed in on' and compare the output. - """ + args_string.format("output", "output") + """ + args_string.format("output") has_equal_error = partial(has_expr, test = 'error') has_equal_error.__doc__ = """Run targeted student and solution code, and compare generated errors. When called on an SCT chain, ``has_equal_error()`` will execute the student and solution code that is 'zoomed in on' and compare the errors that they generate. - """ + args_string.format("error", "error") + """ + args_string.format("error") ## Various has tests ---------------------------------------------------------- diff --git a/pythonwhat/local.py b/pythonwhat/local.py index 85814e94..c69a324e 100644 --- a/pythonwhat/local.py +++ b/pythonwhat/local.py @@ -1,4 +1,6 @@ import io +import random + from pythonwhat.check_syntax import Ex from pythonwhat.State import State from pythonwhat.Reporter import Reporter @@ -16,21 +18,22 @@ def run_code(self, code): class StubProcess(object): - def __init__(self, init_code = None): + def __init__(self, init_code = None, pid = None): self.shell = StubShell(init_code) + self._identity = (pid,) if pid else (random.randint(0, 1e12),) def executeTask(self, task): return task(self.shell) -def setup_state(stu_code = "", sol_code = "", pec = ""): +def setup_state(stu_code = "", sol_code = "", pec = "", pid = None): stu_output = io.StringIO() with redirect_stdout(stu_output): - stu_process = StubProcess(init_code = "%s\n%s" % (pec, stu_code)) + stu_process = StubProcess("%s\n%s" % (pec, stu_code), pid) sol_output = io.StringIO() with redirect_stdout(sol_output): - sol_process = StubProcess(init_code = "%s\n%s" % (pec, sol_code)) + sol_process = StubProcess("%s\n%s" % (pec, sol_code), pid) rep = Reporter() Reporter.active_reporter = rep diff --git a/pythonwhat/tasks.py b/pythonwhat/tasks.py index cfff1d57..601e3162 100644 --- a/pythonwhat/tasks.py +++ b/pythonwhat/tasks.py @@ -315,14 +315,12 @@ def taskRunEval(tree, pre_code = "", expr_code = "", name="", copy=True, tempname='_evaluation_object_', call=None): try: - # Prepare code -------------------------------------------------------- - # If no name given, the object of interest is the output of eval - # otherwise, we'll use name to get the object from the environment - if not isinstance(tree, ast.Module): + # Prepare code and mode ----------------------------------------------- + if (expr_code and name) or (not expr_code and isinstance(tree, ast.Module)): + mode = 'exec' + else: mode = 'eval' tree = ast.Expression(tree) - else: - mode = 'exec' # Expression code takes precedence over tree code if expr_code: code = expr_code diff --git a/tests/test_check_function.py b/tests/test_check_function.py index fa91eedf..107345a4 100644 --- a/tests/test_check_function.py +++ b/tests/test_check_function.py @@ -169,7 +169,6 @@ def test_incorrect_usage(sct, sol): with pytest.raises(KeyError): out = helper.run(data) -@pytest.mark.debug @pytest.mark.parametrize('code', [ 'print(round(1.23))', 'x = print(round(1.23))', diff --git a/tests/test_docs.py b/tests/test_docs.py index 72c94ddc..474ec6fc 100644 --- a/tests/test_docs.py +++ b/tests/test_docs.py @@ -62,7 +62,6 @@ def test_compound_statement_for_2(data2): output = helper.run(data2) assert output['correct'] -@pytest.mark.debug def test_compount_statement_for_3(data2): data2["DC_CODE"] = "my_dict = {'a': 1, 'b': 2}\nfor first, second in my_dict.items():\n mess = first + ' - ' + str(second)\n print(mess)" output = helper.run(data2) diff --git a/tests/test_has_expr.py b/tests/test_has_expr.py new file mode 100644 index 00000000..f930c8b7 --- /dev/null +++ b/tests/test_has_expr.py @@ -0,0 +1,15 @@ +import pytest +from pythonwhat.local import setup_state +import helper + +def test_has_expr_override_pass(): + stu = 'x = [1, 2, 3]' + sol = 'x = [1, 2, 5]' + s = setup_state(stu_code=stu, sol_code=sol) + helper.passes(s.check_object('x').has_equal_value(expr_code = 'x[2]', override=3)) + +def test_has_expr_override_pass_2(): + stu = 'x = [1, 2, 3]' + sol = 'x = [1, 2, 5]' + s = setup_state(stu_code=stu, sol_code=sol) + helper.passes(s.check_object('x').has_equal_value(override=[1, 2, 3])) diff --git a/tests/test_messaging.py b/tests/test_messaging.py index aa6bc441..f322e4d9 100644 --- a/tests/test_messaging.py +++ b/tests/test_messaging.py @@ -225,7 +225,6 @@ def test_check_object_manual(stu, patt): # Check call ------------------------------------------------------------------ -@pytest.mark.debug @pytest.mark.parametrize('stu, patt', [ ('', 'The system wants to check the definition of `test()` but hasn\'t found it.'), ('def test(a, b): return 1', 'Check the definition of `test()`. To verify it, we reran `test(1, 2)`. Expected `3`, but got `1`.'), @@ -364,7 +363,6 @@ def test_nesting(stu, patt): ## test limited stacking ------------------------------------------------------ -@pytest.mark.debug @pytest.mark.parametrize('sct, patt', [ ('Ex().check_for_loop().check_body().check_for_loop().check_body().has_equal_output()', 'Check the first for statement. Did you correctly specify the body? Expected the output `1+1`, but got `1-1`.'), @@ -384,3 +382,19 @@ def test_limited_stacking(sct, patt): }) assert not output['correct'] assert message(output, patt) + +## test has_expr -------------------------------------------------------------- + +@pytest.mark.parametrize('sct, patt', [ + ("Ex().check_object('x').has_equal_value()", 'Did you correctly define the variable `x`? Expected `[1]`, but got `[0]`.'), + ("Ex().has_equal_value(name = 'x')", 'Are you sure you assigned the correct value to `x`?'), + ("Ex().has_equal_value(expr_code = 'x[0]')", "Running the expression `x[0]` didn't generate the expected result.") +]) +def test_has_expr(sct, patt): + output = helper.run({ + 'DC_SOLUTION': 'x = [1]', + 'DC_CODE': 'x = [0]', + 'DC_SCT': sct + }) + assert not output['correct'] + assert message(output, patt) \ No newline at end of file diff --git a/tests/test_test_object.py b/tests/test_test_object.py index ffa07102..7126a647 100644 --- a/tests/test_test_object.py +++ b/tests/test_test_object.py @@ -1,5 +1,7 @@ import unittest import helper +from pythonwhat.local import setup_state +from pythonwhat.Test import TestFail as TF import pytest @pytest.mark.parametrize('sct', [ @@ -50,6 +52,13 @@ def test_check_object_custom_compare(stu_code, passes): }) assert output['correct'] == passes +def test_check_object_single_process(): + state2pids = setup_state('x = 3', '') + with pytest.raises(NameError): + state2pids.check_object('x') + state1pid = setup_state('x = 3', '', pid = 1) + helper.passes(state1pid.check_object('x')) + @pytest.mark.parametrize('stu_code, passes', [ ('arr = 4', False), ('arr = np.array([1])', True) From 426d41d1b783e02fc518a99123f9a5d772cd770d Mon Sep 17 00:00:00 2001 From: Filip Schouwenaars Date: Thu, 30 Aug 2018 17:09:47 +0200 Subject: [PATCH 003/209] bump version + update CHANGELOG --- CHANGELOG.md | 10 ++++++++++ pythonwhat/__init__.py | 2 +- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6d2e8d2d..35d76521 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,16 @@ All notable changes to the pythonwhat project will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). +## 2.15.3 + +### Added + +- `check_object()` does not check whether the targeted object is specified in the solution process if + student and solution process are identical (as is the case in the `SingleProcessExercise`). +- `has_expr()`, the function used by `has_equal_value()`, `has_equal_output()` and `has_equal_error()` can take an `override` argument, + that causes the solution expression _not_ to run and use the value specified in `override` instead. + For more information, have a look at the 'SingleProcessExercise' article on the documentation. + ## 2.15.2 ### Removed diff --git a/pythonwhat/__init__.py b/pythonwhat/__init__.py index d328c25b..737469f0 100644 --- a/pythonwhat/__init__.py +++ b/pythonwhat/__init__.py @@ -1,3 +1,3 @@ -__version__ = '2.15.2' +__version__ = '2.15.3' from .test_exercise import test_exercise, allow_errors From 1003902f8a647a8e34ab0d8e4e37acb59de028b9 Mon Sep 17 00:00:00 2001 From: Filip Schouwenaars Date: Fri, 31 Aug 2018 08:10:56 +0200 Subject: [PATCH 004/209] feat(instructor_warnings): better error messaging in case wrong/nonsensical SCT --- pythonwhat/Feedback.py | 2 + pythonwhat/State.py | 6 +- pythonwhat/check_funcs.py | 30 +++--- pythonwhat/check_function.py | 13 ++- pythonwhat/check_has_context.py | 4 +- pythonwhat/check_logic.py | 12 +-- pythonwhat/check_object.py | 16 ++-- pythonwhat/has_funcs.py | 62 +++++++----- pythonwhat/tasks.py | 13 +-- pythonwhat/test_funcs/test_function.py | 9 +- pythonwhat/test_funcs/test_object.py | 3 +- pythonwhat/utils_ast.py | 14 +++ tests/helper.py | 12 ++- tests/test_author_warnings.py | 128 +++++++++++++++++++++++++ tests/test_check_function.py | 39 ++++---- tests/test_has_import.py | 27 ++---- tests/test_has_printout.py | 44 ++++----- tests/test_instructor_warnings.py | 28 ------ tests/test_set_context.py | 8 -- tests/test_spec.py | 3 +- tests/test_test_object.py | 18 ---- tests/test_v2_only.py | 4 +- 22 files changed, 301 insertions(+), 194 deletions(-) create mode 100644 tests/test_author_warnings.py delete mode 100644 tests/test_instructor_warnings.py diff --git a/pythonwhat/Feedback.py b/pythonwhat/Feedback.py index 7ea4302a..f8ce0345 100644 --- a/pythonwhat/Feedback.py +++ b/pythonwhat/Feedback.py @@ -16,3 +16,5 @@ def __init__(self, message, state = None): except: pass +class InstructorError(Exception): + pass diff --git a/pythonwhat/State.py b/pythonwhat/State.py index c891e835..7463ffb7 100644 --- a/pythonwhat/State.py +++ b/pythonwhat/State.py @@ -5,7 +5,7 @@ from functools import partial from pythonwhat.parsing import TargetVars, FunctionParser, ObjectAccessParser, parser_dict from pythonwhat.Reporter import Reporter -from pythonwhat.Feedback import Feedback +from pythonwhat.Feedback import Feedback, InstructorError from pythonwhat.Test import Test from pythonwhat import signatures from pythonwhat.converters import get_manual_converters @@ -219,6 +219,10 @@ def has_different_processes(self): # play it safe (most common) return True + def assert_parent(self, fun): + if self.parent_state is not None: + raise InstructorError("`%s()` should only be called from the root state, `Ex()`." % fun) + @staticmethod def parse_external(x): rep = Reporter.active_reporter diff --git a/pythonwhat/check_funcs.py b/pythonwhat/check_funcs.py index 1de08039..81920340 100644 --- a/pythonwhat/check_funcs.py +++ b/pythonwhat/check_funcs.py @@ -3,8 +3,9 @@ from pythonwhat.check_logic import multi from pythonwhat.Reporter import Reporter from pythonwhat.Test import Test, EqualTest, TestFail -from pythonwhat.Feedback import Feedback +from pythonwhat.Feedback import Feedback, InstructorError from pythonwhat.utils import get_ord +from pythonwhat.utils_ast import assert_ast from functools import partial import ast @@ -34,7 +35,6 @@ def part_to_child(stu_part, sol_part, append_message, state, node_name=None): solution_subtree=sol_part, append_message=append_message) - def check_part(name, part_msg, missing_msg=None, expand_msg=None, @@ -48,9 +48,12 @@ def check_part(name, part_msg, append_message = {'msg': expand_msg, 'kwargs': { 'part': part_msg }} has_part(name, missing_msg, state, append_message['kwargs']) + stu_part = state.student_parts[name] sol_part = state.solution_parts[name] + assert_ast(state, sol_part, append_message['kwargs']) + return part_to_child(stu_part, sol_part, append_message, state) def check_part_index(name, index, part_msg, @@ -71,8 +74,11 @@ def check_part_index(name, index, part_msg, # create message ordinal = get_ord(index+1) if isinstance(index, int) else "" - fmt_kwargs = {'index': index, 'ordinal': ordinal} - fmt_kwargs['part'] = part_msg.format(**fmt_kwargs) + fmt_kwargs = { + 'index': index, + 'ordinal': ordinal + } + fmt_kwargs.update(part = part_msg.format(**fmt_kwargs)) append_message = { 'msg': expand_msg, @@ -80,7 +86,7 @@ def check_part_index(name, index, part_msg, } # check there are enough parts for index - has_part(name, missing_msg, state, append_message['kwargs'], index) + has_part(name, missing_msg, state, fmt_kwargs, index) # get part at index stu_part = state.student_parts[name] @@ -94,6 +100,8 @@ def check_part_index(name, index, part_msg, stu_part = stu_part[index] sol_part = sol_part[index] + assert_ast(state, sol_part, fmt_kwargs) + # return child state from part return part_to_child(stu_part, sol_part, append_message, state) @@ -142,7 +150,7 @@ def with_context(*args, state=None): solution_res = setUpNewEnvInProcess(process = state.solution_process, context = state.solution_parts['with_items']) if isinstance(solution_res, Exception): - raise Exception("error in the solution, running test_with(): %s" % str(solution_res)) + raise InstructorError("error in the solution, running test_with(): %s" % str(solution_res)) student_res = setUpNewEnvInProcess(process = state.student_process, context = state.student_parts['with_items']) @@ -159,7 +167,7 @@ def with_context(*args, state=None): finally: # exit context if breakDownNewEnvInProcess(process = state.solution_process): - raise Exception("error in the solution, closing the `with` fails with: %s" % (close_solution_context)) + raise InstructorError("error in the solution, closing the `with` fails with: %s" % (close_solution_context)) if breakDownNewEnvInProcess(process = state.student_process): @@ -280,7 +288,7 @@ def run_call(args, node, process, get_func, **kwargs): func_expr = ast.Name(id=node.name, ctx=ast.Load()) elif isinstance(node, ast.Lambda): # lambda body expr func_expr = node - else: raise TypeError("Only function definition or lambda may be called") + else: raise InstructorError("Only function definition or lambda may be called") # args is a call string or argument list/dict if isinstance(args, str): @@ -331,12 +339,12 @@ def call(args, if (test == 'error') ^ isinstance(eval_sol, Exception): _msg = state.build_message("FMT:Calling {argstr} resulted in an error (or not an error if testing for one). Error message: {type_err} {str_sol}", dict(type_err=type(eval_sol), str_sol=str_sol, argstr=argstr)), - raise ValueError(_msg) + raise InstructorError(_msg) if isinstance(eval_sol, ReprFail): _msg = state.build_message("FMT:Can't get the result of calling {argstr}: {eval_sol.info}", dict(argstr = argstr, eval_sol=eval_sol)) - raise ValueError(_msg) + raise InstructorError(_msg) # Run for Submission ------------------------------------------------------ eval_stu, str_stu = run_call(args, state.student_parts['node'], state.student_process, get_func, **kwargs) @@ -363,7 +371,7 @@ def build_call(callstr, node): elif isinstance(node, ast.Lambda): # lambda body expr func_expr = node argstr = 'it with the arguments `{}`'.format(callstr.replace('f', '')) - else: raise TypeError("You can use check_call() only on check_function_def() or check_lambda()") + else: raise InstructorError("You can use check_call() only on check_function_def() or check_lambda()") parsed = ast.parse(callstr).body[0].value parsed.func = func_expr diff --git a/pythonwhat/check_function.py b/pythonwhat/check_function.py index 5f3fe623..4336ddd5 100644 --- a/pythonwhat/check_function.py +++ b/pythonwhat/check_function.py @@ -3,7 +3,7 @@ from pythonwhat.tasks import getSignatureInProcess from pythonwhat.utils import get_ord, get_times from pythonwhat.Test import Test -from pythonwhat.Feedback import Feedback +from pythonwhat.Feedback import Feedback, InstructorError from pythonwhat.parsing import IndexedDict from functools import partial @@ -89,7 +89,12 @@ def check_function(name, index=0, # Get Parts ---- # Copy, otherwise signature binding overwrites sol_out[name][index]['args'] - sol_parts = {**sol_out[name][index]} + try: + sol_parts = {**sol_out[name][index]} + except KeyError: + raise InstructorError("`check_function()` couldn't find a call of `%s()` in the solution code. Make sure you get the mapping right!" % name) + except IndexError: + raise InstructorError("`check_function()` couldn't find %s calls of `%s()` in your solution code." % (index+1, name)) try: # Copy, otherwise signature binding overwrites stu_out[name][index]['args'] @@ -108,9 +113,7 @@ def check_function(name, index=0, sol_sig = get_sig(mapped_name=sol_parts['name'], process=state.solution_process) sol_parts['args'] = bind_args(sol_sig, sol_parts['args']) except: - raise ValueError("Something went wrong in matching call index {index} of {name} to its signature. " - "You might have to manually specify or correct the signature." - .format(index=index, name=name)) + raise InstructorError("`check_function()` couldn't match the %s call of `%s` to its signature. " % (get_ord(index + 1), name)) try: stu_sig = get_sig(mapped_name=stu_parts['name'], process=state.student_process) diff --git a/pythonwhat/check_has_context.py b/pythonwhat/check_has_context.py index f6d6218b..0ef24e1e 100644 --- a/pythonwhat/check_has_context.py +++ b/pythonwhat/check_has_context.py @@ -1,6 +1,6 @@ from pythonwhat.Reporter import Reporter from pythonwhat.Test import Test, EqualTest -from pythonwhat.Feedback import Feedback +from pythonwhat.Feedback import Feedback, InstructorError from pythonwhat.State import State from functools import singledispatch from pythonwhat.check_funcs import check_part_index @@ -43,7 +43,7 @@ def _test(state, incorrect_msg, exact_names, tv_name, highlight_name): @singledispatch def _has_context(state, incorrect_msg, exact_names): - raise BaseException("first argument to _has_context must be a State instance or subclass") + raise InstructorError("first argument to _has_context must be a State instance or subclass") @_has_context.register(State) def has_context_state(*args, **kwargs): diff --git a/pythonwhat/check_logic.py b/pythonwhat/check_logic.py index 8cdf0f95..fd7c0318 100644 --- a/pythonwhat/check_logic.py +++ b/pythonwhat/check_logic.py @@ -2,7 +2,7 @@ from functools import partial from pythonwhat.Reporter import Reporter from pythonwhat.Test import Test, TestFail -from pythonwhat.Feedback import Feedback +from pythonwhat.Feedback import Feedback, InstructorError import copy import ast @@ -184,14 +184,14 @@ def set_context(*args, state=None, **kwargs): # for now, you can't specify both if len(args) > 0 and len(kwargs) > 0: - raise ValueError("In set_context() make sure to specify arguments either by position, either by name") + raise InstructorError("In `set_context()`, specify arguments either by position, either by name.") # set args specified by pos ----------------------------------------------- if args: # stop if too many pos args for solution if len(args) > len(sol_crnt): - raise IndexError("Too many positional args. There are {} context vals, but tried to set {}" - .format(len(sol_crnt), len(args))) + raise InstructorError("Too many positional args. There are {} context vals, but tried to set {}" + .format(len(sol_crnt), len(args))) # set pos args upd_sol = sol_crnt.update(dict(zip(sol_crnt.keys(), args))) upd_stu = stu_crnt.update(dict(zip(stu_crnt.keys(), args))) @@ -203,8 +203,8 @@ def set_context(*args, state=None, **kwargs): if kwargs: # stop if keywords don't match with solution if set(kwargs) - set(upd_sol): - raise KeyError("Context val names are {}, but tried to set {}" - .format(upd_sol or "none", kwargs.keys())) + raise InstructorError("`set_context()` failed: context val names are {}, but you tried to set {}." + .format(upd_sol or "missing", sorted(list(kwargs.keys())))) out_sol = upd_sol.update(kwargs) # need to match keys in kwargs with corresponding keys in stu context # in case they used, e.g., different loop variable names diff --git a/pythonwhat/check_object.py b/pythonwhat/check_object.py index bf477c98..31a97962 100644 --- a/pythonwhat/check_object.py +++ b/pythonwhat/check_object.py @@ -1,7 +1,7 @@ from pythonwhat.parsing import ObjectAssignmentParser from pythonwhat.Test import DefinedProcessTest, InstanceProcessTest, DefinedCollProcessTest from pythonwhat.Reporter import Reporter -from pythonwhat.Feedback import Feedback +from pythonwhat.Feedback import Feedback, InstructorError from pythonwhat.tasks import isDefinedInProcess, isInstanceInProcess, isDefinedCollInProcess from pythonwhat.check_funcs import part_to_child from pythonwhat.has_funcs import has_equal_value @@ -11,12 +11,12 @@ def check_object(index, missing_msg=None, expand_msg=None, state=None, typestr="variable"): """Check object existence (and equality) - Check whether an object is defined in the student's environment, and zoom in on its value in both - student and solution environment to inspect quality (with has_equal_value(). + Check whether an object is defined in the student's process, and zoom in on its value in both + student and solution process to inspect quality (with has_equal_value(). Args: index (str): the name of the object which value has to be checked. - missing_msg (str): feedback message when the object is not defined in the student's environment. + missing_msg (str): feedback message when the object is not defined in the student process. expand_msg (str): prepending message to put in front. :Example: @@ -41,6 +41,8 @@ def check_object(index, missing_msg=None, expand_msg=None, state=None, typestr=" """ + state.assert_parent('check_object') + if missing_msg is None: missing_msg = "__JINJA__:Did you define the {{typestr}} `{{index}}` without errors?" @@ -50,7 +52,7 @@ def check_object(index, missing_msg=None, expand_msg=None, state=None, typestr=" rep = Reporter.active_reporter if not isDefinedInProcess(index, state.solution_process) and state.has_different_processes(): - raise NameError("%r not in solution environment " % index) + raise InstructorError("`check_object()` couldn't find object `%s` in the solution process." % index) append_message = {'msg': expand_msg, 'kwargs': {'index': index, 'typestr': typestr}} @@ -100,7 +102,7 @@ def is_instance(inst, not_instance_msg=None, state=None): if not_instance_msg is None: not_instance_msg = "__JINJA__:Is it a {{inst.__name__}}?" if not isInstanceInProcess(sol_name, inst, state.solution_process): - raise ValueError("%r is not a %s in the solution environment" % (sol_name, type(inst))) + raise InstructorError("`is_instance()` noticed that `%s` is not a `%s` in the solution process." % (sol_name, inst.__name__)) _msg = state.build_message(not_instance_msg, {'inst': inst}) feedback = Feedback(_msg, state) @@ -153,7 +155,7 @@ def check_keys(key, missing_msg=None, expand_msg=None, state=None): stu_name = state.student_parts.get('name') if not isDefinedCollInProcess(sol_name, key, state.solution_process): - raise NameError("Not all keys you specified are actually keys in %s in the solution process" % sol_name) + raise InstructorError("`check_keys()` couldn't find key `%s` in object `%s` in the solution process." % (key, sol_name)) # check if key available _msg = state.build_message(missing_msg, {'key': key}) diff --git a/pythonwhat/has_funcs.py b/pythonwhat/has_funcs.py index 47f5fefc..45be85b0 100644 --- a/pythonwhat/has_funcs.py +++ b/pythonwhat/has_funcs.py @@ -1,7 +1,7 @@ from pythonwhat.tasks import getResultInProcess, getOutputInProcess, getErrorInProcess, ReprFail, isDefinedInProcess, getOptionFromProcess, ReprFail, UndefinedValue from pythonwhat.Reporter import Reporter from pythonwhat.Test import Test, EqualTest -from pythonwhat.Feedback import Feedback +from pythonwhat.Feedback import Feedback, InstructorError from pythonwhat import utils from functools import partial import re @@ -14,22 +14,33 @@ def has_part(name, msg, state=None, fmt_kwargs=None, index=None): rep = Reporter.active_reporter - d = {'sol_part': state.solution_parts, - 'stu_part': state.student_parts, - **fmt_kwargs - } + d = { + 'sol_part': state.solution_parts, + 'stu_part': state.student_parts, + **fmt_kwargs + } - try: - part = state.student_parts[name] + def verify(part, index): if index is not None: if isinstance(index, list): for ind in index: part = part[ind] else: part = part[index] - if part is None: raise KeyError + if part is None: + raise KeyError + + # Chceck if it's there in the solution + _msg = state.build_message(msg, d) + _err_msg = "SCT fails on solution: " + _msg + try: + verify(state.solution_parts[name], index) + except (KeyError, IndexError): + raise InstructorError(_err_msg) + + try: + verify(state.student_parts[name], index) except (KeyError, IndexError): - _msg = state.build_message(msg, d) rep.do_test(Test(Feedback(_msg, state))) return state @@ -67,7 +78,6 @@ def shout(word): SCT that checks number of arguments:: Ex().check_function_def('shout').has_equal_part_len('args', 'not enough args!') - """ rep = Reporter.active_reporter d = dict(stu_len = len(state.student_parts[name]), @@ -131,7 +141,8 @@ def has_equal_ast(incorrect_msg=None, rep = Reporter.active_reporter if code and incorrect_msg is None: - raise ValueError("If you manually specify the code to match inside has_equal_ast(), you have to explicitly set the `incorrect_msg` arugment.") + raise InstructorError("If you manually specify the code to match inside has_equal_ast(), " + "you have to explicitly set the `incorrect_msg` argument.") if append is None: # if not specified, set to False if incorrect_msg was manually specified append = incorrect_msg is None @@ -221,10 +232,10 @@ def has_expr(incorrect_msg=None, env=state.solution_env) if (test == 'error') ^ isinstance(eval_sol, Exception): - raise ValueError("Evaluating expression raised error in solution process (or not an error if testing for one). " + raise InstructorError("Evaluating expression raised error in solution process (or not an error if testing for one). " "Error: {} - {}".format(type(eval_sol), str_sol)) if isinstance(eval_sol, ReprFail): - raise ValueError("Couldn't extract the value for the highlighted expression from the solution process: " + eval_sol.info) + raise InstructorError("Couldn't extract the value for the highlighted expression from the solution process: " + eval_sol.info) eval_stu, str_stu = get_func(tree=state.student_tree, process=state.student_process, @@ -373,7 +384,7 @@ def has_code(text, SCT:: # Verify that student code contains pattern (not robust!!): - Ex().has_code(r"1\s*\+2\s*\+3") + Ex().has_code(r"1\\s*\\+2\\s*\\+3") """ rep = Reporter.active_reporter @@ -433,7 +444,7 @@ def has_import(name, solution_imports = state.solution_imports if name not in solution_imports: - raise NameError("The package you specified is not in the solution imports itself. %r not in solution imports" % name) + raise InstructorError("`has_import()` couldn't find an import of the package %s in your solution code." % name) fmt_kwargs = { 'pkg': name, 'alias': solution_imports[name] } @@ -475,7 +486,6 @@ def has_output(text, if not no_output_msg: no_output_msg = "You did not output the correct things." - # raise ValueError("Inside has_output(), specify the `no_output_msg` manually.") student_output = state.raw_student_output @@ -534,14 +544,15 @@ def has_printout(index, print("random"); print(1, 2, 3, 4) """ + state.assert_parent('has_printout') + if not_printed_msg is None: not_printed_msg = "__JINJA__:Have you used `{{sol_call}}` to do the appropriate printouts?" try: sol_call_ast = state.solution_function_calls['print'][index]['node'] except (KeyError, IndexError): - raise ValueError("Using has_printout() with index {} expects that there is/are at least {} print() call(s) in your solution." - "Is that the case?".format(index, index+1)) + raise InstructorError("`has_printout({})` couldn't find the {} print call in your solution.".format(index, utils.get_ord(index + 1))) out_sol, str_sol = getOutputInProcess( tree = sol_call_ast, @@ -555,8 +566,8 @@ def has_printout(index, sol_call_str = state.solution_tree_tokens.get_text(sol_call_ast) if isinstance(str_sol, Exception): - raise ValueError("Evaluating the solution expression {} raised error in solution process." - "Error: {} - {}".format(sol_call_str, type(out_sol), str_sol)) + raise InstructorError("Evaluating the solution expression {} raised error in solution process." + "Error: {} - {}".format(sol_call_str, type(out_sol), str_sol)) _msg = state.build_message(not_printed_msg, { 'sol_call': sol_call_str }) @@ -578,23 +589,22 @@ def has_chosen(correct, msgs, state=None): student. The list should have the same length as the number of instructions. """ if not issubclass(type(correct), int): - raise ValueError("correct should be an integer") + raise InstructorError("Inside `has_chosen()`, the argument `correct` should be an integer.") rep = Reporter.active_reporter student_process = state.student_process if not isDefinedInProcess(MC_VAR_NAME, student_process): - raise NameError("Option not available in the student process") + raise InstructorError("Option not available in the student process") else: selected_option = getOptionFromProcess(student_process, MC_VAR_NAME) if not issubclass(type(selected_option), int): - raise ValueError("selected_option should be an integer") + raise InstructorError("selected_option should be an integer") if selected_option < 1 or correct < 1: - raise ValueError( - "selected_option and correct should be greater than zero") + raise InstructorError("selected_option and correct should be greater than zero") if selected_option > len(msgs) or correct > len(msgs): - raise ValueError("there are not enough feedback messages defined") + raise InstructorError("there are not enough feedback messages defined") feedback_msg = msgs[selected_option - 1] diff --git a/pythonwhat/tasks.py b/pythonwhat/tasks.py index 601e3162..5c63d15d 100644 --- a/pythonwhat/tasks.py +++ b/pythonwhat/tasks.py @@ -10,6 +10,7 @@ from pythonwhat.utils_env import set_context_vals, assign_from_ast from contextlib import contextmanager from functools import partial, wraps, update_wrapper +from pythonwhat.Feedback import InstructorError def process_task(f): """Decorator to (optionally) run function in a process.""" @@ -84,14 +85,14 @@ def get_signature(name, mapped_name, signature, manual_sigs, env): if signature in manual_sigs: signature = inspect.Signature(manual_sigs[signature]) else: - raise ValueError('signature error - specified signature not found') + raise InstructorError('signature error - specified signature not found') if signature is None: # establish function try: fun = eval(mapped_name, env) except: - raise ValueError("%s() was not found." % mapped_name) + raise InstructorError("%s() was not found." % mapped_name) # first go through manual sigs # try to get signature @@ -106,18 +107,18 @@ def get_signature(name, mapped_name, signature, manual_sigs, env): els[0] = type(eval(els[0], env)).__name__ generic_name = ".".join(els[:]) except: - raise ValueError('signature error - cannot convert call') + raise InstructorError('signature error - cannot convert call') if generic_name in manual_sigs: signature = inspect.Signature(manual_sigs[generic_name]) else: - raise ValueError('signature error - %s not in builtins' % generic_name) + raise InstructorError('signature error - %s not in builtins' % generic_name) else: - raise ValueError('manual signature not found') + raise InstructorError('manual signature not found') except: try: signature = inspect.signature(fun) except: - raise ValueError('signature error - cannot determine signature') + raise InstructorError('signature error - cannot determine signature') return signature diff --git a/pythonwhat/test_funcs/test_function.py b/pythonwhat/test_funcs/test_function.py index 5ad9c3c8..7cb68c03 100644 --- a/pythonwhat/test_funcs/test_function.py +++ b/pythonwhat/test_funcs/test_function.py @@ -1,6 +1,7 @@ import ast from functools import partial from pythonwhat.check_function import check_function +from pythonwhat.Feedback import InstructorError from pythonwhat.Test import TestFail from pythonwhat.check_funcs import check_args from pythonwhat.has_funcs import has_equal_value, has_equal_ast, has_printout @@ -82,27 +83,27 @@ def test_function_v2(name, index = index - 1 if not isinstance(params, list): - raise NameError("Inside test_function_v2, make sure to specify a LIST of params.") + raise InstructorError("Inside test_function_v2, make sure to specify a LIST of params.") if isinstance(do_eval, bool) or do_eval is None: do_eval = [do_eval] * len(params) if len(params) != len(do_eval): - raise NameError("Inside test_function_v2, make sure that do_eval has the same length as params.") + raise InstructorError("Inside test_function_v2, make sure that do_eval has the same length as params.") # if params_not_specified_msg is a str or None, convert into list if isinstance(params_not_specified_msg, str) or params_not_specified_msg is None: params_not_specified_msg = [params_not_specified_msg] * len(params) if len(params) != len(params_not_specified_msg): - raise NameError("Inside test_function_v2, make sure that params_not_specified_msg has the same length as params.") + raise InstructorError("Inside test_function_v2, make sure that params_not_specified_msg has the same length as params.") # if incorrect_msg is a str or None, convert into list if isinstance(incorrect_msg, str) or incorrect_msg is None: incorrect_msg = [incorrect_msg] * len(params) if len(params) != len(incorrect_msg): - raise NameError("Inside test_function_v2, make sure that incorrect_msg has the same length as params.") + raise InstructorError("Inside test_function_v2, make sure that incorrect_msg has the same length as params.") # if root-level (not in compound statement) calls that can be evaluated: use has_printout eligible = do_eval[0] if isinstance(do_eval, list) and len(do_eval) > 0 else do_eval diff --git a/pythonwhat/test_funcs/test_object.py b/pythonwhat/test_funcs/test_object.py index 6a547b49..4e89cdc6 100644 --- a/pythonwhat/test_funcs/test_object.py +++ b/pythonwhat/test_funcs/test_object.py @@ -1,6 +1,7 @@ from pythonwhat.tasks import getColumnsInProcess from pythonwhat.check_object import check_object, check_df, check_keys from pythonwhat.has_funcs import has_equal_value +from pythonwhat.Feedback import InstructorError def test_object(name, eq_condition="equal", @@ -34,7 +35,7 @@ def test_data_frame(name, if columns is None: columns = getColumnsInProcess(name, child.solution_process) if columns is None: - raise ValueError("Something went wrong in figuring out the columns for %s in the solution process" % name) + raise InstructorError("Something went wrong in figuring out the columns for %s in the solution process" % name) for col in columns: colstate = check_keys(col, missing_msg=undefined_cols_msg, state=child) diff --git a/pythonwhat/utils_ast.py b/pythonwhat/utils_ast.py index d12e1a90..b78fbcb0 100644 --- a/pythonwhat/utils_ast.py +++ b/pythonwhat/utils_ast.py @@ -1,4 +1,5 @@ import ast +from pythonwhat.Feedback import InstructorError def wrap_in_module(node): new_node = ast.Module(node) @@ -12,3 +13,16 @@ def wrap_in_module(node): new_node.first_token = node.first_token new_node.last_token = node.first_token return new_node + +def assert_ast(state, element, fmt_kwargs): + patt = "__JINJA__:You are zooming in on the {{part}}, but it is not an AST, so it can't be re-run." + _err_msg = "SCT fails on solution: " + _err_msg += state.build_message(patt, fmt_kwargs) + # element can also be { 'node': AST } + if isinstance(element, dict): + element = element['node'] + if isinstance(element, ast.AST): + return + if isinstance(element, list) and all([isinstance(el, ast.AST) for el in element]): + return + raise InstructorError(_err_msg) \ No newline at end of file diff --git a/tests/helper.py b/tests/helper.py index f2560a1e..ea93a167 100644 --- a/tests/helper.py +++ b/tests/helper.py @@ -2,10 +2,12 @@ import os from pythonwhat.local import StubProcess -from contextlib import redirect_stdout +from contextlib import redirect_stdout, contextmanager +from pythonwhat.Test import TestFail as TF from pythonwhat.test_exercise import test_exercise from pythonwhat.check_syntax import Chain import io +import pytest import tempfile def run(data, run_code = True): @@ -77,6 +79,14 @@ def get_sct_payload(output): def passes(st): assert isinstance(st, Chain) +@contextmanager +def verify_sct(correct): + if correct: + yield + else: + with pytest.raises(TF): + yield + def test_lines(test, sct_payload, ls, le, cs, ce): test.assertEqual(sct_payload['line_start'], ls) test.assertEqual(sct_payload['line_end'], le) diff --git a/tests/test_author_warnings.py b/tests/test_author_warnings.py new file mode 100644 index 00000000..8be6caf5 --- /dev/null +++ b/tests/test_author_warnings.py @@ -0,0 +1,128 @@ +import pytest +import helper +import ast + +from pythonwhat.local import setup_state +from pythonwhat.Feedback import InstructorError +from inspect import signature, Signature, Parameter +from pythonwhat.check_funcs import assert_ast + +# Actually wrong usage that breaks -------------------------------------------- + +@pytest.mark.compiled +def test_converter_err(): + data = { + "DC_SOLUTION": "import numpy as np; x = np.array([1, 2, 3])", + "DC_SCT": """def convert(): return abc\nset_converter('numpy.ndarray', convert); test_object('x') """ + } + data['DC_CODE'] = data['DC_SOLUTION'] + with pytest.raises(InstructorError): + helper.run(data) + +def test_check_syntax_double_getattr(): + data = { + "DC_SOLUTION": "", + "DC_CODE": "", + "DC_SCT": """Ex().check_list_comp.check_body()""" + } + with pytest.raises(AttributeError, match=r'Did you forget to call a statement'): + helper.run(data) + +def test_context_vals_wrong_place_in_chain(): + code = "[(i,j) for i,j in enumerate(range(10))]" + state = setup_state(code, code) + with pytest.raises(InstructorError, match=r"`set_context\(\)` failed: context val names are missing, but you tried to set \['i', 'j'\]\."): + state.check_list_comp(0).set_context(i=1,j=2).check_iter() + +@pytest.fixture +def state(): + return setup_state('round(1)', 'round(1)') + +def test_check_function(state): + with pytest.raises(InstructorError, match=r"`check_function\(\)` couldn't find a call of `roundddd\(\)` in the solution code. Make sure you get the mapping right!"): + state.check_function('roundddd') + +def test_check_function_2(state): + with pytest.raises(InstructorError, match=r"`check_function\(\)` couldn't find 2 calls of `round\(\)` in your solution code\."): + state.check_function('round', 1) + +def test_check_function_3(state): + with pytest.raises(InstructorError, match=r"`check_function\(\)` couldn't match the first call of `round` to its signature\."): + sig = Signature([Parameter('wrong', Parameter.KEYWORD_ONLY)]) + state.check_function('round', 0, signature=sig) + +def test_check_function_4(state): + with pytest.raises(InstructorError, match=r"SCT fails on solution: Check your call of `round\(\)`\. Did you specify the second argument\?"): + state.check_function('round').check_args(1) + +def test_check_function_5(state): + with pytest.raises(InstructorError, match=r"SCT fails on solution: Check your call of `round\(\)`. You are zooming in on the first argument, but it is not an AST, so it can't be re-run\."): + def round(*nums): pass + state.check_function('round', 0, signature=signature(round)).check_args(0).has_equal_value() + +def test_check_object(): + s = setup_state() + with pytest.raises(InstructorError, match=r"`check_object\(\)` couldn't find object `x` in the solution process\."): + s.check_object("x") + +def test_check_object_is_instance(): + s = setup_state('x = 1', 'x = 1') + with pytest.raises(InstructorError, match=r"`is_instance\(\)` noticed that `x` is not a `str` in the solution process\."): + s.check_object('x').is_instance(str) + +def test_check_object_keys(): + s = setup_state('x = {"a": 2}', 'x = {"a": 2}') + with pytest.raises(InstructorError, match=r"`check_keys\(\)` couldn't find key `b` in object `x` in the solution process\."): + s.check_object("x").check_keys("b") + +def test_set_context(): + code = "x = { m:len(m) for m in ['a', 'b', 'c'] }" + s = setup_state(code, code) + with pytest.raises(InstructorError, match=r'In `set_context\(\)`, specify arguments either by position, either by name\.'): + s.check_dict_comp().check_key().set_context('a', m = 'a').has_equal_value() + +def test_has_printout(): + s = setup_state() + with pytest.raises(InstructorError, match=r"`has_printout\(1\)` couldn't find the second print call in your solution\."): + s.has_printout(1) + +def test_has_import(): + s = setup_state() + with pytest.raises(InstructorError, match=r"`has_import\(\)` couldn't find an import of the package numpy in your solution code\."): + s.has_import('numpy') + +# Incorrect usage that wouldn't throw exceptions ------------------------------ + +def test_check_object_not_on_root(): + code = 'for i in range(3): x = 1' + s = setup_state(code, code) + with pytest.raises(InstructorError, match=r"`check_object\(\)` should only be called from the root state, `Ex\(\)`."): + s.check_for_loop().check_body().check_object('x') + +def test_has_printout_not_on_root(): + code = 'for i in range(3): print(i)' + s = setup_state(code, code) + with pytest.raises(InstructorError, match=r"`has_printout\(\)` should only be called from the root state, `Ex\(\)`."): + s.check_for_loop().check_body().has_printout(0) + +# Utility functions to make the above work ------------------------------------ + +@pytest.mark.parametrize('element, no_error', + [ + (ast.AST(), True), + ([ast.AST()], True), + ({'node': ast.AST()}, True), + ({'node': [ast.AST()]}, True), + (1, False), + ([1, 2], False), + ({'node': 1}, False), + ({'node': [1, 2]}, False), + ], +) +def test_assert_ast(element, no_error): + s = setup_state()._state + if no_error: + assert_ast(s, element, {}) + else: + with pytest.raises(InstructorError): + assert_ast(s, element, {}) diff --git a/tests/test_check_function.py b/tests/test_check_function.py index 107345a4..b33e8ce1 100644 --- a/tests/test_check_function.py +++ b/tests/test_check_function.py @@ -2,6 +2,7 @@ import helper from pythonwhat.local import setup_state from pythonwhat.Test import TestFail as TF +from pythonwhat.Feedback import InstructorError from inspect import signature from pythonwhat.check_function import bind_args @@ -150,25 +151,6 @@ def test_method_2(): import pandas as pd helper.passes(s.check_function('df.a.sum', signature = sig_from_obj(pd.Series.sum))) -@pytest.mark.parametrize('sct', [ - "Ex().check_function('round').check_args('ndigits').has_equal_value()", - "Ex().check_correct(check_object('x').has_equal_value(), check_function('round').check_args('ndigits').has_equal_value())", - "Ex().check_function('round', signature = False).check_args('ndigits').has_equal_value()", - "Ex().check_correct(check_object('x').has_equal_value(), check_function('round', signature = False).check_args('ndigits').has_equal_value())" -]) -@pytest.mark.parametrize('sol', [ - 'x = 5', - 'x = round(5.23)' -]) -def test_incorrect_usage(sct, sol): - data = { - 'DC_CODE': 'round(1.23, ndigits = 1)', - 'DC_SOLUTION': sol, - 'DC_SCT': sct - } - with pytest.raises(KeyError): - out = helper.run(data) - @pytest.mark.parametrize('code', [ 'print(round(1.23))', 'x = print(round(1.23))', @@ -184,3 +166,22 @@ def test_function_parser(code): 'DC_SCT': 'Ex().check_function("round").check_args(0).has_equal_value()' }) assert output['correct'] + +@pytest.mark.parametrize('sct', [ + "Ex().check_function('round').check_args('ndigits').has_equal_value()", + "Ex().check_correct(check_object('x').has_equal_value(), check_function('round').check_args('ndigits').has_equal_value())", + "Ex().check_function('round', signature = False).check_args('ndigits').has_equal_value()", + "Ex().check_correct(check_object('x').has_equal_value(), check_function('round', signature = False).check_args('ndigits').has_equal_value())" +]) +@pytest.mark.parametrize('sol', [ + 'x = 5', + 'x = round(5.23)' +]) +def test_check_function_weirdness(sct, sol): + data = { + 'DC_CODE': 'round(1.23, ndigits = 1)', + 'DC_SOLUTION': sol, + 'DC_SCT': sct + } + with pytest.raises(InstructorError): + helper.run(data) \ No newline at end of file diff --git a/tests/test_has_import.py b/tests/test_has_import.py index ed037b2e..28b1b9be 100644 --- a/tests/test_has_import.py +++ b/tests/test_has_import.py @@ -1,6 +1,5 @@ import helper import pytest -from pythonwhat.Test import TestFail as TF from pythonwhat.local import setup_state @pytest.mark.parametrize('stu, correct', [ @@ -12,11 +11,8 @@ ]) def test_basic(stu, correct): s = setup_state(stu_code = stu, sol_code = 'import pandas as pd') - if correct: - helper.passes(s.has_import('pandas')) - else: - with pytest.raises(TF): - s.has_import('pandas') + with helper.verify_sct(correct): + s.has_import('pandas') @pytest.mark.parametrize('stu, same_as, correct', [ ('', True, False), @@ -32,11 +28,8 @@ def test_basic(stu, correct): ]) def test_same_as(stu, same_as, correct): s = setup_state(stu_code = stu, sol_code = 'import pandas as pd') - if correct: - helper.passes(s.has_import('pandas', same_as = same_as)) - else: - with pytest.raises(TF): - s.has_import('pandas', same_as = same_as) + with helper.verify_sct(correct): + s.has_import('pandas', same_as = same_as) @pytest.mark.parametrize('stu, correct', [ ('', False), @@ -46,13 +39,5 @@ def test_same_as(stu, same_as, correct): ]) def test_chaining(stu, correct): s = setup_state(stu_code = stu, sol_code = 'import numpy.random as rand') - if correct: - helper.passes(s.has_import('numpy.random')) - else: - with pytest.raises(TF): - s.has_import('numpy.random') - -def test_wrong_usage(): - s = setup_state(stu_code = '', sol_code = '') - with pytest.raises(NameError): - s.has_import('numpy') \ No newline at end of file + with helper.verify_sct(correct): + s.has_import('numpy.random') diff --git a/tests/test_has_printout.py b/tests/test_has_printout.py index eb5993f8..dc077048 100644 --- a/tests/test_has_printout.py +++ b/tests/test_has_printout.py @@ -3,32 +3,25 @@ from pythonwhat.Test import TestFail as TF import helper -@pytest.mark.parametrize('stu', [ - "print(1, 2, 3)", - "print('1 2 3')", - "print('1', '2 3')", - "print(1, '2 3')", - "print('1 2', '3')", - "print('1 2', 3)", +@pytest.mark.parametrize('stu, correct', [ + ("print(1, 2, 3)", True), + ("print('1 2 3')", True), + ("print('1', '2 3')", True), + ("print(1, '2 3')", True), + ("print('1 2', '3')", True), + ("print('1 2', 3)", True), + ("print(1, 2)", False), + ("print('1 2')", False), + ("print('1 3 2')", False), + ("print(1, 3, 2)", False), + ("print(1, 2, 4, 3)", False), + ("print('1 2 4 3')", False), + ("print('1 2', 4, 3)", False), ]) -def test_basic_has_printout_passing(stu): +def test_basic_has_printout(stu, correct): sol = 'print(1, 2, 3)' s = setup_state(stu_code=stu, sol_code=sol) - helper.passes(s.has_printout(0)) - -@pytest.mark.parametrize('stu', [ - "print(1, 2)", - "print('1 2')", - "print('1 3 2')", - "print(1, 3, 2)", - "print(1, 2, 4, 3)", - "print('1 2 4 3')", - "print('1 2', 4, 3)" - ]) -def test_basic_has_printout_failing(stu): - sol = 'print(1, 2, 3)' - s = setup_state(stu_code=stu, sol_code=sol) - with pytest.raises(TF, match=r'Have you used `print\(1, 2, 3\)`'): + with helper.verify_sct(correct): s.has_printout(0) def test_basic_has_printout_failing_custom(): @@ -51,10 +44,7 @@ def test_has_printout_multiple(stu): s = setup_state(stu_code=stu, sol_code=sol) helper.passes(s.has_printout(1)) -def test_incorrect_use(): - s = setup_state(stu_code='', sol_code='') - with pytest.raises(ValueError, match='Using has_printout'): - s.has_printout(1) + diff --git a/tests/test_instructor_warnings.py b/tests/test_instructor_warnings.py deleted file mode 100644 index 5dadee5b..00000000 --- a/tests/test_instructor_warnings.py +++ /dev/null @@ -1,28 +0,0 @@ -import pytest -import helper - -@pytest.mark.compiled -def test_converter_err(): - data = { - "DC_SOLUTION": "import numpy as np; x = np.array([1, 2, 3])", - "DC_SCT": """def convert(): return abc\nset_converter('numpy.ndarray', convert); test_object('x') """ - } - data['DC_CODE'] = data['DC_SOLUTION'] - with pytest.raises(TypeError): - helper.run(data) - -def test_check_syntax_double_getattr(): - data = { - "DC_SOLUTION": "", - "DC_CODE": "", - "DC_SCT": """Ex().check_list_comp.check_body()""" - } - with pytest.raises(AttributeError): - helper.run(data) - -def test_context_vals_wrong_place_in_chain(): - data = {"DC_SOLUTION": "[(i,j) for i,j in enumerate(range(10))]"} - data["DC_CODE"] = data["DC_SOLUTION"] - data["DC_SCT"] = """Ex().check_list_comp(0).set_context(i=1,j=2).check_iter()""" - with pytest.raises(KeyError): - helper.run(data) diff --git a/tests/test_set_context.py b/tests/test_set_context.py index 38432fbc..772edd36 100644 --- a/tests/test_set_context.py +++ b/tests/test_set_context.py @@ -67,11 +67,3 @@ def test_fail(): output = helper.run(data) assert not output['correct'] -def test_wrong_usage(): - data = { - 'DC_CODE': "x = { m:len(m) for m in ['a', 'b', 'c'] }", - 'DC_SOLUTION': "x = { m*2:len(m) for m in ['a', 'b', 'c'] }", - 'DC_SCT': "Ex().check_dict_comp().check_key().set_context('a', m = 'a').has_equal_value()" - } - with pytest.raises(ValueError, match = 'either'): - helper.run(data) \ No newline at end of file diff --git a/tests/test_spec.py b/tests/test_spec.py index 111cf82d..5c3743e0 100644 --- a/tests/test_spec.py +++ b/tests/test_spec.py @@ -1,6 +1,7 @@ import unittest import helper import pytest +from pythonwhat.Feedback import InstructorError class TestFChain(unittest.TestCase): @@ -158,7 +159,7 @@ def failing_submission(data): def test_has_equal_ast_code_without_msg(data): data["DC_SCT"] = "Ex().has_equal_ast(code = 'test')" - with pytest.raises(ValueError): + with pytest.raises(InstructorError): helper.run(data) def test_has_equal_ast_simple_pass(data): diff --git a/tests/test_test_object.py b/tests/test_test_object.py index 7126a647..bd7edb55 100644 --- a/tests/test_test_object.py +++ b/tests/test_test_object.py @@ -22,12 +22,6 @@ def test_check_object(sct, stu_code, passes, msg): assert output['correct'] == passes if msg: assert output['message'] == msg -def test_check_object_wrong_usage(): - with pytest.raises(NameError): - helper.run({ - 'DC_SCT': 'Ex().check_object("x")' - }) - @pytest.mark.parametrize('stu_code, passes', [ ('x = filter(lambda x: x > 0, [0, 1])', False), ('x = filter(lambda x: x > 0, [1, 1])', True) @@ -53,9 +47,6 @@ def test_check_object_custom_compare(stu_code, passes): assert output['correct'] == passes def test_check_object_single_process(): - state2pids = setup_state('x = 3', '') - with pytest.raises(NameError): - state2pids.check_object('x') state1pid = setup_state('x = 3', '', pid = 1) helper.passes(state1pid.check_object('x')) @@ -137,13 +128,6 @@ def test_check_keys_exotic(sct): }) assert output['correct'] -def test_check_keys_wrong_usage(): - with pytest.raises(NameError): - helper.run({ - 'DC_SOLUTION': 'x = {"a": 2}', - 'DC_CODE': 'x = {"a": 2}', - 'DC_SCT': 'Ex().check_object("x").check_keys("b")' - }) @pytest.mark.need_internet class TestTestObjectNonDillable(unittest.TestCase): @@ -391,7 +375,5 @@ def test_fail(self): helper.test_absent_lines(self, sct_payload) - - if __name__ == "__main__": unittest.main() diff --git a/tests/test_v2_only.py b/tests/test_v2_only.py index 037ce4a9..c55d5ca7 100644 --- a/tests/test_v2_only.py +++ b/tests/test_v2_only.py @@ -47,7 +47,7 @@ def test_without_env_all_works(data, sct): ("test_object('x')", True), ("test_function('round')", True), ("Ex().test_object('x')", True), - ("Ex().test_or(check_object('x').has_equal_value()", True), + ("Ex().test_or(check_object('x').has_equal_value())", True), ("Ex().check_or(test_object('x'))", True), ("Ex().check_object('x').has_equal_value()", False), ("Ex() >> check_object('x').has_equal_value()", False), @@ -59,7 +59,7 @@ def test_with_env_old_fail(data, sct, should_err): with set_pw_env('1'): relooooad() if should_err: - with pytest.raises(Exception): + with pytest.raises((NameError, AttributeError)): sct_payload = helper.run(data) else: sct_payload = helper.run(data) From db52ed98fa66cba55febc3da911aff263e5fc0ca Mon Sep 17 00:00:00 2001 From: Filip Schouwenaars Date: Wed, 5 Sep 2018 09:55:49 +0200 Subject: [PATCH 005/209] docs: add docs for logic tests + refactor reference --- docs/reference.rst | 30 ++++++---------- pythonwhat/check_logic.py | 76 +++++++++++++++++++++++++++++++++++---- 2 files changed, 80 insertions(+), 26 deletions(-) diff --git a/docs/reference.rst b/docs/reference.rst index 4c9244fa..3af35ca1 100644 --- a/docs/reference.rst +++ b/docs/reference.rst @@ -1,53 +1,45 @@ -Simple tests ------------- - .. note:: - ``check_`` functions produce a child state that 'dives' deeper into a part of the state it was passed. They are typically chained off of for further checking. - ``has_`` functions always **return the state that they were intially passed** and are used at the 'end' of a chain. -Basic building blocks -===================== +SCT building blocks +------------------- .. autofunction:: pythonwhat.has_funcs.has_code .. autofunction:: pythonwhat.has_funcs.has_output .. autofunction:: pythonwhat.has_funcs.has_printout .. autofunction:: pythonwhat.has_funcs.has_import .. autofunction:: pythonwhat.has_funcs.has_equal_value -.. autofunction:: pythonwhat.has_funcs.has_equal_error .. autofunction:: pythonwhat.has_funcs.has_equal_output +.. autofunction:: pythonwhat.has_funcs.has_equal_error .. autofunction:: pythonwhat.has_funcs.has_equal_ast Checking objects -================ +---------------- .. autofunction:: pythonwhat.check_object.check_object .. autofunction:: pythonwhat.check_object.is_instance .. autofunction:: pythonwhat.check_object.check_df .. autofunction:: pythonwhat.check_object.check_keys -Checking function calls -======================= - -.. autofunction:: pythonwhat.check_function.check_function -.. autofunction:: pythonwhat.check_funcs.check_args - -Checking function definitions -============================= +Checking function calls and definitions +--------------------------------------- .. autofunction:: pythonwhat.has_funcs.has_equal_part_len -.. autofunction:: pythonwhat.check_funcs.check_args +.. autofunction:: pythonwhat.check_function.check_function .. autofunction:: pythonwhat.check_funcs.check_call +.. autofunction:: pythonwhat.check_funcs.check_args -Logic tests ------------ +Combining SCTs +-------------- .. autofunction:: pythonwhat.check_logic.multi .. autofunction:: pythonwhat.check_logic.check_correct .. autofunction:: pythonwhat.check_logic.check_or .. autofunction:: pythonwhat.check_logic.check_not -State-management +State Management ---------------- .. autofunction:: pythonwhat.check_logic.override diff --git a/pythonwhat/check_logic.py b/pythonwhat/check_logic.py index fd7c0318..3d545838 100644 --- a/pythonwhat/check_logic.py +++ b/pythonwhat/check_logic.py @@ -7,7 +7,27 @@ import ast def multi(*args, state=None): - """Run multiple subtests. Return original state (for chaining).""" + """Run multiple subtests. Return original state (for chaining). + + Args: + state: State instance describing student and solution code. Can be omitted if used with Ex(). + tests: sub-SCTs that all should pass. + + :Example: + + Suppose we want to verify the following function call: :: + + round(1.2345, ndigits=2) + + The following SCT would verify this, using ``multi`` to + 'branch out' the state to two sub-SCTs: :: + + Ex().check_function('round').multi( + check_args(0).has_equal_value(), + check_args('ndigits').has_equal_value() + ) + + """ if any(args): rep = Reporter.active_reporter @@ -28,16 +48,21 @@ def check_not(*tests, msg, state=None): Args: state: State instance describing student and solution code. Can be omitted if used with Ex(). + tests: one or more sub-SCTs that all should not pass. args: one or more sub-SCTs to run. + :Example: - Thh SCT below runs two test_student_typed cases.. :: + The SCT below runs two ``has_code`` cases: :: - Ex().multi(test_student_typed('INNER'), test_student_typed('OUTER')) + Ex().check_not( + has_code('mean'), + has_code('median') + ) - If students use INNER (JOIN) or OUTER (JOIN) in their code, this test will fail. + If students use ``mean`` or ``median`` anywhere in their code, this SCT will fail. Note: - - This function is currently only tested in working with test_student_typed in the subtests. + - This function is currently only tested in working with has_code in the subtests. - This function can be thought as a NOT(x OR y OR ...) statement, since all tests it runs must fail - This function can be considered a direct counterpart of multi. @@ -56,7 +81,27 @@ def check_not(*tests, msg, state=None): return state def check_or(*tests, state=None): - """Test whether at least one SCT passes.""" + """Test whether at least one SCT passes. + + If all of the tests fail, the feedback of the first test will be presented to the student. + + Args: + state: State instance describing student and solution code. Can be omitted if used with Ex(). + tests: one or more sub-SCTs to run. + + :Example: + + The SCT below tests that the student typed either 'mean' or 'median': :: + + Ex().check_or( + has_code('mean'), + has_code('median') + ) + + If the student didn't type either, the feedback message generated by ``has_code(mean)``, + the first SCT, will be presented to the student. + + """ rep = Reporter.active_reporter @@ -74,7 +119,24 @@ def check_or(*tests, state=None): rep.do_test(Test(first_feedback)) def check_correct(check, diagnose, state=None): - """Allows feedback from a diagnostic SCT, only if a check SCT fails. """ + """Allows feedback from a diagnostic SCT, only if a check SCT fails. + + Args: + state: State instance describing student and solution code. Can be omitted if used with Ex(). + check: An sct chain that must succeed. + diagnose: An sct chain to run if the check fails. + + :Example: + + The SCT below tests whether an object is correct. Only if the object is not correct, will + the function calling checks be executed :: + + Ex().check_correct( + check_object('x').has_equal_value(), + check_function('round').check_args(0).has_equal_value() + ) + + """ def diagnose_and_check(state=None): # use multi twice, since diagnose and check may be lists of tests multi(diagnose, state=state) From 590189f6936b1b679373648a129aefb73721800b Mon Sep 17 00:00:00 2001 From: Filip Schouwenaars Date: Wed, 5 Sep 2018 10:58:44 +0200 Subject: [PATCH 006/209] bump version + update CHANGELOG --- CHANGELOG.md | 11 +++++++++++ pythonwhat/__init__.py | 2 +- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 35d76521..ceab7b23 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,17 @@ All notable changes to the pythonwhat project will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). +## 2.16.0 + +### Added + +- Function documentation (with examples) for `multi()`, `check_or()`, `check_correct()` and `check_not()` + +### Changed + +- If an SCT is incorrectly coded, it will generate more easily understandable errors so the author can easily fix the issue. +- If an SCT correctly runs but does not make a lot of sense, easily understanble errors will be thrown so the author can make improvements. + ## 2.15.3 ### Added diff --git a/pythonwhat/__init__.py b/pythonwhat/__init__.py index 737469f0..8e951667 100644 --- a/pythonwhat/__init__.py +++ b/pythonwhat/__init__.py @@ -1,3 +1,3 @@ -__version__ = '2.15.3' +__version__ = '2.16.0' from .test_exercise import test_exercise, allow_errors From 701efa894c0c50afd6f1af1ae2419c61eef98b05 Mon Sep 17 00:00:00 2001 From: Filip Schouwenaars Date: Wed, 5 Sep 2018 16:53:00 +0200 Subject: [PATCH 007/209] feat(author_warnings): Follow-up with edits and improvements - check_object on root state check only if V2_ONLY is enabled - More author warnings for different commonly occurring errors --- pythonwhat/State.py | 14 ++++- pythonwhat/check_funcs.py | 9 +++- pythonwhat/check_object.py | 13 ++++- pythonwhat/has_funcs.py | 6 ++- pythonwhat/utils.py | 3 ++ tests/helper.py | 13 +++++ tests/test_author_warnings.py | 91 ++++++++++++++++++++++++++------ tests/test_check_function_def.py | 1 + tests/test_v2_only.py | 18 +------ 9 files changed, 132 insertions(+), 36 deletions(-) diff --git a/pythonwhat/State.py b/pythonwhat/State.py index 7463ffb7..bd1be96d 100644 --- a/pythonwhat/State.py +++ b/pythonwhat/State.py @@ -219,10 +219,22 @@ def has_different_processes(self): # play it safe (most common) return True - def assert_parent(self, fun): + def assert_root(self, fun): if self.parent_state is not None: raise InstructorError("`%s()` should only be called from the root state, `Ex()`." % fun) + def assert_is(self, klasses, fun, prev_fun): + if self.__class__.__name__ not in klasses: + raise InstructorError("`%s()` can only be called on %s." % + (fun, " or ".join([ '`%s()`' % pf for pf in prev_fun ])) + ) + + def assert_is_not(self, klasses, fun, prev_fun): + if self.__class__.__name__ in klasses: + raise InstructorError("`%s()` should not be called on %s." % + (fun, " or ".join([ '`%s()`' % pf for pf in prev_fun ])) + ) + @staticmethod def parse_external(x): rep = Reporter.active_reporter diff --git a/pythonwhat/check_funcs.py b/pythonwhat/check_funcs.py index 81920340..33e9ec06 100644 --- a/pythonwhat/check_funcs.py +++ b/pythonwhat/check_funcs.py @@ -33,7 +33,8 @@ def part_to_child(stu_part, sol_part, append_message, state, node_name=None): # otherwise, assume they are just nodes return state.to_child_state(student_subtree=stu_part, solution_subtree=sol_part, - append_message=append_message) + append_message=append_message, + node_name=node_name) def check_part(name, part_msg, missing_msg=None, @@ -405,6 +406,12 @@ def my_power(x): ) """ + state.assert_is( + ['function_defs', 'lambda_functions'], + 'check_call', + ['check_function_def', 'check_lambda_function'] + ) + if expand_msg is None: expand_msg = "__JINJA__:To verify it, we reran {{argstr}}. " diff --git a/pythonwhat/check_object.py b/pythonwhat/check_object.py index 31a97962..8e26741f 100644 --- a/pythonwhat/check_object.py +++ b/pythonwhat/check_object.py @@ -4,6 +4,7 @@ from pythonwhat.Feedback import Feedback, InstructorError from pythonwhat.tasks import isDefinedInProcess, isInstanceInProcess, isDefinedCollInProcess from pythonwhat.check_funcs import part_to_child +from pythonwhat.utils import v2_only from pythonwhat.has_funcs import has_equal_value import pandas as pd import ast @@ -41,7 +42,9 @@ def check_object(index, missing_msg=None, expand_msg=None, state=None, typestr=" """ - state.assert_parent('check_object') + # Only do the assertion if PYTHONWHAT_V2_ONLY is set to '1' + if v2_only(): + state.assert_root('check_object') if missing_msg is None: missing_msg = "__JINJA__:Did you define the {{typestr}} `{{index}}` without errors?" @@ -65,7 +68,8 @@ def check_object(index, missing_msg=None, expand_msg=None, state=None, typestr=" _msg = state.build_message(missing_msg, append_message['kwargs']) rep.do_test(DefinedProcessTest(index, state.student_process, Feedback(_msg))) - child = part_to_child(stu_part, sol_part, append_message, state) + child = part_to_child(stu_part, sol_part, append_message, state, + node_name='object_assignments') return child @@ -94,6 +98,9 @@ def is_instance(inst, not_instance_msg=None, state=None): import numpy Ex().check_object('arr').is_instance(numpy.ndarray) """ + + state.assert_is(['object_assignments'], 'is_instance', ['check_object']) + rep = Reporter.active_reporter sol_name = state.solution_parts.get('name') @@ -144,6 +151,8 @@ def check_keys(key, missing_msg=None, expand_msg=None, state=None): """ + state.assert_is(['object_assignments'], 'is_instance', ['check_object', 'check_df']) + if missing_msg is None: missing_msg = "__JINJA__:There is no {{ 'column' if 'DataFrame' in parent.typestr else 'key' }} `'{{key}}'`." if expand_msg is None: diff --git a/pythonwhat/has_funcs.py b/pythonwhat/has_funcs.py index 45be85b0..b2c5000a 100644 --- a/pythonwhat/has_funcs.py +++ b/pythonwhat/has_funcs.py @@ -140,6 +140,10 @@ def has_equal_ast(incorrect_msg=None, """ rep = Reporter.active_reporter + if utils.v2_only(): + state.assert_is_not(['object_assignments'], 'has_equal_ast', ['check_object']) + state.assert_is_not(['function_calls'], 'has_equal_ast', ['check_function']) + if code and incorrect_msg is None: raise InstructorError("If you manually specify the code to match inside has_equal_ast(), " "you have to explicitly set the `incorrect_msg` argument.") @@ -544,7 +548,7 @@ def has_printout(index, print("random"); print(1, 2, 3, 4) """ - state.assert_parent('has_printout') + state.assert_root('has_printout') if not_printed_msg is None: not_printed_msg = "__JINJA__:Have you used `{{sol_call}}` to do the appropriate printouts?" diff --git a/pythonwhat/utils.py b/pythonwhat/utils.py index 7a0a36d6..a842fe24 100644 --- a/pythonwhat/utils.py +++ b/pythonwhat/utils.py @@ -5,6 +5,9 @@ def include_v1(): return os.environ.get('PYTHONWHAT_V2_ONLY', '') != '1' +def v2_only(): + return not include_v1() + def shorten_str(text, to_chars=100): if '\n' in text or len(text) > 50: return None return text diff --git a/tests/helper.py b/tests/helper.py index ea93a167..f2b92772 100644 --- a/tests/helper.py +++ b/tests/helper.py @@ -127,3 +127,16 @@ def remove_lambdas(sct_str, count=0, with_args = False): def replace_test_if(sct): return re.sub(r"test_if_else\(", "test_if_exp(", sct) + +@contextmanager +def set_v2_only_env(new): + key = 'PYTHONWHAT_V2_ONLY' + old = os.environ.get(key) + try: + os.environ[key] = new + yield + finally: + if old is None: + del os.environ[key] + else: + os.environ[key] = old \ No newline at end of file diff --git a/tests/test_author_warnings.py b/tests/test_author_warnings.py index 8be6caf5..c4f8513b 100644 --- a/tests/test_author_warnings.py +++ b/tests/test_author_warnings.py @@ -11,22 +11,19 @@ @pytest.mark.compiled def test_converter_err(): + code = "import numpy as np; x = np.array([1, 2, 3])" data = { - "DC_SOLUTION": "import numpy as np; x = np.array([1, 2, 3])", - "DC_SCT": """def convert(): return abc\nset_converter('numpy.ndarray', convert); test_object('x') """ - } - data['DC_CODE'] = data['DC_SOLUTION'] + "DC_CODE": code, + "DC_SOLUTION": code, + "DC_SCT": """def convert(): return abc\nset_converter('numpy.ndarray', convert); test_object('x') """ + } with pytest.raises(InstructorError): helper.run(data) def test_check_syntax_double_getattr(): - data = { - "DC_SOLUTION": "", - "DC_CODE": "", - "DC_SCT": """Ex().check_list_comp.check_body()""" - } + s = setup_state() with pytest.raises(AttributeError, match=r'Did you forget to call a statement'): - helper.run(data) + s.check_list_comp.check_body() def test_context_vals_wrong_place_in_chain(): code = "[(i,j) for i,j in enumerate(range(10))]" @@ -93,18 +90,82 @@ def test_has_import(): # Incorrect usage that wouldn't throw exceptions ------------------------------ -def test_check_object_not_on_root(): - code = 'for i in range(3): x = 1' +from pythonwhat.check_syntax import v2_check_functions + + +def test_has_printout_on_root(): + code = 'print(1)' s = setup_state(code, code) - with pytest.raises(InstructorError, match=r"`check_object\(\)` should only be called from the root state, `Ex\(\)`."): - s.check_for_loop().check_body().check_object('x') + has_printout = v2_check_functions['has_printout'] + s.check_or(has_printout(0), has_printout(0)) def test_has_printout_not_on_root(): code = 'for i in range(3): print(i)' s = setup_state(code, code) - with pytest.raises(InstructorError, match=r"`has_printout\(\)` should only be called from the root state, `Ex\(\)`."): + with pytest.raises(InstructorError, match=r"`has_printout\(\)` should only be called from the root state, `Ex\(\)`\."): s.check_for_loop().check_body().has_printout(0) +def test_check_object_on_root(): + code = 'x = 1' + check_object = v2_check_functions['check_object'] + s = setup_state(code, code) + s.check_or(check_object('x'), check_object('x')) + +def test_check_object_not_on_root(): + code = 'for i in range(3): x = 1' + s = setup_state(code, code) + with helper.set_v2_only_env(''): + s.check_for_loop().check_body().check_object('x') + +def test_check_object_not_on_root_v2(): + code = 'for i in range(3): x = 1' + s = setup_state(code, code) + with helper.set_v2_only_env('1'): + with pytest.raises(InstructorError, match=r"`check_object\(\)` should only be called from the root state, `Ex\(\)`\."): + s.check_for_loop().check_body().check_object('x') + +def test_is_instance_not_on_check_object(): + code = 'round(3)' + s = setup_state(code, code) + with pytest.raises(InstructorError, match=r"`is_instance\(\)` can only be called on `check_object\(\)`\."): + s.check_function('round').check_args(0).is_instance(int) + +def test_check_keys_not_on_check_object(): + code = 'round(3)' + s = setup_state(code, code) + with pytest.raises(InstructorError, match=r"`is_instance\(\)` can only be called on `check_object\(\)` or `check_df\(\)`\."): + s.check_function('round').check_args(0).check_keys('a') + +def test_has_equal_ast_on_check_object(): + code = 'x = 1' + s = setup_state(code, code) + s.check_object('x').has_equal_ast() + +def test_has_equal_ast_on_check_object_v2(): + code = 'x = 1' + s = setup_state(code, code) + with helper.set_v2_only_env('1'): + with pytest.raises(InstructorError, match=r"`has_equal_ast\(\)` should not be called on `check_object\(\)`\."): + s.check_object('x').has_equal_ast() + +def test_has_equal_ast_on_check_function(): + code = 'round(1)' + s = setup_state(code, code) + s.check_function('round').has_equal_ast() + +def test_has_equal_ast_on_check_function_v2(): + code = 'round(1)' + s = setup_state(code, code) + with helper.set_v2_only_env('1'): + with pytest.raises(InstructorError, match=r"`has_equal_ast\(\)` should not be called on `check_function\(\)`\."): + s.check_function('round').has_equal_ast() + +def test_check_call_not_on_check_function_def(): + code = 'def x(a): pass' + s = setup_state(code, code) + with pytest.raises(InstructorError, match=r"`check_call\(\)` can only be called on `check_function_def\(\)` or `check_lambda_function\(\)`\."): + s.check_object('x').check_call("f(1)") + # Utility functions to make the above work ------------------------------------ @pytest.mark.parametrize('element, no_error', diff --git a/tests/test_check_function_def.py b/tests/test_check_function_def.py index 842fd049..1cdbde3a 100644 --- a/tests/test_check_function_def.py +++ b/tests/test_check_function_def.py @@ -35,6 +35,7 @@ def test(a, b): ('def test(a, b): print(a + b); return a + b', True) ]) def test_check_call(stu, passes): + output = helper.run({ 'DC_CODE': stu, 'DC_SOLUTION': 'def test(a, b): print(a + b); return a + b', diff --git a/tests/test_v2_only.py b/tests/test_v2_only.py index c55d5ca7..b1c30396 100644 --- a/tests/test_v2_only.py +++ b/tests/test_v2_only.py @@ -1,6 +1,5 @@ import pytest import os -from contextlib import contextmanager import helper import importlib @@ -8,19 +7,6 @@ def relooooad(): import pythonwhat.check_syntax importlib.reload(pythonwhat.check_syntax) -@contextmanager -def set_pw_env(new): - key = 'PYTHONWHAT_V2_ONLY' - old = os.environ.get(key) - try: - os.environ[key] = new - yield - finally: - if old is None: - del os.environ[key] - else: - os.environ[key] = old - @pytest.fixture def data(): return { @@ -38,7 +24,7 @@ def data(): ]) def test_without_env_all_works(data, sct): data['DC_SCT'] = sct - with set_pw_env(''): + with helper.set_v2_only_env(''): relooooad() sct_payload = helper.run(data) assert not sct_payload['correct'] @@ -56,7 +42,7 @@ def test_without_env_all_works(data, sct): ]) def test_with_env_old_fail(data, sct, should_err): data['DC_SCT'] = sct - with set_pw_env('1'): + with helper.set_v2_only_env('1'): relooooad() if should_err: with pytest.raises((NameError, AttributeError)): From 76ae22c8cfffd7b778ec3d128c2b05b34d016290 Mon Sep 17 00:00:00 2001 From: Filip Schouwenaars Date: Fri, 7 Sep 2018 13:31:09 +0200 Subject: [PATCH 008/209] bump version + update CHANGELOG --- CHANGELOG.md | 10 ++++++++++ pythonwhat/__init__.py | 2 +- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ceab7b23..7807eb29 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,16 @@ All notable changes to the pythonwhat project will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). +## 2.16.1 + +### Changed + +- The `check_object()` only on root check is only done if `PYTHONWHAT_V2_ONLY` environment variable is set. + +### Added + +- More checks that guard against commonly made mistakes (some in v2 only, others not) + ## 2.16.0 ### Added diff --git a/pythonwhat/__init__.py b/pythonwhat/__init__.py index 8e951667..6f60c8e8 100644 --- a/pythonwhat/__init__.py +++ b/pythonwhat/__init__.py @@ -1,3 +1,3 @@ -__version__ = '2.16.0' +__version__ = '2.16.1' from .test_exercise import test_exercise, allow_errors From 131e49e82add20257535b0e114c19ceb94e64928 Mon Sep 17 00:00:00 2001 From: Filip Schouwenaars Date: Fri, 7 Sep 2018 16:26:08 +0200 Subject: [PATCH 009/209] feat(check_class_def): ability to test class definitions - Similar to how function definitions are checked, but easier, - Tested (both internals and messages) - Example added to 'checking compound statements' article Closes #233 --- .../articles/checking_compound_statements.rst | 31 ++++++++++++ pythonwhat/check_wrappers.py | 50 ++++++++++--------- pythonwhat/parsing.py | 15 +++++- tests/test_check_class_def.py | 43 ++++++++++++++++ tests/test_messaging.py | 40 +++++++++++++++ 5 files changed, 154 insertions(+), 25 deletions(-) create mode 100644 tests/test_check_class_def.py diff --git a/docs/articles/checking_compound_statements.rst b/docs/articles/checking_compound_statements.rst index e03159bc..3b4086c5 100644 --- a/docs/articles/checking_compound_statements.rst +++ b/docs/articles/checking_compound_statements.rst @@ -254,6 +254,37 @@ The second representation has to be followed when writing the corresponding SCT: .check_orelse().check_if_else() \ .check_orelse().has_equal_output() +Class definition +~~~~~~~~~~~~~~~~ + +Suppose you want to check whether a class was defined correctly: + +.. code:: + + +The following SCT would verify this: + +.. code:: + + check_class_def('MyInt').multi( + check_bases(0).has_equal_ast(), + check_body().check_function_def('__init__').multi( + check_args('self'), + check_args('i'), + check_body().set_context(i = 2).multi( + check_function('super', signature=False), + check_function('super.__init__').check_args(0).has_equal_value() + ) + ) + ) + +- ``check_class_def()`` looks for the class definition itself. +- With ``check_bases()``, you can zoom in on the different basse classes that the class definition inherits from. +- With ``check_body()``, you zoom in on the class body, after which you can use other functions such + as ``check_function_def()`` to look for class methods. +- Of course, just like for other examples, you can use ``check_correct()`` where necessary, + e.g. to verify whether class methods give the right behavior with ``check_call()`` + before diving into the body of the method itself. Crazy combo ~~~~~~~~~~~ diff --git a/pythonwhat/check_wrappers.py b/pythonwhat/check_wrappers.py index 6b6b3c7b..c8dd3f02 100644 --- a/pythonwhat/check_wrappers.py +++ b/pythonwhat/check_wrappers.py @@ -8,34 +8,36 @@ import inspect __PART_WRAPPERS__ = { - 'iter': 'iterable part', - 'body': 'body', - 'key' : 'key part', - 'value': 'value part', - 'orelse': 'else part', - 'finalbody': 'finally part', - 'test': 'condition' - } + 'iter': 'iterable part', + 'body': 'body', + 'key' : 'key part', + 'value': 'value part', + 'orelse': 'else part', + 'finalbody': 'finally part', + 'test': 'condition', +} __PART_INDEX_WRAPPERS__ = { - 'ifs': '{ordinal} if', - 'handlers': '`{index}` `except` block', - 'context': '{ordinal} context' - } + 'ifs': '{ordinal} if', + 'bases': '{ordinal} base class', + 'handlers': '`{index}` `except` block', + 'context': '{ordinal} context', +} __NODE_WRAPPERS__ = { - 'list_comp': '{ordinal} list comprehension', - 'generator_exp': '{ordinal} generator expression', - 'dict_comp': '{ordinal} dictionary comprehension', - 'for_loop': '{ordinal} for statement', - 'function_def': 'definition of `{index}()`', - 'if_exp': '{ordinal} if expression', - 'if_else': '{ordinal} if statement', - 'lambda_function': '{ordinal} lambda function', - 'try_except': '{ordinal} try statement', - 'while': '{ordinal} `while` loop', - 'with': '{ordinal} `with` statement' - } + 'list_comp': '{ordinal} list comprehension', + 'generator_exp': '{ordinal} generator expression', + 'dict_comp': '{ordinal} dictionary comprehension', + 'for_loop': '{ordinal} for statement', + 'function_def': 'definition of `{index}()`', + 'class_def': 'class definition of `{index}`', + 'if_exp': '{ordinal} if expression', + 'if_else': '{ordinal} if statement', + 'lambda_function': '{ordinal} lambda function', + 'try_except': '{ordinal} try statement', + 'while': '{ordinal} `while` loop', + 'with': '{ordinal} `with` statement', +} scts = {} diff --git a/pythonwhat/parsing.py b/pythonwhat/parsing.py index f404ccef..3ad499a4 100644 --- a/pythonwhat/parsing.py +++ b/pythonwhat/parsing.py @@ -586,6 +586,19 @@ def visit_For(self, node): '_target_vars': tv }) +class ClassDefParser(Parser): + """Find class definitions + """ + + def __init__(self): + self.out = {} + + def visit_ClassDef(self, node): + self.out[node.name] = { + 'node': node, + 'bases': [ {'node': node } for node in node.bases ], + 'body': node.body, + } class FunctionDefParser(Parser): """Find function definitions @@ -599,7 +612,6 @@ def __init__(self): def visit_FunctionDef(self, node): self.out[node.name] = self.parse_node(node) - @classmethod def parse_node(cls, node): normal_args = cls.get_arg_tuples(node.args.args, node.args.defaults) @@ -837,6 +849,7 @@ def parse_handler(handler): return { "if_exps": IfExpParser, "whiles": WhileParser, "for_loops": ForParser, + "class_defs": ClassDefParser, "function_defs": FunctionDefParser, "lambda_functions": LambdaFunctionParser, "list_comps": ListCompParser, diff --git a/tests/test_check_class_def.py b/tests/test_check_class_def.py new file mode 100644 index 00000000..156bac0a --- /dev/null +++ b/tests/test_check_class_def.py @@ -0,0 +1,43 @@ +import pytest +import helper +from pythonwhat.local import setup_state +from pythonwhat.check_syntax import v2_check_functions +globals().update(v2_check_functions) + + +@pytest.mark.parametrize('stu, passes', [ + ('', False), + ('def A(x): pass', False), + ('class A(): pass', False), + ('class A(int): pass', False), + ('class A(str):\n def __not_init__(self): pass', False), + ('class A(str):\n def __init__(self): print(1)', False), + ('class A(str):\n def __init__(self): pass', True), +]) +def test_check_class_def_pass(stu, passes): + sol = 'class A(str):\n def __init__(self): pass' + s = setup_state(stu, sol) + with helper.verify_sct(passes): + s.check_class_def('A').multi( + check_bases(0).has_equal_ast(), + check_body().check_function_def('__init__').check_body().has_equal_ast() + ) + +def test_check_wiki_example(): + code = ''' +class MyInt(int): + def __init__(self, i): + super().__init__(i + 1) +''' + s = setup_state(code, code) + s.check_class_def('MyInt').multi( + check_bases(0).has_equal_ast(), + check_body().check_function_def('__init__').multi( + check_args('self'), + check_args('i'), + check_body().set_context(i = 2).multi( + check_function('super', signature=False), + check_function('super.__init__').check_args(0).has_equal_value() + ) + ) + ) diff --git a/tests/test_messaging.py b/tests/test_messaging.py index f322e4d9..fae6b36f 100644 --- a/tests/test_messaging.py +++ b/tests/test_messaging.py @@ -223,6 +223,24 @@ def test_check_object_manual(stu, patt): assert not output['correct'] assert message(output, patt) +# Check function def et al ---------------------------------------------------- + +@pytest.mark.debug +@pytest.mark.parametrize('stu, patt', [ + ('', 'The system wants to check the definition of `test()` but hasn\'t found it.'), + ('def test(b): return b', 'Check the definition of `test()`. Did you specify the argument `a`?'), + ('def test(a): return a', 'Check the definition of `test()`. Did you correctly specify the argument `a`? not default'), + ('def test(a = 2): return a', 'Check the definition of `test()`. Did you correctly specify the argument `a`? Expected `1`, but got `2`.'), +]) +def test_check_function_def(stu, patt): + output = helper.run({ + 'DC_SOLUTION': 'def test(a = 1): return a', + 'DC_CODE': stu, + 'DC_SCT': "Ex().check_function_def('test').check_args('a').has_equal_part('is_default', msg='not default').has_equal_value()" + }) + assert not output['correct'] + assert message(output, patt) + # Check call ------------------------------------------------------------------ @pytest.mark.parametrize('stu, patt', [ @@ -265,6 +283,28 @@ def test_check_call_lambda(stu, patt): assert not output['correct'] assert message(output, patt) + +# Check class definition ------------------------------------------------------ + +@pytest.mark.debug +@pytest.mark.parametrize('stu, patt', [ + ('', "The system wants to check the class definition of `A` but hasn't found it."), + ('def A(x): pass', "The system wants to check the class definition of `A` but hasn't found it."), + ('class A(): pass', "Check the class definition of `A`. Are you sure you defined the first base class?"), + ('class A(int): pass', "Check the class definition of `A`. Did you correctly specify the first base class? Expected `str`, but got `int`."), + ('class A(str):\n def __not_init__(self): pass', "Check the class definition of `A`. Did you correctly specify the body? The system wants to check the definition of `__init__()` but hasn't found it."), + ('class A(str):\n def __init__(self): print(1)', "Check the definition of `__init__()`. Did you correctly specify the body? Expected `pass`, but got `print(1)`."), +]) +def test_check_class_def_pass(stu, patt): + sol = 'class A(str):\n def __init__(self): pass' + output = helper.run({ + 'DC_SOLUTION': sol, + 'DC_CODE': stu, + 'DC_SCT': "Ex().check_class_def('A').multi( check_bases(0).has_equal_ast(), check_body().check_function_def('__init__').check_body().has_equal_ast() )", + }) + assert not output['correct'] + assert message(output, patt) + ## has_import ----------------------------------------------------------------- @pytest.mark.parametrize('stu, patt', [ From 9374ecc0b48a302aeb5df57e45459f23df299c96 Mon Sep 17 00:00:00 2001 From: Filip Schouwenaars Date: Mon, 10 Sep 2018 09:04:16 +0200 Subject: [PATCH 010/209] bump version + update CHANGELOG --- CHANGELOG.md | 6 ++++++ pythonwhat/__init__.py | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7807eb29..718f64be 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,12 @@ All notable changes to the pythonwhat project will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). +## 2.16.2 + +### Added + +- Ability to check class definitions with ``check_class_def()`` (and ``check_bases()``). Tested and documented. + ## 2.16.1 ### Changed diff --git a/pythonwhat/__init__.py b/pythonwhat/__init__.py index 6f60c8e8..b56b646f 100644 --- a/pythonwhat/__init__.py +++ b/pythonwhat/__init__.py @@ -1,3 +1,3 @@ -__version__ = '2.16.1' +__version__ = '2.16.2' from .test_exercise import test_exercise, allow_errors From 411a9a38cc68455e1f41972a92f3d157619802c9 Mon Sep 17 00:00:00 2001 From: Filip Schouwenaars Date: Fri, 7 Sep 2018 08:29:03 +0200 Subject: [PATCH 011/209] spec(unit-to-pytest): switch from unittest to pytest entirely - Rewrite all unittest tests to pytest - More sensible file structure for tests - Tests are more self-contained (e.g. messages tests mainly in test_messaging.py) - Instead of using helper.run, try to use more lower-level SCT running (with helper.verify_sct) --- pytest.ini | 2 +- pythonwhat/has_funcs.py | 4 +- pythonwhat/test_funcs/test_function.py | 4 +- tests/helper.py | 30 +- tests/test_ast_operations.py | 137 +--- tests/test_check_function.py | 291 +++++-- tests/test_check_function_def.py | 165 +++- tests/test_check_if_else.py | 160 ++++ tests/test_check_list_comp.py | 157 ++++ tests/test_check_object.py | 301 ++++++++ tests/test_has_code.py | 99 +-- tests/test_has_expr.py | 73 ++ tests/test_has_output.py | 86 +-- tests/test_messaging.py | 29 +- tests/test_set_context.py | 1 - tests/test_set_env.py | 55 +- tests/test_signatures.py | 466 +++--------- tests/test_spec.py | 234 +++--- tests/test_test_compound_statements.py | 73 ++ tests/test_test_expression.py | 136 ---- tests/test_test_function.py | 463 ----------- tests/test_test_function_definition.py | 847 --------------------- tests/test_test_function_v2.py | 666 ---------------- tests/test_test_if_else.py | 449 ----------- tests/test_test_list_comp.py | 316 -------- tests/test_test_loop.py | 273 ------- tests/test_test_object.py | 379 --------- tests/test_test_object_accessed.py | 109 +-- tests/test_test_object_after_expression.py | 58 -- tests/test_test_with.py | 447 ++++------- tests/test_utils.py | 45 +- 31 files changed, 1701 insertions(+), 4854 deletions(-) create mode 100644 tests/test_check_if_else.py create mode 100644 tests/test_check_list_comp.py create mode 100644 tests/test_check_object.py create mode 100644 tests/test_test_compound_statements.py delete mode 100644 tests/test_test_expression.py delete mode 100644 tests/test_test_function.py delete mode 100644 tests/test_test_function_definition.py delete mode 100644 tests/test_test_function_v2.py delete mode 100644 tests/test_test_if_else.py delete mode 100644 tests/test_test_list_comp.py delete mode 100644 tests/test_test_loop.py delete mode 100644 tests/test_test_object.py delete mode 100644 tests/test_test_object_after_expression.py diff --git a/pytest.ini b/pytest.ini index 38b2dde6..015f8591 100644 --- a/pytest.ini +++ b/pytest.ini @@ -1,3 +1,3 @@ [pytest] testpaths = tests/ -addopts =-s -m "not compiled" +addopts =-m "not compiled" diff --git a/pythonwhat/has_funcs.py b/pythonwhat/has_funcs.py index b2c5000a..7aed7cd3 100644 --- a/pythonwhat/has_funcs.py +++ b/pythonwhat/has_funcs.py @@ -491,12 +491,10 @@ def has_output(text, if not no_output_msg: no_output_msg = "You did not output the correct things." - student_output = state.raw_student_output - _msg = state.build_message(no_output_msg) rep.do_test( StringContainsTest( - student_output, + state.raw_student_output, text, pattern, _msg)) diff --git a/pythonwhat/test_funcs/test_function.py b/pythonwhat/test_funcs/test_function.py index 7cb68c03..07ce6bd7 100644 --- a/pythonwhat/test_funcs/test_function.py +++ b/pythonwhat/test_funcs/test_function.py @@ -44,7 +44,7 @@ def test_function(name, try: return has_printout(index=index, not_printed_msg=incorrect_msg, state=state) except TestFail: - # The test didn't pass; just continue with the more struct check_function test. + # The test didn't pass; just continue with the more strict check_function test. pass fun_state = check_function(name=name, index=index, @@ -111,7 +111,7 @@ def test_function_v2(name, try: return has_printout(index=index, not_printed_msg=incorrect_msg[0], state=state) except TestFail: - # The test didn't pass; just continue with the more struct check_function test. + # The test didn't pass; just continue with the more strict check_function test. pass if len(params) == 0: diff --git a/tests/helper.py b/tests/helper.py index f2b92772..2a96a866 100644 --- a/tests/helper.py +++ b/tests/helper.py @@ -53,17 +53,15 @@ def __exit__(self, *args): sol_process = StubProcess() error = None - sct_output = io.StringIO() - with redirect_stdout(sct_output): - res = test_exercise(sct=sct, - student_code=stu_code, - solution_code=sol_code, - pre_exercise_code=pec, - student_process=stu_process, - solution_process=sol_process, - raw_student_output = raw_stu_output, - ex_type = "NormalExercise", - error = error) + res = test_exercise(sct=sct, + student_code=stu_code, + solution_code=sol_code, + pre_exercise_code=pec, + student_process=stu_process, + solution_process=sol_process, + raw_student_output = raw_stu_output, + ex_type = "NormalExercise", + error = error) return res @@ -111,16 +109,6 @@ def no_line_info(output): assert 'column_start' not in output assert 'column_end' not in output -def test_builtin(test, name, params, arguments): - test.data = { - "DC_PEC": "", - "DC_SOLUTION": "%s(%s)" % (name, arguments), - "DC_CODE": "%s(%s)" % (name, arguments), - "DC_SCT": "test_function_v2('%s', params=[%s])" % (name, params) - } - sct_payload = run(test.data) - test.assertTrue(sct_payload['correct']) - def remove_lambdas(sct_str, count=0, with_args = False): if with_args: return re.sub("lambda.*?:", "", sct_str, count=count) else: return re.sub("lambda:", "", sct_str, count=count) diff --git a/tests/test_ast_operations.py b/tests/test_ast_operations.py index c5b9531a..b06a78fd 100644 --- a/tests/test_ast_operations.py +++ b/tests/test_ast_operations.py @@ -1,111 +1,34 @@ import pytest from pythonwhat.State import State -def parsesWithoutError(s): - try: - State.parse_internal(s) - except: - pytest.fail("Parsing failed") - -def test_encoding(): - parsesWithoutError("# encoding: target\nx=4") - -def test_assignment(): - s = "x = 4 + 5 - 7 + 7 * (8) / 9" - parsesWithoutError(s) - -def test_tuple_1(): - s = "(12, 23)" - parsesWithoutError(s) - -def test_tuple_2(): - s = "(12, )" - parsesWithoutError(s) - -def test_list_1(): - s = "[1, 2, 'test', (1, 2)]" - parsesWithoutError(s) - -def test_list_2(): - s = "[1,\n2,\n'test'\n,(1,)]" - parsesWithoutError(s) - -def test_dict_1(): - s = "{'a': 1, 'b':2, 'c': (1, 2)}" - parsesWithoutError(s) - -def test_dict_2(): - s = "{'a':1,\n'b':2,\n'c':(1,)}" - parsesWithoutError(s) - -def test_fun_call_1(): - s = "round(1.213, 2)" - parsesWithoutError(s) - -def test_fun_call_2(): - s = "round(1.213, ndigits = 2)" - parsesWithoutError(s) - -def test_fun_call_3(): - s = "round(abs(1.213), ndigits = 2)" - parsesWithoutError(s) - -def test_fun_call_4(): - s = "round(abs((1.213)), ndigits = 2)" - parsesWithoutError(s) - -def test_fun_call_5(): - s = "round(abs((1.213)), ndigits = abs(2))" - parsesWithoutError(s) - -def test_fun_call_6(): - s = "import numpy as np; np.array([1, 2, 3])" - parsesWithoutError(s) - -def test_fun_call_7(): - s = "import numpy as np; np.array([1, 2, 3])" - parsesWithoutError(s) - -def test_fun_call_8(): - s = "print(file.read())" - parsesWithoutError(s) -def test_if_else(): - s = "if True:\n print('x')\nelif False:\n print('y')\nelse:\n print('z')" - parsesWithoutError(s) - -def test_while(): - s = "while True:\n print(1)" - parsesWithoutError(s) - -def test_for(): - s = "for i in [1, 2, 3]:\n print(i)" - parsesWithoutError(s) - -def test_try_except(): - s = "try:\n x = 4\nexcept:\n print('test')" - parsesWithoutError(s) - -def test_import(): - s = "from numpy import array as arr" - parsesWithoutError(s) - -def test_fun_def_1(): - s = "def my_fun(a, b = 2):\n return a + b" - parsesWithoutError(s) - -def test_fun_def_2(): - s = "def my_fun(a, b = 2):\n return a + abs(b)" - parsesWithoutError(s) - -def test_fun_def_3(): - s = "def my_fun(a, b = 2):\n return a + (abs(b))" - parsesWithoutError(s) - -def test_lambda_1(): - s = "echo_word = lambda word, echo = 1: word * echo" - parsesWithoutError(s) - -def test_lambda_2(): - s = "(lambda word, echo = 1: word * echo)('test', 3)" - parsesWithoutError(s) +@pytest.mark.parametrize('script', [ + "# encoding: target\nx=4", + "x = 4 + 5 - 7 + 7 * (8) / 9", + "(12, 23)", + "(12, )", + "[1, 2, 'test', (1, 2)]", + "[1,\n2,\n'test'\n,(1,)]", + "{'a': 1, 'b':2, 'c': (1, 2)}", + "{'a':1,\n'b':2,\n'c':(1,)}", + "round(1.213, 2)", + "round(1.213, ndigits = 2)", + "round(abs(1.213), ndigits = 2)", + "round(abs((1.213)), ndigits = 2)", + "round(abs((1.213)), ndigits = abs(2))", + "import numpy as np; np.array([1, 2, 3])", + "import numpy as np; np.array([1, 2, 3])", + "print(file.read())", + "if True:\n print('x')\nelif False:\n print('y')\nelse:\n print('z')", + "while True:\n print(1)", + "for i in [1, 2, 3]:\n print(i)", + "try:\n x = 4\nexcept:\n print('test')", + "from numpy import array as arr", + "def my_fun(a, b = 2):\n return a + b", + "def my_fun(a, b = 2):\n return a + abs(b)", + "def my_fun(a, b = 2):\n return a + (abs(b))", + "echo_word = lambda word, echo = 1: word * echo", + "(lambda word, echo = 1: word * echo)('test', 3)", +]) +def test_parses_without_error(script): + State.parse_internal(script) diff --git a/tests/test_check_function.py b/tests/test_check_function.py index b33e8ce1..8cb15ccf 100644 --- a/tests/test_check_function.py +++ b/tests/test_check_function.py @@ -1,45 +1,61 @@ import pytest +from functools import partial import helper from pythonwhat.local import setup_state from pythonwhat.Test import TestFail as TF from pythonwhat.Feedback import InstructorError from inspect import signature from pythonwhat.check_function import bind_args +from pythonwhat.check_syntax import v2_check_functions +globals().update(v2_check_functions) -@pytest.mark.parametrize('arg, stu', [ - ('a', 'my_fun(1, 10)'), - ('a', 'my_fun(1, b=10)'), - ('a', 'my_fun(a = 1, b=10)'), - ('b', 'my_fun(10, 2)'), - ('b', 'my_fun(10, b=2)'), - ('b', 'my_fun(a = 10, b=2)') - ]) -def test_basic_check_function_passing(arg, stu): - pec = 'def my_fun(a, b): pass' - sol = 'my_fun(1, 2)' - s = setup_state(stu_code=stu, sol_code=sol, pec=pec) - helper.passes(s.check_function('my_fun')) - - helper.passes(s.check_function('my_fun').check_args(arg)) - helper.passes(s.check_function('my_fun').check_args(arg).has_equal_value()) - -def test_basic_check_function_failing(): - pec = 'def my_fun(a=1): pass' - sol = 'my_fun(1)' - s = setup_state(stu_code = '', sol_code=sol, pec=pec) - with pytest.raises(TF): - s.check_function('my_fun') - - s = setup_state(stu_code = 'my_fun()', sol_code=sol, pec=pec) - helper.passes(s.check_function('my_fun')) - with pytest.raises(TF): - s.check_function('my_fun').check_args('a') - - s = setup_state(stu_code = 'my_fun(a = 10)', sol_code=sol, pec=pec) - helper.passes(s.check_function('my_fun')) - helper.passes(s.check_function('my_fun').check_args('a')) - with pytest.raises(TF): - s.check_function('my_fun').check_args('a').has_equal_value() +# Basics ---------------------------------------------------------------------- + +@pytest.mark.parametrize('stu, passes', [ + ('', False), + ('my_fun(2, 2)', False), + ('my_fun(2, b=2)', False), + ('my_fun(a=2, b=2)', False), + ('my_fun(1, 3)', False), + ('my_fun(1, b=3)', False), + ('my_fun(a=1, b=3)', False), + ('my_fun(1, 2)', True), + ('my_fun(1, b=2)', True), + ('my_fun(a=1, b=2)', True), +]) +@pytest.mark.parametrize('a_arg', ['a', 0]) +@pytest.mark.parametrize('b_arg', ['b', 1]) +def test_check_function_basic(stu, passes, a_arg, b_arg): + s = setup_state(stu, 'my_fun(1, 2)', pec='def my_fun(a, b): pass') + with helper.verify_sct(passes): + s.check_function('my_fun').multi( + check_args(a_arg).has_equal_value(), + check_args(b_arg).has_equal_value() + ) + +def test_params_not_matched(): + res = helper.run({ + "DC_PEC": 'def my_fun(a, b): pass', + "DC_CODE": 'my_fun(x = 2)', + "DC_SOLUTION": 'my_fun(1, 2)', + "DC_SCT": "Ex().check_function('my_fun')" + }) + assert not res['correct'] + assert res['message'] == "Have you specified the arguments for my_fun() using the right syntax?" + +# Different types of functions ------------------------------------------------ + +@pytest.mark.parametrize('fun, code, arg', [ + ("my_fun", "def my_fun(a): pass\nmy_fun(1)", "a"), # self-defined + ("round", "round(1)", "number"), # builtin + ("pandas.DataFrame", "import pandas as pd\npd.DataFrame({'a': [1]})", "data"), # package + ("numpy.array", "import numpy as np\nnp.array([1, 2, 3])", "object"), # builtin from package +]) +def test_diff_function_types(fun, code, arg): + s = setup_state(code, code) + s.check_function(fun).check_args(arg).has_equal_value() + +# Argument binding ------------------------------------------------------------ def test_bind_args(): from pythonwhat.local import setup_state @@ -110,6 +126,18 @@ def check_function_sig_false_override(): helper.passes(s.override("f(c = 'blue')").check_function('f', 0, signature=False)\ .check_args('c').has_equal_ast()) +@pytest.mark.parametrize('stu, passes', [ + ("max([1, 2, 3, 4])", True), + ("max([1, 2, 3, 400])", False), +]) +def test_sig_from_params(stu, passes): + s = setup_state(stu, "max([1, 2, 3, 4])") + with helper.verify_sct(passes): + sig = sig_from_params(param('iterable', param.POSITIONAL_ONLY)) + s.check_function('max', signature = sig).check_args(0).has_equal_value() + +# Multiple calls -------------------------------------------------------------- + def check_function_multiple_times(): from pythonwhat.local import setup_state s = setup_state(sol_code = "print('test')", @@ -118,17 +146,7 @@ def check_function_multiple_times(): helper.passes(s.check_function('print').check_args(0)) helper.passes(s.check_function('print').check_args('value')) -@pytest.mark.parametrize('stu', [ - 'round(1.23, 2)', - 'round(1.23, ndigits=2)', - 'round(number=1.23, ndigits=2)' -]) -def test_named_vs_positional(stu): - s = setup_state(sol_code = 'round(1.23, 2)', stu_code = stu) - helper.passes(s.check_function('round').check_args(0).has_equal_value()) - helper.passes(s.check_function('round').check_args("number").has_equal_value()) - helper.passes(s.check_function('round').check_args(1).has_equal_value()) - helper.passes(s.check_function('round').check_args("ndigits").has_equal_value()) +# Methods --------------------------------------------------------------------- def test_method_1(): code = "df.groupby('b').sum()" @@ -151,6 +169,10 @@ def test_method_2(): import pandas as pd helper.passes(s.check_function('df.a.sum', signature = sig_from_obj(pd.Series.sum))) +from pythonwhat.signatures import sig_from_params, param + +# Function parser ------------------------------------------------------------- + @pytest.mark.parametrize('code', [ 'print(round(1.23))', 'x = print(round(1.23))', @@ -160,18 +182,17 @@ def test_method_2(): 'x = 0; x > round(1.23)' ]) def test_function_parser(code): - output = helper.run({ - 'DC_CODE': code, - 'DC_SOLUTION': code, - 'DC_SCT': 'Ex().check_function("round").check_args(0).has_equal_value()' - }) - assert output['correct'] + s = setup_state(code, code) + s.check_function("round").check_args(0).has_equal_value() + +# Incorrect usage ------------------------------------------------------------- @pytest.mark.parametrize('sct', [ "Ex().check_function('round').check_args('ndigits').has_equal_value()", "Ex().check_correct(check_object('x').has_equal_value(), check_function('round').check_args('ndigits').has_equal_value())", "Ex().check_function('round', signature = False).check_args('ndigits').has_equal_value()", - "Ex().check_correct(check_object('x').has_equal_value(), check_function('round', signature = False).check_args('ndigits').has_equal_value())" + "Ex().check_correct(check_object('x').has_equal_value(), check_function('round', signature = False).check_args('ndigits').has_equal_value())", + "Ex().check_correct(check_object('x').has_equal_value(), check_function('round', signature = sig_from_params()))", ]) @pytest.mark.parametrize('sol', [ 'x = 5', @@ -184,4 +205,168 @@ def test_check_function_weirdness(sct, sol): 'DC_SCT': sct } with pytest.raises(InstructorError): - helper.run(data) \ No newline at end of file + helper.run(data) + +# Old implementation: test_function ------------------------------------------- +# NOTE: These tests shows how it _currently_ works, +# but test_function can be improved! + +@pytest.mark.parametrize('stu, passes', [ + ('', False), + ('my_fun(2, 2)', False), + ('my_fun(2, b=2)', False), + ('my_fun(a=2, b=2)', False), + ('my_fun(1, 3)', False), + ('my_fun(1, b=3)', False), + ('my_fun(a=1, b=3)', False), + ('my_fun(1, 2)', False), # this failure is THE limitation of test_function! + ('my_fun(1, b=2)', False), # this failure is THE limitation of test_function! + ('my_fun(a=1, b=2)', True), +]) +def test_test_function_basic(stu, passes): + s = setup_state(stu, 'my_fun(a = 1, b = 2)', pec='def my_fun(a, b): pass') + with helper.verify_sct(passes): + s.test_function('my_fun') + +@pytest.mark.parametrize('stu, passes', [ + ('', False), + ('my_fun(2, 2)', False), + ('my_fun(2, b=2)', False), + ('my_fun(a=2, b=2)', False), + ('my_fun(1, 3)', True), + ('my_fun(1, b=3)', True), + ('my_fun(a=1, b=3)', True), + ('my_fun(1, 2)', True), + ('my_fun(1, b=2)', True), + ('my_fun(a=1, b=2)', True), +]) +def test_test_function_args(stu, passes): + s = setup_state(stu, 'my_fun(1, b = 2)', pec='def my_fun(a, b): pass') + with helper.verify_sct(passes): + s.test_function('my_fun', args = [0], keywords = []) + +@pytest.mark.parametrize('stu, passes', [ + ('', False), + ('my_fun(2, 2)', False), + ('my_fun(2, b=2)', True), + ('my_fun(a=2, b=2)', True), + ('my_fun(1, 3)', False), + ('my_fun(1, b=3)', False), + ('my_fun(a=1, b=3)', False), + ('my_fun(1, 2)', False), + ('my_fun(1, b=2)', True), + ('my_fun(a=1, b=2)', True), +]) +def test_test_function_keywords(stu, passes): + s = setup_state(stu, 'my_fun(1, b = 2)', pec='def my_fun(a, b): pass') + with helper.verify_sct(passes): + s.test_function('my_fun', args = [], keywords = ['b']) + +@pytest.mark.parametrize('stu, do_eval, passes', [ + ("round(1)", True, True), + ("round(a)", True, True), + ("round(b)", True, False), + ("round(b - 1)", True, True), + ("round(1)", False, False), + ("round(a)", False, True), + ("round(b)", False, False), + ("round(b - 1)", False, False), + ("a=123; round(a)", False, True), +]) +def test_test_function_do_eval(stu, do_eval, passes): + s = setup_state(stu, 'round(a)', pec='a,b = 1,2') + with helper.verify_sct(passes): + s.test_function('round', do_eval=do_eval) + +@pytest.mark.parametrize('stu, passes', [ + ("print(1)", True), + ("print('1')", True), + ("print(5)", False) +]) +def test_test_function_print(stu, passes): + s = setup_state(stu, "print(1)") + with helper.verify_sct(passes): + s.test_function('print') + + +# Old implementation: test_function_v2 ---------------------------------------- + +@pytest.mark.parametrize('stu, passes', [ + ('', False), + ('my_fun(2, 2)', False), + ('my_fun(2, b=2)', False), + ('my_fun(a=2, b=2)', False), + ('my_fun(1, 3)', False), + ('my_fun(1, b=3)', False), + ('my_fun(a=1, b=3)', False), + ('my_fun(1, 2)', True), # test_function_v2 is better + ('my_fun(1, b=2)', True), # test_function_v2 is better + ('my_fun(a=1, b=2)', True), +]) +def test_test_function_v2_basic(stu, passes): + s = setup_state(stu, 'my_fun(a = 1, b = 2)', pec='def my_fun(a, b): pass') + with helper.verify_sct(passes): + s.test_function_v2('my_fun', params=['a', 'b']) + +@pytest.mark.parametrize('stu, passes', [ + ('', False), + ('my_fun(2, 2)', False), + ('my_fun(2, b=2)', False), + ('my_fun(a=2, b=2)', False), + ('my_fun(1, 3)', True), + ('my_fun(1, b=3)', True), + ('my_fun(a=1, b=3)', True), + ('my_fun(1, 2)', True), + ('my_fun(1, b=2)', True), + ('my_fun(a=1, b=2)', True), +]) +def test_test_function_v2_params(stu, passes): + s = setup_state(stu, 'my_fun(1, b = 2)', pec='def my_fun(a, b): pass') + with helper.verify_sct(passes): + s.test_function_v2('my_fun', params = ['a']) + +@pytest.mark.parametrize('stu, do_eval, passes', [ + ("round(1)", True, True), + ("round(a)", True, True), + ("round(b)", True, False), + ("round(b - 1)", True, True), + ("round(1)", False, False), + ("round(a)", False, True), + ("round(b)", False, False), + ("round(b - 1)", False, False), + ("a=123; round(a)", False, True), +]) +def test_test_function_v2_do_eval(stu, do_eval, passes): + s = setup_state(stu, 'round(a)', pec='a,b = 1,2') + with helper.verify_sct(passes): + s.test_function_v2('round', params=['number'], do_eval=[do_eval]) + +@pytest.mark.parametrize('stu, passes', [ + ("print(1)", True), + ("print('1')", True), + ("print(5)", False) +]) +def test_test_function_v2_print(stu, passes): + s = setup_state(stu, "print(1)") + with helper.verify_sct(passes): + s.test_function_v2('print', params=['value']) + +@pytest.mark.parametrize('sct', [ + "s.test_function_v2('round', params='number')", + "s.test_function_v2('round', params=[], do_eval=[True])", + "s.test_function_v2('round', params=[], params_not_specified_msg=['test'])", + "s.test_function_v2('round', params=[], incorrect_msg=['test'])" +]) +def test_test_function_v2_incorrect_usage(sct): + s = setup_state("", "") + with pytest.raises(InstructorError): + eval(sct) + +def test_test_function_v2_no_sig(): + s = setup_state('np.arange(10)', 'np.arange(10)', pec='import numpy as np') + # test_function_v2 sets signature=False if no params + s.test_function_v2('numpy.arange') + # check_function fails unless explicity setting signature=Fa + s.check_function('numpy.arange', signature=False) + with pytest.raises(InstructorError): + s.check_function('numpy.arange') \ No newline at end of file diff --git a/tests/test_check_function_def.py b/tests/test_check_function_def.py index 1cdbde3a..87e62454 100644 --- a/tests/test_check_function_def.py +++ b/tests/test_check_function_def.py @@ -1,25 +1,97 @@ import pytest import helper +from pythonwhat.local import setup_state +from pythonwhat.check_syntax import v2_check_functions +globals().update(v2_check_functions) + +@pytest.mark.debug @pytest.mark.parametrize('stu, passes', [ ('', False), ('def test(): print(3)', False), + ('def test(y): print(y)', False), ('def test(x): pass', False), ('def test(x): print(x + 2)', False), ('def test(x): print(x)', True) ]) def test_check_function_def_basic(stu, passes): - output = helper.run({ - 'DC_CODE': stu, - 'DC_SOLUTION': 'def test(x): print(x)', - 'DC_SCT': ''' -Ex().check_function_def('test').multi( - check_args(0), - check_body().set_context(1).check_function('print').check_args(0).has_equal_value() -) -''' - }) - assert output['correct'] == passes + s = setup_state(stu, 'def test(x): print(x)') + with helper.verify_sct(passes): + s.check_function_def('test').multi( + check_args(0).has_equal_part('name', msg='wrong'), + check_body().set_context(1).check_function('print').check_args(0).has_equal_value() + ) + +@pytest.mark.parametrize('stu, passes', [ + ("", False), + ("def shout(x): pass", False), + ("def shout(word): print(word + '!!')", True), + ("def shout(x): print(x + '!!')", True), +]) +def test_check_function_def_no_args(stu, passes): + s = setup_state(stu, "def shout(word): print(word + '!!')") + with helper.verify_sct(passes): + s.check_function_def('shout').check_body().set_context('test').has_equal_output() + +@pytest.mark.parametrize('stu, passes', [ + ("", False), + ("def shout(word): pass", False), + ("def shout(word):\n bigword = word*2", False), + ("def shout(word):\n bigword = word+'!!'", True), + ("def shout(word):\n bigword = word+'!!'\n return bigword", True), +]) +def test_check_function_def_name(stu, passes): + s = setup_state(stu, "def shout(word):\n bigword = word + '!!'\n return bigword") + with helper.verify_sct(passes): + s.check_function_def('shout').check_body().set_context('test').has_equal_value(name = 'bigword') + +# Old spec still supported? ------------------------------------------------- + +@pytest.mark.parametrize('sct', [ + "Ex().check_function_def('shout').check_body().set_context('test').has_equal_output()", + "test_function_definition('shout', body = lambda: test_expression_output(context_vals = ['help']))", + "test_function_definition('shout', body = test_expression_output(context_vals = ['help']))", +]) +def test_old_ways_of_calling(sct): + code = "def shout(word): print(word + '!!')" + res = helper.run({ "DC_CODE": code, "DC_SOLUTION": code, "DC_SCT": sct }) + assert res['correct'] + + +# Arguments, lengths, defaults ----------------------------------------------- + +@pytest.mark.parametrize('stu, passes', [ + ('def f(): pass', False), + ('def f(c, b = 3): pass', False), + ('def f(a = 2, b = 3): pass', False), + ('def f(a): pass', False), + ('def f(a, c): pass', False), + ('def f(a, b): pass', False), + ('def f(a, b = 4): pass', False), + ('def f(a, b = 3): pass', True), +]) +def test_check_function_def_args(stu, passes): + s = setup_state(stu, "def f(a, b = 3): pass") + with helper.verify_sct(passes): + s.check_function_def('f').multi( + check_args(0).has_equal_part('name', msg='wrong').has_equal_part('is_default', msg='wrong'), + check_args(1).has_equal_part('name', msg='wrong').has_equal_part('is_default', msg='wrong').has_equal_value() + ) + +@pytest.mark.parametrize('sct', [ + "Ex().check_function_def('f').has_equal_part_len('args', unequal_msg='wrong')", + "Ex().test_function_definition('f')", # does arg len checking internally +]) +@pytest.mark.parametrize('stu, passes', [ + ('def f(): pass', False), + ('def f(a): pass', False), + ('def f(a, b): pass', True) +]) +def test_check_function_equal_part_len(sct, stu, passes): + res = helper.run({ "DC_CODE": stu, "DC_SOLUTION": 'def f(a, b): pass', "DC_SCT": sct }) + assert res['correct'] == passes + +# Check call ------------------------------------------------------------------ @pytest.mark.parametrize('stu, passes', [ ('def test(a, b): return 1', False), @@ -35,40 +107,53 @@ def test(a, b): ('def test(a, b): print(a + b); return a + b', True) ]) def test_check_call(stu, passes): - - output = helper.run({ - 'DC_CODE': stu, - 'DC_SOLUTION': 'def test(a, b): print(a + b); return a + b', - 'DC_SCT': """ -Ex().check_function_def('test').multi( - check_call("f(1,2)").has_equal_value(), - check_call("f(1,2)").has_equal_output(), - check_call("f(3,1)").has_equal_value(), - check_call("f(1, '2')").has_equal_error() -) -"""}) - assert output['correct'] == passes + s = setup_state(stu, 'def test(a, b): print(a + b); return a + b') + with helper.verify_sct(passes): + s.check_function_def('test').multi( + check_call("f(1,2)").has_equal_value(), + check_call("f(1,2)").has_equal_output(), + check_call("f(3,1)").has_equal_value(), + check_call("f(1, '2')").has_equal_error() + ) @pytest.mark.parametrize('stu, passes', [ ('lambda a,b: 1', False), ('lambda a,b: a + b', True) ]) def test_check_call_lambda(stu, passes): - output = helper.run({ - 'DC_CODE': stu, - 'DC_SOLUTION': 'lambda a, b: a + b', - 'DC_SCT': """ -Ex().check_lambda_function().multi( - check_call("f(1,2)").has_equal_value(), - check_call("f(1,2)").has_equal_output() -) -"""}) - assert output['correct'] == passes + s = setup_state(stu, 'lambda a, b: a + b') + with helper.verify_sct(passes): + s.check_lambda_function().multi( + check_call("f(1,2)").has_equal_value(), + check_call("f(1,2)").has_equal_output() + ) def test_check_call_error_types(): - output = helper.run({ - 'DC_CODE': 'def test(): raise ValueError("boooo")', - 'DC_SOLUTION': 'def test(): raise NameError("boooo")', - 'DC_SCT': 'Ex().check_function_def("test").check_call("f()").has_equal_error()' - }) - assert output['correct'] \ No newline at end of file + s = setup_state('def test(): raise NameError("boooo")', + 'def test(): raise ValueError("boooo")') + s.check_function_def("test").check_call("f()").has_equal_error() + +# Lambdas --------------------------------------------------------------------- + +@pytest.mark.parametrize('stu, passes', [ + ('', False), + ('lambda x: x', False), + ('lambda a, b: a', False), + ('lambda x = 1, b = 2: x', False), + ('lambda x, b: x', False), + ('lambda x, y = 1: x', False), + ('lambda x, y = 2: x', False), + ('lambda x, y = 2: print(x + y + 3)', False), + ('lambda x, y = 2: print(x + y)', True), + ('lambda x, y = 2: print(y + x)', True), + ('lambda x, y = 2: print(3)', True), # because set_context(1,2) +]) +def test_check_lambda_full_ast_based(stu, passes): + s = setup_state(stu, 'lambda x, y=2: print(x + y)') + with helper.verify_sct(passes): + s.check_lambda_function(0).multi( + has_equal_part_len('args', unequal_msg='wrong'), + check_args(0).has_equal_part('name', msg='wrong').has_equal_part('is_default', msg='wrong'), + check_args(1).has_equal_part('name', msg='wrong').has_equal_part('is_default', msg='wrong').has_equal_value(), + check_body().set_context(1, 2).has_equal_output() + ) diff --git a/tests/test_check_if_else.py b/tests/test_check_if_else.py new file mode 100644 index 00000000..9b8f33bb --- /dev/null +++ b/tests/test_check_if_else.py @@ -0,0 +1,160 @@ +import helper +import pytest + +@pytest.mark.parametrize('sct', [ + ''' +def condition_test(): + test_expression_result({"offset": 7}) + test_expression_result({"offset": 8}) + test_expression_result({"offset": 9}) +test_if_else(index=1, + test = condition_test, + body = lambda: test_student_typed(r'x\s*=\s*5'), + orelse = lambda: test_function('round')) + ''', + ''' +condition_test = [ + test_expression_result({"offset": 7}), + test_expression_result({"offset": 8}), + test_expression_result({"offset": 9}) +] + +test_if_else(index=1, + test = condition_test, + body = test_student_typed(r'x\s*=\s*5'), + orelse = test_function('round')) + ''', + ''' +Ex().check_if_else().multi( + check_test().multi([ set_env(offset = i).has_equal_value() for i in range(7,10) ]), + check_body().has_code(r'x\s*=\s*5'), + check_orelse().check_function('round').check_args(0).has_equal_value() +) + ''' +]) +@pytest.mark.parametrize('stu, passes', [ + ('', False), + ('if offset > 10: x = 5\nelse: x = round(2.123)', False), + ('if offset > 8: x = 7\nelse: x = round(2.123)', False), + ('if offset > 8: x,y = 7,12\nelse: x = round(2.123)', False), + ('if offset > 8: x = 5\nelse: x = 8', False), + ('if offset > 8: x = 5\nelse: x = round(2.2121314)', False), + ('if offset > 8: x = 5\nelse: x = round(2.123)', True), +]) +def test_check_if_else_basic(sct, stu, passes): + res = helper.run({ + 'DC_PEC': 'offset = 8', + 'DC_SOLUTION': 'if offset > 8: x = 5\nelse: x = round(2.123)', + 'DC_CODE': stu, + 'DC_SCT': sct + }) + assert res['correct'] == passes + + +@pytest.mark.parametrize('sct', [ + ''' +def test_test(): + test_expression_result({"offset": 7}) + test_expression_result({"offset": 8}) + test_expression_result({"offset": 9}) + +def body_test(): + test_student_typed('5') + +def orelse_test(): + def test_test2(): + test_expression_result({"offset": 4}) + test_expression_result({"offset": 5}) + test_expression_result({"offset": 6}) + def body_test2(): + test_student_typed('7') + def orelse_test2(): + test_function('round') + test_if_else(index = 1, + test = test_test2, + body = body_test2, + orelse = orelse_test2, + expand_message = False) + +test_if_else(index=1, + test=test_test, + body=body_test, + orelse=orelse_test, + expand_message = False) + ''', + ''' +test_if_else(index=1, + test=[ + test_expression_result({"offset": 7}), + test_expression_result({"offset": 8}), + test_expression_result({"offset": 9}) + ], + body=test_student_typed('5'), + orelse=test_if_else(index = 1, + test=[ + test_expression_result({"offset": 4}), + test_expression_result({"offset": 5}), + test_expression_result({"offset": 6}) + ], + body = test_student_typed('7'), + orelse = test_function('round') + ) +) + ''', + ''' +Ex().check_if_else().multi( + check_test().multi([ set_env(offset = i).has_equal_value() for i in range(7, 10) ]), + check_body().has_code(r'x\s*=\s*5'), + check_orelse().check_if_else().multi( + check_test().multi([ set_env(offset = i).has_equal_value() for i in range(4, 7) ]), + check_body().has_code('7'), + check_orelse().check_function('round').check_args(0).has_equal_value() + ) +) + ''' +]) +@pytest.mark.parametrize('stu, passes', [ + ('', False), + ('if offset > 9: x = 5\nelif offset > 5: x = 7\nelse: x = round(9)', False), + ('if offset > 8: x = 6\nelif offset > 5: x = 7\nelse: x = round(9)', False), + ('if offset > 8: x = 5\nelif offset > 6: x = 7\nelse: x = round(9)', False), + ('if offset > 8: x = 5\nelif offset > 5: x = 8\nelse: x = round(9)', False), + ('if offset > 8: x = 5\nelif offset > 5: x = 7\nelse: x = round(10)', False), + ('if offset > 8: x = 5\nelif offset > 5: x = 7\nelse: x = round(9)', True), +]) +def test_check_if_else_embedded(sct, stu, passes): + res = helper.run({ + 'DC_PEC': 'offset = 8', + 'DC_SOLUTION': 'if offset > 8: x = 5\nelif offset > 5: x = 7\nelse: x = round(9)', + 'DC_CODE': stu, + 'DC_SCT': sct + }) + assert res['correct'] == passes + +@pytest.mark.parametrize('stu, passes', [ + ('', False), + ('x = 5 if offset > 9 else 7 if offset > 5 else round(9)', False), + ('x = 6 if offset > 8 else 7 if offset > 5 else round(9)', False), + ('x = 5 if offset > 8 else 7 if offset > 6 else round(9)', False), + ('x = 5 if offset > 8 else 8 if offset > 5 else round(9)', False), + ('x = 5 if offset > 8 else 7 if offset > 5 else round(10)', False), + ('x = 5 if offset > 8 else 7 if offset > 5 else round(9)', True), +]) +def test_if_exp(stu, passes): + res = helper.run({ + 'DC_PEC': 'offset = 8', + 'DC_SOLUTION': 'x = 5 if offset > 8 else 7 if offset > 5 else round(9)', + 'DC_CODE': stu, + 'DC_SCT': ''' +Ex().check_if_exp().multi( + check_test().multi([ set_env(offset = i).has_equal_value() for i in range(7, 10) ]), + check_body().has_code('5'), + check_orelse().check_if_exp().multi( + check_test().multi([ set_env(offset = i).has_equal_value() for i in range(4, 7) ]), + check_body().has_code('7'), + check_orelse().check_function('round').check_args(0).has_equal_value() + ) +) +''' + }) + assert res['correct'] == passes diff --git a/tests/test_check_list_comp.py b/tests/test_check_list_comp.py new file mode 100644 index 00000000..a8a62551 --- /dev/null +++ b/tests/test_check_list_comp.py @@ -0,0 +1,157 @@ +import helper +import pytest +from pythonwhat.local import setup_state +from pythonwhat.check_syntax import v2_check_functions +globals().update(v2_check_functions) + +@pytest.mark.parametrize('stu, passes', [ + ("", False), + ("[key for key in x.keys()]", False), + ("[a + str(b) for a,b in x.items()]", False), + ("[key + '_' + str(val) for key,val in x.items()]", False), + ("[key + str(val) for key,val in x.items()]", False), + ("[key + str(val) for key,val in x.items() if hasattr(key, 'test') if hasattr(key, 'test')]", False), + ("[key + str(val) for key,val in x.items() if isinstance(key, str) if hasattr(key, 'test')]", False), + ("[key + str(val) for key,val in x.items() if isinstance(key, str) if isinstance(key, str)]", False), + ("[key + str(val) for key,val in x.items() if isinstance(key, str) if isinstance(val, str)]", True), +]) +def test_check_list_comp_basic(stu, passes): + pec = "x = {'a': 2, 'b':3, 'c':4, 'd':'test'}" + sol = "[key + str(val) for key,val in x.items() if isinstance(key, str) if isinstance(val, int)]" + s = setup_state(stu, sol, pec) + with helper.verify_sct(passes): + s.check_list_comp().multi( + check_iter().has_equal_value(), + check_ifs(0).check_function('isinstance').check_args('obj').has_equal_ast(), + check_ifs(1).check_function('isinstance').check_args('obj').has_equal_ast(), + check_body().has_context(exact_names=True).set_context('a', 2).has_equal_value() + ) + +@pytest.mark.parametrize('stu, passes, patt, lines', [ + ("", False, "The system wants to check the first list comprehension but hasn't found it.", []), + ("[key for key in x.keys()]", False, "Check the first list comprehension. Did you correctly specify the iterable part?", [1, 1, 17, 24]), + ("[a + str(b) for a,b in x.items()]", False, "Have you used the correct iterator variables in the first list comprehension? Be sure to use the correct names.", [1, 1, 17, 19]), + ("[key + '_' + str(val) for key,val in x.items()]", False, "Did you correctly specify the body?", [1, 1, 2, 21]), + ("[key + str(val) for key,val in x.items()]", False, "Have you used 2 ifs inside the first list comprehension?", []), + ("[key + str(val) for key,val in x.items() if hasattr(key, 'test') if hasattr(key, 'test')]", False, "Did you correctly specify the first if? Did you call isinstance()?", [1, 1, 45, 64]), + ("[key + str(val) for key,val in x.items() if isinstance(key, str) if hasattr(key, 'test')]", False, "Did you correctly specify the second if? Did you call isinstance()?", [1, 1, 69, 88]), + ("[key + str(val) for key,val in x.items() if isinstance(key, str) if isinstance(key, str)]", False, "Did you correctly specify the argument obj? Expected val, but got key.", [1, 1, 80, 82]), + ("[key + str(val) for key,val in x.items() if isinstance(key, str) if isinstance(val, str)]", True, "Great", []), +]) +def test_test_list_comp_messaging(stu, passes, patt, lines): + pec = "x = {'a': 2, 'b':3, 'c':4, 'd':'test'}" + sol = "[key + str(val) for key,val in x.items() if isinstance(key, str) if isinstance(val, int)]" + sct = ''' +test_list_comp(index=1, + not_called_msg=None, + comp_iter=lambda: test_expression_result(), + iter_vars_names=True, + incorrect_iter_vars_msg=None, + body=lambda: test_expression_result(context_vals = ['a', 2]), + ifs=[lambda: test_function_v2('isinstance', params = ['obj'], do_eval = [False]), + lambda: test_function_v2('isinstance', params = ['obj'], do_eval = [False])], + insufficient_ifs_msg=None, + expand_message=True) + ''' + res = helper.run({ "DC_PEC": pec, "DC_CODE": stu, "DC_SOLUTION": sol, "DC_SCT": sct }) + assert res['correct'] == passes + assert patt in res['message'] + if lines: helper.with_line_info(res, *lines) + +@pytest.mark.parametrize('stu, passes, patt, lines', [ + ("", False, "notcalled", []), + ("[key for key in x.keys()]", False, "iterincorrect", [1, 1, 17, 24]), + ("[a + str(b) for a,b in x.items()]", False, "incorrectitervars", [1, 1, 17, 19]), + ("[key + '_' + str(val) for key,val in x.items()]", False, "bodyincorrect", [1, 1, 2, 21]), + ("[key + str(val) for key,val in x.items()]", False, "insufficientifs", []), # [1, 1, 2, 41] doesn't work... + ("[key + str(val) for key,val in x.items() if hasattr(key, 'test') if hasattr(key, 'test')]", False, "notcalled1", [1, 1, 45, 64]), + ("[key + str(val) for key,val in x.items() if isinstance(key, str) if hasattr(key, 'test')]", False, "notcalled2", [1, 1, 69, 88]), + ("[key + str(val) for key,val in x.items() if isinstance(key, str) if isinstance(key, str)]", False, "incorrect2", [1, 1, 80, 82]), + ("[key + str(val) for key,val in x.items() if isinstance(key, str) if isinstance(val, str)]", True, "Great", []), +]) +def test_test_list_comp_custom_messaging(stu, passes, patt, lines): + pec = "x = {'a': 2, 'b':3, 'c':4, 'd':'test'}" + sol = "[key + str(val) for key,val in x.items() if isinstance(key, str) if isinstance(val, int)]" + sct = ''' +test_list_comp(index=1, + not_called_msg='notcalled', + comp_iter=lambda: test_expression_result(incorrect_msg = 'iterincorrect'), + iter_vars_names=True, + incorrect_iter_vars_msg='incorrectitervars', + body=lambda: test_expression_result(context_vals = ['a', 2], incorrect_msg = 'bodyincorrect'), + ifs=[lambda: test_function_v2('isinstance', params = ['obj'], do_eval = [False], not_called_msg = 'notcalled1', incorrect_msg = 'incorrect2'), + lambda: test_function_v2('isinstance', params = ['obj'], do_eval = [False], not_called_msg = 'notcalled2', incorrect_msg = 'incorrect2')], + insufficient_ifs_msg='insufficientifs') + ''' + res = helper.run({ "DC_PEC": pec, "DC_CODE": stu, "DC_SOLUTION": sol, "DC_SCT": sct }) + assert res['correct'] == passes + assert patt in res['message'] + if lines: helper.with_line_info(res, *lines) + +@pytest.mark.parametrize('stu, passes', [ + ("[[col + 1 for col in range(5)] for row in range(5)]", False), + ("[[col for col in range(5)] for row in range(5)]", True), +]) +def test_list_comp_nested(stu, passes): + res = helper.run({ + "DC_CODE": stu, + "DC_SOLUTION": "[[col for col in range(5)] for row in range(5)]", + "DC_SCT": "test_list_comp(1, body = lambda: test_list_comp(1, body = lambda: test_expression_result(context_vals = [4])))" + }) + assert res['correct'] == passes + +@pytest.mark.parametrize('sct', [ + "test_list_comp(1, iter_vars_names=False)", + "Ex().check_list_comp(0).has_context()" +]) +@pytest.mark.parametrize('stu, passes', [ + ("[a for a in x.items()]", False), + ("[a for a,b in x.items()]", True) +]) +def test_list_iter_vars(sct, stu, passes): + res = helper.run({ + "DC_PEC": "x = {'a':1, 'b':2}", + "DC_SOLUTION": "[key for key, value in x.items()]", + "DC_CODE": stu, + "DC_SCT": sct + }) + res['correct'] == passes + +# TODO +# class TestListDestructuring(unittest.TestCase): +# def setUp(self): +# self.data = { +# "DC_PEC": "x = {'a':1, 'b':2}", +# "DC_SOLUTION": "[key for key, value in x.items()]", +# "DC_SCT": "test_list_comp(1, body=test_expression_result(context_vals=[(1,2)]), iter_vars_names=False)" +# } + +# @unittest.expectedFailure +# def test_pass_destructuring1(self): +# # TODO: fails because context_vals set by simple iteration and for reason below +# self.data["DC_CODE"] = "[a[0] for *a in x.items()]" +# sct_payload = helper.run(self.data) +# self.assertTrue(sct_payload['correct']) + +# def test_pass_destructuring2(self): +# self.data["DC_CODE"] = "[a for *a, b in x.items()]" +# sct_payload = helper.run(self.data) +# self.assertTrue(sct_payload['correct']) + +# def test_pass_destructuring3(self): +# self.data["DC_CODE"] = "[b for b, *a in x.items()]" +# sct_payload = helper.run(self.data) +# self.assertTrue(sct_payload['correct']) + +# @unittest.expectedFailure +# def test_pass_destructuring4(self): +# # TODO: fails because it tests for exact same number of iter vars +# self.data["DC_CODE"] = "[k for k, v, *a in x.items()]" +# sct_payload = helper.run(self.data) +# self.assertTrue(sct_payload['correct']) + +# def test_fail_destructuring(self): +# self.data["DC_CODE"] = "[a for k, v, *a in x.items()]" +# sct_payload = helper.run(self.data) +# self.assertFalse(sct_payload['correct']) + diff --git a/tests/test_check_object.py b/tests/test_check_object.py new file mode 100644 index 00000000..6716f9f7 --- /dev/null +++ b/tests/test_check_object.py @@ -0,0 +1,301 @@ +import helper +from pythonwhat.local import setup_state +from pythonwhat.Test import TestFail as TF +import pytest + +@pytest.mark.parametrize('sct', [ + "test_object('x', undefined_msg='udm', incorrect_msg='icm')", + "Ex().check_object('x', missing_msg='udm').has_equal_value(incorrect_msg='icm')" +]) +@pytest.mark.parametrize('stu_code, passes, msg', [ + ('', False, 'udm'), + ('x = 1', False, 'icm'), + ('x = 100', True, None) +]) +def test_check_object(sct, stu_code, passes, msg): + output = helper.run({ + 'DC_SOLUTION': 'x = 100', + 'DC_CODE': stu_code, + 'DC_SCT': sct + }) + assert output['correct'] == passes + if msg: assert output['message'] == msg + +@pytest.mark.parametrize('stu_code, passes', [ + ('x = filter(lambda x: x > 0, [0, 1])', False), + ('x = filter(lambda x: x > 0, [1, 1])', True) +]) +def test_check_object_exotic_compare(stu_code, passes): + output = helper.run({ + 'DC_SOLUTION': 'x = filter(lambda x: x > 0, [1, 1])', + 'DC_SCT': "Ex().check_object('x').has_equal_value()", + 'DC_CODE': stu_code + }) + assert output['correct'] == passes + +@pytest.mark.parametrize('stu_code, passes', [ + ('x = [1, 2, 3]', True), + ('x = [1, 2, 3, 4]', False) +]) +def test_check_object_custom_compare(stu_code, passes): + output = helper.run({ + "DC_SOLUTION": 'x = [4, 5, 6]', + 'DC_CODE': stu_code, + 'DC_SCT': 'Ex().check_object("x").has_equal_value(func = lambda x,y: len(x) == len(y))' + }) + assert output['correct'] == passes + +def test_check_object_single_process(): + state1pid = setup_state('x = 3', '', pid = 1) + helper.passes(state1pid.check_object('x')) + +@pytest.mark.parametrize('stu_code, passes', [ + ('arr = 4', False), + ('arr = np.array([1])', True) +]) +def test_is_instance(stu_code, passes): + output = helper.run({ + 'DC_PEC': 'import numpy as np', + 'DC_SOLUTION': 'arr = np.array([1, 2, 3, 4])', + 'DC_SCT': "import numpy; Ex().check_object('arr').is_instance(numpy.ndarray)", + 'DC_CODE': stu_code + }) + assert output['correct'] == passes + +@pytest.mark.parametrize('sct', [ + "test_data_frame('df', columns=['a'], undefined_msg='udm', not_data_frame_msg='ndfm', undefined_cols_msg='ucm', incorrect_msg='icm')", + "test_data_frame('df', columns=None, undefined_msg='udm', not_data_frame_msg='ndfm', undefined_cols_msg='ucm', incorrect_msg='icm')", + """ +import pandas as pd +Ex().check_object('df', missing_msg='udm', expand_msg='').\ + is_instance(pd.DataFrame, not_instance_msg='ndfm').\ + check_keys('a', missing_msg='ucm').has_equal_value(incorrect_msg='icm') + """, + """ +import pandas as pd +Ex().check_df('df', missing_msg='udm', expand_msg='', not_instance_msg='ndfm').\ + check_keys('a', missing_msg='ucm').has_equal_value(incorrect_msg='icm') + """ +]) +@pytest.mark.parametrize('stu_code, passes, msg', [ + ('', False, 'udm'), + ('df = 3', False, 'ndfm'), + ('df = pd.DataFrame({ "b": [1]})', False, 'ucm'), + ('df = pd.DataFrame({ "a": [1]})', False, 'icm'), + ('df = pd.DataFrame({ "a": [1, 2, 3] })', True, None), + ('df = pd.DataFrame({ "a": [1, 2, 3], "b": [3, 4, 5] })', True, None), +]) +def test_test_data_frame(sct, stu_code, passes, msg): + output = helper.run({ + 'DC_PEC': 'import pandas as pd', + 'DC_SOLUTION': 'df = pd.DataFrame({"a": [1, 2, 3]})', + 'DC_CODE': stu_code, + 'DC_SCT': sct + }) + assert output['correct'] == passes + if msg: assert output['message'] == msg + +@pytest.mark.parametrize('stu_code, passes', [ + ('x = {}', False), + ('x = {"b": 3}', False), + ('x = {"a": 3}', False), + ('x = {"a": 2}', True), + ('x = {"a": 2, "b": 3}', True), +]) +def test_check_keys(stu_code, passes): + output = helper.run({ + 'DC_SOLUTION': 'x = {"a": 2}', + 'DC_CODE': stu_code, + 'DC_SCT': 'Ex().check_object("x").check_keys("a").has_equal_value()' + }) + assert output['correct'] == passes + +@pytest.mark.parametrize('sct', [ + "Ex().test_data_frame('pivot')", + "Ex().check_df('pivot').check_keys(('visitors', 'Austin')).has_equal_value()" +]) +def test_check_keys_exotic(sct): + code = "pivot = users.pivot(index='weekday', columns='city')" + output = helper.run({ + 'DC_PEC': ''' +import pandas as pd +users = pd.read_csv('https://s3.amazonaws.com/assets.datacamp.com/production/course_1650/datasets/users.csv') +''', + 'DC_SOLUTION': code, + 'DC_CODE': code, + 'DC_SCT': sct + }) + assert output['correct'] + +def test_non_dillable(): + s = setup_state( + stu_code="xl = pd.ExcelFile('battledeath.xlsx')", + sol_code="xl = pd.ExcelFile('battledeath.xlsx')", + pec="import pandas as pd; from urllib.request import urlretrieve; urlretrieve('https://s3.amazonaws.com/assets.datacamp.com/production/course_998/datasets/battledeath.xlsx', 'battledeath.xlsx')", + ) + s.check_object('xl').has_equal_value() + +from pythonwhat.State import set_converter + +@pytest.mark.compiled +def test_manual_converter(): + s = setup_state( + stu_code="xl = pd.ExcelFile('battledeath2.xlsx')", + sol_code="xl = pd.ExcelFile('battledeath.xlsx')", + pec="import pandas as pd; from urllib.request import urlretrieve; urlretrieve('https://s3.amazonaws.com/assets.datacamp.com/production/course_998/datasets/battledeath.xlsx', 'battledeath.xlsx'); from urllib.request import urlretrieve; urlretrieve('https://s3.amazonaws.com/assets.datacamp.com/production/course_998/datasets/battledeath.xlsx', 'battledeath2.xlsx')", + ) + def my_converter(x): return(x.sheet_names) + set_converter(key = "pandas.io.excel.ExcelFile", fundef = my_converter) + s.check_object('xl').has_equal_value() + +def test_manual_converter_2(): + s = setup_state( + stu_code="my_array = np.array([[0,0], [0,0], [0,0]])", + sol_code="my_array = np.array([[1,2], [3,4], [5,6]])", + pec="import numpy as np" + ) + set_converter(key = 'numpy.ndarray', fundef = lambda x: x.shape) + s.check_object('my_array').has_equal_value() + + +@pytest.mark.parametrize('stu, sol', [ + ("x = 2", "import numpy as np; x = np.mean([1, 2, 3])"), + ("x = 2", "x = 2.0"), + ("x = None", "x = None") +]) +def test_equality_challenges(stu, sol): + s = setup_state(stu, sol) + s.check_object('x').has_equal_value() + +def test_equality_challenge_2(): + code = "mat = scipy.io.loadmat('albeck_gene_expression.mat')" + s = setup_state( + stu_code=code, + sol_code=code, + pec="import scipy.io; from urllib.request import urlretrieve; urlretrieve('https://s3.amazonaws.com/assets.datacamp.com/production/course_998/datasets/ja_data2.mat', 'albeck_gene_expression.mat')" + ) + s.check_object('mat').has_equal_value() + +@pytest.mark.parametrize('name, ls, le, cs, ce', [ + ('a', 3, 3, 5, 9), + ("c", 8, 8, 5, 9), + ("d", 11, 11, 5, 9), + ("e", 15, 15, 5, 9), + ("f", 19, 19, 5, 9), + ("g", 24, 24, 5, 9), + ("h", 28, 28, 5, 9), + ("i", 0, 0, 0, 0), +]) +def test_parsing(name, ls, le, cs, ce): + stu_code = ''' +if True: + a = 1 + +if False: + b = 2 +else: + c = 3 + +for i in range(2): + d = 4 + +x = 2 +while x > 0: + e = 5 + x -= 1 + +try: + f = 6 +except: + pass + +try: + g = 7 +except: + pass +finally: + h = 8 + +# 2 assignments +i = 9 +if True: + i = 9 +''' + sol_code=''' +if True: + a = 10 + +if False: + b = 20 +else: + c = 30 + +for i in range(2): + d = 40 + +x = 2 +while x > 0: + e = 50 + x -= 1 + +try: + f = 60 +except: + pass + +try: + g = 70 +except: + pass +finally: + h = 80 + +# 2 assignments +i = 90 +if True: + i = 90 +''' + res = helper.run({ + 'DC_CODE': stu_code, + 'DC_SOLUTION': sol_code, + 'DC_SCT': 'Ex().check_object("%s").has_equal_value()' % name + }) + assert not res['correct'] + if name == 'i': helper.no_line_info(res) + else: helper.with_line_info(res, ls, le, cs, ce) + +@pytest.fixture() +def diff_assign_data(): + return { + "DC_CODE":''' +import pandas as pd +df = pd.DataFrame({"a": [1, 2, 3], "b": [4, 5, 6]}) +df.columns = ["c", "d"] + +df2 = pd.DataFrame({"a": [1, 2, 3], "b": [4, 5, 6]}) +df2.columns = ["e", "f"] + ''', + "DC_SOLUTION":''' +import pandas as pd +df = pd.DataFrame({"a": [1, 2, 3], "b": [4, 5, 6]}) +df.columns = ["c", "d"] + +df2 = pd.DataFrame({"a": [1, 2, 3], "b": [4, 5, 6]}) +df2.columns = ["c", "d"] + ''' + } + +def test_several_assignments(diff_assign_data): + res = helper.run({ + **diff_assign_data, + "DC_SCT": "Ex().check_object('df').has_equal_value()" + }) + assert res['correct'] + +def test_several_assignments_2(diff_assign_data): + res = helper.run({ + **diff_assign_data, + "DC_SCT": "Ex().check_object('df2').has_equal_value()" + }) + assert not res['correct'] + helper.no_line_info(res) \ No newline at end of file diff --git a/tests/test_has_code.py b/tests/test_has_code.py index 92512ce7..d24fe05f 100644 --- a/tests/test_has_code.py +++ b/tests/test_has_code.py @@ -1,77 +1,24 @@ -import unittest import helper - -class TestStudentTypedComment(unittest.TestCase): - - def setUp(self): - self.data = { - "DC_PEC": '', - "DC_CODE": '# Just testing division\nprint(5 / 8)\n# Addition works too\nprint(7 + 10)', - "DC_SOLUTION": '' - } - - def test_success(self): - self.data["DC_SCT"] = 'test_student_typed(r"# (A|a)ddition works to(o?)\sprint\(7")' - sct_payload = helper.run(self.data) - self.assertTrue(sct_payload['correct']) - - def test_success_new(self): - self.data["DC_SCT"] = 'Ex().has_code(r"# (A|a)ddition works to(o?)\sprint\(7")' - sct_payload = helper.run(self.data) - self.assertTrue(sct_payload['correct']) - -class TestStudentDidntTypeComment(unittest.TestCase): - - def setUp(self): - self.data = { - "DC_PEC": '', - "DC_CODE": '# Just testing division\nprint(5 / 8)\nprint(7 + 10)', - "DC_SOLUTION": '' - } - - def test_fail(self): - self.data["DC_SCT"] = 'test_student_typed(r"# (A|a)ddition works to(o?)\sprint\(7", not_typed_msg = "Wrong.")' - sct_payload = helper.run(self.data) - self.assertFalse(sct_payload['correct']) - self.assertEqual(sct_payload['message'], "Wrong.") - - def test_fail_new(self): - self.data["DC_SCT"] = 'Ex().has_code(r"# (A|a)ddition works to(o?)\sprint\(7", not_typed_msg = "Wrong.")' - sct_payload = helper.run(self.data) - self.assertFalse(sct_payload['correct']) - self.assertEqual(sct_payload['message'], "Wrong.") - -class TestWikiExample(unittest.TestCase): - - def test_wikiexample1(self): - self.data = { - "DC_PEC": '', - "DC_SOLUTION": 's = sum(range(10))\nprint(s)', - "DC_SCT": 'Ex().has_code(r"sum(range(", pattern = False)', - "DC_CODE": 's = sum(range(10))\nprint(s)' - } - sct_payload = helper.run(self.data) - self.assertTrue(sct_payload['correct']) - - def test_wikiexample2(self): - self.data = { - "DC_PEC": '', - "DC_SOLUTION": 's = sum(range(10))\nprint(s)', - "DC_SCT": 'Ex().has_code(r"sum\s*\(\s*range\s*\(")', - "DC_CODE": 's = sum(range(10))\nprint(s)' - } - sct_payload = helper.run(self.data) - self.assertTrue(sct_payload['correct']) - - def test_wikiexample1(self): - self.data = { - "DC_PEC": '', - "DC_SOLUTION": 's = sum(range(10))\nprint(s)', - "DC_SCT": 'Ex().has_code(r"sum\s+\(\s*range\s*\(")', - "DC_CODE": 's = sum(range(10))\nprint(s)' - } - sct_payload = helper.run(self.data) - self.assertFalse(sct_payload['correct']) - -if __name__ == "__main__": - unittest.main() \ No newline at end of file +from pythonwhat.local import setup_state +import pytest + +@pytest.mark.parametrize('stu, passes', [ + ('', False), + ('c', False), + ('a == c', True), +]) +def test_basic(stu, passes): + s = setup_state(stu, '', pec='c,a=0,0') + with helper.verify_sct(passes): + s.has_code('a|b') + +@pytest.mark.parametrize('stu, passes', [ + ('', False), + ('c', False), + ('a == c', False), + ('"a|b"', True) +]) +def test_basic_pattern(stu, passes): + s = setup_state(stu, '', pec='a,c=0,0') + with helper.verify_sct(passes): + s.has_code('a|b', pattern=False) diff --git a/tests/test_has_expr.py b/tests/test_has_expr.py index f930c8b7..aa6aeb60 100644 --- a/tests/test_has_expr.py +++ b/tests/test_has_expr.py @@ -2,6 +2,79 @@ from pythonwhat.local import setup_state import helper +@pytest.mark.parametrize('stu, passes', [ + ('', False), + ('a = 3', False), + ('a = 2', True) +]) +def test_has_equal_value_basic(stu, passes): + s = setup_state(stu, 'a = 2') + with helper.verify_sct(passes): + s.has_equal_value(expr_code = 'a') + +@pytest.mark.parametrize('stu, passes', [ + ('a = 0\nfor i in range(0): pass', False), + ('a = 0\nfor i in range(0): a = a - 1', False), + ('a = 0\nfor i in range(0): a = a + 1', True), +]) +def test_has_equal_value_name(stu, passes): + s = setup_state(stu, 'a = 0\nfor i in range(0): a = 1') + with helper.verify_sct(passes): + s.check_for_loop().check_body().has_equal_value(name = 'a') + +@pytest.mark.parametrize('stu, passes', [ + ('a = 0\nfor i in range(0): pass', False), + ('a = 0\nfor i in range(0): a = a - 1', False), + ('a = 0\nfor i in range(0): a = a + 1', True), +]) +def test_has_equal_value_old(stu, passes): + out = helper.run({ + "DC_CODE": stu, + "DC_SOLUTION": 'a = 0\nfor i in range(0): a = a + 1', + "DC_SCT": "test_for_loop(body = test_object_after_expression('a'))" + }) + out["correct"] == passes + +@pytest.mark.parametrize('stu, passes', [ + ('', False), + ('x = {"a": 2}', False), + ('x = {"a": 1}', True), +]) +def test_has_equal_output_basic(stu, passes): + s = setup_state(stu, 'x = {"a":1, "b":2, "c": 3}') + with helper.verify_sct(passes): + s.test_expression_output(expr_code = 'print(x["a"])') + +@pytest.mark.parametrize('stu, passes', [ + ('', False), + ("for i in range(10):\n print(i + 1)", False), + ("for i in range(10):\n print(i)", True), +]) +@pytest.mark.parametrize('context_vals', [ None, [1]]) +def test_has_equal_output_for(stu, passes, context_vals): + s = setup_state(stu, "for i in range(10):\n print(i)") + with helper.verify_sct(passes): + s.check_for_loop().check_body().has_equal_output(context_vals = context_vals) + +@pytest.mark.parametrize('copy, passes', [ + (False, True), + (True, False), +]) +def test_copy_functionality(copy, passes): + s = setup_state('a = [1]', 'a = [2]') + with helper.verify_sct(passes): + s.has_equal_value(expr_code = 'a[0] = 3', name = 'a', copy = copy).has_equal_value(expr_code = 'a', name = 'a') + +@pytest.mark.parametrize('tol, passes', [ + (0.001, True), + (0.0001, False) +]) +def test_test_custom_equality_func(tol, passes): + s = setup_state("a = [1.011]", "a = [1.01]") + import numpy as np + with helper.verify_sct(passes): + s.check_object('a').has_equal_value(func = lambda x, y: np.allclose(x, y, atol = tol)) + def test_has_expr_override_pass(): stu = 'x = [1, 2, 3]' sol = 'x = [1, 2, 5]' diff --git a/tests/test_has_output.py b/tests/test_has_output.py index 6671e584..66221647 100644 --- a/tests/test_has_output.py +++ b/tests/test_has_output.py @@ -1,54 +1,34 @@ -import unittest +import pytest import helper - -class TestCheckOutput(unittest.TestCase): - - def setUp(self): - self.data = { - "DC_PEC": '', - "DC_SCT": "Ex().has_output(r'[H|h]i,*\\s+there!')", - "DC_SOLUTION": '' - } - - def test_success(self): - self.data["DC_CODE"] = 'print("Hi, there!")' - sct_payload = helper.run(self.data) - self.assertTrue(sct_payload['correct']) - - def test_success2(self): - self.data["DC_CODE"] = 'print("hi there!")' - sct_payload = helper.run(self.data) - self.assertTrue(sct_payload['correct']) - - def test_fail(self): - self.data["DC_CODE"] = 'print("Hello there")' - sct_payload = helper.run(self.data) - self.assertFalse(sct_payload['correct']) - - -class TestOutputContains(unittest.TestCase): - - def setUp(self): - self.data = { - "DC_PEC": '', - "DC_SCT": "test_output_contains(r'[H|h]i,*\\s+there!')", - "DC_SOLUTION": '' - } - - def test_success(self): - self.data["DC_CODE"] = 'print("Hi, there!")' - sct_payload = helper.run(self.data) - self.assertTrue(sct_payload['correct']) - - def test_success2(self): - self.data["DC_CODE"] = 'print("hi there!")' - sct_payload = helper.run(self.data) - self.assertTrue(sct_payload['correct']) - - def test_fail(self): - self.data["DC_CODE"] = 'print("Hello there")' - sct_payload = helper.run(self.data) - self.assertFalse(sct_payload['correct']) - -if __name__ == "__main__": - unittest.main() \ No newline at end of file +from pythonwhat.local import setup_state + +@pytest.mark.parametrize('stu, passes', [ + ('print("Hi, there!")', True), + ('print("hi there!")', True), + ('print("Hello there")', False) +]) +def test_has_output_basic(stu, passes): + s = setup_state(stu, '') + with helper.verify_sct(passes): + s.has_output(r'[H|h]i,*\s+there!') + +@pytest.mark.parametrize('stu, passes', [ + ('print("Hi, there!")', True), + ('print("hi there!")', False), + ('print("Hello there")', False) +]) +def test_has_output_pattern(stu, passes): + s = setup_state(stu, '') + with helper.verify_sct(passes): + s.has_output("Hi, there!", pattern=False) + + +@pytest.mark.parametrize('stu, passes', [ + ('print("Hi, there!")', True), + ('print("hi there!")', True), + ('print("Hello there")', False) +]) +def test_test_output_contains(stu, passes): + s = setup_state(stu, '') + with helper.verify_sct(passes): + s.test_output_contains(r'[H|h]i,*\s+there!') diff --git a/tests/test_messaging.py b/tests/test_messaging.py index fae6b36f..214b3be2 100644 --- a/tests/test_messaging.py +++ b/tests/test_messaging.py @@ -437,4 +437,31 @@ def test_has_expr(sct, patt): 'DC_SCT': sct }) assert not output['correct'] - assert message(output, patt) \ No newline at end of file + assert message(output, patt) + + +## check_if_else -------------------------------------------------------------- + +@pytest.mark.parametrize('stu, patt, lines', [ + ('', "The system wants to check the first if statement but hasn't found it.", []), + ('if offset > 10: x = 5\nelse: x = round(2.123)', 'Check the first if statement. Did you correctly specify the condition? Expected True, but got False.', [1, 1, 4, 14]), + ('if offset > 8: x = 7\nelse: x = round(2.123)', 'Check the first if statement. Did you correctly specify the body? Could not find the correct pattern in your code.', [1, 1, 16, 20]), + ('if offset > 8: x = 5\nelse: x = 8', 'Check the first if statement. Did you correctly specify the else part? Did you call round()?', [2, 2, 7, 11]), + ('if offset > 8: x = 5\nelse: x = round(2.2121314)', 'Check your call of round(). Did you correctly specify the first argument? Expected 2.123, but got 2.2121314.', [2, 2, 17, 25]), +]) +def test_check_if_else_basic(stu, patt, lines): + output = helper.run({ + 'DC_PEC': 'offset = 8', + 'DC_SOLUTION': 'if offset > 8: x = 5\nelse: x = round(2.123)', + 'DC_CODE': stu, + 'DC_SCT': ''' +Ex().check_if_else().multi( + check_test().multi([ set_env(offset = i).has_equal_value() for i in range(7,10) ]), + check_body().has_code(r'x\s*=\s*5'), + check_orelse().check_function('round').check_args(0).has_equal_value() +) + ''' + }) + assert not output['correct'] + assert message(output, patt) + if lines: helper.with_line_info(output, *lines) diff --git a/tests/test_set_context.py b/tests/test_set_context.py index 772edd36..3fe509b9 100644 --- a/tests/test_set_context.py +++ b/tests/test_set_context.py @@ -66,4 +66,3 @@ def test_fail(): } output = helper.run(data) assert not output['correct'] - diff --git a/tests/test_set_env.py b/tests/test_set_env.py index 09219cbe..ea958663 100644 --- a/tests/test_set_env.py +++ b/tests/test_set_env.py @@ -1,35 +1,36 @@ -import unittest import helper +import pytest +from pythonwhat.local import setup_state +from pythonwhat.check_syntax import v2_check_functions -class TestSetEnv(unittest.TestCase): +set_env = v2_check_functions['set_env'] - def test_Pass1(self): - sct_payload = helper.run({ "DC_SCT": "Ex().set_env(x=4).has_equal_value(name='x')"}) - self.assertTrue(sct_payload['correct']) +@pytest.fixture +def state(): + return setup_state() - def test_Pass2(self): - sct_payload = helper.run({ "DC_SCT": "Ex().set_env(x=4).set_env(y = 5).has_equal_value(name='x').has_equal_value(name='y')"}) - self.assertTrue(sct_payload['correct']) +def test_set_env_basic(state): + state.set_env(x=4).has_equal_value(name='x') - def test_Pass3(self): - sct_payload = helper.run({ "DC_SCT": "Ex().set_env(x=4, y = 5).has_equal_value(name='x').has_equal_value(name='y')"}) - self.assertTrue(sct_payload['correct']) +def test_set_env_once(state): + state.set_env(x=4, y = 5).has_equal_value(name='x').has_equal_value(name='y') + +def test_set_env_twice(state): + state.set_env(x=4).set_env(y = 5).has_equal_value(name='x').has_equal_value(name='y') - def test_Fail(self): - # envs are not preserved in other branches of State - sct_payload = helper.run({ "DC_SCT": "Ex().multi(set_env(x=4), set_env(y = 5).has_equal_value(name='x'))"}) - self.assertFalse(sct_payload['correct']) +def test_set_env_fail(state): + with helper.verify_sct(False): + state.multi( + set_env(x=4), + set_env(y = 5).has_equal_value(name='x') + ) - def testExample(self): - data = { - "DC_PEC": "a_list = list(range(100))", - "DC_SOLUTION": "print(a_list[1])", - "DC_SCT": "Ex().set_env(a_list = list(range(10))).has_equal_output()" - } - data["DC_CODE"] = "print(a_list[1])" - sct_payload = helper.run(data) - self.assertTrue(sct_payload['correct']) - data["DC_CODE"] = "print(a_list[2])" - sct_payload = helper.run(data) - self.assertFalse(sct_payload['correct']) +@pytest.mark.parametrize('stu, passes', [ + ("print(a_list[1])", True), + ("print(a_list[2])", False) +]) +def test_set_env_full_example(stu, passes): + s = setup_state(stu, "print(a_list[1])", pec="a_list = [0, 1, 2]") + with helper.verify_sct(passes): + s.set_env(a_list = list(range(10))).has_equal_output() \ No newline at end of file diff --git a/tests/test_signatures.py b/tests/test_signatures.py index 8cfee1df..82959c74 100644 --- a/tests/test_signatures.py +++ b/tests/test_signatures.py @@ -1,353 +1,137 @@ -import unittest -import helper - -class TestBuiltInSignatures(unittest.TestCase): - - # https://docs.python.org/3.x/library/functions.html - # - # Builtins that haven't been implemented/tested yet - # filter(), format(), frozenset(), iter(), map(), max(), min(), - # memoryview(), next(), property(), range(), slice(), super(), zip() - - def test_abs(self): - helper.test_builtin(self, "abs", params="'x'", arguments="1") - - def test_all(self): - helper.test_builtin(self, "all", params="'iterable'", arguments="[True, True]") - - def test_any(self): - helper.test_builtin(self, "any", params="'iterable'", arguments="[True, False]") - - def test_ascii(self): - helper.test_builtin(self, "ascii", params="'obj'", arguments="'test'") - - def test_bin(self): - helper.test_builtin(self, "bin", params="'number'", arguments="123456") - - def test_bool(self): - helper.test_builtin(self, "bool", params="'x'", arguments="1") - - def test_callable(self): - helper.test_builtin(self, "callable", params="'obj'", arguments="round") - - def test_chr(self): - helper.test_builtin(self, "chr", params="'i'", arguments="123") - - def test_classmethod(self): - helper.test_builtin(self, "classmethod", params="'function'", arguments="str") - - def test_complex(self): - helper.test_builtin(self, "complex", params="'real','imag'", arguments="1,2") - - def test_delattr(self): - self.data = { - "DC_PEC": ''' -class Test(): - def __init__(self, a): - self.a = a - def set_a(self, value): - self.a = value -x = Test(123) - ''', - "DC_SOLUTION": "x = delattr(x,'a')", - "DC_CODE": "x = delattr(x,'a')", - "DC_SCT": "test_function_v2('delattr', params=['obj','name'], do_eval=False)"} - sct_payload = helper.run(self.data) - self.assertTrue(sct_payload['correct']) - - def test_dir(self): - helper.test_builtin(self, "dir", params="'object'", arguments="[1,2,3]") - - def test_divmod(self): - helper.test_builtin(self, "divmod", params="'x','y'", arguments="7,3") - - def test_enumerate(self): - helper.test_builtin(self, "enumerate", params="'iterable','start'", arguments="[1,2,3],1") - - def test_float(self): - helper.test_builtin(self, "float", params="'x'", arguments="123") - - def test_getattr(self): - self.data = { - "DC_PEC": ''' -class Test(): - def __init__(self, a): - self.a = a - def set_a(self, value): - self.a = value -x = Test(123) - ''', - "DC_SOLUTION": "x = getattr(x,'a')", - "DC_CODE": "x = getattr(x,'a')", - "DC_SCT": "test_function_v2('getattr', params=['object','name'], do_eval=False)"} - sct_payload = helper.run(self.data) - self.assertTrue(sct_payload['correct']) - - def test_hasattr(self): - self.data = { - "DC_PEC": ''' -class Test(): - def __init__(self, a): - self.a = a - def set_a(self, value): - self.a = value -x = Test(123) - ''', - "DC_SOLUTION": "x = hasattr(x,'a')", - "DC_CODE": "x = hasattr(x,'a')", - "DC_SCT": "test_function_v2('hasattr', params=['obj','name'], do_eval=False)"} - sct_payload = helper.run(self.data) - self.assertTrue(sct_payload['correct']) - - def test_hash(self): - helper.test_builtin(self, "hash", params="'obj'", arguments="123") - - def test_hex(self): - helper.test_builtin(self, "hex", params="'number'", arguments="123") - - def test_id(self): - helper.test_builtin(self, "id", params="'obj'", arguments="123") - - def test_int(self): - helper.test_builtin(self, "int", params="'x','base'", arguments="'1001',2") - - def test_isinstance(self): - helper.test_builtin(self, "isinstance", params="'obj','class_or_tuple'", arguments="[1,2,3],list") - - def test_issubclass(self): - helper.test_builtin(self, "issubclass", params="'cls','class_or_tuple'", arguments="list,str") - - def test_len(self): - helper.test_builtin(self, "len", params="'obj'", arguments="[1,2,3]") - - def test_list(self): - helper.test_builtin(self, "list", params="", arguments="") - helper.test_builtin(self, "list", params="'iterable'", arguments="[1,2,3,4]") - - def test_oct(self): - helper.test_builtin(self, "oct", params="'number'", arguments="12345") - - def test_ord(self): - helper.test_builtin(self, "ord", params="'c'", arguments="'a'") - - def test_pow(self): - helper.test_builtin(self, "pow", params="'x','y'", arguments="3,3") - helper.test_builtin(self, "pow", params="'x','y','z'", arguments="3,3,5") - - def test_print(self): - helper.test_builtin(self, "print", params="'value'", arguments="123") - - def test_repr(self): - helper.test_builtin(self, "repr", params="'obj'", arguments="[1,2,3]") - - def test_reversed(self): - helper.test_builtin(self, "reversed", params = "'sequence'", arguments="[1,2,3]") - - def test_round(self): - helper.test_builtin(self, "round", params = "'number','ndigits'", arguments="2.123123, 2") - - def test_set(self): - helper.test_builtin(self, "set", params="", arguments="") - helper.test_builtin(self, "set", params="'iterable'", arguments="[1,2,3,4]") - - def test_setattr(self): - self.data = { - "DC_PEC": ''' +from pythonwhat.local import setup_state +import pytest + +# https://docs.python.org/3.x/library/functions.html +# +# Builtins that haven't been implemented/tested yet +# filter(), format(), frozenset(), iter(), map(), max(), min(), +# memoryview(), next(), property(), range(), slice(), super(), zip() + +@pytest.mark.parametrize('name, params, arguments', [ + ("abs", ["x"], "1"), + ("all", ["iterable"], "[True, True]"), + ("any", ["iterable"], "[True, False]"), + ("ascii", ["obj"], "'test'"), + ("bin", ["number"], "123456"), + ("bool", ["x"], "1"), + ("callable", ["obj"], "round"), + ("chr", ["i"], "123"), + ("classmethod", ["function"], "str"), + ("complex", ["real","imag"], "1,2"), + ("dir", ["object"], "[1,2,3]"), + ("divmod", ["x", "y"], "7,3"), + ("enumerate", ["iterable", "start"], "[1,2,3],1"), + ("float", ["x"], "123"), + ("hash", ["obj"], "123"), + ("hex", ["number"], "123"), + ("id", ["obj"], "123"), + ("int", ["x", "base"], "'1001',2"), + ("isinstance", ["obj", "class_or_tuple"], "[1,2,3],list"), + ("issubclass", ["cls", "class_or_tuple"], "list,str"), + ("len", ["obj"], "[1,2,3]"), + ("list", [], ""), + ("list", ["iterable"], "[1,2,3,4]"), + ("oct", ["number"], "12345"), + ("ord", ["c"], "'a'"), + ("pow", ["x", "y"], "3,3"), + ("pow", ["x", "y", "z"], "3,3,5"), + ("print", ["value"], "123"), + ("repr", ["obj"], "[1,2,3]"), + ("reversed", ["sequence"], "[1,2,3]"), + ("round", ["number", "ndigits"], "2.123123, 2"), + ("set", [], ""), + ("set", ["iterable"], "[1,2,3,4]"), + ("dir", ["object"], "[1,2,3]"), + ("divmod", ["x", "y"], "7,3"), + ("enumerate", ["iterable", "start"], "[1,2,3],1"), + ("float", ["x"], "123"), + ("sorted", ["iterable"], "[4,3,2,1]"), + ("str", ["object"], "123"), + ("sum", ["iterable", "start"], "[4,3,2,1],3"), + ("tuple", [], ""), + ("tuple", ["iterable"], "[1,2,3,4]"), + ("type", ["object"], "[1,2,3,4]"), +]) +def test_builtins(name, params, arguments): + code = "%s(%s)" % (name, arguments) + s = setup_state(code, code) + fun_state = s.check_function(name) + for param in params: + fun_state.check_args(param).has_equal_value() + +@pytest.mark.debug +@pytest.mark.parametrize('name, values, arguments', [ + ('delattr', "'a'", ['obj', 'name']), + ('getattr', "'a'", ['object','name']), + ('hasattr', "'a'", ['obj','name']), + ('setattr', "'a', 4", ['obj','name','value']) +]) +def test_attrs(name, values, arguments): + pec = ''' class Test(): def __init__(self, a): self.a = a def set_a(self, value): self.a = value x = Test(123) - ''', - "DC_SOLUTION": "setattr(x,'a',4)", - "DC_CODE": "setattr(x,'a',4)", - "DC_SCT": "test_function_v2('setattr', params=['obj','name','value'], do_eval=False)"} - sct_payload = helper.run(self.data) - self.assertTrue(sct_payload['correct']) - - def test_sorted(self): - helper.test_builtin(self, "sorted", params = "'iterable'", arguments="[4,3,2,1]") - - def test_str(self): - helper.test_builtin(self, "str", params = "'object'", arguments="123") - - def test_sum(self): - helper.test_builtin(self, "sum", params = "'iterable','start'", arguments="[4,3,2,1],3") - - def test_tuple(self): - helper.test_builtin(self, "tuple", params="", arguments="") - helper.test_builtin(self, "tuple", params="'iterable'", arguments="[1,2,3,4]") - - def test_type(self): - helper.test_builtin(self, "type", params = "'object'", arguments="[1,2,3,4]") - - def test_vars(self): - self.data = { - "DC_PEC": ''' + ''' + code = '%s(x, %s)' % (name, values) + s = setup_state(code, code, pec=pec) + fun_state = s.check_function(name) + for arg in arguments: + fun_state.check_args(arg).has_equal_ast() + +@pytest.mark.parametrize('name, values, arguments', [ + ('numpy.array', '[1, 2, 3, 4]', ['object']), + ('numpy.random.seed', '123', ['seed']), + ('numpy.random.rand', '3,3,3', ['d0', 'd1', 'd2']), + ('numpy.random.randint', '0, 5, size=(2,2)', ['low', 'high', 'size']), + ('numpy.random.choice', 'a=5, size=3, replace=False, p=[0.1, 0, 0.3, 0.6, 0]', ['a', 'size', 'replace', 'p']), + ('numpy.random.poisson', 'lam=(100., 500.), size=(100, 2)', ['lam', 'size']), + ('numpy.random.normal', 'loc = 0, scale=1.0, size=100', ['loc', 'scale', 'size']), + ('numpy.random.binomial', 'n=10, p=0.5, size=100', ['n', 'p', 'size']), + ('numpy.random.shuffle', 'numpy.arange(10)', ['x']), + ('numpy.random.permutation', 'numpy.arange(10)', ['x']), +]) +def test_numpy_builtins(name, values, arguments): + code = "%s(%s)" % (name, values) + s = setup_state(code, code, pec = 'import numpy') + fun_state = s.check_function(name) + for arg in arguments: + fun_state.check_args(arg).has_equal_value() + +def test_math_builtins(): + code = 'm.radians(100)' + s = setup_state(code, code, pec = 'import math as m') + s.check_function('math.radians').check_args('x').has_equal_value() + +@pytest.mark.parametrize('fun, argument', [ + ('append', 'object'), + ('count', 'value') +]) +def test_list_methods(fun, argument): + code = "x.%s(2)" % fun + s = setup_state(code, code, "x = [1, 2, 3]") + s.check_function('x.%s' % fun).check_args(argument).has_equal_value() + +# One-offs -------------------------------------------------------------------- + +def test_vars(): + pec = ''' class Test(): def __init__(self, a): self.a = a def set_a(self, value): self.a = value x = Test(123) - ''', - "DC_SOLUTION": "vars(x)", - "DC_CODE": "vars(x)", - "DC_SCT": "test_function_v2('vars', params=['object'], do_eval=False)"} - sct_payload = helper.run(self.data) - self.assertTrue(sct_payload['correct']) - -class TestBuiltInMethodsInt(unittest.TestCase): - pass - -class TestBuiltInMethodsStr(unittest.TestCase): - def test_center(self): - self.data = { - "DC_PEC": "x = 'test'", - "DC_SOLUTION": "x.center(10, 's')", - "DC_CODE": "x.center(10, 's')", - "DC_SCT": "test_function_v2('x.center', params=['width','fillchar'])"} - sct_payload = helper.run(self.data) - self.assertTrue(sct_payload['correct']) - -class TestBuiltInMethodsList(unittest.TestCase): - def test_append(self): - self.data = { - "DC_PEC": "x = [1,2,3,4]", - "DC_SOLUTION": "x.append(2)", - "DC_CODE": "x.append(2)", - "DC_SCT": "test_function_v2('x.append', params=['object'])"} - sct_payload = helper.run(self.data) - self.assertTrue(sct_payload['correct']) - - def test_count(self): - self.data = { - "DC_PEC": "x = [1,2,3,4]", - "DC_SOLUTION": "x.count(2)", - "DC_CODE": "x.count(2)", - "DC_SCT": "test_function_v2('x.count', params=['value'])"} - sct_payload = helper.run(self.data) - self.assertTrue(sct_payload['correct']) - -class TestBuiltInMethodsDict(unittest.TestCase): - pass - -class TestBuiltInMethodsNumpy(unittest.TestCase): - def test_array(self): - self.data = { - "DC_PEC": "import numpy as np", - "DC_SOLUTION": "x = np.array([1,2,3,4])", - "DC_CODE": "x = np.array([1,2,3,4])", - "DC_SCT": "test_function_v2('numpy.array', params=['object'])"} - sct_payload = helper.run(self.data) - self.assertTrue(sct_payload['correct']) - - def test_array_spec2(self): - self.data = { - "DC_PEC": "import numpy as np", - "DC_SOLUTION": "x = np.array([1,2,3,4])", - "DC_CODE": "x = np.array([1,2,3,4])", - "DC_SCT": "Ex().check_function('numpy.array', 0).check_args('object')"} - sct_payload = helper.run(self.data) - self.assertTrue(sct_payload['correct']) - - def test_random_seed(self): - self.data = { - "DC_PEC": "import numpy as np", - "DC_SOLUTION": "np.random.seed(123)", - "DC_CODE": "np.random.seed(123)", - "DC_SCT": "test_function_v2('numpy.random.seed', params=['seed'])"} - sct_payload = helper.run(self.data) - self.assertTrue(sct_payload['correct']) - - def test_random_rand(self): - self.data = { - "DC_PEC": "import numpy as np", - "DC_SOLUTION": "np.random.rand(3,3,3)", - "DC_CODE": "np.random.rand(3,3,3)", - "DC_SCT": "test_function_v2('numpy.random.rand', params=['d0', 'd1', 'd2'])"} - sct_payload = helper.run(self.data) - self.assertTrue(sct_payload['correct']) - - def test_random_randint(self): - self.data = { - "DC_PEC": "import numpy as np", - "DC_SOLUTION": "np.random.randint(0, 5, size = (2,2))", - "DC_CODE": "np.random.randint(0, 5, size = (2,2))", - "DC_SCT": "test_function_v2('numpy.random.randint', params=['low', 'high', 'size'])"} - sct_payload = helper.run(self.data) - self.assertTrue(sct_payload['correct']) - - def test_random_choice(self): - code = 'np.random.choice(a=5, size=3, replace=False, p=[0.1, 0, 0.3, 0.6, 0])' - output = helper.run({ - "DC_PEC": "import numpy as np", - "DC_SOLUTION": code, - "DC_CODE": code, - "DC_SCT": "Ex().check_function('numpy.random.choice').multi([check_args(x).has_equal_value() for x in ['a', 'size', 'replace', 'p']])" - }) - self.assertTrue(output['correct']) - - def test_random_poisson(self): - code = 'np.random.poisson(lam=(100., 500.), size=(100, 2))' - output = helper.run({ - "DC_PEC": "import numpy as np", - "DC_SOLUTION": code, - "DC_CODE": code, - "DC_SCT": "Ex().check_function('numpy.random.poisson').multi([check_args(x).has_equal_value() for x in ['lam', 'size']])" - }) - self.assertTrue(output['correct']) - - def test_random_normal(self): - code = 'np.random.normal(loc = 0, scale=1.0, size=100)' - output = helper.run({ - "DC_PEC": "import numpy as np", - "DC_SOLUTION": code, - "DC_CODE": code, - "DC_SCT": "Ex().check_function('numpy.random.normal').multi([check_args(x).has_equal_value() for x in ['loc', 'scale', 'size']])" - }) - self.assertTrue(output['correct']) - - def test_random_binomial(self): - code = 'np.random.binomial(n=10, p=0.5, size=100)' - output = helper.run({ - "DC_PEC": "import numpy as np", - "DC_SOLUTION": code, - "DC_CODE": code, - "DC_SCT": "Ex().check_function('numpy.random.binomial').multi([check_args(x).has_equal_value() for x in ['n', 'p', 'size']])" - }) - self.assertTrue(output['correct']) - - def test_random_shuffle(self): - code = 'np.random.shuffle(np.arange(10))' - output = helper.run({ - "DC_PEC": "import numpy as np", - "DC_SOLUTION": code, - "DC_CODE": code, - "DC_SCT": "Ex().check_function('numpy.random.shuffle').check_args('x').has_equal_value()" - }) - self.assertTrue(output['correct']) - - def test_random_permutation(self): - code = 'np.random.permutation(np.arange(10))' - output = helper.run({ - "DC_PEC": "import numpy as np", - "DC_SOLUTION": code, - "DC_CODE": code, - "DC_SCT": "Ex().check_function('numpy.random.permutation').check_args('x').has_equal_value()" - }) - self.assertTrue(output['correct']) - -class TestBuiltInMethodsOthers(unittest.TestCase): - def test_radians(self): - self.data = { - "DC_PEC": "import math as m", - "DC_SOLUTION": "x = m.radians(100)", - "DC_CODE": "x = m.radians(100)", - "DC_SCT": "test_function_v2('math.radians', params=['x'])"} - sct_payload = helper.run(self.data) - self.assertTrue(sct_payload['correct']) - -if __name__ == "__main__": - unittest.main() + ''' + code = 'vars(x)' + s = setup_state(code, code, pec=pec) + s.check_function('vars').check_args('object').has_equal_ast() + +def test_center(): + code = "x.center(10, 's')" + s = setup_state(code, code, "x = 'test'") + fun_state = s.check_function('x.center') + fun_state.check_args('width').has_equal_value() + fun_state.check_args('fillchar').has_equal_value() diff --git a/tests/test_spec.py b/tests/test_spec.py index 5c3743e0..666cecd9 100644 --- a/tests/test_spec.py +++ b/tests/test_spec.py @@ -1,150 +1,97 @@ -import unittest import helper import pytest from pythonwhat.Feedback import InstructorError -class TestFChain(unittest.TestCase): - def setUp(self): - self.data = { - "DC_PEC": '', - "DC_SOLUTION": ''' -[[ii+1 for ii in range(aa)] for aa in range(2)] - -[[ii*2 for ii in range(bb)] for bb in range(1,3)] -''' - } - self.data["DC_CODE"] = self.data["DC_SOLUTION"] - - def test_F(self): - self.data["DC_SCT"] = ''' +@pytest.mark.parametrize('sct', [ + ''' list_comp = F().check_list_comp(0).check_body().set_context(ii=2).has_equal_value('unequal') Ex().check_list_comp(0).check_body().set_context(aa=2).multi(list_comp) -''' - sct_payload = helper.run(self.data) - self.assertTrue(sct_payload['correct']) - - def test_check_to_F(self): - self.data["DC_SCT"] = ''' +''', ''' list_comp = check_list_comp(0).check_body().set_context(ii=2).has_equal_value('unequal') Ex().check_list_comp(0).check_body().set_context(aa=2).multi(list_comp) -''' - - def test_check_to_F_nested(self): - self.data["DC_SCT"] = ''' +''', ''' # funky, but we're testing nested check functions! multi_test = multi(check_list_comp(0).check_body().set_context(aa=2).has_equal_value('badbody')) Ex().multi(multi_test) -''' - sct_payload = helper.run(self.data) - self.assertTrue(sct_payload['correct']) - - def test_F_reused(self): - self.data["DC_SCT"] = ''' +''', ''' list_comp = F().check_list_comp(0).check_body().set_context(ii=2).has_equal_value('unequal') - Ex().check_list_comp(0).check_body().set_context(aa=2).multi(list_comp) Ex().check_list_comp(1).check_body().set_context(bb=4).multi(list_comp) -''' - sct_payload = helper.run(self.data) - self.assertTrue(sct_payload['correct']) - - def test_F_assign_getattr(self): - self.data["DC_SCT"] = ''' -eq_test = F().check_list_comp(0).check_body().has_equal_value - +''', ''' +eq_test = F().check_list_comp(0).check_body().set_context(1).has_equal_value Ex().multi(eq_test('unequal')) ''' - -class TestSpecInterop(unittest.TestCase): - - def setUp(self): - self.data = { - "DC_PEC": '', - "DC_SOLUTION": '''[aa+1 for aa in range(2)]''' - } - self.data["DC_CODE"] = self.data["DC_SOLUTION"] - self.FAIL_CODE = '''[aa for aa in range(2)]''' - - def test_spec1_in_multi_pass(self): - self.data["DC_SCT"] = ''' +]) +def test_f_chain(sct): + code = '[[ii+1 for ii in range(aa)] for aa in range(2)]\n[[ii*2 for ii in range(bb)] for bb in range(1,3)]' + res = helper.run({ + 'DC_SOLUTION': code, + 'DC_CODE': code, + 'DC_SCT': sct, + }) + assert res['correct'] + +@pytest.mark.parametrize('stu, sct, passes, patt', [ + (None, ''' te = test_expression_result(extra_env={'aa':2}, incorrect_msg='unequal') Ex().multi(test_list_comp(body=te)) # spec 1 inside multi Ex().check_list_comp(0).check_body().multi(te) # half of each spec Ex().check_list_comp(0).check_body().set_context(aa=2).has_equal_value('unequal') # full spec 2 test_list_comp(body=te) # full spec 1 -''' - sct_payload = helper.run(self.data) - self.assertTrue(sct_payload['correct']) - - def test_spec1_in_multi_fail(self): - # TODO: this test fails because spec1 tests are run after spec2 tests, - # even if they come first in the SCT script, due to building the tree - # for spec1 tests but not spec2 (which are run immediately) - self.data["DC_CODE"] = '''for aa in range(3): aa''' - self.data["DC_SCT"] = ''' +''', True, None), + # TODO: this test fails because spec1 tests are run after spec2 tests, + # even if they come first in the SCT script, due to building the tree + # for spec1 tests but not spec2 (which are run immediately) + ('for aa in range(3): aa', ''' test_list_comp(body=test_expression_result(expr_code = 'aa', incorrect_msg='unequal')) Ex().check_list_comp(0).check_body().multi(test_expression_result(incorrect_msg='unequal')) Ex().check_list_comp(0).check_body().has_equal_value('unequal') - -''' - sct_payload = helper.run(self.data) - self.assertFalse(sct_payload['correct']) - - def test_spec_run_order(self): - self.data["DC_CODE"] = '''[aa for aa in range(2)]''' - self.data["DC_SCT"] = ''' + ''', False, None), + ('[aa for aa in range(2)]', ''' Ex().test_list_comp(body=test_expression_result(extra_env={'aa': 2}, incorrect_msg = 'spec1')) Ex().check_list_comp(0).check_body().set_context(aa=2).has_equal_value('spec2') -''' - sct_payload = helper.run(self.data) - self.assertFalse(sct_payload['correct']) - self.assertIn('spec1', sct_payload['message']) - -class TestMulti(unittest.TestCase): - def setUp(self): - self.data = { - "DC_PEC": '', - "DC_SOLUTION": '''[aa+1 for aa in range(2)]''' - } - self.data["DC_CODE"] = self.data["DC_SOLUTION"] - self.FAIL_CODE = '''[aa for aa in range(2)]''' - - def test_nested_multi(self): - self.data["DC_SCT"] = ''' + ''', False, 'spec1'), +]) +def test_spec_interoperability(stu, sct, passes, patt): + code = '[aa+1 for aa in range(2)]' + res = helper.run({ + "DC_SOLUTION": code, + "DC_CODE": stu or code, + "DC_SCT": sct + }) + assert res['correct'] == passes + if patt: assert patt in res['message'] + +@pytest.mark.parametrize('sct', [ + ''' test_body = F().check_body().set_context(aa=2).has_equal_value('wrong') Ex().check_list_comp(0).multi(F().multi(test_body)) -''' - sct_payload = helper.run(self.data) - self.assertTrue(sct_payload['correct']) - - def test_multi_splits_node_and_check(self): - self.data["DC_SCT"] = ''' + ''', ''' test_body = F().check_list_comp(0).check_body().set_context(aa=2).has_equal_value('wrong') Ex().check_list_comp(0).multi(F().check_body().set_context(aa=2).has_equal_value('wrong')) -''' - sct_payload = helper.run(self.data) - self.assertTrue(sct_payload['correct']) - - def test_multi_generator(self): - self.data["DC_SCT"] = """ + ''', ''' Ex().check_list_comp(0).check_body()\ .multi(set_context(aa=i).has_equal_value('wrong') for i in range(2)) -""" - sct_payload = helper.run(self.data) - self.assertTrue(sct_payload['correct']) - -class TestTestFail(unittest.TestCase): - def setUp(self): - self.data = { - "DC_SOLUTION": "", "DC_CODE": "" - } - - def test_fail(self): - self.data["DC_SCT"] = """Ex().fail()""" - sct_payload = helper.run(self.data) - self.assertFalse(sct_payload['correct']) - + ''' +]) +def test_multi(sct): + code = '[aa+1 for aa in range(2)]' + res = helper.run({ + "DC_SOLUTION": code, + "DC_CODE": code, + "DC_SCT": sct + }) + assert res['correct'] + +def test_fail(): + res = helper.run({ + "DC_SOLUTION": "", + "DC_SCT": "", + "DC_SCT": "Ex().fail()" + }) + assert not res['correct'] + @pytest.fixture def data(): return { @@ -208,7 +155,9 @@ def test_has_equal_ast_part_of_method_fail(data): data["DC_SCT"] = """Ex().has_equal_ast(code = 'dict(a = "a")', exact=False, incorrect_msg = 'icr')""" failing_submission(data) -class TestOverride(unittest.TestCase): +# Test overriding fucntionality ----------------------------------------------- + +class OverrideTester(): """ This class is used to test overriding w/ correct and incorrect code. Tests are run for entire nodes (e.g. an if block) and their parts (e.g. body of if block) @@ -231,56 +180,53 @@ def do_exercise(self, code, base_check, parts, override=None, part_name = None, "DC_SCT": sct } sct_payload = helper.run(data) - self.assertTrue(sct_payload['correct']) if passes else self.assertFalse(sct_payload['correct']) - - # used to generate tests - EXPRESSIONS = { - 'if_exp': "{body} if {test} else {orelse}", - 'list_comp': "[{body} for i in {iter}]", - 'dict_comp': "{{ {key}: {value} for i in {iter} }}", - 'for_loop': "for i in {iter}: {body}", - 'while': "while {test}: {body}", - 'try_except': "try: {body}\nexcept: pass\nelse: {orelse}", - 'lambda_function': "lambda a={args}: {body}", - 'function_def': ["'sum'", "def sum(a={args}): {body}"], - 'function': ["'sum', 0", "sum({args})"] - } + assert sct_payload['correct'] == passes PARTS = {'body': "1", "test": "False", 'orelse': "2", 'iter': "range(3)", 'key': "3", 'value': "4", 'args': "(1,2,3)"} import re def gen_exercise(*args, **kwargs): - return lambda self: TestOverride.do_exercise(self, *args, **kwargs) - -for k, code in TestOverride.EXPRESSIONS.items(): + return lambda self: OverrideTester.do_exercise(self, *args, **kwargs) + +@pytest.mark.parametrize('k, code', [ + ('if_exp', "{body} if {test} else {orelse}"), + ('list_comp', "[{body} for i in {iter}]"), + ('dict_comp', "{{ {key}: {value} for i in {iter} }}"), + ('for_loop', "for i in {iter}: {body}"), + ('while', "while {test}: {body}"), + ('try_except', "try: {body}\nexcept: pass\nelse: {orelse}"), + ('lambda_function', "lambda a={args}: {body}"), + ('function_def', ["'sum'", "def sum(a={args}): {body}"]), + ('function', ["'sum', 0", "sum({args})"]), +]) +def test_override(k, code): # base SCT, w/ special indexing if function checks if isinstance(code, list): indx, code = code else: indx = '0' base_check = "Ex().check_{}({})".format(k, indx) # pass overall test ---- - pf = gen_exercise(code, base_check, TestOverride.PARTS) - setattr(TestOverride, 'test_{}_pass'.format(k), pf) + pf = gen_exercise(code, base_check, OverrideTester.PARTS) + setattr(OverrideTester, 'test_{}_pass'.format(k), pf) # fail overall test ---- - pf = gen_exercise(code, base_check, TestOverride.PARTS, override="'WRONG ANSWER'", passes=False) - setattr(TestOverride, 'test_{}_fail'.format(k), pf) + pf = gen_exercise(code, base_check, OverrideTester.PARTS, override="'WRONG ANSWER'", passes=False) + setattr(OverrideTester, 'test_{}_fail'.format(k), pf) # test individual pieces -------------------------------------------------- for part in re.findall("\{([^{]*?)\}", code): # find all str.format vars, e.g. {body} part_index = "" if part != 'args' else 0 # pass individual piece ---- test_name = 'test_{}_{}_pass'.format(k, part) - pf = gen_exercise(code, base_check, TestOverride.PARTS, part_name=part, part_index=part_index) - setattr(TestOverride, test_name, pf) + pf = gen_exercise(code, base_check, OverrideTester.PARTS, part_name=part, part_index=part_index) + setattr(OverrideTester, test_name, pf) # fail individual piece ---- test_name = 'test_{}_{}_fail'.format(k, part) - bad_code = code.format(**{part: "[]", **TestOverride.PARTS}) - pf = gen_exercise(code, base_check, TestOverride.PARTS, part_name=part, part_index=part_index, override=bad_code, passes=False) - setattr(TestOverride, test_name, pf) + bad_code = code.format(**{part: "[]", **OverrideTester.PARTS}) + pf = gen_exercise(code, base_check, OverrideTester.PARTS, part_name=part, part_index=part_index, override=bad_code, passes=False) + setattr(OverrideTester, test_name, pf) # Test SCT Ex syntax (copied from sqlwhat) ----------------------------------- -import pytest from pythonwhat.check_syntax import Ex, F, state_dec @pytest.fixture @@ -344,7 +290,3 @@ def test_ex_add_ex_err(ex): def test_f_add_ex_err(f, ex): with pytest.raises(BaseException): f >> ex - - -if __name__ == "__main__": - unittest.main() diff --git a/tests/test_test_compound_statements.py b/tests/test_test_compound_statements.py new file mode 100644 index 00000000..c67f7d4c --- /dev/null +++ b/tests/test_test_compound_statements.py @@ -0,0 +1,73 @@ +import pytest +import helper +from pythonwhat.local import setup_state +from pythonwhat.check_syntax import v2_check_functions +globals().update(v2_check_functions) + +# Check for loop -------------------------------------------------------------- + +@pytest.mark.parametrize('sct', [ + "test_for_loop(for_iter=lambda: test_expression_result(), body=lambda: test_expression_output())", + "Ex().test_for_loop(for_iter=lambda: test_expression_result(), body=lambda: test_expression_output())", + "Ex().check_for_loop().multi(check_iter().has_equal_value(), check_body().has_equal_output())" +]) +@pytest.mark.parametrize('stu, passes', [ + ('', False), + ('for i in range(4): pass', False), + ('for i in range(3): pass', False), + ('for i in range(3): print(i)', True), + ('for j in range(3): print(j)', True), +]) +def test_for_loop(sct, stu, passes): + res = helper.run({ + "DC_CODE": stu, + "DC_SOLUTION": 'for i in range(3): print(i)', + "DC_SCT": sct + }) + assert res['correct'] == passes + + +@pytest.mark.parametrize('stu, passes', [ + ('', False), + ('for i in range(3):\n pass', False), + ('for i in range(3):\n for j in range(3):\n pass', False), + ('for i in range(3):\n for j in range(4):\n pass', False), + ('for i in range(3):\n for j in range(4):\n print(i + j)', True), + ('for j in range(3):\n for i in range(4):\n print(i + j)', True) +]) +def test_for_loop_nested(stu, passes): + s = setup_state(stu, 'for i in range(3):\n for j in range(4):\n print(i + j)') + with helper.verify_sct(passes): + s.check_for_loop().multi( + check_iter().has_equal_value(), + check_body().set_context(2).check_for_loop().multi( + check_iter(), + check_body().set_context(3).has_equal_output() + ) + ) + +@pytest.mark.parametrize('stu, passes', [ + ('for i in range(1):\n pass', False), + ('for i in range(1):\n pass\nfor j in range(2): pass', False), + ('for i in range(3):\n pass\nfor j in range(4): pass', False), + ('for i in range(3):\n pass\nfor j in range(4): print(j)', True), + ('for i in range(3):\n pass\nfor i in range(4): print(i)', True) +]) +def test_two_for_loops(stu, passes): + s = setup_state(stu, 'for i in range(1):\n pass\nfor j in range(4): print(j)') + with helper.verify_sct(passes): + s.check_for_loop(index=1).multi( + check_iter().has_equal_value(), + check_body().set_context(2).has_equal_output() + ) + +@pytest.mark.parametrize('stu, exact, passes', [ + ('for i in range(2): pass', False, True), + ('for j in range(2): pass', False, True), + ('for i in range(2): pass', True, True), + ('for j in range(2): pass', True, False), +]) +def test_has_context(stu, exact, passes): + s = setup_state(stu, 'for i in range(2): pass') + with helper.verify_sct(passes): + s.check_for_loop().check_body().has_context(exact_names=exact) \ No newline at end of file diff --git a/tests/test_test_expression.py b/tests/test_test_expression.py deleted file mode 100644 index 6b39ac10..00000000 --- a/tests/test_test_expression.py +++ /dev/null @@ -1,136 +0,0 @@ -import unittest -import helper - -class TestExpressionOutputBasic(unittest.TestCase): - - def setUp(self): - self.data = { - "DC_PEC": "", - "DC_SOLUTION": "x = {'a': 1, 'b':2, 'c':3}", - "DC_SCT": "test_expression_output(expr_code = \"print(x['a'])\")" - } - - def test_fun_step1(self): - self.data["DC_CODE"] = "" - sct_payload = helper.run(self.data) - self.assertFalse(sct_payload['correct']) - - def test_fun_step2(self): - self.data["DC_CODE"] = "x = {'a': 2}" - sct_payload = helper.run(self.data) - self.assertFalse(sct_payload['correct']) - - def test_fun_step3(self): - self.data["DC_CODE"] = "x = {'a': 1}" - sct_payload = helper.run(self.data) - self.assertTrue(sct_payload['correct']) - -class TestExpressionOutputInsideFor(unittest.TestCase): - def setUp(self): - self.data = { - "DC_PEC": "", - "DC_SOLUTION": "for i in range(10):\n print(i)", - "DC_SCT": "test_for_loop(body = lambda: test_expression_output())" - } - - def test_fun_step1(self): - self.data["DC_CODE"] = "for i in range(10):\n print(i + 1)" - sct_payload = helper.run(self.data) - self.assertFalse(sct_payload['correct']) - - def test_fun_step2(self): - self.data["DC_CODE"] = "for i in range(10):\n print(i)" - sct_payload = helper.run(self.data) - self.assertTrue(sct_payload['correct']) - - def test_fun_step1_no_lam(self): - self.data["DC_SCT"] = helper.remove_lambdas(self.data["DC_SCT"]) - self.test_fun_step1() - - def test_fun_step2_no_lam(self): - self.data["DC_SCT"] = helper.remove_lambdas(self.data["DC_SCT"]) - self.test_fun_step2() - -class TestExpressionOutputInsideFor2(unittest.TestCase): - def setUp(self): - self.data = { - "DC_PEC": "", - "DC_SOLUTION": "for i in range(10):\n print(i)", - "DC_SCT": "test_for_loop(body = lambda: test_expression_output(context_vals = [1]))" - } - - def test_fun_step1(self): - self.data["DC_CODE"] = "for i in range(10):\n print(i + 1)" - sct_payload = helper.run(self.data) - self.assertFalse(sct_payload['correct']) - - def test_fun_step2(self): - self.data["DC_CODE"] = "for i in range(10):\n print(i)" - sct_payload = helper.run(self.data) - self.assertTrue(sct_payload['correct']) - - def test_fun_step1_no_lam(self): - self.data["DC_SCT"] = helper.remove_lambdas(self.data["DC_SCT"]) - self.test_fun_step1() - - def test_fun_step2_no_lam(self): - self.data["DC_SCT"] = helper.remove_lambdas(self.data["DC_SCT"]) - self.test_fun_step2() - -class TestExpressionOutputBasic(unittest.TestCase): - - def setUp(self): - self.data = { - "DC_PEC": "", - "DC_SOLUTION": "a = 2", - "DC_SCT": "test_expression_result(expr_code = 'a', error_msg = 'cough')" - } - - def test_fail_1(self): - self.data["DC_CODE"] = "" - sct_payload = helper.run(self.data) - self.assertFalse(sct_payload['correct']) - self.assertEqual(sct_payload['message'], 'cough') - - def test_pass(self): - self.data["DC_CODE"] = self.data["DC_SOLUTION"] - sct_payload = helper.run(self.data) - self.assertTrue(sct_payload['correct']) - - def test_no_copy_bad_sct_passes(self): - self.data["DC_SOLUTION"] = "a = [2]" - self.data["DC_CODE"] = "a = [1]" - self.data["DC_SCT"] = "Ex().has_equal_value(expr_code = 'a[0] = 3', name = 'a', copy = False).has_equal_value(expr_code = 'a', name = 'a')" - sct_payload = helper.run(self.data) - self.assertTrue(sct_payload['correct']) - - def test_copy_sct_fails(self): - self.data["DC_SOLUTION"] = "a = [2]" - self.data["DC_CODE"] = "a = [1]" - self.data["DC_SCT"] = "Ex().has_equal_value(expr_code = 'a[0] = 3', name = 'a', copy = True).has_equal_value(name = 'a')" - sct_payload = helper.run(self.data) - self.assertFalse(sct_payload['correct']) - - def test_test_expression_result_copy_pass(self): - self.data["DC_CODE"] = self.data["DC_SOLUTION"] - self.data["DC_SCT"] = "test_expression_result(expr_code = 'a', error_msg = 'cough', copy = False)" - sct_payload = helper.run(self.data) - self.assertTrue(sct_payload['correct']) - - def test_test_custom_equality_func(self): - self.data["DC_SOLUTION"] = "a = [1.01]" - self.data["DC_CODE"] = "a = [1.011]" - self.data["DC_SCT"] = "import numpy as np; Ex().check_object('a').has_equal_value(func = lambda x, y: np.allclose(x, y, atol = .001))" - sct_payload = helper.run(self.data) - self.assertTrue(sct_payload['correct']) - - def test_test_custom_equality_func_fail(self): - self.data["DC_SOLUTION"] = "a = [1.01]" - self.data["DC_CODE"] = "a = [1.011]" - self.data["DC_SCT"] = "import numpy as np; Ex().check_object('a').has_equal_value(func = lambda x, y: np.allclose(x, y, atol = .0001))" - sct_payload = helper.run(self.data) - self.assertFalse(sct_payload['correct']) - - -if __name__ == "__main__": - unittest.main() diff --git a/tests/test_test_function.py b/tests/test_test_function.py deleted file mode 100644 index d5623bac..00000000 --- a/tests/test_test_function.py +++ /dev/null @@ -1,463 +0,0 @@ -import unittest -import helper -import pytest - -class TestFunctionBase(unittest.TestCase): - - def setUp(self): - self.data = { - "DC_PEC": ''' -# pec comes here - ''', - "DC_CODE": ''' -print(5 / 8) -print(7 + 10) - ''', - "DC_SOLUTION": ''' -print(5 / 8) -print(7 + 10) -''' - } - - def test_Pass(self): - self.data["DC_SCT"] = ''' -msg = "Don't remove the first statement. It is an example which is coded for you!" -test_function("print", 1) -test_function("print", 2) - -success_msg("Great!") - ''' - sct_payload = helper.run(self.data) - self.assertTrue(sct_payload['correct']) - - def test_Pass_spec2(self): - self.data['DC_SCT'] = """ -Ex().check_function('print', 0).check_args(0).has_equal_ast() -""" - sct_payload = helper.run(self.data) - self.assertTrue(sct_payload['correct']) - -class TestBlacklisting(unittest.TestCase): - - def test_bookkeeping(self): - self.data = { - "DC_PEC": '', - "DC_SOLUTION": ''' -round(1.23456, ndigits = 1) - ''', - "DC_CODE": ''' -round(1.23456, ndigits = 1) - ''', - "DC_SCT": ''' -test_function('round', index = 1) # all in one -test_function('round', index = 1) # same call, should be fine. - ''' - } - sct_payload = helper.run(self.data) - self.assertTrue(sct_payload['correct']) - - def test_bookkeeping2(self): - self.data = { - "DC_PEC": '', - "DC_SOLUTION": ''' -round(1.23456, ndigits = 1) -round(1.65432, ndigits = 3) - ''', - "DC_CODE": ''' -round(1.23456, ndigits = 1) -round(1.65432, ndigits = 3) - ''', - "DC_SCT": ''' -test_function('round', index = 1) # all in one -test_function('round', args = [0], index = 2) # separate first -test_function('round', keywords = ['ndigits'], index = 2) # separate second -test_function('round', index = 2) # all-in-one - ''' - } - sct_payload = helper.run(self.data) - self.assertTrue(sct_payload['correct']) - - def test_bookkeeping3(self): - self.data = { - "DC_PEC": '', - "DC_SOLUTION": ''' -round(1.23456, ndigits = 1) -round(1.65432, ndigits = 3) - ''', - "DC_CODE": ''' -round(1.23456, ndigits = 1) -round(1.65432, ndigits = 4) - ''', - "DC_SCT": ''' -test_function('round', index = 1) # all in one -test_function('round', args = [0], index = 2) # separate first -test_function('round', keywords = ['ndigits'], index = 2) # separate second - ''' - } - sct_payload = helper.run(self.data) - self.assertFalse(sct_payload['correct']) - - -class TestMessaging(unittest.TestCase): - def setUp(self): - self.data = { - "DC_PEC": '', - "DC_CODE": ''' -import pandas as pd -x = pd.DataFrame({"a":[1, 2, 3]}) -print(x) -# no data_range call -# no type(y) call -z = pd.Series([1, 2, 3]) -len(z) -print(z) - ''', - "DC_SOLUTION": ''' -import pandas as pad -x = pad.DataFrame({"a":[1, 2, 3]}) # correct -print(x) # correct -y = pad.date_range('1/1/2000', periods=8) -type(y) -z = pad.Series([1, 2, 4]) # incorrect -len(z) # incorrect -print(z) # incorrect - ''' - } - - def test_auto(self): - self.data["DC_SCT"] = ''' -test_function("pandas.DataFrame") -test_function("print") - ''' - sct_payload = helper.run(self.data) - self.assertTrue(sct_payload['correct']) - - self.data["DC_SCT"] = 'test_function("pandas.date_range")' - sct_payload = helper.run(self.data) - self.assertFalse(sct_payload['correct']) - - self.data["DC_SCT"] = 'test_function("type")' - sct_payload = helper.run(self.data) - self.assertFalse(sct_payload['correct']) - - self.data["DC_SCT"] = 'test_function("pandas.Series")' - sct_payload = helper.run(self.data) - self.assertFalse(sct_payload['correct']) - - self.data["DC_SCT"] = 'test_function("len")' - sct_payload = helper.run(self.data) - self.assertFalse(sct_payload['correct']) - - self.data["DC_SCT"] = 'test_function("print", index = 1); test_function("print", index = 2)' - sct_payload = helper.run(self.data) - self.assertFalse(sct_payload['correct']) - - - def test_custom(self): - self.data["DC_SCT"] = ''' -test_function("pandas.DataFrame") -test_function("print") - ''' - sct_payload = helper.run(self.data) - self.assertTrue(sct_payload['correct']) - - self.data["DC_SCT"] = 'test_function("pandas.date_range", not_called_msg = "stupid")' - sct_payload = helper.run(self.data) - self.assertFalse(sct_payload['correct']) - self.assertEqual(sct_payload['message'], "stupid") - - self.data["DC_SCT"] = 'test_function("type", not_called_msg = "stupid")' - sct_payload = helper.run(self.data) - self.assertFalse(sct_payload['correct']) - self.assertEqual(sct_payload['message'], "stupid") - - self.data["DC_SCT"] = 'test_function("pandas.Series", incorrect_msg = "stupid")' - sct_payload = helper.run(self.data) - self.assertFalse(sct_payload['correct']) - self.assertIn("stupid", sct_payload['message']) - - self.data["DC_SCT"] = 'test_function("len", incorrect_msg = "stupid")' - sct_payload = helper.run(self.data) - self.assertFalse(sct_payload['correct']) - self.assertIn("stupid", sct_payload['message']) - -class TestLineNumbers(unittest.TestCase): - def test_line_numbers1(self): - self.data = {"DC_PEC": '', - "DC_SOLUTION": "round(1.23456, ndigits = 1)", - "DC_CODE": "round(1.34567, ndigits = 1)", - "DC_SCT": "test_function('round', index = 1)"} - sct_payload = helper.run(self.data) - self.assertFalse(sct_payload['correct']) - helper.test_lines(self, sct_payload, 1, 1, 7, 13) - - def test_line_numbers(self): - self.data = {"DC_PEC": '', - "DC_SOLUTION": "round(1.23456, ndigits = 1)", - "DC_CODE": "round(1.23456, ndigits = 3)", - "DC_SCT": "test_function('round', index = 1)"} - sct_payload = helper.run(self.data) - self.assertFalse(sct_payload['correct']) - helper.test_lines(self, sct_payload, 1, 1, 16, 26) - - -class TestFunctionNested(unittest.TestCase): - def test_nested_arg1(self): - self.data = {"DC_PEC": '', - "DC_SOLUTION": "print(type([1, 2, 3]))", - "DC_CODE": "print(type([1, 2, 3]))", - "DC_SCT": "test_function('type')"} - sct_payload = helper.run(self.data) - self.assertTrue(sct_payload['correct']) - - def test_nested_arg2(self): - self.data = {"DC_PEC": '', - "DC_SOLUTION": "print(type([1, 2, 3]))", - "DC_CODE": "print(type([1, 2, 4]))", - "DC_SCT": "test_function('type')"} - sct_payload = helper.run(self.data) - self.assertFalse(sct_payload['correct']) - helper.test_lines(self, sct_payload, 1, 1, 12, 20) - - def test_nested_keyw1(self): - self.data = {"DC_PEC": '', - "DC_SOLUTION": "round(1.1234, ndigits = max([1, 2, 3]))", - "DC_CODE": "round(1.1234, ndigits = max([1, 2, 3]))", - "DC_SCT": "test_function('max')"} - sct_payload = helper.run(self.data) - self.assertTrue(sct_payload['correct']) - - def test_nested_keyw2(self): - self.data = {"DC_PEC": '', - "DC_SOLUTION": "round(1.1234, ndigits = max([1, 2, 3]))", - "DC_CODE": "round(1.1234, ndigits = max([1, 2]))", - "DC_SCT": "test_function('max')"} - sct_payload = helper.run(self.data) - self.assertFalse(sct_payload['correct']) - helper.test_lines(self, sct_payload, 1, 1, 29, 34) - -class TestFunctionDoEval(unittest.TestCase): - def test_do_eval_true_pass(self): - self.data = {"DC_PEC": '', - "DC_SOLUTION": "round(2.1234, ndigits = 4)", - "DC_CODE": "round(2.1234, ndigits = 4)", - "DC_SCT": "test_function('round', do_eval = True)"} - sct_payload = helper.run(self.data) - self.assertTrue(sct_payload['correct']) - - def test_do_eval_true_fail(self): - self.data = {"DC_PEC": '', - "DC_SOLUTION": "round(2.1234, ndigits = 4)", - "DC_CODE": "round(2.123456, ndigits = 4)", - "DC_SCT": "test_function('round', do_eval = True)"} - sct_payload = helper.run(self.data) - self.assertFalse(sct_payload['correct']) - - def test_do_eval_false_pass(self): - self.data = {"DC_PEC": '', - "DC_SOLUTION": "x = 2.12309123; round(x, ndigits = 4)", - "DC_CODE": "x = 2.123450; round(x, ndigits = 4)", - "DC_SCT": "test_function('round', do_eval = False)"} - sct_payload = helper.run(self.data) - self.assertTrue(sct_payload['correct']) - - def test_do_eval_false_fail(self): - self.data = {"DC_PEC": '', - "DC_SOLUTION": "y = 2.12309123; round(y, ndigits = 4)", - "DC_CODE": "x = 2.123450; round(x, ndigits = 4)", - "DC_SCT": "test_function('round', do_eval = False)"} - sct_payload = helper.run(self.data) - self.assertFalse(sct_payload['correct']) - - def test_do_eval_none_pass(self): - self.data = {"DC_PEC": '', - "DC_SOLUTION": "round(123.123, ndigits = 2)", - "DC_CODE": "round(123.123, ndigits = 2)", - "DC_SCT": "test_function('round', do_eval = None)"} - sct_payload = helper.run(self.data) - self.assertTrue(sct_payload['correct']) - - def test_do_eval_none_fail1(self): - self.data = {"DC_PEC": '', - "DC_SOLUTION": "round(123.123, ndigits = 2)", - "DC_CODE": "round(123.123)", - "DC_SCT": "test_function('round', do_eval = None)"} - sct_payload = helper.run(self.data) - self.assertFalse(sct_payload['correct']) - helper.test_lines(self, sct_payload, 1, 1, 1, 14) - - def test_do_eval_none_pass2(self): - self.data = {"DC_PEC": '', - "DC_SOLUTION": "round(123.123)", # args = [0] - "DC_CODE": "round(number = 123.123)", # student args is len 0 - "DC_SCT": "test_function('round', do_eval = None)"} - sct_payload = helper.run(self.data) - self.assertTrue(sct_payload['correct']) - - def test_do_eval_none_fail3(self): - self.data = {"DC_PEC": '', - "DC_SOLUTION": "round(123.123, 2)", # args = [0, 1] - "DC_CODE": "round(123.123)", # student_args is len 1 - "DC_SCT": "test_function('round', do_eval = None)"} - sct_payload = helper.run(self.data) - self.assertFalse(sct_payload['correct']) - helper.test_lines(self, sct_payload, 1, 1, 1, 14) - -class TestCheckFunction(unittest.TestCase): - def setUp(self): - self.data = { - "DC_PEC": "import numpy as np", - "DC_CODE": "np.array([1,2,3])", - "DC_SOLUTION": "np.array([1,2,3])", - "DC_SCT": "Ex().check_function('numpy.array', 0)" - } - - def run_append(self, sct): - self.data["DC_SCT"] += sct - return helper.run(self.data) - - def run_pass(self, sct): - sct_payload = self.run_append(sct) - self.assertTrue(sct_payload['correct']) - return sct_payload - - def run_fail(self, sct): - self.assertFalse(self.run_append(sct)['correct']) - - def test_pass_np_call_exists(self): - sct_payload = helper.run(self.data) - self.assertTrue(sct_payload['correct']) - - def test_pass_test_student_typed(self): - self.run_pass(".test_student_typed(r'np\.array\(\[1,2,3\]\)')") - - def test_fail_test_student_typed(self): - self.data["DC_CODE"] = "np.array([1,2])" - self.run_fail(".test_student_typed(r'np\.array\(\[1,2,3\]\)')") - - def test_pass_func_has_equal_ast(self): - self.run_pass(".has_equal_ast()") - - def test_fail_func_has_equal_ast(self): - self.data["DC_CODE"] = "np.array([1,2])" - self.run_fail(".has_equal_ast()") - - def test_pass_check_args_pos_0(self): - self.run_pass(".check_args(0)") - - def test_fail_check_args_pos_0(self): - self.data["DC_CODE"] = "np.array()" - self.run_fail(".check_args(0)") - - def test_pass_pos_0_test_student_typed(self): - self.run_pass(".check_args(0).test_student_typed(r'\[1,2,3\]')") - - def test_fail_pos_0_test_student_typed(self): - self.data["DC_CODE"] = "np.array([1,2])" - self.run_fail(".check_args(0).test_student_typed(r'\[1,2,3\]')") - - def test_pass_pos_0_has_equal_ast(self): - self.run_pass(".check_args(0).has_equal_ast()") - - def test_fail_pos_0_has_equal_ast(self): - self.data["DC_CODE"] = "np.array([1,2])" - self.run_fail(".check_args(0).has_equal_ast()") - - def test_pass_pos_0_has_equal_value(self): - self.run_pass(".check_args(0).has_equal_value()") - - def test_fail_pos_0_has_equal_value(self): - self.data["DC_CODE"] = "np.array([1,2])" - self.run_fail(".check_args(0).has_equal_value()") - - def test_pass_pos_0_inline_if_body(self): - self.data["DC_CODE"] = "np.array([1,2,3] if True else [1])" - self.data["DC_SOLUTION"] = "np.array([1,2,3] if False else [1])" - self.run_pass(".check_args(0).check_if_exp(0).check_body().has_equal_ast()") - - def test_fail_pos_0_inline_if_body(self): - self.data["DC_CODE"] = "np.array([1,2,3] if True else [1])" - self.data["DC_SOLUTION"] = "np.array([1,2] if False else [1])" - self.run_fail(".check_args(0).check_if_exp(0).check_body().has_equal_ast()") - - def test_test_function_kwargs(self): - self.data["DC_SCT"] = "test_function('np.array', copy = False)" - - def test_test_function2_kwargs(self): - self.data["DC_SCT"] = "test_function2('np.array', params = ['object'], copy = False)" - - -class TestFunctionComplexArgs(unittest.TestCase): - def setUp(self): - self.data = { - "DC_SOLUTION": """ -def sum2(arr): return sum(arr) - -def apply(f, arr): return f(arr) - -apply(sum2, [1,2,3]) -""", - "DC_SCT": """ -test_function('apply') -""" - } - self.data["DC_CODE"] = self.data["DC_SOLUTION"] - - def test_function_with_funcarg_fails(self): - sct_payload = helper.run(self.data) - self.assertFalse(sct_payload['correct']) - - def test_pass_with_no_eval(self): - self.data["DC_SCT"] = """test_function_v2('apply', params=['f', 'arr'], do_eval=[False, True])""" - sct_payload = helper.run(self.data) - self.assertTrue(sct_payload['correct']) - - def test_fail_undillable_args(self): - self.data = { - "DC_PEC": """ -import pickle; from io import BytesIO - -file = BytesIO(pickle.dumps('abc')) - """, - "DC_SOLUTION": "d = pickle.load(file); print(d)", - "DC_CODE": "print(file)", - "DC_SCT": """test_function("print", index=1)""" - } - sct_payload = helper.run(self.data) - self.assertFalse(sct_payload['correct']) - -@pytest.mark.parametrize('stu, correct', [ - ('round(1)', False), - ('round(1)\nround(2)', False), - ('round(1)\nround(2)\nround(3)', True), - ('round(3)\nround(2)\nround(3)', False), - ('round(1)\round(1234)\nround(3)', False), - ('round(1)\nround(2)\nround(4)', False) -]) -def test_multiple_calls(stu, correct): - data = { - 'DC_SOLUTION': 'round(1)\nround(2)\nround(3)', - 'DC_CODE': stu, - } - data['DC_SCT'] = """ -test_function("round", index = 1) -test_function("round", index = 2) -test_function("round", index = 3) -""" - payload = helper.run(data) - assert payload['correct'] == correct - data['DC_SCT'] = """ -test_function_v2("round", index = 1) -test_function_v2("round", index = 2) -test_function_v2("round", index = 3) -""" - payload = helper.run(data) - -def test_has_output_fallback(): - code = "a = 3\nprint(a)\na = 4" - data = { "DC_CODE": code, "DC_SOLUTION": code, "DC_SCT": "test_function('print')" } - payload = helper.run(data) - assert payload['correct'] - -if __name__ == "__main__": - unittest.main() diff --git a/tests/test_test_function_definition.py b/tests/test_test_function_definition.py deleted file mode 100644 index 08a971fd..00000000 --- a/tests/test_test_function_definition.py +++ /dev/null @@ -1,847 +0,0 @@ -import unittest -import helper -import pytest - -class TestFunctionDefinitionStepByStep(unittest.TestCase): - def setUp(self): - self.data = { - "DC_PEC": '', - "DC_SOLUTION": "def test(a, b = 2):\n print('prod of ' + str(a) + ' and ' + str(b))\n return a * b", - "DC_SCT": "test_function_definition('test', results = [[2, 3]], outputs = [[2,3]], errors = [['a', 'b']])" - } - self.data["DC_CODE"] = self.data["DC_SOLUTION"] - - def tearDown(self): - sct_payload = helper.run(self.data) - self.assertTrue(sct_payload['correct']) - - def test_step_x(self): - pass - - def test_step_x_spec2_str_call(self): - self.data['DC_SCT'] = """ -Ex().check_function_def('test').multi( - check_call("f(1,2)").multi( - has_equal_value(), - has_equal_output() - ), - check_call("f('a','b')").has_equal_error() -) -""" - - def test_step_x_spec2_func_arg(self): - self.data['DC_SCT'] = """ -import numpy as np -Ex().check_function_def('test').check_call("f(1,2)").has_equal_value(func = lambda x, y: np.allclose(x, y)) -""" - - -class TestExercise1(unittest.TestCase): - - def setUp(self): - self.data = { - "DC_PEC": '', - "DC_SOLUTION": ''' -def shout ( word ): - shout_word = word + '!!!' - print( shout_word ) -shout( 'help' ) - ''', - "DC_SCT": ''' -test_function_definition("shout", body = lambda: test_expression_output(context_vals = ['help'], incorrect_msg = 'Make sure to output the correct string.')) -success_msg("Nice work!") - ''' - } - - def test_Pass(self): - self.data["DC_CODE"] = ''' -def shout ( word ): - shout_word = word + '!!!' - print( shout_word ) -shout( 'help' ) - ''' - sct_payload = helper.run(self.data) - self.assertTrue(sct_payload['correct']) - - def test_Fail(self): - self.data["DC_CODE"] = ''' -def shout ( word ): - shout_word = word + '!!!' - print( shout_word + "!!" ) -shout( 'help' ) - ''' - sct_payload = helper.run(self.data) - self.assertFalse(sct_payload['correct']) - self.assertEqual(sct_payload['message'], "Make sure to output the correct string.") - helper.test_lines(self, sct_payload, 3, 4, 5, 30) - - def test_Pass_no_lam(self): - self.data["DC_SCT"] = helper.remove_lambdas(self.data["DC_SCT"]) - self.test_Pass() - - def test_Fail_no_lam(self): - self.data["DC_SCT"] = helper.remove_lambdas(self.data["DC_SCT"]) - self.test_Fail() - - def test_Pass_spec2(self): - self.data["DC_SCT"] = """Ex().check_function_def("shout").check_body().set_context(word="help").test_expression_output()""" - self.test_Pass() - -class TestExercise2(unittest.TestCase): - - def setUp(self): - self.data = { - "DC_PEC": '''''', - "DC_CODE": ''' -def shout ( word ): - shout_word = word + '!!!' - print( shout_word ) -shout( 'help' ) - ''', - "DC_SOLUTION": ''' -def shout ( word, times = None): - shout_word = word + '!!!' - print( shout_word ) -shout( 'help' ) -''' - } - - def test_Pass(self): - self.data["DC_SCT"] = ''' -test_function_definition("shout", arg_names=False, arg_defaults=False, body = lambda: test_expression_output(context_vals = ['help'], incorrect_msg = 'make sure to output the correct string.')) -success_msg("Nice work man!") - ''' - sct_payload = helper.run(self.data) - self.assertTrue(sct_payload['correct']) - - def test_Fail(self): - self.data["DC_SCT"] = ''' -test_function_definition("shout", body = lambda: test_expression_output(context_vals = ['help'], incorrect_msg = 'make sure to output the correct string.')) -success_msg("Nice work!") - ''' - sct_payload = helper.run(self.data) - self.assertFalse(sct_payload['correct']) - self.assertEqual(sct_payload['message'], "You should define shout() with 2 arguments, instead got 1.") - - helper.test_lines(self, sct_payload, 2, 4, 1, 23) - - def test_Fail_spec2(self): - self.data["DC_SCT"] = "Ex().check_function_def('shout').has_equal_part_len('args', 'wrong')" - sct_payload = helper.run(self.data) - self.assertTrue('wrong' in sct_payload['message']) - sct_payload = helper.run(self.data) - self.assertFalse(sct_payload['correct']) - -class TestExercise3(unittest.TestCase): - - def setUp(self): - self.data = { - "DC_PEC": '''''', - "DC_CODE": ''' -def shout ( word, times = 3 ): - shout_word = word + '???' - print( shout_word ) - return word * times - ''', - "DC_SOLUTION": ''' -def shout ( word = 'help', times = 3 ): - shout_word = word + '!!!' - print( shout_word ) - return word * times - ''' - } - - def test_Fail1(self): - self.data["DC_SCT"] = ''' -test_function_definition('shout') - ''' - sct_payload = helper.run(self.data) - self.assertFalse(sct_payload['correct']) - helper.test_lines(self, sct_payload, 2, 2, 13, 16) - - def test_Pass1(self): - self.data["DC_SCT"] = ''' -test_function_definition('shout', arg_defaults = False) - ''' - sct_payload = helper.run(self.data) - self.assertTrue(sct_payload['correct']) - - def test_Fail2(self): - self.data["DC_SCT"] = ''' -test_function_definition('shout', arg_defaults = False, outputs = [('help')], wrong_output_msg = "WRONG") - ''' - sct_payload = helper.run(self.data) - self.assertFalse(sct_payload['correct']) - self.assertEqual(sct_payload['message'], "WRONG") - self.assertFalse(sct_payload['correct']) - - def test_Pass2(self): - self.data["DC_SCT"] = ''' -test_function_definition('shout', arg_defaults = False, results = [('help')]) - ''' - sct_payload = helper.run(self.data) - self.assertTrue(sct_payload['correct']) - - def test_Pass3(self): - self.data["DC_SCT"] = ''' -test_function_definition('shout', arg_defaults = False, body = lambda: test_function('print', args=[])) - ''' - sct_payload = helper.run(self.data) - self.assertTrue(sct_payload['correct']) - - -class TestExercise4(unittest.TestCase): - - def setUp(self): - self.data = { - "DC_PEC": '''''', - "DC_CODE": ''' -def shout (word1, word2): - shout1 = word1 + '!!!' - shout2 = word2 + '!!!' - new_shout = word1 + word2 - return new_shout - ''', - "DC_SOLUTION": ''' -def shout (word1, word2): - shout1 = word1 + '!!!' - shout2 = word2 + '!!!' - new_shout = word1 + word2 - print(new_shout) - return new_shout -''' - } - - def test_Pass1(self): - self.data["DC_SCT"] = ''' -test_function_definition("shout") -success_msg("Nice work man!") - ''' - sct_payload = helper.run(self.data) - self.assertTrue(sct_payload['correct']) - - def test_Pass2(self): - self.data["DC_SCT"] = ''' -test_function_definition("shout", results=[('help', 'fire')]) -success_msg("Nice work!") - ''' - sct_payload = helper.run(self.data) - self.assertTrue(sct_payload['correct']) - - def test_Fail1(self): - self.data["DC_SCT"] = ''' -test_function_definition("shout", outputs=[('help', 'fire')]) -success_msg("Nice work!") - ''' - sct_payload = helper.run(self.data) - self.assertFalse(sct_payload['correct']) - self.assertEqual(sct_payload['message'], "Calling shout('help', 'fire') should print out helpfire, instead got no printouts.") - -class TestExercise5(unittest.TestCase): - - def setUp(self): - self.data = { - "DC_PEC": '', - "DC_CODE": ''' -def shout (word1, word2): - shout1 = word1 + '!!!' - shout2 = word2 + '!!!' - new_shout = word1 + word2 - return new_shout - ''', - "DC_SOLUTION": ''' -def shout (word1, word2, word3 = "nothing"): - shout1 = word1 + '!!!' - shout2 = word2 + '!!!' - new_shout = word1 + word2 - print(new_shout) - return new_shout - ''' - } - - def test_Fail1(self): - self.data["DC_SCT"] = ''' -test_function_definition("shout") -success_msg("Nice work!") - ''' - sct_payload = helper.run(self.data) - self.assertFalse(sct_payload['correct']) - self.assertEqual(sct_payload['message'], 'You should define shout() with 3 arguments, instead got 2.') - helper.test_lines(self, sct_payload, 2, 6, 1, 20) - -class TestExercise6(unittest.TestCase): - def setUp(self): - self.data = { - "DC_PEC": '', - "DC_CODE": ''' -def shout (word1, word2): - shout1 = word1 + '!!!' - shout2 = word2 + '!!!' - new_shout = word1 + word2 - return new_shout - ''', - "DC_SOLUTION": ''' -def shout (word1, word2): - shout1 = word1 + '!!!' - shout2 = word2 + '!!!' - new_shout = word1 + word2 - print(new_shout) - return new_shout -''' - } - - def test_Pass1(self): - self.data["DC_SCT"] = ''' -test_function_definition("shout") -success_msg("Nice work man!") - ''' - sct_payload = helper.run(self.data) - self.assertTrue(sct_payload['correct']) - - def test_Pass2(self): - self.data["DC_SCT"] = ''' -test_function_definition("shout", results=[('help', 'fire')]) -success_msg("Nice work!") - ''' - sct_payload = helper.run(self.data) - self.assertTrue(sct_payload['correct']) - - def test_Fail1(self): - self.data["DC_SCT"] = ''' -test_function_definition("shout", outputs=[('help', 'fire')]) -success_msg("Nice work!") - ''' - sct_payload = helper.run(self.data) - self.assertFalse(sct_payload['correct']) - self.assertEqual(sct_payload['message'], "Calling shout('help', 'fire') should print out helpfire, instead got no printouts.") - - -class TestExercise7(unittest.TestCase): - - def setUp(self): - self.data = { - "DC_PEC": '''''', - "DC_SOLUTION": ''' -def to_decimal(number, base = 2): - print("Converting %d from base %s to base 10" % (number, base)) - number_str = str(number) - number_range = range(len(number_str)) - multipliers = [base ** ((len(number_str) - 1) - i) for i in number_range] - decimal = sum([int(number_str[i]) * multipliers[i] for i in number_range]) - return decimal - ''', - "DC_SCT": ''' -test_function_definition("to_decimal", arg_defaults = True, arg_names = False) -test_function_definition("to_decimal", arg_names = False, arg_defaults = False, # Already tested this - results = [(1001101, 2),(1212357, 8)]) -test_function_definition("to_decimal", arg_names = False, arg_defaults = False, # Already tested this - outputs = [(1234, 6),(8888888, 9)]) -test_function_definition("to_decimal", arg_names = False, arg_defaults = False, # Already tested this - body = lambda: test_function("sum", args = [], incorrect_msg = "you should use the `sum()` function.")) -''' - } - - def test_Fail1(self): - self.data["DC_CODE"] = ''' -def to_decimal(number, base = 3): - print("Converting %d from base %s to base 10" % (number, base)) - number_str = str(number) - number_range = range(len(number_str)) - multipliers = [base ** ((len(number_str) - 1) - i) for i in number_range] - decimal = sum([int(number_str[i]) * multipliers[i] for i in number_range]) - return decimal - ''' - sct_payload = helper.run(self.data) - self.assertFalse(sct_payload['correct']) - self.assertEqual(sct_payload['message'], 'Check your definition of to_decimal(). The argument base does not have the correct default.') - helper.test_lines(self, sct_payload, 2, 2, 31, 31) - - def test_Fail2(self): - self.data["DC_CODE"] = self.data["DC_SOLUTION"] - sct_payload = helper.run(self.data) - self.assertTrue(sct_payload['correct']) - - def test_Fail1_no_lam(self): - self.data["DC_SCT"] = helper.remove_lambdas(self.data["DC_SCT"]) - self.test_Fail1() - - def test_Fail2_no_lam(self): - self.data["DC_SCT"] = helper.remove_lambdas(self.data["DC_SCT"]) - self.test_Fail2() - -class TestExercise8(unittest.TestCase): - - def setUp(self): - self.data = { - "DC_PEC": '''''', - "DC_SOLUTION": ''' -def shout(): - shout_word = 'congratulations' + '!!!' - print(shout_word) - ''', - "DC_SCT": ''' -test_function_definition( - "shout", - arg_names = False, - body = lambda: test_object_after_expression("shout_word")) - -''' - } - - def test_Pass(self): - self.data["DC_CODE"] = ''' -def shout(): - shout_word = 'congratulations' + '!!!' - ''' - sct_payload = helper.run(self.data) - self.assertTrue(sct_payload['correct']) - - def test_Fail1(self): - self.data["DC_CODE"] = ''' -def shout(): - shout_word = 'congratulations' + '!!' - ''' - sct_payload = helper.run(self.data) - self.assertFalse(sct_payload['correct']) - self.assertEqual(sct_payload['message'], 'Check your definition of shout(). Did you correctly specify the body? Are you sure you assigned the correct value to shout_word?') - # line info specific to test_object_after_expression! - helper.test_lines(self, sct_payload, 3, 3, 5, 41) - - def test_Fail2(self): - self.data["DC_CODE"] = ''' -def shout(): - shout_word = 'congratulations' + '!!' - ''' - sct_payload = helper.run(self.data) - self.assertFalse(sct_payload['correct']) - self.assertEqual(sct_payload['message'], 'Check your definition of shout(). Did you correctly specify the body? Are you sure you assigned the correct value to shout_word?') - # line info specific to test_object_after_expression! - helper.test_lines(self, sct_payload, 3, 3, 5, 41) - - def test_Pass_no_lam(self): - self.data["DC_SCT"] = helper.remove_lambdas(self.data["DC_SCT"]) - self.test_Pass() - - def test_Fail1_no_lam(self): - self.data["DC_SCT"] = helper.remove_lambdas(self.data["DC_SCT"]) - self.test_Fail1() - - def test_Fail2_no_lam(self): - self.data["DC_SCT"] = helper.remove_lambdas(self.data["DC_SCT"]) - self.test_Fail2() - -class TestFunctionDefintionError1(unittest.TestCase): - - def setUp(self): - self.data = { - "DC_PEC": "", - "DC_SOLUTION": ''' -def inc(num): - if num < 0: - raise ValueError('num is negative') - return(num + 1) - ''', - "DC_SCT": ''' -test_function_definition("inc", - errors = [[-1]]) - ''' - } - - def test_pass(self): - self.data["DC_CODE"] = ''' -def inc(num): - if num < 0: - raise ValueError('num is negative') - return(num + 1) - ''' - sct_payload = helper.run(self.data) - self.assertTrue(sct_payload['correct']) - - def test_pass_2(self): - self.data["DC_CODE"] = ''' -def inc(num): - if num < 0: - raise NameError('num is negative') - return(num + 1) - ''' - sct_payload = helper.run(self.data) - self.assertTrue(sct_payload['correct']) - - def test_fail_1(self): - self.data["DC_CODE"] = ''' -def inc(num): - return(num + 1) - ''' - sct_payload = helper.run(self.data) - self.assertFalse(sct_payload['correct']) - self.assertEqual("Calling inc(-1) should error out with the message num is negative, instead got 0.", sct_payload['message']) - -class TestFunctionDefintionError2(unittest.TestCase): - - def setUp(self): - self.data = { - "DC_PEC": "", - "DC_SOLUTION": ''' -def inc(num): - if num < 0: - raise ValueError('num is negative') - return(num + 1) - ''', - "DC_SCT": ''' -test_function_definition("inc", - errors = [[-1]], - no_error_msg = 'noerror!', - wrong_error_msg = 'wrongerror!') - ''' - } - - def test_pass(self): - self.data["DC_CODE"] = ''' -def inc(num): - if num < 0: - raise ValueError('num is negative') - return(num + 1) - ''' - sct_payload = helper.run(self.data) - self.assertTrue(sct_payload['correct']) - - def test_pass_2(self): - self.data["DC_CODE"] = ''' -def inc(num): - if num < 0: - raise NameError('num is negative') - return(num + 1) - ''' - sct_payload = helper.run(self.data) - self.assertTrue(sct_payload['correct']) - - def test_fail_1(self): - self.data["DC_CODE"] = ''' -def inc(num): - return(num + 1) - ''' - sct_payload = helper.run(self.data) - self.assertFalse(sct_payload['correct']) - self.assertEqual("noerror!", sct_payload['message']) - - - -class TestFunctionDefinitionOnlyReturn(unittest.TestCase): - def test_pass(self): - self.data = { - "DC_PEC": "", - "DC_SCT": ''' -def inner_test(): - test_object_after_expression("shout_word", - context_vals = ["congratulations"]) -test_function_definition("shout", body = inner_test, results = [("congratulations")]) - ''', - "DC_SOLUTION": ''' -def shout(word): - shout_word = word + '!!!' - return shout_word - ''', - "DC_CODE": ''' -def shout(word): - # shout_word = word + '!!!' - return shout_word - ''' - } - sct_payload = helper.run(self.data) - self.assertFalse(sct_payload['correct']) - self.assertEqual("Check your definition of shout(). Did you correctly specify the body? Running it generated an error: name 'shout_word' is not defined.", sct_payload['message']) - -class TestFunctionDefinitionNonLocal(unittest.TestCase): - def test_pass(self): - self.data = { - "DC_PEC": "", - "DC_SOLUTION": ''' -def echo_shout(word): - echo_word = word*2 - print(echo_word) - def shout(): - nonlocal echo_word - echo_word = echo_word + '!!!' - shout() - print(echo_word) -echo_shout('hello') - ''', - "DC_SCT": ''' -def inner_test(): - test_object_after_expression("echo_word", context_vals=["hello"]) - test_function_definition("shout") - test_function("shout") - test_function("print", args=[], index=1) - test_function("print", args=[], index=2) -test_function_definition("echo_shout", body=inner_test) -test_function("echo_shout") - ''' - } - self.data["DC_CODE"] = self.data["DC_SOLUTION"] - sct_payload = helper.run(self.data) - self.assertTrue(sct_payload['correct']) - -class TestFunctionDefinitionArgs(unittest.TestCase): - def setUp(self): - self.data = { - "DC_PEC": "", - "DC_SOLUTION": ''' -def my_fun(x, y = 4, z = ['a', 'b'], *args, **kwargs): - k = len(args) - l = len(kwargs) - print("hello mister") - return k + l - ''', - "DC_SCT": ''' -def inner_test(): - context = ['r', 's', ['c', 'd'], ['t', 'u'], {'a': 2, 'b': 3, 'd':4}] - test_object_after_expression('k', context_vals = context) - test_object_after_expression('l', context_vals = context) -test_function_definition("my_fun", body = inner_test, - results = [{'args': ['r', 's', ['c', 'd'], 't', 'u', 'v'], 'kwargs': {'a': 2, 'b': 3, 'd': 4}}], - outputs = [{'args': ['r', 's', ['c', 'd'], 't', 'u', 'v'], 'kwargs': {'a': 2, 'b': 3, 'd': 4}}]) - '''} - - def test_fail_1(self): - self.data["DC_CODE"] = ''' -def my_fun(x): - print(x) - ''' - sct_payload = helper.run(self.data) - self.assertFalse(sct_payload['correct']) - self.assertEqual("You should define my_fun() with 3 arguments, instead got 1.", sct_payload['message']) - - def test_fail_2(self): - self.data["DC_CODE"] = ''' -def my_fun(x, y): - print(x) - ''' - sct_payload = helper.run(self.data) - self.assertFalse(sct_payload['correct']) - self.assertEqual("You should define my_fun() with 3 arguments, instead got 2.", sct_payload['message']) - - def test_fail_2(self): - self.data["DC_CODE"] = ''' -def my_fun(x, y = 3): - print(x) - ''' - sct_payload = helper.run(self.data) - self.assertFalse(sct_payload['correct']) - self.assertEqual("You should define my_fun() with 3 arguments, instead got 2.", sct_payload['message']) - - def test_fail_3(self): - self.data["DC_CODE"] = ''' -def my_fun(x, y = 4): - print(x) - ''' - sct_payload = helper.run(self.data) - self.assertFalse(sct_payload['correct']) - self.assertEqual("You should define my_fun() with 3 arguments, instead got 2.", sct_payload['message']) - - def test_fail_4(self): - self.data["DC_CODE"] = ''' -def my_fun(x, y = 4, z = ['a', 'c']): - print(x) - ''' - sct_payload = helper.run(self.data) - self.assertFalse(sct_payload['correct']) - self.assertEqual("Check your definition of my_fun(). The argument z does not have the correct default.", sct_payload['message']) - - def test_fail_5(self): - self.data["DC_CODE"] = ''' -def my_fun(x, y = 4, z = ['a', 'b']): - print(x) - ''' - sct_payload = helper.run(self.data) - self.assertFalse(sct_payload['correct']) - self.assertEqual("Check your definition of my_fun(). Have you specified an argument to take a * argument and named it args?", sct_payload['message']) - - def test_fail_6a(self): - self.data["DC_CODE"] = ''' -def my_fun(x, y = 4, z = ['a', 'b'], *asdfasdf): - print(x) - ''' - sct_payload = helper.run(self.data) - self.assertFalse(sct_payload['correct']) - self.assertEqual("Check your definition of my_fun(). Have you specified an argument to take a * argument and named it args?", sct_payload['message']) - - - def test_fail_6b(self): - self.data["DC_CODE"] = ''' -def my_fun(x, y = 4, z = ['a', 'b'], *args): - print(x) - ''' - sct_payload = helper.run(self.data) - self.assertFalse(sct_payload['correct']) - self.assertEqual("Check your definition of my_fun(). Have you specified an argument to take a ** argument and named it kwargs?", sct_payload['message']) - - def test_fail_7a(self): - self.data["DC_CODE"] = ''' -def my_fun(x, y = 4, z = ['a', 'b'], *args, **asdfasdf): - print(x) - ''' - sct_payload = helper.run(self.data) - self.assertFalse(sct_payload['correct']) - self.assertEqual("Check your definition of my_fun(). Have you specified an argument to take a ** argument and named it kwargs?", sct_payload['message']) - - def test_fail_7b(self): - self.data["DC_CODE"] = ''' -def my_fun(x, y = 4, z = ['a', 'b'], *args, **kwargs): - print(x) - ''' - sct_payload = helper.run(self.data) - self.assertFalse(sct_payload['correct']) - self.assertEqual("Check your definition of my_fun(). Did you correctly specify the body? Running it should define a variable k without errors, but it doesn't.", sct_payload['message']) - - def test_fail_8(self): - self.data["DC_CODE"] = ''' -def my_fun(x, y = 4, z = ['a', 'b'], *args, **kwargs): - k = len(kwargs) - l = len(args) - return k + l - ''' - sct_payload = helper.run(self.data) - self.assertFalse(sct_payload['correct']) - self.assertEqual("Check your definition of my_fun(). Did you correctly specify the body? Are you sure you assigned the correct value to k?", sct_payload['message']) - - def test_fail_9(self): - self.data["DC_CODE"] = ''' -def my_fun(x, y = 4, z = ['a', 'b'], *args, **kwargs): - k = len(args) - l = len(kwargs) - return k + l - ''' - sct_payload = helper.run(self.data) - self.assertFalse(sct_payload['correct']) - # in two pieces because order of dict not fixed - self.assertIn("Calling my_fun('r', 's', ['c', 'd'], 't', 'u', 'v'", sct_payload['message']) - self.assertIn(") should print out hello mister, instead got no printouts.", sct_payload['message']) - - def test_pass(self): - self.data["DC_CODE"] = self.data["DC_SOLUTION"] - sct_payload = helper.run(self.data) - self.assertTrue(sct_payload['correct']) - -class TestFunctionSpec2(unittest.TestCase): - def setUp(self): - self.data = { - "DC_PEC": "", - "DC_SOLUTION": ''' -def my_fun(x, y = 4, z = ('a', 'b'), *args, **kwargs): - return [x, y, *z, *args] - ''' - } - - self.MULTI_SCT = """ -varnames = ['x', 'y', 'z', '*args', '**kwargs'] -test_names = [check_args(name).has_equal_part('name', 'bad%s'%name) for name in varnames] -Ex().check_function_def('my_fun').multi(test_names) -""" - self.SCT_CHECK = "Ex().check_function_def('my_fun')" - self.SCT_KW = "Ex().check_function_def('my_fun').check_args('x').has_equal_part('name', 'badx')" - self.SCT_POS = "Ex().check_function_def('my_fun').check_args(0).has_equal_part('name', 'badx')" - self.SCT_CHECK_ONE = "Ex().check_function_def('my_fun').check_args(1)" - self.SCT_CHECK_Y = "Ex().check_function_def('my_fun').check_args('y')" - self.SCT_CHECK_X = "Ex().check_function_def('my_fun').check_args('x')" - self.SCT_CHECK_ARGS = "Ex().check_function_def('my_fun').check_args('*args')" - self.SCT_CHECK_KWARGS = "Ex().check_function_def('my_fun').check_args('**kwargs')" - - def when_code_is_sol(self): - self.data['DC_CODE'] = self.data['DC_SOLUTION'] - self.sct_payload = helper.run(self.data) - return self.sct_payload['correct'] - - def when_replace(self, orig, new): - self.data['DC_CODE'] = self.data['DC_SOLUTION'].replace(orig, new) - self.sct_payload = helper.run(self.data) - return self.sct_payload['correct'] - - def test_pass_kw(self): - self.data['DC_SCT'] = self.SCT_KW - self.assertTrue(self.when_code_is_sol()) - - def test_fail_kw(self): - self.data['DC_SCT'] = self.SCT_KW - self.assertFalse(self.when_replace('x', 'x2')) - - def test_pass_pos(self): - self.data['DC_SCT'] = self.SCT_POS - self.assertTrue(self.when_code_is_sol()) - - def test_fail_pos(self): - self.data['DC_SCT'] = self.SCT_POS - self.assertFalse(self.when_replace('x', 'x2')) - - def test_fail_pos_is_default(self): - self.data['DC_SCT'] = self.SCT_CHECK_ONE + ".is_default()" - self.assertFalse(self.when_replace('y = 4', 'y')) - - def test_fail_kw_is_default(self): - self.data['DC_SCT'] = self.SCT_CHECK_Y + ".is_default()" - self.assertFalse(self.when_replace('y = 4', 'y')) - - def test_fail_kw_not_default(self): - self.data['DC_SCT'] = self.SCT_CHECK_X + ".is_default()" - self.assertFalse(self.when_replace('x, y = 4', 'x = 2, y = 4')) - - def test_fail_star_args_undef(self): - self.data['DC_CODE'] = """def my_fun(x, y = 4, z = ('a', 'b'), args=2, **kwargs): pass""" - self.data['DC_SCT'] = self.SCT_CHECK_ARGS - sct_payload = helper.run(self.data) - self.assertFalse(sct_payload['correct']) - - def test_fail_star_args_name(self): - self.data['DC_CODE'] = """def my_fun(x, y = 4, z = ('a', 'b'), *wrongargsname, **kwargs): pass""" - self.data['DC_SCT'] = self.SCT_CHECK_ARGS + '.has_equal_name()' - sct_payload = helper.run(self.data) - self.assertFalse(sct_payload['correct']) - - def test_fail_kwargs_undef(self): - self.data['DC_CODE'] = """def my_fun(x, y = 4, z = ('a', 'b'), args=2, kwargs=2): pass""" - self.data['DC_SCT'] = self.SCT_CHECK_KWARGS - sct_payload = helper.run(self.data) - self.assertFalse(sct_payload['correct']) - - def test_fail_kwargs_name(self): - self.data['DC_CODE'] = """def my_fun(x, y = 4, z = ('a', 'b'), *args, **wrongkwargsname): pass""" - self.data['DC_SCT'] = self.SCT_CHECK_KWARGS + '.has_equal_name()' - sct_payload = helper.run(self.data) - self.assertFalse(sct_payload['correct']) - - def test_pass_equal_value(self): - self.data['DC_SCT'] = self.SCT_CHECK_Y + ".has_equal_value('unequal values')" - self.assertTrue(self.when_code_is_sol()) - - def test_fail_equal_value(self): - self.data['DC_SCT'] = self.SCT_CHECK_Y + ".has_equal_value('unequal values')" - self.assertFalse(self.when_replace('y = 4', 'y = 2')) - - def test_pass_call_str(self): - self.data['DC_SCT'] = self.SCT_CHECK + """.check_call("f(1, 2, (3,4), 5, kw_arg='ok')")""" - self.assertTrue(self.when_replace('x', 'x2')) - - @unittest.skip("Tries to evaluate ast tree but gets None when no default") - def test_check_value_when_no_default(self): - self.data['DC_SCT'] = self.SCT_CHECK_X + ".has_equal_value('unequal values')" - self.assertTrue(self.when_code_is_sol()) - - def test_pass_multi(self): - self.data['DC_SCT'] = self.MULTI_SCT - self.assertTrue(self.when_code_is_sol()) - - def test_fail_multi(self): - self.data['DC_SCT'] = self.MULTI_SCT - self.assertFalse(self.when_replace('x', 'x2')) - - -class TestLambdaFunctionSpec2(TestFunctionSpec2): - def setUp(self): - super().setUp() - self.data['DC_SOLUTION'] = "lambda x, y = 4, z = ('a', 'b'), *args, **kwargs: [x, y, *z, *args]" - for attr in ['MULTI_SCT', 'SCT_CHECK', 'SCT_KW', 'SCT_POS', 'SCT_CHECK_ONE', 'SCT_CHECK_Y', 'SCT_CHECK_X', 'SCT_CHECK_ARGS', 'SCT_CHECK_KWARGS']: - lam_sct = getattr(self, attr).replace("check_function_def('my_fun')", 'check_lambda_function(0)') - setattr(self, attr, lam_sct) - -if __name__ == "__main__": - unittest.main() - diff --git a/tests/test_test_function_v2.py b/tests/test_test_function_v2.py deleted file mode 100644 index 4dd1b81e..00000000 --- a/tests/test_test_function_v2.py +++ /dev/null @@ -1,666 +0,0 @@ -import unittest -import helper -import pytest - -class TestFunctionBase(unittest.TestCase): - - def test_fun_pass(self): - self.data = { - "DC_PEC": "def my_fun(a):\n pass", - "DC_SOLUTION": "my_fun(1)", - "DC_CODE": "my_fun(1)", - "DC_SCT": "test_function_v2('my_fun', params = ['a'])" - } - sct_payload = helper.run(self.data) - self.assertTrue(sct_payload['correct']) - - def test_fun_fail(self): - self.data = { - "DC_PEC": "def my_fun(a):\n pass", - "DC_SOLUTION": "my_fun(2)", - "DC_CODE": "my_fun(1)", - "DC_SCT": "test_function_v2('my_fun', params = ['a'])" - } - sct_payload = helper.run(self.data) - self.assertFalse(sct_payload['correct']) - - def test_builtin_pass(self): - self.data = { - "DC_PEC": "", - "DC_SOLUTION": "round(1)", - "DC_CODE": "round(1)", - "DC_SCT": "test_function_v2('round', params = ['number'])" - } - sct_payload = helper.run(self.data) - self.assertTrue(sct_payload['correct']) - - def test_builtin_fail(self): - self.data = { - "DC_PEC": "", - "DC_SOLUTION": "round(1)", - "DC_CODE": "round(2)", - "DC_SCT": "test_function_v2('round', params = ['number'])" - } - sct_payload = helper.run(self.data) - self.assertFalse(sct_payload['correct']) - - def test_custom_builtin_pass(self): - self.data = { - "DC_PEC": "", - "DC_SOLUTION": "max([1, 2, 3, 4])", - "DC_CODE": "max([1, 2, 3, 4])", - "DC_SCT": ''' -sig = sig_from_params(param('iterable', param.POSITIONAL_ONLY)) -test_function_v2('max', params = ['iterable'], signature = sig) - ''' - } - sct_payload = helper.run(self.data) - self.assertTrue(sct_payload['correct']) - - def test_custom_builtin_fail(self): - self.data = { - "DC_PEC": "", - "DC_SOLUTION": "max([1, 2, 3, 4])", - "DC_CODE": "max([1, 2, 3, 412])", - "DC_SCT": ''' -sig = sig_from_params(param('iterable', param.POSITIONAL_ONLY)) -test_function_v2('max', params = ['iterable'], signature = sig) - ''' - } - sct_payload = helper.run(self.data) - self.assertFalse(sct_payload['correct']) - - def test_package_fun_pass(self): - self.data = { - "DC_PEC": "import pandas as pd", - "DC_SOLUTION": "df = pd.DataFrame({'a': [1, 2, 3]})", - "DC_CODE": "df = pd.DataFrame({'a': [1, 2, 3]})", - "DC_SCT": "test_function_v2('pandas.DataFrame', params = ['data'])" - } - sct_payload = helper.run(self.data) - self.assertTrue(sct_payload['correct']) - - def test_package_fun_fail(self): - self.data = { - "DC_PEC": "import pandas as pd", - "DC_SOLUTION": "df = pd.DataFrame({'a': [1, 2, 3]})", - "DC_CODE": "df = pd.DataFrame({'a': [1, 2, 312]})", - "DC_SCT": "test_function_v2('pandas.DataFrame', params = ['data'])" - } - sct_payload = helper.run(self.data) - self.assertFalse(sct_payload['correct']) - - def test_package_builtin_pass(self): - self.data = { - "DC_PEC": "import numpy as np", - "DC_SOLUTION": "arr = np.array([1, 2, 3])", - "DC_CODE": "arr = np.array([1, 2, 3])", - "DC_SCT": "test_function_v2('numpy.array', params = ['object'])" - } - sct_payload = helper.run(self.data) - self.assertTrue(sct_payload['correct']) - - def test_package_builtin_fail(self): - self.data = { - "DC_PEC": "import numpy as np", - "DC_SOLUTION": "arr = np.array([1, 2, 3])", - "DC_CODE": "arr = np.array([1, 2, 123])", - "DC_SCT": "test_function_v2('numpy.array', params = ['object'])" - } - sct_payload = helper.run(self.data) - self.assertFalse(sct_payload['correct']) - - def test_custom_package_builtin_pass(self): - self.data = { - "DC_PEC": "import numpy as np", - "DC_SOLUTION": "com = np.complex(2, 4)", - "DC_CODE": "com = np.complex(2, 4)", - "DC_SCT": ''' -sig = sig_from_params(param('real', param.POSITIONAL_OR_KEYWORD), param('imag', param.POSITIONAL_OR_KEYWORD, default=0)) -test_function_v2('numpy.complex', params = ['real', 'imag'], signature=sig) - '''} - sct_payload = helper.run(self.data) - self.assertTrue(sct_payload['correct']) - - def test_custom_package_builtin_fail(self): - self.data = { - "DC_PEC": "import numpy as np", - "DC_SOLUTION": "com = np.complex(2, 4)", - "DC_CODE": "com = np.complex(2, 5)", - "DC_SCT": ''' -sig = sig_from_params(param('real', param.POSITIONAL_OR_KEYWORD), param('imag', param.POSITIONAL_OR_KEYWORD, default=0)) -test_function_v2('numpy.complex', params = ['real', 'imag'], signature=sig) - '''} - sct_payload = helper.run(self.data) - self.assertFalse(sct_payload['correct']) - - def test_method_pass(self): - self.data = { - "DC_PEC": ''' -class Test(): - def __init__(self, a): - self.a = a - - def set_a(self, value): - self.a = value -x = Test(123) - ''', - "DC_SOLUTION": "x.set_a(4)", - "DC_CODE": "x.set_a(4)", - "DC_SCT": "test_function_v2('x.set_a', params = ['value'])" - } - sct_payload = helper.run(self.data) - self.assertTrue(sct_payload['correct']) - - def test_method_fail(self): - self.data = { - "DC_PEC": ''' -class Test(): - def __init__(self, a): - self.a = a - - def set_a(self, value): - self.a = value -x = Test(123) - ''', - "DC_SOLUTION": "x.set_a(4)", - "DC_CODE": "x.set_a(4123)", - "DC_SCT": "test_function_v2('x.set_a', params = ['value'])" - } - sct_payload = helper.run(self.data) - self.assertFalse(sct_payload['correct']) - - def test_method_builtin_pass(self): - self.data = { - "DC_PEC": "arr = [1, 2, 3, 4]", - "DC_SOLUTION": "arr.append(5)", - "DC_CODE": "arr.append(5)", - "DC_SCT": "test_function_v2('arr.append', params = ['object'])" - } - sct_payload = helper.run(self.data) - self.assertTrue(sct_payload['correct']) - - def test_method_builtin_fail(self): - self.data = { - "DC_PEC": "arr = [1, 2, 3, 4]", - "DC_SOLUTION": "arr.append(5)", - "DC_CODE": "arr.append(5123)", - "DC_SCT": "test_function_v2('arr.append', params = ['object'])" - } - sct_payload = helper.run(self.data) - self.assertFalse(sct_payload['correct']) - - def test_custom_method_builtin_pass(self): - self.data = { - "DC_PEC": "arr = [1, 2, 3, 4]", - "DC_SOLUTION": "res = arr.count(2)", - "DC_CODE": "res = arr.count(2)", - "DC_SCT": ''' -sig = sig_from_params(param('value', param.POSITIONAL_ONLY)) -test_function_v2('arr.count', params = ['value'], signature=sig) - ''' - } - sct_payload = helper.run(self.data) - self.assertTrue(sct_payload['correct']) - - def test_custom_method_builtin_fail(self): - self.data = { - "DC_PEC": "arr = [1, 2, 3, 4]", - "DC_SOLUTION": "res = arr.count(2)", - "DC_CODE": "res = arr.count(2123)", - "DC_SCT": ''' -sig = sig_from_params(param('value', param.POSITIONAL_ONLY)) -test_function_v2('arr.count', params = ['value'], signature=sig) - ''' - } - sct_payload = helper.run(self.data) - self.assertFalse(sct_payload['correct']) - - def test_chained_method_builtin_pass(self): - self.data = { - "DC_PEC": "x = 'test'", - "DC_SOLUTION": "x.upper().center(8)", - "DC_CODE": "x.upper().center(8)", - "DC_SCT": "test_function_v2('x.upper.center', params=['width'], signature='str.center')" - } - sct_payload = helper.run(self.data) - self.assertTrue(sct_payload['correct']) - - def test_chained_method_builtin_fail(self): - self.data = { - "DC_PEC": "x = 'test'", - "DC_SOLUTION": "x.upper().center(8)", - "DC_CODE": "x.upper().center(7)", - "DC_SCT": "test_function_v2('x.upper.center', params=['width'], signature='str.center')" - } - sct_payload = helper.run(self.data) - self.assertFalse(sct_payload['correct']) - - def test_chained_method_builtin_v2_pass(self): - self.data = { - "DC_PEC": ''' -class Test(): - def __init__(self, a): - self.a = a - - def set_a(self, value): - self.a = value - return(self) -x = Test(123) - ''', - "DC_SOLUTION": "x.set_a(843).set_a(102)", - "DC_CODE": "x.set_a(843).set_a(102)", - "DC_SCT": ''' -sig = sig_from_obj('x.set_a') -test_function_v2('x.set_a.set_a', params = ['value'], signature=sig) - ''' - } - sct_payload = helper.run(self.data) - self.assertTrue(sct_payload['correct']) - - def test_chained_method_builtin_v2_fail(self): - self.data = { - "DC_PEC": ''' -class Test(): - def __init__(self, a): - self.a = a - - def set_a(self, value): - self.a = value - return(self) -x = Test(123) - ''', - "DC_SOLUTION": "x.set_a(843).set_a(102)", - "DC_CODE": "x.set_a(843).set_a(103)", - "DC_SCT": ''' -sig = sig_from_obj('x.set_a') -test_function_v2('x.set_a.set_a', params = ['value'], signature=sig) - ''' - } - sct_payload = helper.run(self.data) - self.assertFalse(sct_payload['correct']) - -class TestArgsKeywords(unittest.TestCase): - - def test_fun1(self): - self.data = { - "DC_SOLUTION": "round(2, 3)", - "DC_CODE": "round(2, 3)", - "DC_SCT": 'test_function_v2("round", params=["number", "ndigits"])' - } - sct_payload = helper.run(self.data) - self.assertTrue(sct_payload['correct']) - - def test_fun2(self): - self.data = { - "DC_SOLUTION": "round(2, 3)", - "DC_CODE": "round(2, ndigits=3)", - "DC_SCT": 'test_function_v2("round", params=["number", "ndigits"])' - } - sct_payload = helper.run(self.data) - self.assertTrue(sct_payload['correct']) - - def test_fun3(self): - self.data = { - "DC_SOLUTION": "round(2, ndigits=3)", - "DC_CODE": "round(2, ndigits=3)", - "DC_SCT": 'test_function_v2("round", params=["number", "ndigits"])' - } - sct_payload = helper.run(self.data) - self.assertTrue(sct_payload['correct']) - - def test_fun4(self): - self.data = { - "DC_SOLUTION": "round(2, ndigits=3)", - "DC_CODE": "round(2, 3)", - "DC_SCT": 'test_function_v2("round", params=["number", "ndigits"])' - } - sct_payload = helper.run(self.data) - self.assertTrue(sct_payload['correct']) - - def test_class0(self): - self.data = { - "DC_PEC": "", - "DC_SOLUTION": "import pandas as pad; import numpy as nump; pad.DataFrame(nump.zeros((5,2)), columns = ['a', 'b'])", - "DC_CODE": "import pandas as pd; import numpy as np; pd.DataFrame(np.zeros((5,2)), columns = ['a', 'b'])", - "DC_SCT": "test_function_v2('pandas.DataFrame', params = ['data', 'columns'])" - } - sct_payload = helper.run(self.data) - self.assertTrue(sct_payload['correct']) - - def test_class1(self): - self.data = { - "DC_PEC": "import pandas as pd; import numpy as np", - "DC_SOLUTION": "pd.DataFrame(np.zeros((5,2)), columns = ['a', 'b'])", - "DC_CODE": "pd.DataFrame(np.zeros((5,2)), columns = ['a', 'b'])", - "DC_SCT": "test_function_v2('pandas.DataFrame', params = ['data', 'columns'])" - } - sct_payload = helper.run(self.data) - self.assertTrue(sct_payload['correct']) - - def test_class2(self): - self.data = { - "DC_PEC": "import pandas as pd; import numpy as np", - "DC_SOLUTION": "pd.DataFrame(data = np.zeros((5,2)), columns = ['a', 'b'])", - "DC_CODE": "pd.DataFrame(np.zeros((5,2)), columns = ['a', 'b'])", - "DC_SCT": "test_function_v2('pandas.DataFrame', params = ['data', 'columns'])" - } - sct_payload = helper.run(self.data) - self.assertTrue(sct_payload['correct']) - - def test_class3(self): - self.data = { - "DC_PEC": "import pandas as pd; import numpy as np", - "DC_SOLUTION": "pd.DataFrame(columns = ['a', 'b'], data = np.zeros((5,2)))", - "DC_CODE": "pd.DataFrame(data = np.zeros((5,2)), columns = ['a', 'b'])", - "DC_SCT": "test_function_v2('pandas.DataFrame', params = ['data', 'columns'])" - } - sct_payload = helper.run(self.data) - self.assertTrue(sct_payload['correct']) - - def test_builtin1(self): - self.data = { - "DC_PEC": "", - "DC_SOLUTION": "round(1.123, ndigits = 2)", - "DC_CODE": "round(1.123, ndigits = 2)", - "DC_SCT": "test_function_v2('round', params = ['number', 'ndigits'])" - } - sct_payload = helper.run(self.data) - self.assertTrue(sct_payload['correct']) - - def test_builtin2(self): - self.data = { - "DC_PEC": "", - "DC_SOLUTION": "round(1.123, ndigits = 2)", - "DC_CODE": "round(1.123, 2)", - "DC_SCT": "test_function_v2('round', params = ['number', 'ndigits'])" - } - sct_payload = helper.run(self.data) - self.assertTrue(sct_payload['correct']) - - def test_builtin3(self): - self.data = { - "DC_PEC": "", - "DC_SOLUTION": "round(1.123, 2)", - "DC_CODE": "round(1.123, ndigits = 2)", - "DC_SCT": "test_function_v2('round', params = ['number', 'ndigits'])" - } - sct_payload = helper.run(self.data) - self.assertTrue(sct_payload['correct']) - -class TestStepByStep(unittest.TestCase): - def setUp(self): - self.data = { - "DC_PEC": "import pandas as pd", - "DC_SOLUTION": "df = pd.DataFrame([1, 2, 3], columns=['a'])", - "DC_SCT": "test_function_v2('pandas.DataFrame', params=['data', 'columns'])" - } - - def test_step1(self): - self.data["DC_CODE"] = "" - sct_payload = helper.run(self.data) - self.assertFalse(sct_payload['correct']) - helper.test_absent_lines(self, sct_payload) - - def test_step2(self): - self.data["DC_CODE"] = "df = pd.DataFrame(x=[1, 2, 3])" - sct_payload = helper.run(self.data) - self.assertFalse(sct_payload['correct']) - - def test_step3(self): - self.data["DC_CODE"] = "df = pd.DataFrame(data=[1, 2, 3])" - sct_payload = helper.run(self.data) - self.assertFalse(sct_payload['correct']) - - def test_step4(self): - self.data["DC_CODE"] = "df = pd.DataFrame(data=[1, 2, 3], columns=['b'])" - sct_payload = helper.run(self.data) - self.assertFalse(sct_payload['correct']) - - def test_step5(self): - self.data["DC_CODE"] = "df = pd.DataFrame(data=[1, 2, 3], columns=['a'])" - sct_payload = helper.run(self.data) - self.assertTrue(sct_payload['correct']) - -class TestStepByStepCustom(unittest.TestCase): - def setUp(self): - self.data = { - "DC_PEC": "import pandas as pd", - "DC_SOLUTION": "df = pd.DataFrame([1, 2, 3], columns=['a'])", - "DC_SCT": ''' -test_function_v2('pandas.DataFrame', params=['data', 'columns'], - not_called_msg='notcalledmsg', - params_not_matched_msg='paramsnotmatchedmsg', - params_not_specified_msg='paramsnotspecifiedmsg', - incorrect_msg='incorrectmsg') - ''' - } - - self.SPEC2_SCT = """ -Ex().check_function('pandas.DataFrame', 0, missing_msg = "notcalledmsg", expand_msg="", params_not_matched_msg='paramsnotmatchedmsg')\ - .multi( - check_args('data', missing_msg='paramsnotspecifiedmsg'), - check_args('columns', missing_msg='paramsnotspecifiedmsg')) -""" - - def test_step1(self): - self.data["DC_CODE"] = "" - sct_payload = helper.run(self.data) - self.assertFalse(sct_payload['correct']) - self.assertEqual('notcalledmsg', sct_payload['message']) - helper.test_absent_lines(self, sct_payload) - - def test_step1_spec2(self): - self.data["DC_SCT"] = self.SPEC2_SCT - self.test_step1() - - def test_step2(self): - self.data["DC_CODE"] = "df = pd.DataFrame(x=[1, 2, 3])" - sct_payload = helper.run(self.data) - self.assertFalse(sct_payload['correct']) - self.assertEqual('paramsnotmatchedmsg', sct_payload['message']) - helper.test_lines(self, sct_payload, 1, 1, 6, 30) - - def test_step2_spec2(self): - self.data["DC_SCT"] = self.SPEC2_SCT - self.test_step2() - - def test_step3(self): - self.data["DC_CODE"] = "df = pd.DataFrame(data=[1, 2, 3])" - sct_payload = helper.run(self.data) - self.assertFalse(sct_payload['correct']) - self.assertIn('paramsnotspecifiedmsg', sct_payload['message']) - helper.test_lines(self, sct_payload, 1, 1, 6, 33) - - def test_step3_spec2(self): - self.data["DC_SCT"] = self.SPEC2_SCT - self.test_step3() - - def test_step4(self): - self.data["DC_CODE"] = "df = pd.DataFrame(data=[1, 2, 3], columns=['b'])" - sct_payload = helper.run(self.data) - self.assertFalse(sct_payload['correct']) - self.assertEqual('incorrectmsg', sct_payload['message']) - helper.test_lines(self, sct_payload, 1, 1, 35, 47) - - def test_step5(self): - self.data["DC_CODE"] = "df = pd.DataFrame(data=[1, 2, 3], columns=['a'])" - sct_payload = helper.run(self.data) - self.assertTrue(sct_payload['correct']) - -class TestStepByStepCustom2(unittest.TestCase): - def setUp(self): - self.data = { - "DC_PEC": "import pandas as pd", - "DC_SOLUTION": "df = pd.DataFrame([1, 2, 3], columns=['a'])", - "DC_SCT": ''' -test_function_v2('pandas.DataFrame', params=['data', 'columns'], - incorrect_msg=['dataincorrect', 'columnsincorrect']) - ''' - } - - def test_step4(self): - self.data["DC_CODE"] = "df = pd.DataFrame(data=[1, 2, 3, 4], columns=['a'])" - sct_payload = helper.run(self.data) - self.assertFalse(sct_payload['correct']) - self.assertEqual('dataincorrect', sct_payload['message']) - helper.test_lines(self, sct_payload, 1, 1, 19, 35) - - def test_step4b(self): - self.data["DC_CODE"] = "df = pd.DataFrame(data=[1, 2, 3], columns=['b'])" - sct_payload = helper.run(self.data) - self.assertFalse(sct_payload['correct']) - self.assertEqual('columnsincorrect', sct_payload['message']) - helper.test_lines(self, sct_payload, 1, 1, 35, 47) - - def test_step5(self): - self.data["DC_CODE"] = "df = pd.DataFrame(data=[1, 2, 3], columns=['a'])" - sct_payload = helper.run(self.data) - self.assertTrue(sct_payload['correct']) - -class TestStepByStepCustom3(unittest.TestCase): - def setUp(self): - self.data = { - "DC_PEC": "import pandas as pd", - "DC_SOLUTION": "df = pd.DataFrame([1, 2, 3], columns=['a'])", - "DC_SCT": ''' -test_function_v2('pandas.DataFrame', params=['data', 'columns'], - params_not_specified_msg=['datanotspecified', 'columnsnotspecified']) - ''' - } - - def test_step4(self): - self.data["DC_CODE"] = "df = pd.DataFrame(data=[1, 2, 3])" - sct_payload = helper.run(self.data) - self.assertFalse(sct_payload['correct']) - self.assertIn('columnsnotspecified', sct_payload['message']) - helper.test_lines(self, sct_payload, 1, 1, 6, 33) - - def test_step5(self): - self.data["DC_CODE"] = "df = pd.DataFrame(data=[1, 2, 3], columns=['a'])" - sct_payload = helper.run(self.data) - self.assertTrue(sct_payload['correct']) - -class TestStepByStepPositional(unittest.TestCase): - def setUp(self): - self.data = { - "DC_PEC": "x = 'test'", - "DC_SOLUTION": "x.center(50, 't')", - "DC_SCT": "test_function_v2('x.center', params=['width','fillchar'])" - } - - def test_step1(self): - self.data["DC_CODE"] = "" - sct_payload = helper.run(self.data) - self.assertFalse(sct_payload['correct']) - helper.test_absent_lines(self, sct_payload) - - def test_step2(self): - self.data["DC_CODE"] = "x.center(width = 50)" - sct_payload = helper.run(self.data) - self.assertFalse(sct_payload['correct']) - helper.test_lines(self, sct_payload, 1, 1, 1, 20) - - def test_step3(self): - self.data["DC_CODE"] = "x.center(50)" - sct_payload = helper.run(self.data) - self.assertFalse(sct_payload['correct']) - helper.test_lines(self, sct_payload, 1, 1, 1, 12) - - def test_step4(self): - self.data["DC_CODE"] = "x.center(50, 'c')" - sct_payload = helper.run(self.data) - self.assertFalse(sct_payload['correct']) - helper.test_lines(self, sct_payload, 1, 1, 14, 16) - - def test_step5(self): - self.data["DC_CODE"] = "x.center(50, 't')" - sct_payload = helper.run(self.data) - self.assertTrue(sct_payload['correct']) - -class TestDoEval(unittest.TestCase): - def test_do_eval_true_pass(self): - self.data = {"DC_PEC": '', - "DC_SOLUTION": "round(2.1234, ndigits = 4)", - "DC_CODE": "round(2.1234, ndigits = 4)", - "DC_SCT": "test_function_v2('round', params=['number', 'ndigits'], do_eval = True)"} - sct_payload = helper.run(self.data) - self.assertTrue(sct_payload['correct']) - - def test_do_eval_true_fail(self): - self.data = {"DC_PEC": '', - "DC_SOLUTION": "round(2.1234, ndigits = 4)", - "DC_CODE": "round(2.123456, ndigits = 4)", - "DC_SCT": "test_function_v2('round', params=['number', 'ndigits'], do_eval = True)"} - sct_payload = helper.run(self.data) - self.assertFalse(sct_payload['correct']) - - def test_do_eval_false_pass(self): - self.data = {"DC_PEC": '', - "DC_SOLUTION": "x = 2.12309123; round(x, ndigits = 4)", - "DC_CODE": "x = 2.123450; round(x, ndigits = 4)", - "DC_SCT": "test_function_v2('round', params=['number', 'ndigits'], do_eval = False)"} - sct_payload = helper.run(self.data) - self.assertTrue(sct_payload['correct']) - - def test_do_eval_false_fail(self): - self.data = {"DC_PEC": '', - "DC_SOLUTION": "y = 2.12309123; round(y, ndigits = 4)", - "DC_CODE": "x = 2.123450; round(x, ndigits = 4)", - "DC_SCT": "test_function_v2('round', params=['number', 'ndigits'], do_eval = False)"} - sct_payload = helper.run(self.data) - self.assertFalse(sct_payload['correct']) - - def test_do_eval_none_pass(self): - self.data = {"DC_PEC": '', - "DC_SOLUTION": "round(123.123, ndigits = 2)", - "DC_CODE": "round(123.123, ndigits = 2)", - "DC_SCT": "test_function_v2('round', params=['number', 'ndigits'], do_eval = None)"} - sct_payload = helper.run(self.data) - self.assertTrue(sct_payload['correct']) - - def test_do_eval_none_fail1(self): - self.data = {"DC_PEC": '', - "DC_SOLUTION": "round(123.123, ndigits = 2)", - "DC_CODE": "round(123.123)", - "DC_SCT": "test_function_v2('round', params=['number', 'ndigits'], do_eval = None)"} - sct_payload = helper.run(self.data) - self.assertFalse(sct_payload['correct']) - - def test_do_eval_none_fail2(self): - self.data = {"DC_PEC": '', - "DC_SOLUTION": "round(123.123, 2)", # args = [0, 1] - "DC_CODE": "round(123.123)", # student_args is len 1 - "DC_SCT": "test_function_v2('round', params=['number', 'ndigits'], do_eval = None)"} - sct_payload = helper.run(self.data) - self.assertFalse(sct_payload['correct']) - -class TestDoEvalList(unittest.TestCase): - def setUp(self): - self.data = {"DC_PEC": '', - "DC_SOLUTION": "pow(3, 2, 4)", - "DC_SCT": "test_function_v2('pow', params=['x','y','z'], do_eval = [True, False, None])"} - - def test_do_eval_1(self): - self.data["DC_CODE"] = "pow(3, 2, 4)" - sct_payload = helper.run(self.data) - self.assertTrue(sct_payload['correct']) - - def test_do_eval_2(self): - self.data["DC_CODE"] = "pow(4, 2, 4)" - sct_payload = helper.run(self.data) - self.assertFalse(sct_payload['correct']) - - def test_do_eval_3(self): - self.data["DC_CODE"] = "x = 2; pow(3, x, 4)" - sct_payload = helper.run(self.data) - self.assertFalse(sct_payload['correct']) - - def test_do_eval_4(self): - self.data["DC_CODE"] = "pow(3, 2, 3)" - sct_payload = helper.run(self.data) - self.assertTrue(sct_payload['correct']) - - -if __name__ == "__main__": - unittest.main() diff --git a/tests/test_test_if_else.py b/tests/test_test_if_else.py deleted file mode 100644 index 01a948fb..00000000 --- a/tests/test_test_if_else.py +++ /dev/null @@ -1,449 +0,0 @@ -import unittest -import helper -import pytest - -class TestIfElse(unittest.TestCase): - - def setUp(self): - self.data = { - "DC_PEC": '', - "DC_SOLUTION": ''' -# Initialize offset -offset = 8 - -# Code the while loop -if offset > 8: - x = 5 -else: - x = round(2.123) - ''', - "DC_SCT": ''' -def condition_test(): - test_expression_result({"offset": 7}) - test_expression_result({"offset": 8}) - test_expression_result({"offset": 9}) - - -test_if_else(index=1, - test = condition_test, - body = lambda: test_student_typed('x\s*=\s*5', not_typed_msg = "you did something wrong"), - orelse = lambda: test_function('round')) -success_msg("Nice") - ''' - } - - self.DC_SCT_NO_LAM = ''' -condition_test = [ - test_expression_result({"offset": 7}), - test_expression_result({"offset": 8}), - test_expression_result({"offset": 9}) -] - - -test_if_else(index=1, - test = condition_test, - body = test_student_typed('x\s*=\s*5', not_typed_msg = "you did something wrong"), - orelse = test_function('round')) -success_msg("Nice") - ''' - - def test_Pass(self): - self.data["DC_CODE"] = ''' -# Initialize offset -offset = 8 - -# Code the while loop -if offset > 8: - x = 5 -else: - x = round(2.123) - ''' - sct_payload = helper.run(self.data) - self.assertTrue(sct_payload['correct']) - self.assertEqual(sct_payload['message'], "Nice") - - def test_Fail0(self): - self.data["DC_CODE"] = ''' -# Initialize offset -offset = 8 - ''' - sct_payload = helper.run(self.data) - self.assertFalse(sct_payload['correct']) - self.assertEqual(sct_payload['message'], "The system wants to check the first if statement, but it hasn't found it. Have another look at your code.") - - def test_Fail1(self): - self.data["DC_CODE"] = ''' -# Initialize offset -offset = 8 - -# Code the while loop -if offset > 10: - x = 5 -else: - x = round(2.123) - ''' - sct_payload = helper.run(self.data) - self.assertFalse(sct_payload['correct']) - self.assertIn("Expected ", sct_payload['message']) - helper.test_lines(self, sct_payload, 6, 6, 4, 14) - - def test_Fail2(self): - self.data["DC_CODE"] = ''' -# Initialize offset -offset = 8 - -# Code the while loop -if offset > 8: - x = 7 -else: - x = round(2.123) - ''' - sct_payload = helper.run(self.data) - self.assertFalse(sct_payload['correct']) - self.assertIn("Check the first if statement. Did you correctly specify the body? you did something wrong", sct_payload['message']) - helper.test_lines(self, sct_payload, 7, 7, 5, 9) - - def test_Fail2a(self): - self.data["DC_CODE"] = ''' -# Initialize offset -offset = 8 - -# Code the while loop -if offset > 8: - x = 7 - y = 12 -else: - x = round(2.123) - ''' - sct_payload = helper.run(self.data) - self.assertFalse(sct_payload['correct']) - self.assertIn("Check the first if statement. Did you correctly specify the body? you did something wrong", sct_payload['message']) - helper.test_lines(self, sct_payload, 7, 8, 5, 10) - - def test_Fail3(self): - self.data["DC_CODE"] = ''' -# Initialize offset -offset = 8 - -# Code the while loop -if offset > 8: - x = 5 -else: - x = 8 - ''' - sct_payload = helper.run(self.data) - self.assertFalse(sct_payload['correct']) - self.assertIn("Did you call round()", sct_payload["message"]) - helper.test_lines(self, sct_payload, 9, 9, 5, 9) - - def test_Fail3b(self): - self.data["DC_CODE"] = ''' -# Initialize offset -offset = 8 - -# Code the while loop -if offset > 8: - x = 5 -else: - x = round(2.2121314) - ''' - sct_payload = helper.run(self.data) - self.assertFalse(sct_payload['correct']) - # should give line numbers of more detailed test_function test. - helper.test_lines(self, sct_payload, 9, 9, 15, 23) - - def test_Pass_no_lam(self): - self.data["DC_SCT"] = self.DC_SCT_NO_LAM - self.test_Pass() - - def test_Fail0_no_lam(self): - self.data["DC_SCT"] = self.DC_SCT_NO_LAM - self.test_Fail0() - - def test_Fail3_no_lam(self): - self.data["DC_SCT"] = self.DC_SCT_NO_LAM - self.test_Fail3() - - def test_Fail3b_no_lam(self): - self.data["DC_SCT"] = self.DC_SCT_NO_LAM - self.test_Fail3b() - - -class CheckIfElse(unittest.TestCase): - - def setUp(self): - self.data = { - "DC_PEC": '', - "DC_SOLUTION": ''' -offset = 8 -if offset > 8: - x = 5 -else: - x = round(2.123) - ''', - "DC_SCT": ''' -Ex().check_if_else().multi( - check_test().multi( - set_env(offset = 7).has_equal_value(), - set_env(offset = 8).has_equal_value(), - set_env(offset = 9).has_equal_value()), - check_body().has_code('x\s*=\s*5'), - check_orelse().check_function('round').check_args('number').has_equal_value() -) - ''' - } - - def test_Pass(self): - self.data["DC_CODE"] = ''' -# Initialize offset -offset = 8 - -# Code the while loop -if offset > 8: - x = 5 -else: - x = round(2.123) - ''' - sct_payload = helper.run(self.data) - self.assertTrue(sct_payload['correct']) - - def test_Fail0(self): - self.data["DC_CODE"] = ''' -# Initialize offset -offset = 8 - ''' - sct_payload = helper.run(self.data) - self.assertFalse(sct_payload['correct']) - - -class TestIfElseEmbedded(unittest.TestCase): - - def setUp(self): - self.data = { - "DC_PEC": '', - "DC_SOLUTION": ''' -offset = 8 -if offset > 8: - x = 5 -elif offset > 5: - x = 7 -else: - x = round(9) - ''', - "DC_SCT": ''' -def test_test(): - test_expression_result({"offset": 7}) - test_expression_result({"offset": 8}) - test_expression_result({"offset": 9}) - -def body_test(): - test_student_typed('5', not_typed_msg = "incorrect_if") - -def orelse_test(): - def test_test2(): - test_expression_result({"offset": 4}) - test_expression_result({"offset": 5}) - test_expression_result({"offset": 6}) - def body_test2(): - test_student_typed('7', not_typed_msg = 'incorrect_elif') - def orelse_test2(): - test_function('round') - test_if_else(index = 1, - test = test_test2, - body = body_test2, - orelse = orelse_test2, - expand_message = False) - -test_if_else(index=1, - test=test_test, - body=body_test, - orelse=orelse_test, - expand_message = False) - -success_msg("Nice") - ''' - } - self.IF_EXP_SOLUTION = ''' -offset = 8 -x = 5 if offset > 8 else 7 if offset > 5 else round(9) -''' - - def testPass(self): - self.data["DC_CODE"] = ''' -offset = 8 -if offset > 8: - x = 5 -elif offset > 5: - x = 7 -else: - x = round(9) - ''' - sct_payload = helper.run(self.data) - self.assertTrue(sct_payload['correct']) - - def test_file_if_cond(self): - self.data["DC_CODE"] = ''' -offset = 8 -if offset > 9: - x = 5 -elif offset > 5: - x = 7 -else: - x = round(9) - ''' - sct_payload = helper.run(self.data) - self.assertFalse(sct_payload['correct']) - self.assertIn("Expected ", sct_payload['message']) - helper.test_lines(self, sct_payload, 3, 3, 4, 13) - - def test_fail_if_body(self): - self.data["DC_CODE"] = ''' -offset = 8 -if offset > 8: - x = 6 -elif offset > 5: - x = 7 -else: - x = round(9) - ''' - sct_payload = helper.run(self.data) - self.assertFalse(sct_payload['correct']) - self.assertEqual(sct_payload['message'], "incorrect_if") - helper.test_lines(self, sct_payload, 4, 4, 5, 9) - - def test_fail_elif_cond(self): - self.data["DC_CODE"] = ''' -offset = 8 -if offset > 8: - x = 5 -elif offset > 6: - x = 7 -else: - x = round(9) - ''' - sct_payload = helper.run(self.data) - self.assertFalse(sct_payload['correct']) - self.assertIn("Expected ", sct_payload['message']) - helper.test_lines(self, sct_payload, 5, 5, 6, 15) - - def test_fail_elif_body(self): - self.data["DC_CODE"] = ''' -offset = 8 -if offset > 8: - x = 5 -elif offset > 5: - x = 8 -else: - x = round(9) - ''' - sct_payload = helper.run(self.data) - self.assertFalse(sct_payload['correct']) - self.assertEqual(sct_payload['message'], "incorrect_elif") - helper.test_lines(self, sct_payload, 6, 6, 5, 9) - - def test_fail_else_body(self): - self.data["DC_CODE"] = ''' -offset = 8 -if offset > 8: - x = 5 -elif offset > 5: - x = 7 -else: - x = round(10) - ''' - sct_payload = helper.run(self.data) - self.assertFalse(sct_payload['correct']) - self.assertIn("Check your call of round().", sct_payload['message']) - helper.test_lines(self, sct_payload, 8, 8, 15, 16) - - def testPass_if_exp(self): - self.data["DC_SOLUTION"] = self.IF_EXP_SOLUTION - self.data["DC_SCT"] = helper.replace_test_if(self.data["DC_SCT"]) - self.data["DC_CODE"] = self.data["DC_SOLUTION"] - sct_payload = helper.run(self.data) - self.assertTrue(sct_payload['correct']) - - def test_fail_if_cond_if_exp(self): - self.data["DC_SOLUTION"] = self.IF_EXP_SOLUTION - self.data["DC_CODE"] = "x = 5 if offset > 9 else 7 if offset > 5 else round(9)" - self.data["DC_SCT"] = helper.replace_test_if(self.data["DC_SCT"]) - sct_payload = helper.run(self.data) - self.assertFalse(sct_payload['correct']) - self.assertIn("Expected ", sct_payload['message']) - helper.test_lines(self, sct_payload, 1, 1, 10, 19) - - def test_fail_if_body_if_exp(self): - self.data["DC_SOLUTION"] = self.IF_EXP_SOLUTION - self.data["DC_CODE"] = "x = 6 if offset > 8 else 7 if offset > 5 else round(9)" - self.data["DC_SCT"] = helper.replace_test_if(self.data["DC_SCT"]) - sct_payload = helper.run(self.data) - self.assertFalse(sct_payload['correct']) - self.assertEqual(sct_payload['message'], "incorrect_if") - - def test_fail_elif_cond_if_exp(self): - self.data["DC_SOLUTION"] = self.IF_EXP_SOLUTION - self.data["DC_CODE"] = "x = 5 if offset > 8 else 7 if offset > 6 else round(9)" - self.data["DC_SCT"] = helper.replace_test_if(self.data["DC_SCT"]) - sct_payload = helper.run(self.data) - self.assertFalse(sct_payload['correct']) - self.assertIn("Expected ", sct_payload['message']) - - def test_fail_else_body_if_exp(self): - self.data["DC_SOLUTION"] = self.IF_EXP_SOLUTION - self.data["DC_CODE"] = "5 if offset > 8 else 7 if offset > 5 else round(10)" - self.data["DC_SCT"] = helper.replace_test_if(self.data["DC_SCT"]) - sct_payload = helper.run(self.data) - self.assertFalse(sct_payload['correct']) - self.assertIn("Check your call of round().", sct_payload['message']) - -class TestIfExp(unittest.TestCase): - def setUp(self): - self.data = { - "DC_SOLUTION": """ -x = 2 if True else 1 -def f(): 4 if True else 3 -y = 5 if True else 4 - """, - "DC_SCT": "test_if_exp(index=2, body=lambda: test_expression_result())", - "DC_CODE": "x = 2 if True else 1; y = 5 if True else 4" - } - - def test_if_exp_skips_func_body(self): - sct_payload = helper.run(self.data) - self.assertTrue(sct_payload['correct']) - - def test_if_exp_within_func(self): - self.data["DC_SCT"] = "test_function_definition('f', body=lambda: test_if_exp(1, body=lambda: test_expression_result()))" - self.data["DC_CODE"] = "def f(): return 4 if True else 3" - sct_payload = helper.run(self.data) - self.assertTrue(sct_payload['correct']) - - def test_if_exp_within_func_fail(self): - self.data["DC_SCT"] = "test_function_definition('f', body=lambda: test_if_exp(body=lambda: test_expression_result()))" - self.data["DC_CODE"] = "def f(): return 'wrong' if True else 3" - sct_payload = helper.run(self.data) - self.assertFalse(sct_payload['correct']) -class TestIfExpListComp(unittest.TestCase): - def setUp(self): - self.data = { - "DC_SOLUTION": """[i**2 if i> 5 else 0 for i in range(0,10)]""", - "DC_SCT": """ -test_list_comp(body = lambda: test_if_exp( - body=lambda: test_student_typed(r"\s*i\*\*2\s*"), - test= lambda: test_expression_result(context_vals = [6]), - orelse= lambda: test_expression_result())) """ - } - - def test_pass(self): - self.data["DC_CODE"] = self.data["DC_SOLUTION"] - sct_payload = helper.run(self.data) - self.assertTrue(sct_payload['correct']) - - def test_fail_body(self): - self.data["DC_CODE"] = """[i**3 if i> 5 else 0 for i in range(0,10)]""" - sct_payload = helper.run(self.data) - self.assertFalse(sct_payload['correct']) - - -if __name__ == "__main__": - unittest.main() diff --git a/tests/test_test_list_comp.py b/tests/test_test_list_comp.py deleted file mode 100644 index ae71772f..00000000 --- a/tests/test_test_list_comp.py +++ /dev/null @@ -1,316 +0,0 @@ -import unittest -import helper -import pytest - -class TestListCompStepByStep(unittest.TestCase): - - def setUp(self): - self.data = { - "DC_PEC": "x = {'a': 2, 'b':3, 'c':4, 'd':'test'}", - "DC_SOLUTION": "[key + str(val) for key,val in x.items() if isinstance(key, str) if isinstance(val, int)]", - "DC_SCT": ''' -test_list_comp(index=1, - not_called_msg=None, - comp_iter=lambda: test_expression_result(), - iter_vars_names=True, - incorrect_iter_vars_msg=None, - body=lambda: test_expression_result(context_vals = ['a', 2]), - ifs=[lambda: test_function_v2('isinstance', params = ['obj'], do_eval = [False]), - lambda: test_function_v2('isinstance', params = ['obj'], do_eval = [False])], - insufficient_ifs_msg=None, - expand_message=True) - ''' - } - - def test_fail_1(self): - self.data["DC_CODE"] = "" - sct_payload = helper.run(self.data) - self.assertFalse(sct_payload['correct']) - self.assertEqual(sct_payload['message'], "The system wants to check the first list comprehension but hasn't found it.") - - def test_fail_2(self): - self.data["DC_CODE"] = "[key for key in x.keys()]" - sct_payload = helper.run(self.data) - self.assertFalse(sct_payload['correct']) - self.assertIn("Check the first list comprehension. Did you correctly specify the iterable part?", sct_payload['message']) - helper.test_lines(self, sct_payload, 1, 1, 17, 24) - - def test_fail_3(self): - self.data["DC_CODE"] = "[a + str(b) for a,b in x.items()]" - sct_payload = helper.run(self.data) - self.assertFalse(sct_payload['correct']) - self.assertEqual(sct_payload['message'], "Have you used the correct iterator variables in the first list comprehension? Be sure to use the correct names.") - helper.test_lines(self, sct_payload, 1, 1, 17, 19) - - def test_fail_4(self): - self.data["DC_CODE"] = "[key + '_' + str(val) for key,val in x.items()]" - sct_payload = helper.run(self.data) - self.assertIn("Check the first list comprehension. Did you correctly specify the body?", sct_payload['message']) - helper.test_lines(self, sct_payload, 1, 1, 2, 21) - - def test_fail_5(self): - self.data["DC_CODE"] = "[key + str(val) for key,val in x.items()]" - sct_payload = helper.run(self.data) - self.assertFalse(sct_payload['correct']) - self.assertEqual("Have you used 2 ifs inside the first list comprehension?", sct_payload['message']) - # helper.test_lines(self, sct_payload, 1, 1, 2, 41) # small hiccup! - - def test_fail_6(self): - self.data["DC_CODE"] = "[key + str(val) for key,val in x.items() if hasattr(key, 'test') if hasattr(key, 'test')]" - sct_payload = helper.run(self.data) - self.assertFalse(sct_payload['correct']) - self.assertEqual("Check the first list comprehension. Did you correctly specify the first if? Did you call isinstance()?", sct_payload['message']) - helper.test_lines(self, sct_payload, 1, 1, 45, 64) - - def test_fail_7(self): - self.data["DC_CODE"] = "[key + str(val) for key,val in x.items() if isinstance(key, str) if hasattr(key, 'test')]" - sct_payload = helper.run(self.data) - self.assertFalse(sct_payload['correct']) - self.assertEqual("Check the first list comprehension. Did you correctly specify the second if? Did you call isinstance()?", sct_payload['message']) - helper.test_lines(self, sct_payload, 1, 1, 69, 88) - - def test_fail_8(self): - self.data["DC_CODE"] = "[key + str(val) for key,val in x.items() if isinstance(key, str) if isinstance(key, str)]" - sct_payload = helper.run(self.data) - self.assertFalse(sct_payload['correct']) - helper.test_lines(self, sct_payload, 1, 1, 80, 82) - - def test_pass(self): - self.data["DC_CODE"] = "[key + str(val) for key,val in x.items() if isinstance(key, str) if isinstance(val, str)]" - sct_payload = helper.run(self.data) - self.assertTrue(sct_payload['correct']) - - def test_pass_no_lam(self): - self.data["DC_CODE"] = "[key + str(val) for key,val in x.items() if isinstance(key, str) if isinstance(val, str)]" - self.data["DC_SCT"] = helper.remove_lambdas(self.data["DC_SCT"]) - sct_payload = helper.run(self.data) - self.assertTrue(sct_payload['correct']) - - def test_pass_mix_lam(self): - self.data["DC_CODE"] = "[key + str(val) for key,val in x.items() if isinstance(key, str) if isinstance(val, str)]" - self.data["DC_SCT"] = ''' -test_list_comp(index=1, - not_called_msg=None, - comp_iter=lambda: test_expression_result(), - iter_vars_names=True, - incorrect_iter_vars_msg=None, - body=test_expression_result(context_vals = ['a', 2]), - ifs=[test_function_v2('isinstance', params = ['obj'], do_eval = [False]), - test_function_v2('isinstance', params = ['obj'], do_eval = [False])], - insufficient_ifs_msg=None, - expand_message=True) - ''' - sct_payload = helper.run(self.data) - self.assertTrue(sct_payload['correct']) - - def test_pass_exchain(self): - self.data["DC_CODE"] = "[key + str(val) for key,val in x.items() if isinstance(key, str) if isinstance(val, str)]" - self.data["DC_SCT"] = "Ex().\\" + helper.remove_lambdas(self.data["DC_SCT"]) - - sct_payload = helper.run(self.data) - self.assertTrue(sct_payload['correct']) - -class TestListCompStepByStepCustom(unittest.TestCase): - - def setUp(self): - self.data = { - "DC_PEC": "x = {'a': 2, 'b':3, 'c':4, 'd':'test'}", - "DC_SOLUTION": "[key + str(val) for key,val in x.items() if isinstance(key, str) if isinstance(val, int)]", - "DC_SCT": ''' -test_list_comp(index=1, - not_called_msg='notcalled', - comp_iter=lambda: test_expression_result(incorrect_msg = 'iterincorrect'), - iter_vars_names=True, - incorrect_iter_vars_msg='incorrectitervars', - body=lambda: test_expression_result(context_vals = ['a', 2], incorrect_msg = 'bodyincorrect'), - ifs=[lambda: test_function_v2('isinstance', params = ['obj'], do_eval = [False], not_called_msg = 'notcalled1', incorrect_msg = 'incorrect2'), - lambda: test_function_v2('isinstance', params = ['obj'], do_eval = [False], not_called_msg = 'notcalled2', incorrect_msg = 'incorrect2')], - insufficient_ifs_msg='insufficientifs') - ''' - } - - def test_fail_1(self): - self.data["DC_CODE"] = "" - sct_payload = helper.run(self.data) - self.assertFalse(sct_payload['correct']) - self.assertEqual(sct_payload['message'], "notcalled") - - def test_fail_2(self): - self.data["DC_CODE"] = "[key for key in x.keys()]" - sct_payload = helper.run(self.data) - self.assertFalse(sct_payload['correct']) - self.assertEqual("iterincorrect", sct_payload['message']) - helper.test_lines(self, sct_payload, 1, 1, 17, 24) - - def test_fail_3(self): - self.data["DC_CODE"] = "[a + str(b) for a,b in x.items()]" - sct_payload = helper.run(self.data) - self.assertFalse(sct_payload['correct']) - self.assertEqual(sct_payload['message'], "incorrectitervars") - helper.test_lines(self, sct_payload, 1, 1, 17, 19) - - def test_fail_4(self): - self.data["DC_CODE"] = "[key + '_' + str(val) for key,val in x.items()]" - sct_payload = helper.run(self.data) - self.assertEqual("bodyincorrect", sct_payload['message']) - helper.test_lines(self, sct_payload, 1, 1, 2, 21) - - def test_fail_5(self): - self.data["DC_CODE"] = "[key + str(val) for key,val in x.items()]" - sct_payload = helper.run(self.data) - self.assertFalse(sct_payload['correct']) - self.assertEqual("insufficientifs", sct_payload['message']) - # helper.test_lines(self, sct_payload, 1, 1, 2, 41) # small hiccup! - - def test_fail_6(self): - self.data["DC_CODE"] = "[key + str(val) for key,val in x.items() if hasattr(key, 'test') if hasattr(key, 'test')]" - sct_payload = helper.run(self.data) - self.assertFalse(sct_payload['correct']) - self.assertEqual("notcalled1", sct_payload['message']) - helper.test_lines(self, sct_payload, 1, 1, 45, 64) - - def test_fail_7(self): - self.data["DC_CODE"] = "[key + str(val) for key,val in x.items() if isinstance(key, str) if hasattr(key, 'test')]" - sct_payload = helper.run(self.data) - self.assertFalse(sct_payload['correct']) - self.assertEqual("notcalled2", sct_payload['message']) - helper.test_lines(self, sct_payload, 1, 1, 69, 88) - - def test_fail_8(self): - self.data["DC_CODE"] = "[key + str(val) for key,val in x.items() if isinstance(key, str) if isinstance(key, str)]" - sct_payload = helper.run(self.data) - self.assertFalse(sct_payload['correct']) - self.assertEqual("incorrect2", sct_payload['message']) - helper.test_lines(self, sct_payload, 1, 1, 80, 82) - - def test_pass(self): - self.data["DC_CODE"] = "[key + str(val) for key,val in x.items() if isinstance(key, str) if isinstance(val, str)]" - sct_payload = helper.run(self.data) - self.assertTrue(sct_payload['correct']) - - - def test_pass_no_lam(self): - self.data["DC_CODE"] = "[key + str(val) for key,val in x.items() if isinstance(key, str) if isinstance(val, str)]" - self.data["DC_SCT"] = helper.remove_lambdas(self.data["DC_SCT"]) - sct_payload = helper.run(self.data) - self.assertTrue(sct_payload['correct']) - - -class TestListCompNested(unittest.TestCase): - def setUp(self): - self.data = { - "DC_PEC": "", - "DC_SOLUTION": "[[col for col in range(5)] for row in range(5)]", - "DC_SCT": "test_list_comp(1, body = lambda: test_list_comp(1, body = lambda: test_expression_result(context_vals = [4])))" - } - - def test_fail(self): - self.data["DC_CODE"] = "[[col + 1 for col in range(5)] for row in range(5)]" - sct_payload = helper.run(self.data) - self.assertFalse(sct_payload['correct']) - - def test_pass(self): - self.data["DC_CODE"] = "[[col for col in range(5)] for row in range(5)]" - sct_payload = helper.run(self.data) - self.assertTrue(sct_payload['correct']) - - def test_fail_no_lam(self): - self.data["DC_CODE"] = "[[col + 1 for col in range(5)] for row in range(5)]" - self.data["DC_SCT"] = helper.remove_lambdas(self.data["DC_SCT"]) - sct_payload = helper.run(self.data) - self.assertFalse(sct_payload['correct']) - - def test_pass_no_lam(self): - self.data["DC_CODE"] = "[[col for col in range(5)] for row in range(5)]" - self.data["DC_SCT"] = helper.remove_lambdas(self.data["DC_SCT"]) - sct_payload = helper.run(self.data) - self.assertTrue(sct_payload['correct']) - - def test_pass_mix_lam1(self): - self.data["DC_CODE"] = "[[col for col in range(5)] for row in range(5)]" - self.data["DC_SCT"] = "test_list_comp(1, body = test_list_comp(1, body = lambda: test_expression_result(context_vals = [4])))" - sct_payload = helper.run(self.data) - self.assertTrue(sct_payload['correct']) - - def test_pass_mix_lam2(self): - self.data["DC_CODE"] = "[[col for col in range(5)] for row in range(5)]" - self.data["DC_SCT"] = "test_list_comp(1, body = lambda: test_list_comp(1, body = test_expression_result(context_vals = [4])))" - sct_payload = helper.run(self.data) - self.assertTrue(sct_payload['correct']) - - -class TestListIterVars(unittest.TestCase): - def setUp(self): - self.data = { - "DC_PEC": "x = {'a':1, 'b':2}", - "DC_SOLUTION": "[key for key, value in x.items()]", - "DC_SCT": "test_list_comp(1, iter_vars_names=False)" - } - - def test_fail(self): - self.data["DC_CODE"] = "[a for a in x.items()]" - sct_payload = helper.run(self.data) - self.assertFalse(sct_payload['correct']) - self.assertEqual(sct_payload['message'], "Have you used 2 iterator variables in the first list comprehension?") - - def test_pass(self): - self.data["DC_CODE"] = "[a for a,b in x.items()]" - sct_payload = helper.run(self.data) - self.assertTrue(sct_payload['correct']) - - def test_fail_spec2(self): - self.data["DC_SCT"] = "Ex().check_list_comp(0).has_context()" - self.data["DC_CODE"] = "[a for a in x.items()]" - sct_payload = helper.run(self.data) - self.assertFalse(sct_payload['correct']) - - def test_pass_spec2(self): - self.data["DC_SCT"] = "Ex().check_list_comp(0).has_context()" - self.test_pass() - - def test_fail_spec2_exact_names(self): - self.data["DC_CODE"] = "[a for a,b in x.items()]" - self.data["DC_SCT"] = "Ex().check_list_comp(0).has_context(exact_names=True)" - sct_payload = helper.run(self.data) - self.assertFalse(sct_payload['correct']) - - -class TestListDestructuring(unittest.TestCase): - def setUp(self): - self.data = { - "DC_PEC": "x = {'a':1, 'b':2}", - "DC_SOLUTION": "[key for key, value in x.items()]", - "DC_SCT": "test_list_comp(1, body=test_expression_result(context_vals=[(1,2)]), iter_vars_names=False)" - } - - @unittest.expectedFailure - def test_pass_destructuring1(self): - # TODO: fails because context_vals set by simple iteration and for reason below - self.data["DC_CODE"] = "[a[0] for *a in x.items()]" - sct_payload = helper.run(self.data) - self.assertTrue(sct_payload['correct']) - - def test_pass_destructuring2(self): - self.data["DC_CODE"] = "[a for *a, b in x.items()]" - sct_payload = helper.run(self.data) - self.assertTrue(sct_payload['correct']) - - def test_pass_destructuring3(self): - self.data["DC_CODE"] = "[b for b, *a in x.items()]" - sct_payload = helper.run(self.data) - self.assertTrue(sct_payload['correct']) - - @unittest.expectedFailure - def test_pass_destructuring4(self): - # TODO: fails because it tests for exact same number of iter vars - self.data["DC_CODE"] = "[k for k, v, *a in x.items()]" - sct_payload = helper.run(self.data) - self.assertTrue(sct_payload['correct']) - - def test_fail_destructuring(self): - self.data["DC_CODE"] = "[a for k, v, *a in x.items()]" - sct_payload = helper.run(self.data) - self.assertFalse(sct_payload['correct']) - -if __name__ == "__main__": - unittest.main() diff --git a/tests/test_test_loop.py b/tests/test_test_loop.py deleted file mode 100644 index 28a2385f..00000000 --- a/tests/test_test_loop.py +++ /dev/null @@ -1,273 +0,0 @@ -import unittest -import helper -import pytest - -class TestForLoop(unittest.TestCase): - - def setUp(self): - self.data = { - "DC_PEC": '', - "DC_SOLUTION": ''' -size = 1 -for n in range(10): - size = size + 2*n - size = size - n - x = "%d:%d" % (n, size) -''', - "DC_SCT": ''' -test_for_loop(1, - lambda: test_function("range"), - lambda: test_object_after_expression("size", {"size": 1}, [1])) -success_msg("Great!") -''' - } - - def test_Pass(self): - self.data["DC_CODE"] = ''' -size = 1 -for i in range(10): - size = size + i - x = "%d:%d" % (i, size) - ''' - sct_payload = helper.run(self.data) - self.assertTrue(sct_payload['correct']) - self.assertEqual(sct_payload['message'], "Great!") - - def test_Fail(self): - self.data["DC_CODE"] = ''' -size = 1 -for i in range(20): - size = size + i - x = "%d:%d" % (i, size) - ''' - sct_payload = helper.run(self.data) - self.assertFalse(sct_payload['correct']) - helper.test_lines(self, sct_payload, 3, 3, 16, 17) - - def test_Fail2(self): - self.data["DC_CODE"] = ''' -size = 1 -for i in range(10): - size = size + i + 1 - x = "%d:%d" % (i, size) - ''' - sct_payload = helper.run(self.data) - self.assertFalse(sct_payload['correct']) - # should be detailed - helper.test_lines(self, sct_payload, 4, 4, 5, 23) - - def test_Pass_no_lam(self): - self.data["DC_SCT"] = helper.remove_lambdas(self.data["DC_SCT"]) - self.test_Pass() - - def test_Fail_no_lam(self): - self.data["DC_SCT"] = helper.remove_lambdas(self.data["DC_SCT"]) - self.test_Fail() - - def test_Fail_mix_lam(self): - self.data["DC_SCT"] = helper.remove_lambdas(self.data["DC_SCT"], count=1) - self.test_Fail() - - def test_Pass_exchain(self): - self.data["DC_SCT"] = "Ex().\\" + helper.remove_lambdas(self.data["DC_SCT"]) - self.test_Pass() - - def test_has_context_pass(self): - self.data["DC_CODE"] = "for i in range(10): pass" - self.data["DC_SCT"] = "Ex().check_for_loop(0).has_context()" - sct_payload = helper.run(self.data) - self.assertTrue(sct_payload['correct']) - - def test_has_context_mult_pass(self): - self.data["DC_SOLUTION"] = "for x,y in zip(range(10), range(10)): pass" - self.data["DC_CODE"] = "for i,j in zip(range(10), range(10)): pass" - self.data["DC_SCT"] = "Ex().check_for_loop(0).has_context()" - sct_payload = helper.run(self.data) - self.assertTrue(sct_payload['correct']) - - def test_has_context_fail(self): - self.data["DC_CODE"] = "for i in range(10): pass" - self.data["DC_SCT"] = "Ex().check_for_loop(0).has_context(exact_names=True)" - sct_payload = helper.run(self.data) - self.assertFalse(sct_payload['correct']) - - def test_has_context_mult_fail(self): - self.data["DC_SOLUTION"] = "for x,y in zip(range(10), range(10)): pass" - self.data["DC_CODE"] = "for i,j in zip(range(10), range(10)): pass" - self.data["DC_SCT"] = "Ex().check_for_loop(0).has_context(exact_names=True)" - sct_payload = helper.run(self.data) - self.assertFalse(sct_payload['correct']) - -class TestForLoop2(unittest.TestCase): - - def setUp(self): - self.data = { - "DC_PEC": '', - "DC_SOLUTION": ''' -areas = [11.25, 18.0, 20.0, 10.75, 9.50] -for index, area in enumerate(areas) : - print("room " + str(index) + ": " + str(area)) - ''', - "DC_SCT": ''' -msg = "loopinggonewrong" -test_for_loop(1, for_iter=lambda msg=msg: test_function("enumerate", incorrect_msg = msg)) - -msg = "blabla" -test_for_loop(1, body=lambda msg=msg: test_expression_output(incorrect_msg = msg, context_vals = [2, "test"])) -success_msg("Well done!") - ''' - } - - def test_Pass(self): - self.data["DC_CODE"] = ''' -areas = [11.25, 18.0, 20.0, 10.75, 9.50] -for test in enumerate(areas) : - print("room " + str(test[0]) + ": " + str(test[1])) - ''' - sct_payload = helper.run(self.data) - self.assertTrue(sct_payload['correct']) - self.assertEqual(sct_payload['message'], "Well done!") - - def test_Fail(self): - self.data["DC_CODE"] = ''' -areas = [11.25, 18.0, 20.0, 10.75, 9.50] -for test in enumerate(areas) : - print("roomrettektetetet" + str(test[0]) + ": " + str(test[1])) - ''' - sct_payload = helper.run(self.data) - self.assertFalse(sct_payload['correct']) - self.assertEqual("blabla", sct_payload['message']) - helper.test_lines(self, sct_payload, 4, 4, 5, 67) - - def test_Pass_no_lam(self): - self.data["DC_SCT"] = helper.remove_lambdas(self.data["DC_SCT"]) - self.test_Pass() - - def test_Fail_no_lam(self): - self.data["DC_SCT"] = helper.remove_lambdas(self.data["DC_SCT"]) - self.test_Fail() - - def test_Fail_mix_lam(self): - self.data["DC_SCT"] = helper.remove_lambdas(self.data["DC_SCT"], count=1) - self.test_Fail() - - def test_Pass_spec(self): - self.data["DC_SCT"] = ''' -SPEC2 = True -msg = "loopinggonewrong" -forl = Ex().check_for_loop(0) - -forl.check_iter()\ - .multi(test_function("enumerate", incorrect_msg=msg)) - -msg = "blabla" -forl.check_body()\ - .multi(test_expression_output(incorrect_msg = msg, context_vals = [2, "test"])) - -success_msg("Well done!") -''' - self.test_Pass() - -class TestForLoopNested(unittest.TestCase): - - def setUp(self): - self.data = { - "DC_PEC": '', - "DC_SOLUTION": ''' -for ii in range(1, 2): - for jj in list(range(ii)): - x = sum([ii,jj]) - ''', - "DC_SCT": ''' - -Ex().check_for_loop(0)\ - .check_body()\ - .set_context(ii=1)\ - .check_for_loop(0)\ - .check_body()\ - .set_context(jj=2)\ - .multi(test_function('sum', incorrect_msg="wronginnerfor")) - ''' - } - - def test_Pass(self): - self.data["DC_CODE"] = self.data['DC_SOLUTION'].replace('ii', 'aa').replace('jj', 'bb') - sct_payload = helper.run(self.data) - self.assertTrue(sct_payload['correct']) - - def test_Fail(self): - self.data["DC_CODE"] = ''' -for ii in range(1, 2): - for jj in list(range(ii)): - x = sum([ii+1,jj]) - ''' - sct_payload = helper.run(self.data) - self.assertFalse(sct_payload['correct']) - self.assertIn('wronginnerfor', sct_payload['message']) - -class TestWhileLoop(unittest.TestCase): - - def setUp(self): - self.data = { - "DC_PEC": '', - "DC_SOLUTION": ''' -offset = 8 -while offset != 0 : - offset = offset - 1 - ''', - "DC_SCT": ''' -for i in range(-1,2): - test_while_loop(1, test=lambda i=i: test_expression_result({"offset": i})) - -for i in range(3,4): - test_while_loop(1, body=lambda i=i: test_object_after_expression("offset", {"offset": i})) - -success_msg("Great!") - ''' - } - - def test_Pass(self): - self.data["DC_CODE"] = ''' -offset = 8 -while offset != 0 : - offset = offset - 1 - ''' - sct_payload = helper.run(self.data) - self.assertTrue(sct_payload['correct']) - self.assertEqual(sct_payload['message'], "Great!") - - def test_Fail(self): - self.data["DC_CODE"] = ''' -offset = 8 -while offset != 4 : - offset = offset - 1 - ''' - sct_payload = helper.run(self.data) - self.assertFalse(sct_payload['correct']) - self.assertIn("Check the first while loop. Did you correctly specify the condition?", sct_payload['message']) - self.assertIn("Expected ", sct_payload['message']) - helper.test_lines(self, sct_payload, 3, 3, 7, 17) - - def test_Fail2(self): - self.data["DC_CODE"] = ''' -offset = 8 -while offset != 0 : - offset = offset - 2 - ''' - sct_payload = helper.run(self.data) - self.assertFalse(sct_payload['correct']) - self.assertIn("Check the first while loop. Did you correctly specify the body?", sct_payload['message']) - self.assertIn("Are you sure you assigned the correct value to offset", sct_payload['message']) - helper.test_lines(self, sct_payload, 4, 4, 5, 23) - - def test_Pass_no_lam(self): - self.data["DC_SCT"] = helper.remove_lambdas(self.data["DC_SCT"], with_args=True) - self.test_Pass() - - def test_Fail2_no_lam(self): - self.data["DC_SCT"] = helper.remove_lambdas(self.data["DC_SCT"], with_args=True) - self.test_Fail2() - - -if __name__ == "__main__": - unittest.main() diff --git a/tests/test_test_object.py b/tests/test_test_object.py deleted file mode 100644 index bd7edb55..00000000 --- a/tests/test_test_object.py +++ /dev/null @@ -1,379 +0,0 @@ -import unittest -import helper -from pythonwhat.local import setup_state -from pythonwhat.Test import TestFail as TF -import pytest - -@pytest.mark.parametrize('sct', [ - "test_object('x', undefined_msg='udm', incorrect_msg='icm')", - "Ex().check_object('x', missing_msg='udm').has_equal_value(incorrect_msg='icm')" -]) -@pytest.mark.parametrize('stu_code, passes, msg', [ - ('', False, 'udm'), - ('x = 1', False, 'icm'), - ('x = 100', True, None) -]) -def test_check_object(sct, stu_code, passes, msg): - output = helper.run({ - 'DC_SOLUTION': 'x = 100', - 'DC_CODE': stu_code, - 'DC_SCT': sct - }) - assert output['correct'] == passes - if msg: assert output['message'] == msg - -@pytest.mark.parametrize('stu_code, passes', [ - ('x = filter(lambda x: x > 0, [0, 1])', False), - ('x = filter(lambda x: x > 0, [1, 1])', True) -]) -def test_check_object_exotic_compare(stu_code, passes): - output = helper.run({ - 'DC_SOLUTION': 'x = filter(lambda x: x > 0, [1, 1])', - 'DC_SCT': "Ex().check_object('x').has_equal_value()", - 'DC_CODE': stu_code - }) - assert output['correct'] == passes - -@pytest.mark.parametrize('stu_code, passes', [ - ('x = [1, 2, 3]', True), - ('x = [1, 2, 3, 4]', False) -]) -def test_check_object_custom_compare(stu_code, passes): - output = helper.run({ - "DC_SOLUTION": 'x = [4, 5, 6]', - 'DC_CODE': stu_code, - 'DC_SCT': 'Ex().check_object("x").has_equal_value(func = lambda x,y: len(x) == len(y))' - }) - assert output['correct'] == passes - -def test_check_object_single_process(): - state1pid = setup_state('x = 3', '', pid = 1) - helper.passes(state1pid.check_object('x')) - -@pytest.mark.parametrize('stu_code, passes', [ - ('arr = 4', False), - ('arr = np.array([1])', True) -]) -def test_is_instance(stu_code, passes): - output = helper.run({ - 'DC_PEC': 'import numpy as np', - 'DC_SOLUTION': 'arr = np.array([1, 2, 3, 4])', - 'DC_SCT': "import numpy; Ex().check_object('arr').is_instance(numpy.ndarray)", - 'DC_CODE': stu_code - }) - assert output['correct'] == passes - -@pytest.mark.parametrize('sct', [ - "test_data_frame('df', columns=['a'], undefined_msg='udm', not_data_frame_msg='ndfm', undefined_cols_msg='ucm', incorrect_msg='icm')", - "test_data_frame('df', columns=None, undefined_msg='udm', not_data_frame_msg='ndfm', undefined_cols_msg='ucm', incorrect_msg='icm')", - """ -import pandas as pd -Ex().check_object('df', missing_msg='udm', expand_msg='').\ - is_instance(pd.DataFrame, not_instance_msg='ndfm').\ - check_keys('a', missing_msg='ucm').has_equal_value(incorrect_msg='icm') - """, - """ -import pandas as pd -Ex().check_df('df', missing_msg='udm', expand_msg='', not_instance_msg='ndfm').\ - check_keys('a', missing_msg='ucm').has_equal_value(incorrect_msg='icm') - """ -]) -@pytest.mark.parametrize('stu_code, passes, msg', [ - ('', False, 'udm'), - ('df = 3', False, 'ndfm'), - ('df = pd.DataFrame({ "b": [1]})', False, 'ucm'), - ('df = pd.DataFrame({ "a": [1]})', False, 'icm'), - ('df = pd.DataFrame({ "a": [1, 2, 3] })', True, None), - ('df = pd.DataFrame({ "a": [1, 2, 3], "b": [3, 4, 5] })', True, None), -]) -def test_test_data_frame(sct, stu_code, passes, msg): - output = helper.run({ - 'DC_PEC': 'import pandas as pd', - 'DC_SOLUTION': 'df = pd.DataFrame({"a": [1, 2, 3]})', - 'DC_CODE': stu_code, - 'DC_SCT': sct - }) - assert output['correct'] == passes - if msg: assert output['message'] == msg - -@pytest.mark.parametrize('stu_code, passes', [ - ('x = {}', False), - ('x = {"b": 3}', False), - ('x = {"a": 3}', False), - ('x = {"a": 2}', True), - ('x = {"a": 2, "b": 3}', True), -]) -def test_check_keys(stu_code, passes): - output = helper.run({ - 'DC_SOLUTION': 'x = {"a": 2}', - 'DC_CODE': stu_code, - 'DC_SCT': 'Ex().check_object("x").check_keys("a").has_equal_value()' - }) - assert output['correct'] == passes - -@pytest.mark.parametrize('sct', [ - "Ex().test_data_frame('pivot')", - "Ex().check_df('pivot').check_keys(('visitors', 'Austin')).has_equal_value()" -]) -def test_check_keys_exotic(sct): - code = "pivot = users.pivot(index='weekday', columns='city')" - output = helper.run({ - 'DC_PEC': ''' -import pandas as pd -users = pd.read_csv('https://s3.amazonaws.com/assets.datacamp.com/production/course_1650/datasets/users.csv') -''', - 'DC_SOLUTION': code, - 'DC_CODE': code, - 'DC_SCT': sct - }) - assert output['correct'] - - -@pytest.mark.need_internet -class TestTestObjectNonDillable(unittest.TestCase): - def setUp(self): - self.data = { - "DC_PEC": "import pandas as pd; from urllib.request import urlretrieve; urlretrieve('https://s3.amazonaws.com/assets.datacamp.com/production/course_998/datasets/battledeath.xlsx', 'battledeath.xlsx')", - "DC_SOLUTION": "xl = pd.ExcelFile('battledeath.xlsx')", - "DC_SCT": "test_object('xl')" - } - - def test_step_1(self): - self.data["DC_CODE"] = "xl = pd.ExcelFile('battledeath.xlsx')" - sct_payload = helper.run(self.data) - self.assertTrue(sct_payload['correct']) - -class TestTestObjectManualConverter(unittest.TestCase): - - @pytest.mark.need_internet - @pytest.mark.compiled - def test_pass_1(self): - self.data = { - "DC_PEC": "import pandas as pd; from urllib.request import urlretrieve; urlretrieve('https://s3.amazonaws.com/assets.datacamp.com/production/course_998/datasets/battledeath.xlsx', 'battledeath.xlsx'); from urllib.request import urlretrieve; urlretrieve('https://s3.amazonaws.com/assets.datacamp.com/production/course_998/datasets/battledeath.xlsx', 'battledeath2.xlsx')", - "DC_SOLUTION": "xl = pd.ExcelFile('battledeath.xlsx')", - "DC_CODE": "xl = pd.ExcelFile('battledeath2.xlsx')", - "DC_SCT": ''' -def my_converter(x): - return(x.sheet_names) -set_converter(key = "pandas.io.excel.ExcelFile", fundef = my_converter) -test_object('xl') -''' - } - sct_payload = helper.run(self.data) - self.assertTrue(sct_payload['correct']) - -class TestTestObjectManualConverter2(unittest.TestCase): - - @pytest.mark.compiled - def test_pass_1(self): - self.data = { - "DC_PEC": "import numpy as np", - "DC_SOLUTION": "my_array = np.array([[1,2], [3,4], [5,6]])", - "DC_CODE": "my_array = np.array([[0,0], [0,0], [0,0]])", - "DC_SCT": "set_converter(key = 'numpy.ndarray', fundef = lambda x: x.shape); test_object('my_array')" - } - sct_payload = helper.run(self.data) - self.assertTrue(sct_payload['correct']) - - -class TestTestObjectEqualityChallenges(unittest.TestCase): - def test_pass1(self): - self.data = { - "DC_PEC": "import numpy as np", - "DC_SOLUTION": "x = np.mean([1, 2, 3])", - "DC_CODE": "x = 2", - "DC_SCT": "test_object('x')" - } - sct_payload = helper.run(self.data) - self.assertTrue(sct_payload['correct']) - - def test_pass2(self): - self.data = { - "DC_PEC": "import numpy as np", - "DC_SOLUTION": "x = 2.0", - "DC_CODE": "x = 2", - "DC_SCT": "test_object('x')" - } - sct_payload = helper.run(self.data) - self.assertTrue(sct_payload['correct']) - - def test_pass3(self): - self.data = { - "DC_PEC": "", - "DC_SOLUTION": "x = None", - "DC_CODE": "x = None", - "DC_SCT": "test_object('x')" - } - sct_payload = helper.run(self.data) - self.assertTrue(sct_payload['correct']) - - @pytest.mark.need_internet - def test_pass4(self): - self.data = { - "DC_PEC": "import scipy.io; from urllib.request import urlretrieve; urlretrieve('https://s3.amazonaws.com/assets.datacamp.com/production/course_998/datasets/ja_data2.mat', 'albeck_gene_expression.mat')", - "DC_SOLUTION": "mat = scipy.io.loadmat('albeck_gene_expression.mat')\nprint(type(mat))", - "DC_CODE": "mat = scipy.io.loadmat('albeck_gene_expression.mat')\nprint(type(mat))", - "DC_SCT": "test_object('mat')" - } - sct_payload = helper.run(self.data) - self.assertTrue(sct_payload['correct']) - -class TestTestObjectDeep(unittest.TestCase): - def setUp(self): - self.data = { - "DC_PEC": '', - "DC_CODE": ''' -if True: - a = 1 - -if False: - b = 2 -else: - c = 3 - -for i in range(2): - d = 4 - -x = 2 -while x > 0: - e = 5 - x -= 1 - -try: - f = 6 -except: - pass - -try: - g = 7 -except: - pass -finally: - h = 8 - -# 2 assignments -i = 9 -if True: - i = 9 - ''', - "DC_SOLUTION": ''' -if True: - a = 10 - -if False: - b = 20 -else: - c = 30 - -for i in range(2): - d = 40 - -x = 2 -while x > 0: - e = 50 - x -= 1 - -try: - f = 60 -except: - pass - -try: - g = 70 -except: - pass -finally: - h = 80 - -# 2 assignments -i = 90 -if True: - i = 90 - ''' - } - - def test_fail_if(self): - self.data["DC_SCT"] = 'test_object("a")' - sct_payload = helper.run(self.data) - self.assertFalse(sct_payload['correct']) - helper.test_lines(self, sct_payload, 3, 3, 5, 9) - - def test_fail_else(self): - self.data["DC_SCT"] = 'test_object("c")' - sct_payload = helper.run(self.data) - self.assertFalse(sct_payload['correct']) - helper.test_lines(self, sct_payload, 8, 8, 5, 9) - - def test_fail_for(self): - self.data["DC_SCT"] = 'test_object("d")' - sct_payload = helper.run(self.data) - self.assertFalse(sct_payload['correct']) - helper.test_lines(self, sct_payload, 11, 11, 5, 9) - - def test_fail_for_2(self): - self.data["DC_SCT"] = 'test_object("e")' - sct_payload = helper.run(self.data) - self.assertFalse(sct_payload['correct']) - helper.test_lines(self, sct_payload, 15, 15, 5, 9) - - def test_fail_try(self): - self.data["DC_SCT"] = 'test_object("f")' - sct_payload = helper.run(self.data) - self.assertFalse(sct_payload['correct']) - helper.test_lines(self, sct_payload, 19, 19, 5, 9) - - def test_fail_try_finally_1(self): - self.data["DC_SCT"] = 'test_object("g")' - sct_payload = helper.run(self.data) - self.assertFalse(sct_payload['correct']) - helper.test_lines(self, sct_payload, 24, 24, 5, 9) - - def test_fail_try_finally_2(self): - self.data["DC_SCT"] = 'test_object("h")' - sct_payload = helper.run(self.data) - self.assertFalse(sct_payload['correct']) - helper.test_lines(self, sct_payload, 28, 28, 5, 9) - - def test_fail_if2(self): - self.data["DC_SCT"] = 'test_object("i")' - sct_payload = helper.run(self.data) - self.assertFalse(sct_payload['correct']) - helper.test_absent_lines(self, sct_payload) - - -class TestTestObjectDifferentAssignments(unittest.TestCase): - def setUp(self): - self.data = { - "DC_PEC": '', - "DC_CODE": ''' -import pandas as pd -df = pd.DataFrame({"a": [1, 2, 3], "b": [4, 5, 6]}) -df.columns = ["c", "d"] - -df2 = pd.DataFrame({"a": [1, 2, 3], "b": [4, 5, 6]}) -df2.columns = ["e", "f"] - ''', - "DC_SOLUTION": ''' -import pandas as pd -df = pd.DataFrame({"a": [1, 2, 3], "b": [4, 5, 6]}) -df.columns = ["c", "d"] - -df2 = pd.DataFrame({"a": [1, 2, 3], "b": [4, 5, 6]}) -df2.columns = ["c", "d"] - ''' - } - - def test_pass(self): - self.data["DC_SCT"] = 'Ex().check_object("df").has_equal_value()' - sct_payload = helper.run(self.data) - self.assertTrue(sct_payload['correct']) - - def test_fail(self): - self.data["DC_SCT"] = 'Ex().check_object("df2").has_equal_value()' - sct_payload = helper.run(self.data) - self.assertFalse(sct_payload['correct']) - helper.test_absent_lines(self, sct_payload) - - -if __name__ == "__main__": - unittest.main() diff --git a/tests/test_test_object_accessed.py b/tests/test_test_object_accessed.py index 4e31971a..634c13d8 100644 --- a/tests/test_test_object_accessed.py +++ b/tests/test_test_object_accessed.py @@ -1,11 +1,23 @@ -import unittest import helper - -class TestTestObjectAccessed(unittest.TestCase): - - def setUp(self): - self.data = { - "DC_PEC": '', +import pytest + +@pytest.mark.parametrize('sct, passes, mess', [ + ('test_object_accessed("arr")', True, None), + ('test_object_accessed("ar")', False, None), + ('test_object_accessed("arr", times=2)', True, None), + ('test_object_accessed("arr", times=3)', False, "Have you accessed arr at least three times?"), + ('test_object_accessed("arr", times=3, not_accessed_msg="silly")', False, "silly"), + ('test_object_accessed("arr.shape")', True, None), + ('test_object_accessed("arr.shape", times=2)', False, "Have you accessed arr.shape at least twice?"), + ('test_object_accessed("arr.shape", times=2, not_accessed_msg="silly")', False, "silly"), + ('test_object_accessed("arr.dtype")', False, "Have you accessed arr.dtype?"), + ('test_object_accessed("arr.dtype", not_accessed_msg="silly")', False, "silly"), + ('test_object_accessed("math.e")', True, None), + ('test_object_accessed("math.pi")', False, "Have you accessed m.pi?"), + ('test_object_accessed("math.pi", not_accessed_msg="silly")', False, "silly"), +]) +def test_test_object_accessed(sct, passes, mess): + res = helper.run({ "DC_CODE": ''' import numpy as np import math as m @@ -14,81 +26,8 @@ def setUp(self): print(arr.data) print(m.e) ''', - "DC_SOLUTION": '# not used' - } - - def test_objectArr(self): - self.data["DC_SCT"] = 'test_object_accessed("arr")' - sct_payload = helper.run(self.data) - self.assertTrue(sct_payload['correct']) - - def test_objectAr(self): - self.data["DC_SCT"] = 'test_object_accessed("ar")' - sct_payload = helper.run(self.data) - self.assertFalse(sct_payload['correct']) - - def test_objectArrTwice(self): - self.data["DC_SCT"] = 'test_object_accessed("arr", times=2)' - sct_payload = helper.run(self.data) - self.assertTrue(sct_payload['correct']) - - def test_objectArrThrice(self): - self.data["DC_SCT"] = 'test_object_accessed("arr", times=3)' - sct_payload = helper.run(self.data) - self.assertFalse(sct_payload['correct']) - self.assertEqual(sct_payload['message'], "Have you accessed arr at least three times?") - - def test_objectArrThriceCustom(self): - self.data["DC_SCT"] = 'test_object_accessed("arr", times=3, not_accessed_msg="silly")' - sct_payload = helper.run(self.data) - self.assertFalse(sct_payload['correct']) - self.assertEqual(sct_payload['message'], "silly") - - def test_objectAndAttribute(self): - self.data["DC_SCT"] = 'test_object_accessed("arr.shape")' - sct_payload = helper.run(self.data) - self.assertTrue(sct_payload['correct']) - - def test_objectAndAttributeTwice(self): - self.data["DC_SCT"] = 'test_object_accessed("arr.shape", times=2)' - sct_payload = helper.run(self.data) - self.assertFalse(sct_payload['correct']) - self.assertEqual(sct_payload['message'], "Have you accessed arr.shape at least twice?") - - def test_objectAndAttributeTwiceCustom(self): - self.data["DC_SCT"] = 'test_object_accessed("arr.shape", times=2, not_accessed_msg="silly")' - sct_payload = helper.run(self.data) - self.assertFalse(sct_payload['correct']) - self.assertEqual(sct_payload['message'], "silly") - - def test_objectAndAttributeOnce(self): - self.data["DC_SCT"] = 'test_object_accessed("arr.dtype")' - sct_payload = helper.run(self.data) - self.assertFalse(sct_payload['correct']) - self.assertEqual(sct_payload['message'], "Have you accessed arr.dtype?") - - def test_objectAndAttributeOnceCustom(self): - self.data["DC_SCT"] = 'test_object_accessed("arr.dtype", not_accessed_msg="silly")' - sct_payload = helper.run(self.data) - self.assertFalse(sct_payload['correct']) - self.assertEqual(sct_payload['message'], "silly") - - def test_objectInPackageOK(self): - self.data["DC_SCT"] = 'test_object_accessed("math.e")' - sct_payload = helper.run(self.data) - self.assertTrue(sct_payload['correct']) - - def test_objectInPackageNOK(self): - self.data["DC_SCT"] = 'test_object_accessed("math.pi")' - sct_payload = helper.run(self.data) - self.assertFalse(sct_payload['correct']) - self.assertEqual(sct_payload['message'], "Have you accessed m.pi?") - - def test_objectInPackageNOKCustom(self): - self.data["DC_SCT"] = 'test_object_accessed("math.pi", not_accessed_msg="silly")' - sct_payload = helper.run(self.data) - self.assertFalse(sct_payload['correct']) - self.assertEqual(sct_payload['message'], "silly") - -if __name__ == "__main__": - unittest.main() + "DC_SOLUTION": '# not used', + "DC_SCT": sct + }) + assert res['correct'] == passes + if mess: assert res['message'] == mess diff --git a/tests/test_test_object_after_expression.py b/tests/test_test_object_after_expression.py deleted file mode 100644 index aeb61a7a..00000000 --- a/tests/test_test_object_after_expression.py +++ /dev/null @@ -1,58 +0,0 @@ -import unittest -import helper - -class TestExercise1(unittest.TestCase): - - def setUp(self): - self.data = { - "DC_PEC": '''''', - "DC_CODE": ''' -def shout(): - shout_word = 'congratulation' + '!!!' - return(shout_word) - ''', - "DC_SOLUTION": ''' -def shout(): - shout_word = 'congratulations' + '!!!' - return(shout_word) - ''', - "DC_SCT": ''' -# Test the value of shout_word -test_function_definition("shout", arg_names = False,body = lambda: test_object_after_expression("shout_word",undefined_msg = "have you defined `shout_word`?", incorrect_msg = "test")) -success_msg("Nice work!") - ''' - } - - def test_Pass(self): - sct_payload = helper.run(self.data) - self.assertFalse(sct_payload['correct']) - self.assertEqual(sct_payload['message'], 'test') - helper.test_lines(self, sct_payload, 3, 3, 5, 41) - - def test_Pass2(self): - self.data["DC_SCT"] = ''' -# Test the value of shout_word -test_function_definition("shout", arg_names = False, body = lambda: test_object_after_expression("shout_word", undefined_msg = "have you defined `shout_word`?")) -success_msg("Nice work!") - ''' - sct_payload = helper.run(self.data) - self.assertFalse(sct_payload['correct']) - self.assertEqual(sct_payload['message'], 'Check your definition of shout(). Did you correctly specify the body? Are you sure you assigned the correct value to shout_word?') - helper.test_lines(self, sct_payload, 3, 3, 5, 41) - - def test_Pass_expr_code(self): - self.data["DC_SCT"] = ''' -# Test the value of shout_word -test_function_definition("shout", arg_names = False, body = lambda: test_object_after_expression("a", expr_code = "a = 1", undefined_msg = "have you defined `a`?")) -success_msg("Nice work!") -''' - sct_payload = helper.run(self.data) - self.assertTrue(sct_payload['correct']) - - - def test_Pass_no_lam(self): - self.data["DC_SCT"] = helper.remove_lambdas(self.data["DC_SCT"]) - self.test_Pass() - -if __name__ == "__main__": - unittest.main() diff --git a/tests/test_test_with.py b/tests/test_test_with.py index bdcce8df..4f61a305 100644 --- a/tests/test_test_with.py +++ b/tests/test_test_with.py @@ -1,15 +1,63 @@ -import unittest import helper import pytest -class TestExercise1(unittest.TestCase): - - def setUp(self): - self.data = { - "DC_PEC": ''' +@pytest.mark.parametrize('sct, passes, patt, lines', [ + ( + "test_with(1, body = lambda: [test_function('print', index = i + 1) for i in range(3)])", + False, + "Check your third call of print(). Did you correctly specify the first argument? Expected something different.", + [6, 6, 11, 16] + ), + ( + "test_with(2, body = lambda: test_for_loop(1, body = lambda: test_if_else(1, body = lambda: test_function('print'))))", + True, + None, + None + ), + ( + "test_with(1, body = [test_function('print', index = i + 1) for i in range(3)], expand_message = False)", + False, + None, + [6, 6, 11, 16] + ), + ( + "test_with(2, body = test_for_loop(1, body = test_if_else(1, body = test_function('print'))))", + True, + None, + None, + ), + ( + """ +for_test = test_for_loop(1, body = test_if_else(1, body = test_function('print'))) +Ex().check_with(1).check_body().with_context(for_test) + """, + True, + None, + None + ), + ( + "Ex().check_with(0).check_body().with_context([test_function('print', index = i+1) for i in range(3)])", + False, + "Check your third call of print()", + [6, 6, 11, 16] + ), + ( + """ +# since the print func is being tested w/o SCTs setting any variables, don't need with_context +for_test = test_for_loop(1, body = test_if_else(1, body = test_function('print'))) +Ex().check_with(1).check_body().multi(for_test) + """, + True, + None, + None + ) +]) +def test_test_with_1(sct, passes, patt, lines): + res = helper.run({ + "DC_PEC": ''' from urllib.request import urlretrieve; urlretrieve('http://s3.amazonaws.com/assets.datacamp.com/production/course_998/datasets/moby_opens.txt', 'moby_dick.txt') - ''', - "DC_CODE": ''' + ''', + "DC_CODE": ''' # Read & print the first 3 lines with open('moby_dick.txt') as file: print(file.readline()) @@ -24,8 +72,8 @@ def setUp(self): for i, row in enumerate(file): if i in I: print(row) - ''', - "DC_SOLUTION": ''' + ''', + "DC_SOLUTION": ''' # Read & print the first 3 lines with open('moby_dick.txt') as file: print(file.readline()) @@ -40,78 +88,28 @@ def setUp(self): for i, row in enumerate(file): if i in I: print(row) -''' - } - - def test_Fail1(self): - self.data["DC_SCT"] = ''' -test_with(1, body = lambda: [test_function('print', index = i + 1) for i in range(3)]) -success_msg("Nice work!") - ''' - sct_payload = helper.run(self.data) - self.assertFalse(sct_payload['correct']) - self.assertIn("Check your third call of print(). Did you correctly specify the first argument? Expected something different.", sct_payload['message']) - # line info should be specific to test_function - helper.test_lines(self, sct_payload, 6, 6, 11, 16) - - def test_Pass1(self): - self.data["DC_SCT"] = ''' -test_with(2, body = lambda: test_for_loop(1, body = lambda: test_if_else(1, body = lambda: test_function('print')))) -success_msg("Nice work!") - ''' - sct_payload = helper.run(self.data) - self.assertTrue(sct_payload['correct']) - - def test_Fail2_no_lam(self): - self.data["DC_SCT"] = ''' -test_with(1, body = [test_function('print', index = i + 1) for i in range(3)], expand_message = False) -success_msg("Nice work!") - ''' - sct_payload = helper.run(self.data) - self.assertFalse(sct_payload['correct']) - # line info should be specific to test_function - helper.test_lines(self, sct_payload, 6, 6, 11, 16) - - def test_Pass1_no_lam(self): - self.data["DC_SCT"] = ''' -test_with(2, body = test_for_loop(1, body = test_if_else(1, body = test_function('print')))) -success_msg("Nice work!") - ''' - sct_payload = helper.run(self.data) - self.assertTrue(sct_payload['correct']) - - def test_Pass1_spec2(self): - self.data["DC_SCT"] = ''' -for_test = test_for_loop(1, body = test_if_else(1, body = test_function('print'))) -Ex().check_with(1).check_body().with_context(for_test) - ''' - sct_payload = helper.run(self.data) - self.assertTrue(sct_payload['correct']) - - def test_Fail1_spec2(self): - self.data["DC_SCT"] = ''' -Ex().check_with(0).check_body().with_context([test_function('print', index = i+1) for i in range(3)]) - ''' - sct_payload = helper.run(self.data) - self.assertFalse(sct_payload['correct']) - self.assertIn("Check your third call of print()", sct_payload['message']) - # line info should be specific to test_function - helper.test_lines(self, sct_payload, 6, 6, 11, 16) - - def test_Pass1_spec2_no_ctx(self): - self.data["DC_SCT"] = ''' -# since the print func is being tested w/o SCTs setting any variables, don't need with_context -for_test = test_for_loop(1, body = test_if_else(1, body = test_function('print'))) -Ex().check_with(1).check_body().multi(for_test) - ''' - sct_payload = helper.run(self.data) - self.assertTrue(sct_payload['correct']) - - -class TestExercise2(unittest.TestCase): - - def setUp(self): - self.data = { +''', "DC_SCT": sct + }) + assert res['correct'] == passes + if patt: assert patt in res['message'] + if lines: helper.with_line_info(res, *lines) + +@pytest.mark.parametrize('sct, passes, patt, lines', [ + ( + "test_with(1, context_vals=True)", + False, + "Check the first with statement. Make sure to use the correct number of context variables. It seems you defined too many.", + [3, 6, 1, 17] + ), + ( + "test_with(2, context_vals=True)", + False, + "Check the second with statement. Did you correctly specify the first context? Make sure to use the correct context variable names. Was expecting file but got not_file.", + [12, 15, 1, 22] + ) +]) +def test_test_with_2(sct, passes, patt, lines): + res = helper.run({ "DC_PEC": ''' from urllib.request import urlretrieve; urlretrieve('http://s3.amazonaws.com/assets.datacamp.com/production/course_998/datasets/moby_opens.txt', 'moby_dick.txt') ''', @@ -146,38 +144,48 @@ def setUp(self): for i, row in enumerate(file): if i in I: print(row) -''' - } - - def test_Fail1(self): - self.data["DC_SCT"] = ''' -test_with(1, context_vals=True) -success_msg("Nice work!") +''', + "DC_SCT": sct + }) + assert res['correct'] == passes + if patt: assert patt in res['message'] + if lines: helper.with_line_info(res, *lines) + +@pytest.mark.parametrize('sct, passes, patt, lines', [ + ( + "test_with(1, context_tests=lambda: test_function('open'))", + True, + None, + None, + ), + ( ''' - sct_payload = helper.run(self.data) - self.assertFalse(sct_payload['correct']) - self.assertEqual(sct_payload['message'], "Check the first with statement. Make sure to use the correct number of context variables. It seems you defined too many.") - helper.test_lines(self, sct_payload, 3, 6, 1, 17) - - def test_Fail2(self): - self.data["DC_SCT"] = ''' -test_with(2, context_vals=True) -success_msg("Nice work!") +test_with(1, context_tests=[ + lambda: test_function('open'), + lambda: test_function('open')]) + ''', + False, + "Check the first with statement. Make sure to use the correct number of context variables. It seems you defined too little.", + [3, 6, 1, 17] + ), + ( ''' - sct_payload = helper.run(self.data) - self.assertFalse(sct_payload['correct']) - self.assertEqual(sct_payload['message'], "Check the second with statement. Did you correctly specify the first context? Make sure to use the correct context variable names. Was expecting file but got not_file.") - helper.test_lines(self, sct_payload, 12, 15, 1, 22) - -class TestExercise3(unittest.TestCase): - - def setUp(self): - self.data = { - "DC_PEC": ''' +test_with(2, context_tests=[ + lambda: test_function('open'), + lambda: test_function('open')]) + ''', + False, + "Check your call of open().", + [12, 12, 46, 60] + ) +]) +def test_test_with_3(sct, passes, patt, lines): + res = helper.run({ + "DC_PEC": ''' from urllib.request import urlretrieve; urlretrieve('http://s3.amazonaws.com/assets.datacamp.com/production/course_998/datasets/moby_opens.txt', 'moby_dick.txt') from urllib.request import urlretrieve; urlretrieve('https://s3.amazonaws.com/assets.datacamp.com/production/course_998/datasets/moby_opens.txt', 'not_moby_dick.txt') - ''', - "DC_CODE": ''' + ''', + "DC_CODE": ''' # Read & print the first 3 lines with open('moby_dick.txt') as file: print(file.readline()) @@ -192,8 +200,8 @@ def setUp(self): for i, row in enumerate(not_file): if i in I: print(row) - ''', - "DC_SOLUTION": ''' + ''', + "DC_SOLUTION": ''' # Read & print the first 3 lines with open('moby_dick.txt') as file, open('moby_dick.txt'): print(file.readline()) @@ -208,187 +216,46 @@ def setUp(self): for i, row in enumerate(file): if i in I: print(row) - '''} - - def test_Pass1(self): - self.data["DC_SCT"] = ''' -test_with(1, context_tests=lambda: test_function('open')) -success_msg("Nice work!") - ''' - sct_payload = helper.run(self.data) - self.assertTrue(sct_payload['correct']) - - def test_Fail1(self): - self.data["DC_SCT"] = ''' -test_with(1, context_tests=[ - lambda: test_function('open'), - lambda: test_function('open')]) -success_msg("Nice work!") - ''' - sct_payload = helper.run(self.data) - self.assertFalse(sct_payload['correct']) - self.assertEqual(sct_payload['message'], "Check the first with statement. Make sure to use the correct number of context variables. It seems you defined too little.") - helper.test_lines(self, sct_payload, 3, 6, 1, 17) - - def test_Fail2(self): - self.data["DC_SCT"] = ''' -test_with(2, context_tests=[ - lambda: test_function('open'), - lambda: test_function('open')]) -success_msg("Nice work!") - ''' - sct_payload = helper.run(self.data) - self.assertFalse(sct_payload['correct']) - self.assertIn("Check your call of open().", sct_payload['message']) - helper.test_lines(self, sct_payload, 12, 12, 46, 60) - -class TestExercise4(unittest.TestCase): - - def setUp(self): - self.data = { - "DC_PEC": ''' -from urllib.request import urlretrieve; urlretrieve('http://s3.amazonaws.com/assets.datacamp.com/production/course_998/datasets/moby_opens.txt', 'moby_dick.txt') -f = open('cars.csv', "w") -f.write(""",cars_per_cap,country,drives_right -US,809,United States,True -AUS,731,Australia,False -JAP,588,Japan,False -IN,18,India,False -RU,200,Russia,True -MOR,70,Morocco,True -EG,45,Egypt,True""") -f.close() - ''', - "DC_CODE": ''' -with open('moby_dick.txt') as moby, open('cars.csv') as lotr: - print("First line of Moby Dick: %r." % moby.readline()) - print("First line of The Lord of The Rings: The Two Towers: %r." % lotr.readline()) - ''', - "DC_SOLUTION": ''' -with open('moby_dick.txt') as moby, open('cars.csv') as cars: - print("First line of Moby Dick: %r." % moby.readline()) - print("First line of The Lord of The Rings: The Two Towers: %r." % cars.readline()) -''' - } - - def test_Pass1(self): - self.data["DC_SCT"] = ''' -def test_with_body(): - test_function('print', 1) - test_function('print', 2) - -test_with(1, - context_tests = [ - lambda: test_function('open'), - lambda: test_function('open')], - body = test_with_body -) - ''' - sct_payload = helper.run(self.data) - self.assertTrue(sct_payload['correct']) - - def test_Pass1_no_lam(self): - self.data["DC_SCT"] = ''' -test_with(1, - context_tests = [test_function('open'), test_function('open')], - body = [test_function('print', 1), test_function('print', 2)]) - ''' - sct_payload = helper.run(self.data) - self.assertTrue(sct_payload['correct']) - -class TestExercise5(unittest.TestCase): - - def setUp(self): - self.data = { - "DC_PEC": ''' -import pandas as pd -from urllib.request import urlretrieve; urlretrieve('https://s3.amazonaws.com/assets.datacamp.com/production/course_998/datasets/sales.sas7bdat', 'sales.sas7bdat') - ''', - "DC_CODE": ''' -from sas7bdat import SAS7BDAT - -with SAS7BDAT('sales.sas7bdat') as file: - df_sas = file.to_data_frame() - -print(df_sas.head()) - ''', - "DC_SOLUTION": ''' -from sas7bdat import SAS7BDAT -with SAS7BDAT('sales.sas7bdat') as file: - df_sas = file.to_data_frame() - -print(df_sas.head()) + ''', + "DC_SCT": sct + }) + assert res['correct'] == passes + if patt: assert patt in res['message'] + if lines: helper.with_line_info(res, *lines) + +def test_test_with_destructuring(): + code = ''' +with A() as (one, *others): + print(one) + print(others) ''' - } - - def test_Pass1(self): - self.data["DC_SCT"] = ''' -test_import("sas7bdat.SAS7BDAT", same_as = False) -test_with(1, context_tests = lambda: test_function('SAS7BDAT')) -test_with(1, body = lambda: test_object_after_expression('df_sas')) -test_function('print') -test_function('df_sas.head') -success_msg("NICE WORK!!!!") - ''' - sct_payload = helper.run(self.data) - self.assertTrue(sct_payload['correct']) - -class TestDestructuring(unittest.TestCase): - def setUp(self): - self.data = { - "DC_PEC": ''' + res = helper.run({ + "DC_PEC": ''' class A: def __enter__(self): return [1,2, 3] def __exit__(self, *args, **kwargs): return - ''', - "DC_SOLUTION": ''' -with A() as (one, *others): - print(one) - print(others) -''', - "DC_SCT": ''' + ''', + "DC_SOLUTION": code, + "DC_CODE": code, + "DC_SCT": ''' test_with(1, body=[test_function('print'), test_function('print')]) ''' - } - - def test_pass(self): - self.data["DC_CODE"] = self.data["DC_SOLUTION"] - sct_payload = helper.run(self.data) - self.assertTrue(sct_payload['correct']) - -class TestHasContext(unittest.TestCase): - def setUp(self): - self.data = { - "DC_PEC": "from io import StringIO", - "DC_SOLUTION": "with StringIO() as f1, StringIO() as f2: pass", - "DC_CODE": "with StringIO() as f1, StringIO() as f2: pass", - "DC_SCT": "Ex().check_with(0).has_context()" - } - - def test_pass(self): - sct_payload = helper.run(self.data) - self.assertTrue(sct_payload['correct']) - - def test_fail(self): - self.data["DC_CODE"] = "with StringIO() as f1: pass" - sct_payload = helper.run(self.data) - self.assertFalse(sct_payload['correct']) - - def test_fail_exact_names(self): - self.data["DC_CODE"] = "with StringIO() as f3, StringIO() as f4: pass" - self.data["DC_SCT"] = "Ex().check_with(0).has_context(exact_names=True)" - sct_payload = helper.run(self.data) - self.assertFalse(sct_payload['correct']) - - def test_context_pass(self): - self.data["DC_SCT"] = "Ex().check_with(0).check_context(0).has_context()" - self.test_pass() - - def test_context_fail(self): - self.data["DC_CODE"] = "with StringIO() as f3: pass" - self.data["DC_SCT"] = "Ex().check_with(0).check_context(0).has_context(exact_names=True)" - sct_payload = helper.run(self.data) - self.assertFalse(sct_payload['correct']) - -if __name__ == "__main__": - unittest.main() + }) + assert res['correct'] + + +@pytest.mark.parametrize('sct, stu, passes', [ + ("Ex().check_with(0).has_context()", "with StringIO() as f1, StringIO() as f2: pass", True), + ("Ex().check_with(0).has_context()", "with StringIO() as f1: pass", False), + ("Ex().check_with(0).has_context(exact_names=True)", "with StringIO() as f3, StringIO() as f4: pass", False), + ("Ex().check_with(0).check_context(0).has_context()", "with StringIO() as f1, StringIO() as f2: pass", True), + ("Ex().check_with(0).check_context(0).has_context(exact_names=True)", "with StringIO() as f3: pass", False) +]) +def test_test_with_has_context(sct, stu, passes): + res = helper.run({ + "DC_PEC": "from io import StringIO", + "DC_SOLUTION": "with StringIO() as f1, StringIO() as f2: pass", + "DC_CODE": stu, + "DC_SCT": sct + }) + assert res['correct'] == passes diff --git a/tests/test_utils.py b/tests/test_utils.py index 19c81d2b..6002e441 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -1,25 +1,30 @@ -import unittest +import pytest from pythonwhat import utils -class TestUtils(unittest.TestCase): +@pytest.mark.parametrize('input, output', [ + (1, 'first'), + (2, 'second'), + (3, 'third'), + (11, '11th'), +]) +def test_get_ord(input, output): + assert utils.get_ord(input) == output - def test_get_ord(self): - self.assertEqual(utils.get_ord(1), "first") - self.assertEqual(utils.get_ord(2), "second") - self.assertEqual(utils.get_ord(3), "third") - self.assertEqual(utils.get_ord(11), "11th") +@pytest.mark.parametrize('input, output', [ + (1, 'one'), + (2, 'two'), + (3, 'three'), + (11, '11') +]) +def test_get_num(input, output): + assert utils.get_num(input) == output - def test_get_times(self): - self.assertEqual(utils.get_times(1), "once") - self.assertEqual(utils.get_times(2), "twice") - self.assertEqual(utils.get_times(3), "three times") - self.assertEqual(utils.get_times(11), "11 times") +@pytest.mark.parametrize('input, output', [ + (1, 'once'), + (2, 'twice'), + (3, 'three times'), + (11, '11 times'), +]) +def test_get_times(input, output): + assert utils.get_times(input) == output - def test_get_times(self): - self.assertEqual(utils.get_num(1), "one") - self.assertEqual(utils.get_num(2), "two") - self.assertEqual(utils.get_num(3), "three") - self.assertEqual(utils.get_num(11), "11") - -if __name__ == "__main__": - unittest.main() From e0a9921a37f3d57019eba28e4b68140bfa6f1b8a Mon Sep 17 00:00:00 2001 From: Filip Schouwenaars Date: Wed, 12 Sep 2018 14:41:08 +0200 Subject: [PATCH 012/209] spec(improve_coverage): Improve code coverage --- pythonwhat/Feedback.py | 1 - pythonwhat/State.py | 11 +-- pythonwhat/Test.py | 16 +--- pythonwhat/check_funcs.py | 23 +----- pythonwhat/check_logic.py | 9 --- pythonwhat/check_wrappers.py | 2 +- pythonwhat/parsing.py | 44 ----------- pythonwhat/probe.py | 13 +--- pythonwhat/tasks.py | 5 +- .../test_funcs/test_compound_statement.py | 2 +- pythonwhat/test_funcs/test_object.py | 2 - tests/test_check_function.py | 14 +++- tests/test_check_function_def.py | 29 ++++++- tests/test_check_if_else.py | 17 +++++ tests/test_check_object.py | 50 ++++++------ tests/test_has_import.py | 4 +- tests/test_messaging.py | 2 - tests/test_signatures.py | 1 - tests/test_spec.py | 76 +++++++++---------- tests/test_state.py | 11 +++ ...nts.py => test_test_compound_statement.py} | 23 +++++- tests/test_test_object_accessed.py | 19 +++++ 22 files changed, 184 insertions(+), 190 deletions(-) create mode 100644 tests/test_state.py rename tests/{test_test_compound_statements.py => test_test_compound_statement.py} (78%) diff --git a/pythonwhat/Feedback.py b/pythonwhat/Feedback.py index f8ce0345..2325ad6d 100644 --- a/pythonwhat/Feedback.py +++ b/pythonwhat/Feedback.py @@ -12,7 +12,6 @@ def __init__(self, message, state = None): self.line_info["column_start"] = state.highlight.first_token.start[1] self.line_info["line_end"] = state.highlight.last_token.end[0] self.line_info["column_end"] = state.highlight.last_token.end[1] - except: pass diff --git a/pythonwhat/State.py b/pythonwhat/State.py index bd1be96d..6113bb9f 100644 --- a/pythonwhat/State.py +++ b/pythonwhat/State.py @@ -172,8 +172,6 @@ def to_child_state(self, student_subtree=None, solution_subtree=None, messages = [*self.messages, append_message] if not (solution_subtree and student_subtree): - if student_parts and solution_parts: - self.update(student_parts = student_parts, solution_parts = solution_parts) return self.update(student_context = student_context, solution_context = solution_context, student_env = student_env, solution_env = solution_env, highlight = highlight, @@ -267,13 +265,8 @@ def parse_internal(x): try: res = asttokens.ASTTokens(x, parse = True) return(res, res._tree) - - except SyntaxError as e: - raise SyntaxError(str(e)) - except TypeError as e: - raise TypeError(str(e)) - - return(res) + except Exception as e: + raise InstructorError("Something went wrong when parsing PEC or solution code: %s" % str(e)) # add property methods for retrieving parser outputs -------------------------- # note that this code is an alternative means of using something like.. diff --git a/pythonwhat/Test.py b/pythonwhat/Test.py index ec5ff42b..76eacc10 100644 --- a/pythonwhat/Test.py +++ b/pythonwhat/Test.py @@ -111,15 +111,6 @@ def specific_test(self): ## Testing class -class InstanceTest(Test): - def __init__(self, obj, cls, feedback): - super().__init__(feedback) - self.obj = obj - self.cls = cls - - def specific_test(self): - self.result = isinstance(self.obj, self.cls) - class InstanceProcessTest(Test): def __init__(self, name, klass, process, feedback): super().__init__(feedback) @@ -130,9 +121,6 @@ def __init__(self, name, klass, process, feedback): def specific_test(self): self.result = isInstanceInProcess(self.name, self.klass, self.process) - - - ## Testing equality class EqualTest(Test): @@ -173,7 +161,7 @@ def objs_are(x, y, list_of_classes): def is_equal(x, y): try: if objs_are(x, y, [Exception]): - # Types of errors don't matter + # Types of errors don't matter (this is debatable) return str(x) == str(y) if objs_are(x, y, [np.ndarray, dict, list]): if np.array_equal(x, y): return True @@ -189,8 +177,6 @@ def is_equal(x, y): if x.equals(y): return True pd.util.testing.assert_series_equal(x, y) return True - elif objs_are(x, y, [Exception]): - return type(x) == type(y) and str(x) == str(y) else: return x == y diff --git a/pythonwhat/check_funcs.py b/pythonwhat/check_funcs.py index 33e9ec06..a890fdaf 100644 --- a/pythonwhat/check_funcs.py +++ b/pythonwhat/check_funcs.py @@ -291,17 +291,8 @@ def run_call(args, node, process, get_func, **kwargs): func_expr = node else: raise InstructorError("Only function definition or lambda may be called") - # args is a call string or argument list/dict - if isinstance(args, str): - parsed = ast.parse(args).body[0].value - parsed.func = func_expr - ast.fix_missing_locations(parsed) - return get_func(process = process, tree = parsed, **kwargs) - else: - # e.g. list -> {args: [...], kwargs: {}} - fmt_args = fix_format(args) - ast.fix_missing_locations(func_expr) - return get_func(process = process, tree=func_expr, call = fmt_args, **kwargs) + ast.fix_missing_locations(func_expr) + return get_func(process = process, tree=func_expr, call = args, **kwargs) MSG_CALL_INCORRECT = "__JINJA__:Calling {{argstr}} should {{action}} `{{str_sol}}`, instead got {{str_stu if str_stu == 'no printouts' else '`' + str_stu + '`'}}." MSG_CALL_ERROR = "__JINJA__:Calling {{argstr}} should {{action}} `{{str_sol}}`, instead it errored out: `{{str_stu}}`." @@ -321,13 +312,6 @@ def call(args, if error_msg is None: error_msg = MSG_CALL_ERROR_INV if test == 'error' else MSG_CALL_ERROR - if argstr is None: - bracks = stringify(fix_format(args)) - if hasattr(state.student_parts['node'], 'name'): # Lambda function doesn't have name - argstr = '`{}{}`'.format(state.student_parts['node'].name, bracks) - else: - argstr = 'it with the arguments `{}`'.format(bracks) - rep = Reporter.active_reporter assert test in ('value', 'output', 'error') @@ -372,7 +356,8 @@ def build_call(callstr, node): elif isinstance(node, ast.Lambda): # lambda body expr func_expr = node argstr = 'it with the arguments `{}`'.format(callstr.replace('f', '')) - else: raise InstructorError("You can use check_call() only on check_function_def() or check_lambda()") + else: + raise TypeError("Can't handle AST that is passed.") parsed = ast.parse(callstr).body[0].value parsed.func = func_expr diff --git a/pythonwhat/check_logic.py b/pythonwhat/check_logic.py index 3d545838..3bb63a69 100644 --- a/pythonwhat/check_logic.py +++ b/pythonwhat/check_logic.py @@ -146,21 +146,12 @@ def diagnose_and_check(state=None): # utility functions ----------------------------------------------------------- -def quiet(n = 0, state=None): - """Turn off prepended messages. Defaults to turning all off.""" - cpy = copy.copy(state) - hushed = [{**m, 'msg': ""} for m in cpy.messages] - cpy.messages = hushed - return cpy - def fail(msg="", state=None): """Fail test with message""" rep = Reporter.active_reporter _msg = state.build_message(msg) rep.do_test(Test(Feedback(_msg, state))) - return state - def override(solution, state=None): """Override the solution code with something arbitrary. diff --git a/pythonwhat/check_wrappers.py b/pythonwhat/check_wrappers.py index c8dd3f02..ac0e417b 100644 --- a/pythonwhat/check_wrappers.py +++ b/pythonwhat/check_wrappers.py @@ -56,7 +56,7 @@ for k, v in __NODE_WRAPPERS__.items(): scts['check_'+k] = partial(check_node, k+'s', typestr=v) -for k in ['set_context', 'set_env', 'disable_highlighting', 'check_not', 'check_or', 'check_correct', 'fail', 'quiet', 'override', 'multi']: +for k in ['set_context', 'set_env', 'disable_highlighting', 'check_not', 'check_or', 'check_correct', 'fail', 'override', 'multi']: scts[k] = getattr(check_logic, k) for k in ['with_context', 'check_args', 'check_call']: diff --git a/pythonwhat/parsing.py b/pythonwhat/parsing.py index 3ad499a4..b86c4073 100644 --- a/pythonwhat/parsing.py +++ b/pythonwhat/parsing.py @@ -472,10 +472,6 @@ def visit_Try(self, node): self.visit_each(node.body) self.visit_each(node.finalbody) - def visit_TryFinally(self, node): - self.visit_each(node.body) - self.visit_each(node.finalbody) - @staticmethod def get_part(name_node, ass_node=None): # either name node or simply str or name itself @@ -536,15 +532,6 @@ def visit_Compare(self, node): def visit_UnaryOp(self, node): self.visit(node.operand) - def visit_Expr(self, node): - self.visit(node.value) - - def visit_Call(self, node): - self.visit(node.func) - - def visit_Return(self, node): - self.visit(node.value) - class WhileParser(Parser): """Find while structures. @@ -670,29 +657,6 @@ def visit_Call(self, node): for key in node.keywords: self.visit(key.value) - def visit_If(self, node): - self.visit_each(node.body) - self.visit_each(node.orelse) - - def visit_While(self, node): - self.visit_each(node.body) - self.visit_each(node.orelse) - - def visit_For(self, node): - self.visit_each(node.body) - self.visit_each(node.orelse) - - def visit_With(self, node): - self.visit_each(node.body) - - def visit_Try(self, node): - self.visit_each(node.body) - self.visit_each(node.finalbody) - - def visit_TryFinally(self, node): - self.visit_each(node.body) - self.visit_each(node.finalbody) - def visit_Lambda(self, node): self.out.append(FunctionDefParser.parse_node(node)) @@ -707,14 +671,6 @@ def visit_Assign(self, node): def visit_AugAssign(self, node): self.visit(node.value) - def visit_Try(self, node): - self.visit_each(node.body) - self.visit_each(node.finalbody) - - def visit_TryFinally(self, node): - self.visit_each(node.body) - self.visit_each(node.finalbody) - def build_comp(self, node): target = node.generators[0].target tv = Parser.get_target_vars(target) diff --git a/pythonwhat/probe.py b/pythonwhat/probe.py index dcb8a1af..3995239d 100644 --- a/pythonwhat/probe.py +++ b/pythonwhat/probe.py @@ -149,13 +149,6 @@ def partial(self): def update_child_calls(self): pass -class NodeDict(Node): - def partial(self): - return OrderedDict((node.arg_name, node.partial()) for node in self.child_list) - - def update_child_calls(self): - pass - class Probe(object): def __init__(self, tree, f, eval_on_call=False): self.tree = tree @@ -203,11 +196,7 @@ def __call__(self, *args, **kwargs): def build_sub_test_nodes(test, tree, node, arg_name): # note that I've made the strong assumption that # if not a function, then test is a dict, list or tuple of them - if isinstance(test, dict): - nd = NodeDict(name = "Dict", arg_name = arg_name) - node.add_child(nd) - for k, f in test.items(): Probe.build_sub_test_nodes(f, tree, nd, k) - elif isinstance(test, (list, tuple)): + if isinstance(test, (list, tuple)): nl = NodeList(name = "List", arg_name = arg_name) node.add_child(nl) for ii, f in enumerate(test): Probe.build_sub_test_nodes(f, tree, nl, str(ii)) diff --git a/pythonwhat/tasks.py b/pythonwhat/tasks.py index 5c63d15d..af320c2e 100644 --- a/pythonwhat/tasks.py +++ b/pythonwhat/tasks.py @@ -68,10 +68,7 @@ def isInstanceInProcess(name, klass, process, shell): # Get the columns of a Pandas data frame in the process @process_task def getColumnsInProcess(name, process, shell): - try: - return list(get_env(shell.user_ns)[name].columns) - except: - return None + return list(get_env(shell.user_ns)[name].columns) # Is a key defined in a collection in the process? @process_task diff --git a/pythonwhat/test_funcs/test_compound_statement.py b/pythonwhat/test_funcs/test_compound_statement.py index 61a862e5..fa03a3bd 100644 --- a/pythonwhat/test_funcs/test_compound_statement.py +++ b/pythonwhat/test_funcs/test_compound_statement.py @@ -1,7 +1,7 @@ from pythonwhat.check_funcs import check_part, check_node, multi from pythonwhat.Reporter import Reporter from pythonwhat.check_funcs import check_node, check_part, check_part_index, call, fix_format, stringify, with_context -from pythonwhat.check_logic import multi, quiet +from pythonwhat.check_logic import multi from pythonwhat.has_funcs import has_equal_part_len, has_equal_part, has_equal_value, has_equal_output from pythonwhat.check_has_context import has_context from functools import partial, update_wrapper diff --git a/pythonwhat/test_funcs/test_object.py b/pythonwhat/test_funcs/test_object.py index 4e89cdc6..ca0c65d6 100644 --- a/pythonwhat/test_funcs/test_object.py +++ b/pythonwhat/test_funcs/test_object.py @@ -34,8 +34,6 @@ def test_data_frame(name, # if columns not set, figure them out from solution if columns is None: columns = getColumnsInProcess(name, child.solution_process) - if columns is None: - raise InstructorError("Something went wrong in figuring out the columns for %s in the solution process" % name) for col in columns: colstate = check_keys(col, missing_msg=undefined_cols_msg, state=child) diff --git a/tests/test_check_function.py b/tests/test_check_function.py index 8cb15ccf..533352a6 100644 --- a/tests/test_check_function.py +++ b/tests/test_check_function.py @@ -173,18 +173,24 @@ def test_method_2(): # Function parser ------------------------------------------------------------- +from pythonwhat.parsing import FunctionParser +import ast + @pytest.mark.parametrize('code', [ 'print(round(1.23))', 'x = print(round(1.23))', 'x = [round(1.23)]', 'x = {"a": round(1.23)}', 'x = 0; x += round(1.23)', - 'x = 0; x > round(1.23)' + 'x = 0; x > round(1.23)', + 'not round(1.23)' ]) def test_function_parser(code): - s = setup_state(code, code) - s.check_function("round").check_args(0).has_equal_value() - + p = FunctionParser() + p.visit(ast.parse(code)) + assert 'round' in p.out + + # Incorrect usage ------------------------------------------------------------- @pytest.mark.parametrize('sct', [ diff --git a/tests/test_check_function_def.py b/tests/test_check_function_def.py index 87e62454..65f30014 100644 --- a/tests/test_check_function_def.py +++ b/tests/test_check_function_def.py @@ -5,7 +5,6 @@ from pythonwhat.check_syntax import v2_check_functions globals().update(v2_check_functions) -@pytest.mark.debug @pytest.mark.parametrize('stu, passes', [ ('', False), ('def test(): print(3)', False), @@ -57,6 +56,19 @@ def test_old_ways_of_calling(sct): res = helper.run({ "DC_CODE": code, "DC_SOLUTION": code, "DC_SCT": sct }) assert res['correct'] +@pytest.mark.parametrize('sct', [ + """ +Ex().check_function_def('my_fun').multi( + check_args('*args').has_equal_part('name', msg='x'), + check_args('**kwargs').has_equal_part('name', msg='x') +) + """, + "Ex().test_function_definition('my_fun')", +]) +def test_old_ways_of_calling_starargs(sct): + code = "def my_fun(*x, **y): pass" + res = helper.run({ "DC_CODE": code, "DC_SOLUTION": code, "DC_SCT": sct }) + assert res['correct'] # Arguments, lengths, defaults ----------------------------------------------- @@ -133,6 +145,21 @@ def test_check_call_error_types(): 'def test(): raise ValueError("boooo")') s.check_function_def("test").check_call("f()").has_equal_error() + +@pytest.mark.parametrize('sct', [ + "Ex().test_function_definition('my_fun', results=[[1]])", + "Ex().test_function_definition('my_fun', results=[(1,)])", + "Ex().test_function_definition('my_fun', outputs=[[1]])", + "Ex().test_function_definition('my_fun', outputs=[(1,)])", + "Ex().test_function_definition('my_fun', errors=[['1']])", + "Ex().test_function_definition('my_fun', errors=[('1',)])", + "Ex().test_function_definition('my_fun', errors=['1'])", +]) +def test_check_call_old_way_of_calling(sct): + code = 'def my_fun(a):\n print(a + 2)\n return a + 2' + res = helper.run({ "DC_CODE": code, "DC_SOLUTION": code, "DC_SCT": sct }) + assert res['correct'] + # Lambdas --------------------------------------------------------------------- @pytest.mark.parametrize('stu, passes', [ diff --git a/tests/test_check_if_else.py b/tests/test_check_if_else.py index 9b8f33bb..85e990ec 100644 --- a/tests/test_check_if_else.py +++ b/tests/test_check_if_else.py @@ -158,3 +158,20 @@ def test_if_exp(stu, passes): ''' }) assert res['correct'] == passes + +from pythonwhat.parsing import IfExpParser +import ast + +@pytest.mark.parametrize('stu', [ + 'x = 3 if True else False', + 'x += 3 if True else False', + 'y = x or 3 if True else False', + '3 if True else False + 4 if True else False', + 'not 3 if True else False' +]) +def test_if_exp_findable(stu): + p = IfExpParser() + p.visit(ast.parse(stu)) + assert 'test' in p.out[0] + assert 'body' in p.out[0] + assert 'orelse' in p.out[0] \ No newline at end of file diff --git a/tests/test_check_object.py b/tests/test_check_object.py index 6716f9f7..84088bc7 100644 --- a/tests/test_check_object.py +++ b/tests/test_check_object.py @@ -128,25 +128,30 @@ def test_check_keys_exotic(sct): assert output['correct'] def test_non_dillable(): - s = setup_state( - stu_code="xl = pd.ExcelFile('battledeath.xlsx')", - sol_code="xl = pd.ExcelFile('battledeath.xlsx')", - pec="import pandas as pd; from urllib.request import urlretrieve; urlretrieve('https://s3.amazonaws.com/assets.datacamp.com/production/course_998/datasets/battledeath.xlsx', 'battledeath.xlsx')", - ) - s.check_object('xl').has_equal_value() - -from pythonwhat.State import set_converter + code = "xl = pd.ExcelFile('battledeath.xlsx')" + res = helper.run({ + 'DC_PEC': "import pandas as pd; from urllib.request import urlretrieve; urlretrieve('https://s3.amazonaws.com/assets.datacamp.com/production/course_998/datasets/battledeath.xlsx', 'battledeath.xlsx')", + 'DC_SOLUTION': code, + 'DC_CODE': code, + 'DC_SCT': "Ex().check_object('xl').has_equal_value()" + }) + assert res['correct'] @pytest.mark.compiled def test_manual_converter(): - s = setup_state( - stu_code="xl = pd.ExcelFile('battledeath2.xlsx')", - sol_code="xl = pd.ExcelFile('battledeath.xlsx')", - pec="import pandas as pd; from urllib.request import urlretrieve; urlretrieve('https://s3.amazonaws.com/assets.datacamp.com/production/course_998/datasets/battledeath.xlsx', 'battledeath.xlsx'); from urllib.request import urlretrieve; urlretrieve('https://s3.amazonaws.com/assets.datacamp.com/production/course_998/datasets/battledeath.xlsx', 'battledeath2.xlsx')", - ) - def my_converter(x): return(x.sheet_names) - set_converter(key = "pandas.io.excel.ExcelFile", fundef = my_converter) - s.check_object('xl').has_equal_value() + res = helper.run({ + "DC_CODE": "xl = pd.ExcelFile('battledeath2.xlsx')", + "DC_SOLUTION": "xl = pd.ExcelFile('battledeath.xlsx')", + "DC_PEC": "import pandas as pd; from urllib.request import urlretrieve; urlretrieve('https://s3.amazonaws.com/assets.datacamp.com/production/course_998/datasets/battledeath.xlsx', 'battledeath.xlsx'); from urllib.request import urlretrieve; urlretrieve('https://s3.amazonaws.com/assets.datacamp.com/production/course_998/datasets/battledeath.xlsx', 'battledeath2.xlsx')", + "DC_SCT": """ +def my_converter(x): return(x.sheet_names) +set_converter(key = "pandas.io.excel.ExcelFile", fundef = my_converter) +Ex().check_object('xl').has_equal_value() +""" + }) + assert res['correct'] + +from pythonwhat.State import set_converter def test_manual_converter_2(): s = setup_state( @@ -169,12 +174,13 @@ def test_equality_challenges(stu, sol): def test_equality_challenge_2(): code = "mat = scipy.io.loadmat('albeck_gene_expression.mat')" - s = setup_state( - stu_code=code, - sol_code=code, - pec="import scipy.io; from urllib.request import urlretrieve; urlretrieve('https://s3.amazonaws.com/assets.datacamp.com/production/course_998/datasets/ja_data2.mat', 'albeck_gene_expression.mat')" - ) - s.check_object('mat').has_equal_value() + res = helper.run({ + "DC_CODE": code, + "DC_SOLUTION": code, + "DC_PEC": "import scipy.io; from urllib.request import urlretrieve; urlretrieve('https://s3.amazonaws.com/assets.datacamp.com/production/course_998/datasets/ja_data2.mat', 'albeck_gene_expression.mat')", + "DC_SCT": "Ex().check_object('mat').has_equal_value()" + }) + assert res['correct'] @pytest.mark.parametrize('name, ls, le, cs, ce', [ ('a', 3, 3, 5, 9), diff --git a/tests/test_has_import.py b/tests/test_has_import.py index 28b1b9be..ec669f53 100644 --- a/tests/test_has_import.py +++ b/tests/test_has_import.py @@ -35,7 +35,9 @@ def test_same_as(stu, same_as, correct): ('', False), ('import numpy.random', True), ('import numpy.random as x', True), - ('import numpy.random as rand', True) + ('import numpy.random as rand', True), + ('from numpy import random as x', True), + ('from numpy import random as rand', True) ]) def test_chaining(stu, correct): s = setup_state(stu_code = stu, sol_code = 'import numpy.random as rand') diff --git a/tests/test_messaging.py b/tests/test_messaging.py index 214b3be2..4642d326 100644 --- a/tests/test_messaging.py +++ b/tests/test_messaging.py @@ -225,7 +225,6 @@ def test_check_object_manual(stu, patt): # Check function def et al ---------------------------------------------------- -@pytest.mark.debug @pytest.mark.parametrize('stu, patt', [ ('', 'The system wants to check the definition of `test()` but hasn\'t found it.'), ('def test(b): return b', 'Check the definition of `test()`. Did you specify the argument `a`?'), @@ -286,7 +285,6 @@ def test_check_call_lambda(stu, patt): # Check class definition ------------------------------------------------------ -@pytest.mark.debug @pytest.mark.parametrize('stu, patt', [ ('', "The system wants to check the class definition of `A` but hasn't found it."), ('def A(x): pass', "The system wants to check the class definition of `A` but hasn't found it."), diff --git a/tests/test_signatures.py b/tests/test_signatures.py index 82959c74..983fc7ca 100644 --- a/tests/test_signatures.py +++ b/tests/test_signatures.py @@ -59,7 +59,6 @@ def test_builtins(name, params, arguments): for param in params: fun_state.check_args(param).has_equal_value() -@pytest.mark.debug @pytest.mark.parametrize('name, values, arguments', [ ('delattr', "'a'", ['obj', 'name']), ('getattr', "'a'", ['object','name']), diff --git a/tests/test_spec.py b/tests/test_spec.py index 666cecd9..060cfa7e 100644 --- a/tests/test_spec.py +++ b/tests/test_spec.py @@ -157,37 +157,30 @@ def test_has_equal_ast_part_of_method_fail(data): # Test overriding fucntionality ----------------------------------------------- -class OverrideTester(): - """ - This class is used to test overriding w/ correct and incorrect code. Tests are - run for entire nodes (e.g. an if block) and their parts (e.g. body of if block) - """ - - def do_exercise(self, code, base_check, parts, override=None, part_name = None, part_index = "", passes=True): - """High level function used to generate tests""" - if part_name: - if not override: override = parts[part_name] - sct = base_check + '.check_{}({}).override("""{}""").has_equal_ast()'\ - .format(part_name, part_index, override) - else: - # whole code (e.g. if expression, or for loop) - if not override: override = code.format(**parts) - sct = base_check + '.override("""{}""").has_equal_ast()'.format(override) - - data = { - "DC_SOLUTION": code.format(**parts), - "DC_CODE": code.format(**parts), - "DC_SCT": sct - } - sct_payload = helper.run(data) - assert sct_payload['correct'] == passes - - PARTS = {'body': "1", "test": "False", 'orelse': "2", 'iter': "range(3)", - 'key': "3", 'value': "4", 'args': "(1,2,3)"} +def do_override_test(code, base_check, parts, override=None, part_name = None, part_index = "", passes=True): + """High level function used to generate tests""" + if part_name: + if not override: override = parts[part_name] + sct = base_check + '.check_{}({}).override("""{}""").has_equal_ast()'\ + .format(part_name, part_index, override) + else: + # whole code (e.g. if expression, or for loop) + if not override: override = code.format(**parts) + sct = base_check + '.override("""{}""").has_equal_ast()'.format(override) + + data = { + "DC_SOLUTION": code.format(**parts), + "DC_CODE": code.format(**parts), + "DC_SCT": sct + } + # import pdb; pdb.set_trace() + sct_payload = helper.run(data) + assert sct_payload['correct'] == passes + +PARTS = {'body': "1", "test": "False", 'orelse': "2", 'iter': "range(3)", + 'key': "3", 'value': "4", 'args': "(1,2,3)"} import re -def gen_exercise(*args, **kwargs): - return lambda self: OverrideTester.do_exercise(self, *args, **kwargs) @pytest.mark.parametrize('k, code', [ ('if_exp', "{body} if {test} else {orelse}"), @@ -204,25 +197,26 @@ def test_override(k, code): # base SCT, w/ special indexing if function checks if isinstance(code, list): indx, code = code else: indx = '0' + base_check = "Ex().check_{}({})".format(k, indx) + # pass overall test ---- - pf = gen_exercise(code, base_check, OverrideTester.PARTS) - setattr(OverrideTester, 'test_{}_pass'.format(k), pf) + do_override_test(code, base_check, PARTS) + # fail overall test ---- - pf = gen_exercise(code, base_check, OverrideTester.PARTS, override="'WRONG ANSWER'", passes=False) - setattr(OverrideTester, 'test_{}_fail'.format(k), pf) + do_override_test(code, base_check, PARTS, override="'WRONG ANSWER'", passes=False) + # test individual pieces -------------------------------------------------- - for part in re.findall("\{([^{]*?)\}", code): # find all str.format vars, e.g. {body} + # find all str.format vars, e.g. {body} + for part in re.findall("\{([^{]*?)\}", code): part_index = "" if part != 'args' else 0 + # pass individual piece ---- - test_name = 'test_{}_{}_pass'.format(k, part) - pf = gen_exercise(code, base_check, OverrideTester.PARTS, part_name=part, part_index=part_index) - setattr(OverrideTester, test_name, pf) + do_override_test(code, base_check, PARTS, part_name=part, part_index=part_index) + # fail individual piece ---- - test_name = 'test_{}_{}_fail'.format(k, part) - bad_code = code.format(**{part: "[]", **OverrideTester.PARTS}) - pf = gen_exercise(code, base_check, OverrideTester.PARTS, part_name=part, part_index=part_index, override=bad_code, passes=False) - setattr(OverrideTester, test_name, pf) + bad_code = code.format(**{part: "[]", **PARTS}) + do_override_test(code, base_check, PARTS, part_name=part, part_index=part_index, override=bad_code, passes=False) # Test SCT Ex syntax (copied from sqlwhat) ----------------------------------- diff --git a/tests/test_state.py b/tests/test_state.py new file mode 100644 index 00000000..48f6c9f7 --- /dev/null +++ b/tests/test_state.py @@ -0,0 +1,11 @@ +import pytest +from pythonwhat.State import State +from pythonwhat.Feedback import InstructorError + +def test_pec_parsing_error(): + with pytest.raises(InstructorError): + State( + student_code = 'parses', + solution_code = 'parses', + pre_exercise_code = 'does not parse', + ) diff --git a/tests/test_test_compound_statements.py b/tests/test_test_compound_statement.py similarity index 78% rename from tests/test_test_compound_statements.py rename to tests/test_test_compound_statement.py index c67f7d4c..9e89584a 100644 --- a/tests/test_test_compound_statements.py +++ b/tests/test_test_compound_statement.py @@ -70,4 +70,25 @@ def test_two_for_loops(stu, passes): def test_has_context(stu, exact, passes): s = setup_state(stu, 'for i in range(2): pass') with helper.verify_sct(passes): - s.check_for_loop().check_body().has_context(exact_names=exact) \ No newline at end of file + s.check_for_loop().check_body().has_context(exact_names=exact) + +# Check while loop ------------------------------------------------------------ + +@pytest.mark.parametrize('sct', [ + "test_while_loop(test = lambda: test_student_typed('3'), body = lambda: test_student_typed('print'))", + "Ex().test_while_loop(test = test_student_typed('3'), body = test_student_typed('print'))", + "Ex().check_while().multi(check_test().has_code('3'), check_body().has_code('print'))" +]) +@pytest.mark.parametrize('stu, passes', [ + ('', False), + ('while False: pass', False), + ('while 3 > 4: pass', False), + ('while 3 > 4: print(2)', True), +]) +def test_while_loop(sct, stu, passes): + res = helper.run({ + "DC_CODE": stu, + "DC_SOLUTION": 'while 3 > 4: print(2)', + "DC_SCT": sct + }) + assert res['correct'] == passes \ No newline at end of file diff --git a/tests/test_test_object_accessed.py b/tests/test_test_object_accessed.py index 634c13d8..bc912358 100644 --- a/tests/test_test_object_accessed.py +++ b/tests/test_test_object_accessed.py @@ -31,3 +31,22 @@ def test_test_object_accessed(sct, passes, mess): }) assert res['correct'] == passes if mess: assert res['message'] == mess + + +# ObjectAccess parser ------------------------------------------------------------- + +from pythonwhat.parsing import ObjectAccessParser +import ast + +@pytest.mark.parametrize('code', [ + 'x.a[1]', + 'x.a', + 'print(x.a)', + 'print(kw = x.a)', + '(x.a, y.a)', + '[x.a, y.a]', +]) +def test_object_access_parser(code): + p = ObjectAccessParser() + p.visit(ast.parse(code)) + assert 'x.a' in p.out From 17ffea9b33ea1ab8893a245e9a82b8448b0960f6 Mon Sep 17 00:00:00 2001 From: Filip Schouwenaars Date: Thu, 13 Sep 2018 15:47:57 +0200 Subject: [PATCH 013/209] refactor(messaging): always Jinja + consistent messaging v1 vs v2 - FMT: prefix no longer supported. - Jinja templating used for all; __JINJA__ prefix simply removed. - Get rid of custom messaging in test_compound_statement - Some changes are breaking, but only require rewrite of a couple of SCTs Closes #137 #299 --- pythonwhat/State.py | 11 +- pythonwhat/check_funcs.py | 50 +++--- pythonwhat/check_function.py | 6 +- pythonwhat/check_has_context.py | 6 +- pythonwhat/check_object.py | 12 +- pythonwhat/check_wrappers.py | 36 ++-- pythonwhat/has_funcs.py | 22 +-- pythonwhat/probe.py | 2 - pythonwhat/test_funcs/__init__.py | 2 +- .../test_funcs/test_compound_statement.py | 170 ++++++------------ pythonwhat/utils_ast.py | 2 +- tests/test_check_if_else.py | 6 +- tests/test_check_list_comp.py | 7 +- tests/test_messaging.py | 23 ++- tests/test_test_with.py | 2 +- 15 files changed, 152 insertions(+), 205 deletions(-) diff --git a/pythonwhat/State.py b/pythonwhat/State.py index 6113bb9f..cc3cae9a 100644 --- a/pythonwhat/State.py +++ b/pythonwhat/State.py @@ -103,14 +103,9 @@ def build_message(self, tail="", fmt_kwargs=None, append=True): 'this': d['kwargs'], **d['kwargs']} # don't bother appending if there is no message - if not d['msg']: continue - if d['msg'].startswith('FMT:'): - out = d['msg'].replace('FMT:', "").format(**tmp_kwargs) - elif d['msg'].startswith('__JINJA__:'): - out = Template(d['msg'].replace('__JINJA__:', "")).render(**tmp_kwargs) - else: - out = d['msg'] - + if not d['msg']: + continue + out = Template(d['msg'].replace('__JINJA__:', "")).render(**tmp_kwargs) out_list.append(out) # if highlighting info is available, don't put all expand messages diff --git a/pythonwhat/check_funcs.py b/pythonwhat/check_funcs.py index a890fdaf..3da2c2f3 100644 --- a/pythonwhat/check_funcs.py +++ b/pythonwhat/check_funcs.py @@ -8,6 +8,10 @@ from pythonwhat.utils_ast import assert_ast from functools import partial import ast +from jinja2 import Template + +def render(template, kwargs): + return Template(template).render(**kwargs) class StubState(): def __init__(self, highlight, highlighting_disabled): @@ -42,8 +46,8 @@ def check_part(name, part_msg, state=None): """Return child state with name part as its ast tree""" - if missing_msg is None: missing_msg = "__JINJA__:Are you sure you defined the {{part}}? " - if expand_msg is None: expand_msg = "__JINJA__:Did you correctly specify the {{part}}? " + if missing_msg is None: missing_msg = "Are you sure you defined the {{part}}? " + if expand_msg is None: expand_msg = "Did you correctly specify the {{part}}? " if not part_msg: part_msg = name append_message = {'msg': expand_msg, 'kwargs': { 'part': part_msg }} @@ -70,8 +74,8 @@ def check_part_index(name, index, part_msg, - a list of indices (which can be integer or string), in which case the student parts are indexed step by step. """ - if missing_msg is None: missing_msg = "__JINJA__:Are you sure you defined the {{part}}? " - if expand_msg is None: expand_msg = "__JINJA__:Did you correctly specify the {{part}}? " + if missing_msg is None: missing_msg = "Are you sure you defined the {{part}}? " + if expand_msg is None: expand_msg = "Did you correctly specify the {{part}}? " # create message ordinal = get_ord(index+1) if isinstance(index, int) else "" @@ -79,7 +83,7 @@ def check_part_index(name, index, part_msg, 'index': index, 'ordinal': ordinal } - fmt_kwargs.update(part = part_msg.format(**fmt_kwargs)) + fmt_kwargs.update(part = render(part_msg, fmt_kwargs)) append_message = { 'msg': expand_msg, @@ -106,23 +110,27 @@ def check_part_index(name, index, part_msg, # return child state from part return part_to_child(stu_part, sol_part, append_message, state) -def check_node(name, index=0, typestr='{ordinal} node', +def check_node(name, + index=0, + typestr='{{ordinal}} node', missing_msg=None, expand_msg=None, state=None): - if missing_msg is None: missing_msg = "__JINJA__:The system wants to check the {{typestr}} but hasn't found it." - if expand_msg is None: expand_msg = "__JINJA__:Check the {{typestr}}. " + if missing_msg is None: missing_msg = "The system wants to check the {{typestr}} but hasn't found it." + if expand_msg is None: expand_msg = "Check the {{typestr}}. " rep = Reporter.active_reporter stu_out = getattr(state, 'student_'+name) sol_out = getattr(state, 'solution_'+name) # check if there are enough nodes for index - fmt_kwargs = {'ordinal': get_ord(index+1) if isinstance(index, int) else "", - 'index': index, - 'name': name} - fmt_kwargs['typestr'] = typestr.format(**fmt_kwargs) + fmt_kwargs = { + 'ordinal': get_ord(index+1) if isinstance(index, int) else "", + 'index': index, + 'name': name + } + fmt_kwargs['typestr'] = render(typestr, fmt_kwargs) # test if node can be indexed succesfully try: stu_out[index] @@ -226,7 +234,7 @@ def my_power(x): """ if missing_msg is None: - missing_msg = '__JINJA__:Did you specify the {{part}}?' + missing_msg = 'Did you specify the {{part}}?' if name in ['*args', '**kwargs']: # for check_function_def return check_part(name, name, state=state, missing_msg = missing_msg) @@ -294,9 +302,9 @@ def run_call(args, node, process, get_func, **kwargs): ast.fix_missing_locations(func_expr) return get_func(process = process, tree=func_expr, call = args, **kwargs) -MSG_CALL_INCORRECT = "__JINJA__:Calling {{argstr}} should {{action}} `{{str_sol}}`, instead got {{str_stu if str_stu == 'no printouts' else '`' + str_stu + '`'}}." -MSG_CALL_ERROR = "__JINJA__:Calling {{argstr}} should {{action}} `{{str_sol}}`, instead it errored out: `{{str_stu}}`." -MSG_CALL_ERROR_INV = "__JINJA__:Calling {{argstr}} should {{action}} `{{str_sol}}`, instead got `{{str_stu}}`." +MSG_CALL_INCORRECT = "Calling {{argstr}} should {{action}} `{{str_sol}}`, instead got {{str_stu if str_stu == 'no printouts' else '`' + str_stu + '`'}}." +MSG_CALL_ERROR = "Calling {{argstr}} should {{action}} `{{str_sol}}`, instead it errored out: `{{str_stu}}`." +MSG_CALL_ERROR_INV = "Calling {{argstr}} should {{action}} `{{str_sol}}`, instead got `{{str_stu}}`." def call(args, test='value', incorrect_msg=None, @@ -322,12 +330,12 @@ def call(args, eval_sol, str_sol = run_call(args, state.solution_parts['node'], state.solution_process, get_func, **kwargs) if (test == 'error') ^ isinstance(eval_sol, Exception): - _msg = state.build_message("FMT:Calling {argstr} resulted in an error (or not an error if testing for one). Error message: {type_err} {str_sol}", + _msg = state.build_message("Calling {{argstr}} resulted in an error (or not an error if testing for one). Error message: {{type_err}} {{str_sol}}", dict(type_err=type(eval_sol), str_sol=str_sol, argstr=argstr)), raise InstructorError(_msg) if isinstance(eval_sol, ReprFail): - _msg = state.build_message("FMT:Can't get the result of calling {argstr}: {eval_sol.info}", + _msg = state.build_message("Can't get the result of calling {{argstr}}: {{eval_sol.info}}", dict(argstr = argstr, eval_sol=eval_sol)) raise InstructorError(_msg) @@ -352,10 +360,10 @@ def call(args, def build_call(callstr, node): if isinstance(node, ast.FunctionDef): # function name func_expr = ast.Name(id=node.name, ctx=ast.Load()) - argstr = "`{}`".format(callstr.replace('f', node.name)) + argstr = "`%s`" % callstr.replace('f', node.name) elif isinstance(node, ast.Lambda): # lambda body expr func_expr = node - argstr = 'it with the arguments `{}`'.format(callstr.replace('f', '')) + argstr = 'it with the arguments `%s`' % callstr.replace('f', '') else: raise TypeError("Can't handle AST that is passed.") @@ -398,7 +406,7 @@ def my_power(x): ) if expand_msg is None: - expand_msg = "__JINJA__:To verify it, we reran {{argstr}}. " + expand_msg = "To verify it, we reran {{argstr}}. " stu_part, _argstr = build_call(callstr, state.student_parts['node']) sol_part, _ = build_call(callstr, state.solution_parts['node']) diff --git a/pythonwhat/check_function.py b/pythonwhat/check_function.py index 4336ddd5..717a0e52 100644 --- a/pythonwhat/check_function.py +++ b/pythonwhat/check_function.py @@ -23,9 +23,9 @@ def get_mapped_name(name, mappings): if name.startswith(full_name): return name.replace(full_name, orig) return name -MISSING_MSG = "__JINJA__:Did you call `{{mapped_name}}()`{{' ' + times if index>0}}?" -SIG_ISSUE_MSG = "__JINJA__:Have you specified the arguments for `{{mapped_name}}()` using the right syntax?" -PREPEND_MSG = "__JINJA__:Check your {{ord + ' ' if index>0}}call of `{{mapped_name}}()`. " +MISSING_MSG = "Did you call `{{mapped_name}}()`{{' ' + times if index>0}}?" +SIG_ISSUE_MSG = "Have you specified the arguments for `{{mapped_name}}()` using the right syntax?" +PREPEND_MSG = "Check your {{ord + ' ' if index>0}}call of `{{mapped_name}}()`. " def check_function(name, index=0, missing_msg=None, params_not_matched_msg=None, diff --git a/pythonwhat/check_has_context.py b/pythonwhat/check_has_context.py index 0ef24e1e..e2800daa 100644 --- a/pythonwhat/check_has_context.py +++ b/pythonwhat/check_has_context.py @@ -5,8 +5,8 @@ from functools import singledispatch from pythonwhat.check_funcs import check_part_index -MSG_INCORRECT_LOOP = "FMT:Have you used the correct iterator variable names? Was expecting `{sol_vars}` but got `{stu_vars}`." -MSG_INCORRECT_WITH = "FMT:Make sure to use the correct context variable names. Was expecting `{sol_vars}` but got `{stu_vars}`." +MSG_INCORRECT_LOOP = "Have you used the correct iterator variable names? Was expecting `{{sol_vars}}` but got `{{stu_vars}}`." +MSG_INCORRECT_WITH = "Make sure to use the correct context variable names. Was expecting `{{sol_vars}}` but got `{{stu_vars}}`." def has_context(incorrect_msg=None, exact_names=False, state=None): # call _has_context, since the built-in singledispatch can only use 1st pos arg @@ -74,7 +74,7 @@ def has_context_with(state, incorrect_msg, exact_names): """ for i in range(len(state.solution_parts['context'])): - ctxt_state = check_part_index('context', i, '{ordinal} context', state=state) + ctxt_state = check_part_index('context', i, '{{ordinal}} context', state=state) _has_context(ctxt_state, incorrect_msg or MSG_INCORRECT_WITH, exact_names) return state diff --git a/pythonwhat/check_object.py b/pythonwhat/check_object.py index 8e26741f..bb643c59 100644 --- a/pythonwhat/check_object.py +++ b/pythonwhat/check_object.py @@ -47,10 +47,10 @@ def check_object(index, missing_msg=None, expand_msg=None, state=None, typestr=" state.assert_root('check_object') if missing_msg is None: - missing_msg = "__JINJA__:Did you define the {{typestr}} `{{index}}` without errors?" + missing_msg = "Did you define the {{typestr}} `{{index}}` without errors?" if expand_msg is None: - expand_msg = "__JINJA__:Did you correctly define the {{typestr}} `{{index}}`? " + expand_msg = "Did you correctly define the {{typestr}} `{{index}}`? " rep = Reporter.active_reporter @@ -106,7 +106,7 @@ def is_instance(inst, not_instance_msg=None, state=None): sol_name = state.solution_parts.get('name') stu_name = state.student_parts.get('name') - if not_instance_msg is None: not_instance_msg = "__JINJA__:Is it a {{inst.__name__}}?" + if not_instance_msg is None: not_instance_msg = "Is it a {{inst.__name__}}?" if not isInstanceInProcess(sol_name, inst, state.solution_process): raise InstructorError("`is_instance()` noticed that `%s` is not a `%s` in the solution process." % (sol_name, inst.__name__)) @@ -154,9 +154,9 @@ def check_keys(key, missing_msg=None, expand_msg=None, state=None): state.assert_is(['object_assignments'], 'is_instance', ['check_object', 'check_df']) if missing_msg is None: - missing_msg = "__JINJA__:There is no {{ 'column' if 'DataFrame' in parent.typestr else 'key' }} `'{{key}}'`." + missing_msg = "There is no {{ 'column' if 'DataFrame' in parent.typestr else 'key' }} `'{{key}}'`." if expand_msg is None: - expand_msg = "__JINJA__:Did you correctly set the {{ 'column' if 'DataFrame' in parent.typestr else 'key' }} `'{{key}}'`? " + expand_msg = "Did you correctly set the {{ 'column' if 'DataFrame' in parent.typestr else 'key' }} `'{{key}}'`? " rep = Reporter.active_reporter @@ -175,7 +175,7 @@ def get_part(name, key, highlight): if isinstance(key, str): slice_val = ast.Str(s=key) else: - slice_val = ast.parse('{}'.format(key)).body[0].value + slice_val = ast.parse(str(key)).body[0].value expr = ast.Subscript(value=ast.Name(id=name, ctx=ast.Load()), slice=ast.Index(value=slice_val), ctx=ast.Load()) diff --git a/pythonwhat/check_wrappers.py b/pythonwhat/check_wrappers.py index ac0e417b..994dfe29 100644 --- a/pythonwhat/check_wrappers.py +++ b/pythonwhat/check_wrappers.py @@ -18,33 +18,33 @@ } __PART_INDEX_WRAPPERS__ = { - 'ifs': '{ordinal} if', - 'bases': '{ordinal} base class', - 'handlers': '`{index}` `except` block', - 'context': '{ordinal} context', + 'ifs': '{{ordinal}} if', + 'bases': '{{ordinal}} base class', + 'handlers': '`{{index}}` `except` block', + 'context': '{{ordinal}} context', } __NODE_WRAPPERS__ = { - 'list_comp': '{ordinal} list comprehension', - 'generator_exp': '{ordinal} generator expression', - 'dict_comp': '{ordinal} dictionary comprehension', - 'for_loop': '{ordinal} for statement', - 'function_def': 'definition of `{index}()`', - 'class_def': 'class definition of `{index}`', - 'if_exp': '{ordinal} if expression', - 'if_else': '{ordinal} if statement', - 'lambda_function': '{ordinal} lambda function', - 'try_except': '{ordinal} try statement', - 'while': '{ordinal} `while` loop', - 'with': '{ordinal} `with` statement', + 'list_comp': '{{ordinal}} list comprehension', + 'generator_exp': '{{ordinal}} generator expression', + 'dict_comp': '{{ordinal}} dictionary comprehension', + 'for_loop': '{{ordinal}} for loop', + 'function_def': 'definition of `{{index}}()`', + 'class_def': 'class definition of `{{index}}`', + 'if_exp': '{{ordinal}} if expression', + 'if_else': '{{ordinal}} if statement', + 'lambda_function': '{{ordinal}} lambda function', + 'try_except': '{{ordinal}} try statement', + 'while': '{{ordinal}} `while` loop', + 'with': '{{ordinal}} `with` statement', } scts = {} # make has_equal_part wrappers -scts['has_equal_name'] = partial(has_equal_part, 'name', msg='Make sure to use the correct {name}, was expecting {sol_part[name]}, instead got {stu_part[name]}.') -scts['is_default'] = partial(has_equal_part, 'is_default', msg="__JINJA__:Make sure it {{ 'has' if sol_part.is_default else 'does not have'}} a default argument.") +scts['has_equal_name'] = partial(has_equal_part, 'name', msg='Make sure to use the correct {{name}}, was expecting {{sol_part[name]}}, instead got {{stu_part[name]}}.') +scts['is_default'] = partial(has_equal_part, 'is_default', msg="Make sure it {{ 'has' if sol_part.is_default else 'does not have'}} a default argument.") # include rest of wrappers for k, v in __PART_WRAPPERS__.items(): diff --git a/pythonwhat/has_funcs.py b/pythonwhat/has_funcs.py index 7aed7cd3..96ad9610 100644 --- a/pythonwhat/has_funcs.py +++ b/pythonwhat/has_funcs.py @@ -151,7 +151,7 @@ def has_equal_ast(incorrect_msg=None, if append is None: # if not specified, set to False if incorrect_msg was manually specified append = incorrect_msg is None if incorrect_msg is None: - incorrect_msg = "__JINJA__:Expected `{{sol_str}}`, but got `{{stu_str}}`." + incorrect_msg = "Expected `{{sol_str}}`, but got `{{stu_str}}`." def parse_tree(tree): # get contents of module.body if only 1 element @@ -177,12 +177,12 @@ def parse_tree(tree): return state -DEFAULT_INCORRECT_MSG="__JINJA__:Expected {{test_desc}}`{{sol_eval}}`, but got `{{stu_eval}}`." -DEFAULT_ERROR_MSG="__JINJA__:Running {{'it' if parent['part'] else 'the higlighted expression'}} generated an error: `{{stu_str}}`." -DEFAULT_ERROR_MSG_INV="__JINJA__:Running {{'it' if parent['part'] else 'the higlighted expression'}} didn't generate an error, but it should!" -DEFAULT_UNDEFINED_NAME_MSG="__JINJA__:Running {{'it' if parent['part'] else 'the higlighted expression'}} should define a variable `{{name}}` without errors, but it doesn't." -DEFAULT_INCORRECT_NAME_MSG="__JINJA__:Are you sure you assigned the correct value to `{{name}}`?" -DEFAULT_INCORRECT_EXPR_CODE_MSG="__JINJA__:Running the expression `{{expr_code}}` didn't generate the expected result." +DEFAULT_INCORRECT_MSG="Expected {{test_desc}}`{{sol_eval}}`, but got `{{stu_eval}}`." +DEFAULT_ERROR_MSG="Running {{'it' if parent['part'] else 'the higlighted expression'}} generated an error: `{{stu_str}}`." +DEFAULT_ERROR_MSG_INV="Running {{'it' if parent['part'] else 'the higlighted expression'}} didn't generate an error, but it should!" +DEFAULT_UNDEFINED_NAME_MSG="Running {{'it' if parent['part'] else 'the higlighted expression'}} should define a variable `{{name}}` without errors, but it doesn't." +DEFAULT_INCORRECT_NAME_MSG="Are you sure you assigned the correct value to `{{name}}`?" +DEFAULT_INCORRECT_EXPR_CODE_MSG="Running the expression `{{expr_code}}` didn't generate the expected result." def has_expr(incorrect_msg=None, error_msg=None, undefined_msg=None, @@ -237,7 +237,7 @@ def has_expr(incorrect_msg=None, if (test == 'error') ^ isinstance(eval_sol, Exception): raise InstructorError("Evaluating expression raised error in solution process (or not an error if testing for one). " - "Error: {} - {}".format(type(eval_sol), str_sol)) + "Error: {} - {}".format(type(eval_sol), str_sol)) if isinstance(eval_sol, ReprFail): raise InstructorError("Couldn't extract the value for the highlighted expression from the solution process: " + eval_sol.info) @@ -410,8 +410,8 @@ def has_code(text, def has_import(name, same_as=False, - not_imported_msg="__JINJA__:Did you import `{{pkg}}`?", - incorrect_as_msg="__JINJA__:Did you import `{{pkg}}` as `{{alias}}`?", + not_imported_msg="Did you import `{{pkg}}`?", + incorrect_as_msg="Did you import `{{pkg}}` as `{{alias}}`?", state=None): """Checks whether student imported a package or function correctly. @@ -549,7 +549,7 @@ def has_printout(index, state.assert_root('has_printout') if not_printed_msg is None: - not_printed_msg = "__JINJA__:Have you used `{{sol_call}}` to do the appropriate printouts?" + not_printed_msg = "Have you used `{{sol_call}}` to do the appropriate printouts?" try: sol_call_ast = state.solution_function_calls['print'][index]['node'] diff --git a/pythonwhat/probe.py b/pythonwhat/probe.py index 3995239d..ce3aa21d 100644 --- a/pythonwhat/probe.py +++ b/pythonwhat/probe.py @@ -13,7 +13,6 @@ "test_object", "test_correct", "test_if_else", - "test_if_exp", "test_for_loop", "test_function", "test_list_comp", @@ -31,7 +30,6 @@ SUB_TESTS = { "test_if_else": ['test', 'body', 'orelse'], - "test_if_exp": ['test', 'body', 'orelse'], "test_list_comp": ['comp_iter', 'body', 'ifs'], "check_correct": ['check', 'diagnose'], "test_for_loop": ['for_iter', 'body', 'orelse'], diff --git a/pythonwhat/test_funcs/__init__.py b/pythonwhat/test_funcs/__init__.py index c7a3b043..096325b5 100644 --- a/pythonwhat/test_funcs/__init__.py +++ b/pythonwhat/test_funcs/__init__.py @@ -1,6 +1,6 @@ from .test_compound_statement import test_with, test_list_comp, \ - test_if_else, test_if_exp, test_for_loop, test_while_loop, \ + test_if_else, test_for_loop, test_while_loop, \ test_expression_output, test_expression_result, \ test_object_after_expression, test_function_definition diff --git a/pythonwhat/test_funcs/test_compound_statement.py b/pythonwhat/test_funcs/test_compound_statement.py index fa03a3bd..736d22ec 100644 --- a/pythonwhat/test_funcs/test_compound_statement.py +++ b/pythonwhat/test_funcs/test_compound_statement.py @@ -13,8 +13,6 @@ def test_if_else(index=1, test=None, body=None, orelse=None, - expand_message=True, - use_if_exp=False, state=None): """Test parts of the if statement. @@ -44,8 +42,6 @@ def test_if_else(index=1, It should be passed as a lambda expression or a function definition. The functions that are ran should be other pythonwhat test functions, and they will be tested specifically on only the else part of the if statement. - expand_message (bool): if true, feedback messages will be expanded with :code:`in the ___ of the if statement on - line ___`. Defaults to True. If False, :code:`test_if_else()` will generate no extra feedback. :Example: @@ -71,32 +67,15 @@ def test_if_else(index=1, This SCT will pass as :code:`test_expression_output()` is ran on the body of the if statement and it will output the same thing in the solution as in the student code. """ - - MSG_MISSING = "FMT:The system wants to check the {typestr}, but it hasn't found it. Have another look at your code." - MSG_PREPEND = "FMT:Check the {typestr}. " - - # get state with specific if block - node_name = 'if_exps' if use_if_exp else 'if_elses' - # TODO original typestr for check_node used if rather than `if` - state = check_node(node_name, index-1, "{ordinal} if statement", MSG_MISSING, MSG_PREPEND if expand_message else "", state=state) - - # run sub tests - multi(test, state = check_part('test', 'condition', expand_msg=None if expand_message else "", state=state)) - multi(body, state = check_part('body', 'body', expand_msg=None if expand_message else "", state=state)) - multi(orelse, state = check_part('orelse', 'else part', expand_msg=None if expand_message else "", state=state)) - - -test_if_exp = partial(test_if_else, use_if_exp = True) -# update test_if_exp function signature (docstring, etc..) -update_wrapper(test_if_exp, test_if_else) -test_if_exp.__name__ = 'test_if_exp' - + state = check_node('if_elses', index-1, typestr='{{ordinal}} if expression', state=state) + multi(test, state = check_part('test', 'condition', state=state)) + multi(body, state = check_part('body', 'body', state=state)) + multi(orelse, state = check_part('orelse', 'else part', state=state)) def test_for_loop(index=1, for_iter=None, body=None, orelse=None, - expand_message=True, state=None): """Test parts of the for loop. @@ -124,8 +103,6 @@ def test_for_loop(index=1, It should be passed as a lambda expression or a function. The functions that are ran should be other pythonwhat test functions, and they will be tested specifically on only the else part of the for loop. - expand_message (bool): if true, feedback messages will be expanded with :code:`in the ___ of the for loop on - line ___`. Defaults to True. If False, :code:`test_for_loop()` will generate no extra feedback. :Example: Student code:: @@ -147,22 +124,16 @@ def test_for_loop(index=1, This SCT will evaluate to True as the function :code:`range` is used in the sequence and the function :code:`test_exression_output()` will pass on the body code. """ - MSG_MISSING = "FMT:Define more for loops." - MSG_PREPEND = "FMT:Check the {typestr}. " - - - state = check_node('for_loops', index-1, "{ordinal} for loop", MSG_MISSING, MSG_PREPEND, state=state) + state = check_node('for_loops', index-1, "{{ordinal}} for loop", state=state) - # TODO for_iter is a level up, so shouldn't have targets set, but this is done is check_node - multi(for_iter, state = check_part('iter', 'sequence part', expand_msg=None if expand_message else "", state=state)) - multi(body, state = check_part('body', 'body', expand_msg=None if expand_message else "", state=state)) - multi(orelse, state = check_part('orelse', 'else part', expand_msg=None if expand_message else "", state=state)) + multi(for_iter, state = check_part('iter', 'sequence part', state=state)) + multi(body, state = check_part('body', 'body', state=state)) + multi(orelse, state = check_part('orelse', 'else part', state=state)) def test_while_loop(index=1, test=None, body=None, orelse=None, - expand_message=True, state=None): """Test parts of the while loop. @@ -192,8 +163,6 @@ def test_while_loop(index=1, It should be passed as a lambda expression or a function definition. The functions that are ran should be other pythonwhat test functions, and they will be tested specifically on only the else part of the while loop. - expand_message (bool): if true, feedback messages will be expanded with :code:`in the ___ of the while loop on - line ___`. Defaults to True. If False, `test_for_loop()` will generate no extra feedback. :Example: @@ -220,16 +189,10 @@ def test_while_loop(index=1, This SCT will evaluate to True as condition test will have thes same result in student and solution code and `test_exression_output()` will pass on the body code. """ - - MSG_MISSING = "FMT:Define more while loops." - MSG_PREPEND = "FMT:Check the {typestr}. " - - state = check_node('whiles', index-1, "{ordinal} while loop", MSG_MISSING, MSG_PREPEND if expand_message else "", state=state) - - expand_msg = None if expand_message else "" - multi(test, state = check_part('test', 'condition', expand_msg=expand_msg, state=state)) - multi(body, state = check_part('body', 'body', expand_msg=expand_msg, state=state)) - multi(orelse, state = check_part('orelse', 'else part', expand_msg=expand_msg, state=state)) + state = check_node('whiles', index-1, "{{ordinal}} while loop", state=state) + multi(test, state = check_part('test', 'condition', state=state)) + multi(body, state = check_part('body', 'body', state=state)) + multi(orelse, state = check_part('orelse', 'else part', state=state)) def test_function_definition(name, @@ -248,7 +211,6 @@ def test_function_definition(name, wrong_output_msg=None, no_error_msg=None, wrong_error_msg=None, - expand_message=True, state=None): """Test a function definition. @@ -289,9 +251,6 @@ def test_function_definition(name, wrong_output_msg (str): message if one of the tested functions calls' output did not match. no_error_msg (str): message if one of the tested function calls' result did not generate an error. wrong_error_msg (str): message if the error that one of the tested function calls generated did not match. - expand_message (bool): only relevant if there is a body test. If True, feedback messages defined in the - body test will be preceded by 'In your definition of ___, '. If False, `test_function_definition()` - will generate no extra feedback if the body test fails. Defaults to True. :Example: @@ -322,36 +281,25 @@ def shout( word = 'help', times = 3 ): test_function_definition('shout', args_defaults = False # pass body = test_function('print', args = []])) """ - MSG_MISSING = "FMT:You didn't define the following function: {typestr}." - MSG_PREPEND = "FMT:Check your definition of {typestr}. " # what the function will be referred to as - typestr = "`{}()`".format(name) - get_func_child = partial(check_node, 'function_defs', name, typestr, not_called_msg or MSG_MISSING, state=state) - child = get_func_child(expand_msg = MSG_PREPEND if expand_message else "") - - # make a temporary child state, to reflect that there were two types of - # messages prepended in the original function - quiet_child = get_func_child(expand_msg = "") - prep_child2 = get_func_child(expand_msg = MSG_PREPEND) + child = check_node('function_defs', name, 'definition of `{{index}}()`', state=state) test_args(arg_names, arg_defaults, nb_args_msg, arg_names_msg, arg_defaults_msg, - prep_child2, quiet_child) + child) - multi(body, state=check_part('body', "", expand_msg=None if expand_message else "", state=child)) + multi(body, state=check_part('body', "", state=child)) # Test function calls ----------------------------------------------------- - #fun_name = ("`%s()`" % name) - for el in (results or []): el = fix_format(el) call(el, 'value', incorrect_msg = wrong_result_msg, error_msg = wrong_result_msg, argstr = '`{}{}`'.format(name, stringify(el)), - state = quiet_child) + state=child) for el in (outputs or []): el = fix_format(el) @@ -359,7 +307,7 @@ def shout( word = 'help', times = 3 ): incorrect_msg = wrong_output_msg, error_msg = wrong_output_msg, argstr = '`{}{}`'.format(name, stringify(el)), - state = quiet_child) + state=child) for el in (errors or []): el = fix_format(el) @@ -367,31 +315,31 @@ def shout( word = 'help', times = 3 ): incorrect_msg = wrong_error_msg, error_msg = no_error_msg, argstr = '`{}{}`'.format(name, stringify(el)), - state = quiet_child) + state=child) def test_args(arg_names, arg_defaults, nb_args_msg, arg_names_msg, arg_defaults_msg, - child, quiet_child): + child): - MSG_NUM_ARGS = "FMT:You should define {parent[typestr]} with {sol_len} arguments, instead got {stu_len}." - MSG_BAD_ARG_NAME = "FMT:The {parent[ordinal]} {parent[part]} should be called `{sol_part[name]}`, instead got `{stu_part[name]}`." - MSG_BAD_DEFAULT = "FMT:The {parent[part]} `{stu_part[name]}` should have no default." - MSG_INC_DEFAULT = "FMT:The {parent[part]} `{stu_part[name]}` does not have the correct default." + MSG_NUM_ARGS = "You should define {{parent[typestr]}} with {{sol_len}} arguments, instead got {{stu_len}}." + MSG_BAD_ARG_NAME = "The {{parent[ordinal]}} {{parent[part]}} should be called `{{sol_part[name]}}`, instead got `{{stu_part[name]}}`." + MSG_BAD_DEFAULT = "The {{parent[part]}} `{{stu_part[name]}}` should have no default." + MSG_INC_DEFAULT = "The {{parent[part]}} `{{stu_part[name]}}` does not have the correct default." - MSG_NO_VARARG = "FMT:Have you specified an argument to take a `*` argument and named it `{sol_part[*args][name]}`?" - MSG_NO_KWARGS = "FMT:Have you specified an argument to take a `**` argument and named it `{sol_part[**kwargs][name]}`?" - MSG_VARARG_NAME = "FMT:Have you specified an argument to take a `*` argument and named it `{sol_part[name]}`?" - MSG_KWARG_NAME = "FMT:Have you specified an argument to take a `**` argument and named it `{sol_part[name]}`?" + MSG_NO_VARARG = "Have you specified an argument to take a `*` argument and named it `{{sol_part['*args'][name]}}`?" + MSG_NO_KWARGS = "Have you specified an argument to take a `**` argument and named it `{{sol_part['**kwargs'][name]}}`?" + MSG_VARARG_NAME = "Have you specified an argument to take a `*` argument and named it `{{sol_part[name]}}`?" + MSG_KWARG_NAME = "Have you specified an argument to take a `**` argument and named it `{{sol_part[name]}}`?" if arg_names or arg_defaults: # test number of args - has_equal_part_len('_spec1_args', nb_args_msg or MSG_NUM_ARGS, state=quiet_child) + has_equal_part_len('_spec1_args', nb_args_msg or MSG_NUM_ARGS, state=child) # iterate over each arg, testing name and default for ii in range(len(child.solution_parts['_spec1_args'])): # get argument state - arg_state = check_part_index('_spec1_args', ii, 'argument', "NO MISSING MSG", expand_msg="", state=child) + arg_state = check_part_index('_spec1_args', ii, 'argument', "NO MISSING MSG", state=child) # test exact name has_equal_part('name', arg_names_msg or MSG_BAD_ARG_NAME, arg_state) @@ -400,15 +348,15 @@ def test_args(arg_names, arg_defaults, has_equal_part('is_default', arg_defaults_msg or MSG_BAD_DEFAULT, arg_state) # test default value, use if to prevent running a process no default if arg_state.solution_parts['is_default']: - has_equal_value(incorrect_msg = arg_defaults_msg or MSG_INC_DEFAULT, error_msg="error message", append=True, state=arg_state) + has_equal_value(incorrect_msg = arg_defaults_msg or MSG_INC_DEFAULT, append=True, state=arg_state) # test *args and **kwargs if child.solution_parts['*args']: - vararg = check_part('*args', "", missing_msg=MSG_NO_VARARG, expand_msg="", state=child) + vararg = check_part('*args', "", missing_msg=MSG_NO_VARARG, state=child) has_equal_part('name', MSG_VARARG_NAME, state=vararg) if child.solution_parts['**kwargs']: - kwarg = check_part('**kwargs', "", missing_msg=MSG_NO_KWARGS, expand_msg="", state=child) + kwarg = check_part('**kwargs', "", missing_msg=MSG_NO_KWARGS, state=child) has_equal_part('name', MSG_KWARG_NAME, state=kwarg) @@ -474,7 +422,6 @@ def test_with(index, undefined_msg=None, context_vals_len_msg=None, context_vals_msg=None, - expand_message=True, state=None): """Test a with statement. with open_file('...') as bla: @@ -487,16 +434,14 @@ def test_with(index, """ - MSG_MISSING = "Define more `with` statements." - MSG_PREPEND = "FMT:Check the {typestr}. " MSG_NUM_CTXT = "Make sure to use the correct number of context variables. It seems you defined too many." MSG_NUM_CTXT2 = "Make sure to use the correct number of context variables. It seems you defined too little." - MSG_CTXT_NAMES = "FMT:Make sure to use the correct context variable names. Was expecting `{sol_vars}` but got `{stu_vars}`." + MSG_CTXT_NAMES = "Make sure to use the correct context variable names. Was expecting `{{sol_vars}}` but got `{{stu_vars}}`." - check_with = partial(check_node, 'withs', index-1, "{ordinal} `with` statement", MSG_MISSING, state=state) + check_with = partial(check_node, 'withs', index-1, "{{ordinal}} `with` statement", state=state) - child = check_with(MSG_PREPEND if expand_message else "") - child2 = check_with(MSG_PREPEND if expand_message else "") + child = check_with() + child2 = check_with() if context_vals: # test context var names ---- @@ -509,10 +454,9 @@ def test_with(index, # Context sub tests ---- if context_tests and not isinstance(context_tests, list): context_tests = [context_tests] - expand_msg = None if expand_message else "" for i, context_test in enumerate(context_tests or []): # partial the substate check, because the function uses two prepended messages - check_context = partial(check_part_index, 'context', i, "%s context"%utils.get_ord(i+1), missing_msg=MSG_NUM_CTXT2, expand_msg=expand_msg) + check_context = partial(check_part_index, 'context', i, "%s context"%utils.get_ord(i+1), missing_msg=MSG_NUM_CTXT2) check_context(state=child) # test exist @@ -521,7 +465,7 @@ def test_with(index, # Body sub tests ---- if body is not None: - body_state = check_part('body', 'body', expand_msg=expand_msg, state=child2) + body_state = check_part('body', 'body', state=child2) with_context(body, state=body_state) @@ -533,53 +477,43 @@ def test_list_comp(index=1, body=None, ifs=None, insufficient_ifs_msg=None, - expand_message=True, state=None): """Test list comprehension.""" - test_comp("{ordinal} list comprehension", 'list_comps', **(locals())) + test_comp("{{ordinal}} list comprehension", 'list_comps', **(locals())) def test_comp(typestr, comptype, index, iter_vars_names, not_called_msg, insufficient_ifs_msg, incorrect_iter_vars_msg, comp_iter, ifs, key=None, body=None, value=None, - expand_message = True, rep=None, state=None): - MSG_NOT_CALLED = "FMT:The system wants to check the {typestr} but hasn't found it." - MSG_PREPEND = "FMT:Check the {typestr}. " - - MSG_INCORRECT_ITER_VARS = "FMT:Have you used the correct iterator variables in the {parent[typestr]}? Be sure to use the correct names." - MSG_INCORRECT_NUM_ITER_VARS = "FMT:Have you used {num_vars} iterator variables in the {parent[typestr]}?" - MSG_INSUFFICIENT_IFS = "FMT:Have you used {sol_len} ifs inside the {parent[typestr]}?" + MSG_INCORRECT_ITER_VARS = "Have you used the correct iterator variables?" + MSG_INCORRECT_NUM_ITER_VARS = "Have you used {{num_vars}} iterator variables?" + MSG_INSUFFICIENT_IFS = "Have you used {{sol_len}} ifs?" - # if true, set expand_message to default (for backwards compatibility) - expand_message = MSG_PREPEND if expand_message is True else (expand_message or "") # make sure other messages are set to default if None - if insufficient_ifs_msg is None: insufficient_ifs_msg = MSG_INSUFFICIENT_IFS - if not_called_msg is None: not_called_msg = MSG_NOT_CALLED - - # TODO MSG: function was not consistent with prepending, so use state w/o expand_message - quiet_state = check_node(comptype, index-1, typestr, not_called_msg, expand_msg="", state=state) + if insufficient_ifs_msg is None: + insufficient_ifs_msg = MSG_INSUFFICIENT_IFS # get comprehension - state = check_node(comptype, index-1, typestr, not_called_msg, expand_msg=None if expand_message else "", state=state) + child = check_node(comptype, index-1, typestr, missing_msg=not_called_msg, state=state) # test comprehension iter and its variable names (or number of variables) - if comp_iter: multi(comp_iter, state=check_part("iter", "iterable part", state=state)) + if comp_iter: multi(comp_iter, state=check_part("iter", "iterable part", state=child)) # test iterator variables default_msg = MSG_INCORRECT_ITER_VARS if iter_vars_names else MSG_INCORRECT_NUM_ITER_VARS - has_context(incorrect_iter_vars_msg or default_msg, iter_vars_names, state=quiet_state) + has_context(incorrect_iter_vars_msg or default_msg, iter_vars_names, state=child) # test the main expressions. - if body: multi(body, state=check_part("body", "body", expand_msg=None if expand_message else "", state=state)) # list and gen comp - if key: multi(key, state=check_part("key", "key part", expand_msg=None if expand_message else "", state=state)) # dict comp - if value: multi(value, state=check_part("value", "value part", expand_msg=None if expand_message else "", state=state)) # "" + if body: multi(body, state=check_part("body", "body", state=child)) # list and gen comp + if key: multi(key, state=check_part("key", "key part", state=child)) # dict comp + if value: multi(value, state=check_part("value", "value part", state=child)) # "" # test a list of ifs. each entry corresponds to a filter in the comprehension. for i, if_test in enumerate(ifs or []): # test that ifs are same length - has_equal_part_len('ifs', insufficient_ifs_msg, state=quiet_state) + has_equal_part_len('ifs', insufficient_ifs_msg, state=child) # test individual ifs - multi(if_test, state=check_part_index("ifs", i, utils.get_ord(i+1) + " if", state=state)) + multi(if_test, state=check_part_index("ifs", i, utils.get_ord(i+1) + " if", state=child)) diff --git a/pythonwhat/utils_ast.py b/pythonwhat/utils_ast.py index b78fbcb0..fbede1fd 100644 --- a/pythonwhat/utils_ast.py +++ b/pythonwhat/utils_ast.py @@ -15,7 +15,7 @@ def wrap_in_module(node): return new_node def assert_ast(state, element, fmt_kwargs): - patt = "__JINJA__:You are zooming in on the {{part}}, but it is not an AST, so it can't be re-run." + patt = "You are zooming in on the {{part}}, but it is not an AST, so it can't be re-run." _err_msg = "SCT fails on solution: " _err_msg += state.build_message(patt, fmt_kwargs) # element can also be { 'node': AST } diff --git a/tests/test_check_if_else.py b/tests/test_check_if_else.py index 85e990ec..0a919b3e 100644 --- a/tests/test_check_if_else.py +++ b/tests/test_check_if_else.py @@ -73,14 +73,12 @@ def orelse_test2(): test_if_else(index = 1, test = test_test2, body = body_test2, - orelse = orelse_test2, - expand_message = False) + orelse = orelse_test2) test_if_else(index=1, test=test_test, body=body_test, - orelse=orelse_test, - expand_message = False) + orelse=orelse_test) ''', ''' test_if_else(index=1, diff --git a/tests/test_check_list_comp.py b/tests/test_check_list_comp.py index a8a62551..6d82eb3f 100644 --- a/tests/test_check_list_comp.py +++ b/tests/test_check_list_comp.py @@ -30,9 +30,9 @@ def test_check_list_comp_basic(stu, passes): @pytest.mark.parametrize('stu, passes, patt, lines', [ ("", False, "The system wants to check the first list comprehension but hasn't found it.", []), ("[key for key in x.keys()]", False, "Check the first list comprehension. Did you correctly specify the iterable part?", [1, 1, 17, 24]), - ("[a + str(b) for a,b in x.items()]", False, "Have you used the correct iterator variables in the first list comprehension? Be sure to use the correct names.", [1, 1, 17, 19]), + ("[a + str(b) for a,b in x.items()]", False, 'Check the first list comprehension. Have you used the correct iterator variables?', [1, 1, 17, 19]), ("[key + '_' + str(val) for key,val in x.items()]", False, "Did you correctly specify the body?", [1, 1, 2, 21]), - ("[key + str(val) for key,val in x.items()]", False, "Have you used 2 ifs inside the first list comprehension?", []), + ("[key + str(val) for key,val in x.items()]", False, "Check the first list comprehension. Have you used 2 ifs?", []), ("[key + str(val) for key,val in x.items() if hasattr(key, 'test') if hasattr(key, 'test')]", False, "Did you correctly specify the first if? Did you call isinstance()?", [1, 1, 45, 64]), ("[key + str(val) for key,val in x.items() if isinstance(key, str) if hasattr(key, 'test')]", False, "Did you correctly specify the second if? Did you call isinstance()?", [1, 1, 69, 88]), ("[key + str(val) for key,val in x.items() if isinstance(key, str) if isinstance(key, str)]", False, "Did you correctly specify the argument obj? Expected val, but got key.", [1, 1, 80, 82]), @@ -50,8 +50,7 @@ def test_test_list_comp_messaging(stu, passes, patt, lines): body=lambda: test_expression_result(context_vals = ['a', 2]), ifs=[lambda: test_function_v2('isinstance', params = ['obj'], do_eval = [False]), lambda: test_function_v2('isinstance', params = ['obj'], do_eval = [False])], - insufficient_ifs_msg=None, - expand_message=True) + insufficient_ifs_msg=None) ''' res = helper.run({ "DC_PEC": pec, "DC_CODE": stu, "DC_SOLUTION": sol, "DC_SCT": sct }) assert res['correct'] == passes diff --git a/tests/test_messaging.py b/tests/test_messaging.py index 4642d326..cbe29baf 100644 --- a/tests/test_messaging.py +++ b/tests/test_messaging.py @@ -337,10 +337,10 @@ def test_has_import_custom(stu, patt): @pytest.mark.parametrize('stu, patt, cols, cole', [ ("my_dict = {'a': 1, 'b': 2}\nfor key, value in my_dict.items(): x = key + ' -- ' + str(value)", - "Check the first for statement. Did you correctly specify the body? Are you sure you assigned the correct value to `x`?", + "Check the first for loop. Did you correctly specify the body? Are you sure you assigned the correct value to `x`?", 36, 64), ("my_dict = {'a': 1, 'b': 2}\nfor key, value in my_dict.items(): x = key + ' - ' + str(value)", - "Check the first for statement. Did you correctly specify the body? Expected the output `a - 1`, but got `no printouts`.", + "Check the first for loop. Did you correctly specify the body? Expected the output `a - 1`, but got `no printouts`.", 36, 63) ]) def test_has_equal_x(stu, patt, cols, cole): @@ -403,9 +403,9 @@ def test_nesting(stu, patt): @pytest.mark.parametrize('sct, patt', [ ('Ex().check_for_loop().check_body().check_for_loop().check_body().has_equal_output()', - 'Check the first for statement. Did you correctly specify the body? Expected the output `1+1`, but got `1-1`.'), + 'Check the first for loop. Did you correctly specify the body? Expected the output `1+1`, but got `1-1`.'), ('Ex().check_for_loop().check_body().check_for_loop().disable_highlighting().check_body().has_equal_output()', - 'Check the first for statement. Did you correctly specify the body? Check the first for statement. Did you correctly specify the body? Expected the output `1+1`, but got `1-1`.') + 'Check the first for loop. Did you correctly specify the body? Check the first for loop. Did you correctly specify the body? Expected the output `1+1`, but got `1-1`.') ]) def test_limited_stacking(sct, patt): code = ''' @@ -463,3 +463,18 @@ def test_check_if_else_basic(stu, patt, lines): assert not output['correct'] assert message(output, patt) if lines: helper.with_line_info(output, *lines) + +## Jinja handling ------------------------------------------------------------- + +@pytest.mark.parametrize('msgpart', [ + "__JINJA__:You did {{stu_eval}}, but should be {{sol_eval}}!", + "You did {{stu_eval}}, but should be {{sol_eval}}!" +]) +def test_jinja_in_custom_msg(msgpart): + output = helper.run({ + 'DC_SOLUTION': 'x = 4', + 'DC_CODE': 'x = 3', + 'DC_SCT': "Ex().check_object('x').has_equal_value(incorrect_msg=\"%s\")" % msgpart + }) + assert not output['correct'] + assert message(output, 'You did 3, but should be 4!') \ No newline at end of file diff --git a/tests/test_test_with.py b/tests/test_test_with.py index 4f61a305..f3361b69 100644 --- a/tests/test_test_with.py +++ b/tests/test_test_with.py @@ -15,7 +15,7 @@ None ), ( - "test_with(1, body = [test_function('print', index = i + 1) for i in range(3)], expand_message = False)", + "test_with(1, body = [test_function('print', index = i + 1) for i in range(3)])", False, None, [6, 6, 11, 16] From 0274b95bb154fc48ce2b33157da9ba15c31875ae Mon Sep 17 00:00:00 2001 From: Filip Schouwenaars Date: Fri, 14 Sep 2018 10:40:11 +0200 Subject: [PATCH 014/209] fix typo --- docs/articles/checking_compound_statements.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/articles/checking_compound_statements.rst b/docs/articles/checking_compound_statements.rst index 3b4086c5..88f564e7 100644 --- a/docs/articles/checking_compound_statements.rst +++ b/docs/articles/checking_compound_statements.rst @@ -113,8 +113,8 @@ However, because we are not merely string-matching the SCT allows for much more rerunning the expressions and comparing the results. -``check_for()`` -~~~~~~~~~~~~~~~ +``check_for_loop()`` +~~~~~~~~~~~~~~~~~~~~ The following example checks whether the student properly iterates over a dictionary and does the appropriate printouts: From 8ca6f721a59a17034e457fc98e5934b6de6d01a9 Mon Sep 17 00:00:00 2001 From: Filip Schouwenaars Date: Fri, 14 Sep 2018 16:41:22 +0200 Subject: [PATCH 015/209] Add failing test for #353 --- tests/test_check_function.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/tests/test_check_function.py b/tests/test_check_function.py index 533352a6..c965485b 100644 --- a/tests/test_check_function.py +++ b/tests/test_check_function.py @@ -190,6 +190,19 @@ def test_function_parser(code): p.visit(ast.parse(code)) assert 'round' in p.out +def test_check_function_parser_mappings_1(): + code = "import numpy as np\nnp.random.randint(1, 7)" + s = setup_state(code, code) + s.check_function('numpy.random.randint') + +# Because the mappings are only found for the substate that is zoomed in on, +# The `import numpy as np` part is not found, because it's outside of the for loop. +# This should be fixed!!! +@pytest.mark.xfail +def test_check_function_parser_mappings_2(): + code = "import numpy as np\nfor x in range(0): np.random.randint(1, 7)" + s = setup_state(code, code) + s.check_for_loop().check_body().check_function('numpy.random.randint') # Incorrect usage ------------------------------------------------------------- From 0bf8eda84d1cd61a8638c11ca8e1170efa1956ec Mon Sep 17 00:00:00 2001 From: Filip Schouwenaars Date: Fri, 21 Sep 2018 15:14:30 +0200 Subject: [PATCH 016/209] add object assignment tests --- tests/test_check_object.py | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/tests/test_check_object.py b/tests/test_check_object.py index 84088bc7..7fbd4b4b 100644 --- a/tests/test_check_object.py +++ b/tests/test_check_object.py @@ -304,4 +304,24 @@ def test_several_assignments_2(diff_assign_data): "DC_SCT": "Ex().check_object('df2').has_equal_value()" }) assert not res['correct'] - helper.no_line_info(res) \ No newline at end of file + helper.no_line_info(res) + +# Object Assignment parser ------------------------------------------------------------- + +from pythonwhat.parsing import ObjectAssignmentParser +import ast + +@pytest.mark.parametrize('code', [ + 'x = 2', + 'x = a[1]', + 'x = a.b[1]', + 'x = sales.loc[(["ca", "tx"], 2), :]', + 'x = fun(a)', + 'x = a + b', + 'x = (a + b) + c', + 'x = [ a for b in c ]', +]) +def test_object_assignment_parser(code): + p = ObjectAssignmentParser() + p.visit(ast.parse(code)) + assert 'x' in p.out From 34994ccdaef1f240781a15e370652ecdffdacd4c Mon Sep 17 00:00:00 2001 From: Jeroen Hermans Date: Mon, 24 Sep 2018 13:50:50 +0200 Subject: [PATCH 017/209] feat(has_no_error): check runtime errors early on Closes #355 --- pythonwhat/check_wrappers.py | 2 +- pythonwhat/has_funcs.py | 19 +++++++++++++++++++ tests/test_author_warnings.py | 6 ++++++ tests/test_has_no_error.py | 14 ++++++++++++++ tests/test_messaging.py | 11 +++++++++++ 5 files changed, 51 insertions(+), 1 deletion(-) create mode 100644 tests/test_has_no_error.py diff --git a/pythonwhat/check_wrappers.py b/pythonwhat/check_wrappers.py index 994dfe29..d9b08d4d 100644 --- a/pythonwhat/check_wrappers.py +++ b/pythonwhat/check_wrappers.py @@ -63,7 +63,7 @@ scts[k] = getattr(check_funcs, k) for k in ['has_equal_value', 'has_equal_output', 'has_equal_error', 'has_equal_ast', 'has_equal_part_len', - 'has_equal_part', 'has_import', 'has_output', 'has_printout', 'has_code', 'has_chosen']: + 'has_equal_part', 'has_import', 'has_output', 'has_printout', 'has_code', 'has_no_error', 'has_chosen']: scts[k] = getattr(has_funcs, k) # include check_object and friends ------ diff --git a/pythonwhat/has_funcs.py b/pythonwhat/has_funcs.py index 96ad9610..3e1cdec2 100644 --- a/pythonwhat/has_funcs.py +++ b/pythonwhat/has_funcs.py @@ -577,6 +577,25 @@ def has_printout(index, return state +def has_no_error(incorrect_msg="Have a look at the console: your code contains an error. Fix it and try again!", state=None): + """Check whether the submission did not generate a runtime error. + + If all SCTs for an exercise pass, before succeeding pythonwhat will automatically check whether + the student submission generated an error. If you want to verify whether an error was generated + earlier during SCT execution, you can use ``Ex().has_no_error()``. + + Args: + incorrect_msg: if specified, this overrides the default message if the student code generated an error. + """ + state.assert_root('has_no_error') + + rep = Reporter.active_reporter + if rep.error: + _msg = state.build_message(incorrect_msg, { "error": str(rep.error) }) + rep.do_test(Test(Feedback(_msg, state))) + + return state + MC_VAR_NAME = "selected_option" def has_chosen(correct, msgs, state=None): diff --git a/tests/test_author_warnings.py b/tests/test_author_warnings.py index c4f8513b..3bba42e6 100644 --- a/tests/test_author_warnings.py +++ b/tests/test_author_warnings.py @@ -105,6 +105,12 @@ def test_has_printout_not_on_root(): with pytest.raises(InstructorError, match=r"`has_printout\(\)` should only be called from the root state, `Ex\(\)`\."): s.check_for_loop().check_body().has_printout(0) +def test_has_no_error_not_on_root(): + code = 'for i in range(3): pass' + s = setup_state(code, code) + with pytest.raises(InstructorError, match=r"`has_no_error\(\)` should only be called from the root state, `Ex\(\)`\."): + s.check_for_loop().check_body().has_no_error() + def test_check_object_on_root(): code = 'x = 1' check_object = v2_check_functions['check_object'] diff --git a/tests/test_has_no_error.py b/tests/test_has_no_error.py new file mode 100644 index 00000000..fd2ebcc7 --- /dev/null +++ b/tests/test_has_no_error.py @@ -0,0 +1,14 @@ +import helper +import pytest + +@pytest.mark.parametrize('stu, passes', [ + ('c', False), + ('a = 3', True), +]) +def test_basic(stu, passes): + res = helper.run({ + 'DC_CODE': stu, + 'DC_SOLUTION': '', + 'DC_SCT': 'Ex().has_no_error()' + }) + assert res['correct'] == passes diff --git a/tests/test_messaging.py b/tests/test_messaging.py index cbe29baf..34258bcf 100644 --- a/tests/test_messaging.py +++ b/tests/test_messaging.py @@ -367,6 +367,17 @@ def test_has_equal_x_2(stu, patt, cols, cole): assert message(output, patt) assert lines(output, cols, cole) +## Check has no error --------------------------------------------------------- + +def test_has_no_error(): + output = helper.run({ + 'DC_CODE': 'c', + 'DC_SOLUTION': '', + 'DC_SCT': 'Ex().has_no_error()' + }) + assert not output['correct'] + assert message(output, "Have a look at the console: your code contains an error. Fix it and try again!") + ## test_correct --------------------------------------------------------------- @pytest.mark.parametrize('stu, patt', [ From c51f92229fb856d4c2c86c2dbb6e47d9ac20ef9b Mon Sep 17 00:00:00 2001 From: Filip Schouwenaars Date: Mon, 24 Sep 2018 14:27:42 +0200 Subject: [PATCH 018/209] add has_no_error to reference doc --- docs/reference.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/reference.rst b/docs/reference.rst index 3af35ca1..cef0bf19 100644 --- a/docs/reference.rst +++ b/docs/reference.rst @@ -9,6 +9,7 @@ SCT building blocks .. autofunction:: pythonwhat.has_funcs.has_code .. autofunction:: pythonwhat.has_funcs.has_output .. autofunction:: pythonwhat.has_funcs.has_printout +.. autofunction:: pythonwhat.has_funcs.has_no_error .. autofunction:: pythonwhat.has_funcs.has_import .. autofunction:: pythonwhat.has_funcs.has_equal_value .. autofunction:: pythonwhat.has_funcs.has_equal_output From 464c4d9340e5e59a60d48ac97b9c1e52b8c2349c Mon Sep 17 00:00:00 2001 From: Filip Schouwenaars Date: Mon, 24 Sep 2018 17:06:38 +0200 Subject: [PATCH 019/209] bump version + update CHANGELOG --- CHANGELOG.md | 15 +++++++++++++++ pythonwhat/__init__.py | 2 +- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 718f64be..a152f2fc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,21 @@ All notable changes to the pythonwhat project will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). +## 2.17.0 + +### Added + +- Function `has_no_error()` to check earlier on whether the student did not generate any errors. + +### Improved + +- Messaging between V1 and V2 is entirely consistent now. +- No need for `__JINJA__` prefix in custom messages specified in SCTs anymore. + +### Removed + +- No more support for `expand_message` argument in old 'node checking functions' such as `test_for_loop()`. + ## 2.16.2 ### Added diff --git a/pythonwhat/__init__.py b/pythonwhat/__init__.py index b56b646f..e6b23f4e 100644 --- a/pythonwhat/__init__.py +++ b/pythonwhat/__init__.py @@ -1,3 +1,3 @@ -__version__ = '2.16.2' +__version__ = '2.17.0' from .test_exercise import test_exercise, allow_errors From 34169a9e1f43767d2149741e7f59b22f8ee11da7 Mon Sep 17 00:00:00 2001 From: Filip Schouwenaars Date: Wed, 26 Sep 2018 09:49:14 +0200 Subject: [PATCH 020/209] fix(tuple_comparison): tuple of numpy arrays can now be compared --- pythonwhat/Test.py | 2 +- tests/test_check_object.py | 13 +++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/pythonwhat/Test.py b/pythonwhat/Test.py index 76eacc10..079f45a8 100644 --- a/pythonwhat/Test.py +++ b/pythonwhat/Test.py @@ -163,7 +163,7 @@ def is_equal(x, y): if objs_are(x, y, [Exception]): # Types of errors don't matter (this is debatable) return str(x) == str(y) - if objs_are(x, y, [np.ndarray, dict, list]): + if objs_are(x, y, [np.ndarray, dict, list, tuple]): if np.array_equal(x, y): return True np.testing.assert_equal(x, y) return True diff --git a/tests/test_check_object.py b/tests/test_check_object.py index 7fbd4b4b..1d00eb25 100644 --- a/tests/test_check_object.py +++ b/tests/test_check_object.py @@ -33,6 +33,19 @@ def test_check_object_exotic_compare(stu_code, passes): }) assert output['correct'] == passes +@pytest.mark.parametrize('stu_code, passes', [ + ('x = (np.array([1, 2]), np.array([1, 2]))', False), + ('x = (np.array([1, 2]), np.array([3, 4]))', True), +]) +def test_check_object_exotic_compare2(stu_code, passes): + output = helper.run({ + 'DC_PEC': 'import numpy as np', + 'DC_SOLUTION': 'x = (np.array([1, 2]), np.array([3, 4]))', + 'DC_SCT': "Ex().check_object('x').has_equal_value()", + 'DC_CODE': stu_code + }) + assert output['correct'] == passes + @pytest.mark.parametrize('stu_code, passes', [ ('x = [1, 2, 3]', True), ('x = [1, 2, 3, 4]', False) From b1264023e9a15b06cb4a9db958308b2b9cf28754 Mon Sep 17 00:00:00 2001 From: Filip Schouwenaars Date: Wed, 26 Sep 2018 10:21:36 +0200 Subject: [PATCH 021/209] cleanup: assignments not used in objectassignmentparser --- pythonwhat/parsing.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/pythonwhat/parsing.py b/pythonwhat/parsing.py index b86c4073..430a00fd 100644 --- a/pythonwhat/parsing.py +++ b/pythonwhat/parsing.py @@ -439,7 +439,6 @@ def visit_Name(self, node): self.out[node.id] = self.get_part(node, self.active_assignment) else: self.out[node.id]['highlight'] = None - self.out[node.id]['assignments'].append(self.active_assignment) self.active_assignment = None def visit_Attribute(self, node): @@ -478,11 +477,11 @@ def get_part(name_node, ass_node=None): name = getattr(name_node, 'id', name_node) load_name = ast.Name(id=name, ctx=ast.Load()) ast.fix_missing_locations(load_name) - return {'name': name, - 'node': load_name, - 'highlight': ass_node or name_node, - 'assignments': [] if not ass_node else [ass_node] - } + return { + 'name': name, + 'node': load_name, + 'highlight': ass_node or name_node, + } class IfParser(Parser): From b4f329fe59365b85694dd21eef4f25b5c807da3d Mon Sep 17 00:00:00 2001 From: Filip Schouwenaars Date: Wed, 26 Sep 2018 11:10:11 +0200 Subject: [PATCH 022/209] bump version + update CHANGELOG --- CHANGELOG.md | 10 ++++++++++ pythonwhat/__init__.py | 2 +- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a152f2fc..6cc90abb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,16 @@ All notable changes to the pythonwhat project will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). +## 2.17.1 + +### Fixed + +- Tuples of Numpy arrays can now be checked properly. + +### Removed + +- Code in `ObjectAssignmentParser` that is not used. + ## 2.17.0 ### Added diff --git a/pythonwhat/__init__.py b/pythonwhat/__init__.py index e6b23f4e..27c9ea9d 100644 --- a/pythonwhat/__init__.py +++ b/pythonwhat/__init__.py @@ -1,3 +1,3 @@ -__version__ = '2.17.0' +__version__ = '2.17.1' from .test_exercise import test_exercise, allow_errors From ff64f31de756d141c853ed31287e9b458519e800 Mon Sep 17 00:00:00 2001 From: Filip Schouwenaars Date: Tue, 25 Sep 2018 18:05:17 +0200 Subject: [PATCH 023/209] docs(compound_statements): Add all compound statement checks to reference - Every check has an example with explanation - Common usecases have additional examples - Checking_compound_statements article has been simplified, limits to context now. - More helpful message in case check_object or has_printout not called on root state - More helpful message in case the zoomed in on object is not an AST --- .../articles/checking_compound_statements.rst | 285 +-------- docs/articles/checking_imports.rst | 40 -- docs/articles/checking_objects.rst | 2 +- docs/articles/electives.rst | 27 - docs/glossary.rst | 88 +-- docs/index.rst | 2 - docs/reference.rst | 66 +- pythonwhat/State.py | 4 +- pythonwhat/check_logic.py | 28 +- pythonwhat/check_object.py | 3 +- pythonwhat/check_wrappers.py | 591 +++++++++++++++++- pythonwhat/has_funcs.py | 47 +- pythonwhat/test_exercise.py | 1 - .../test_funcs/test_compound_statement.py | 2 +- pythonwhat/test_funcs/test_function.py | 1 - pythonwhat/utils_ast.py | 5 +- tests/test_author_warnings.py | 4 +- 17 files changed, 728 insertions(+), 468 deletions(-) delete mode 100644 docs/articles/checking_imports.rst diff --git a/docs/articles/checking_compound_statements.rst b/docs/articles/checking_compound_statements.rst index 88f564e7..13c7022e 100644 --- a/docs/articles/checking_compound_statements.rst +++ b/docs/articles/checking_compound_statements.rst @@ -15,7 +15,7 @@ The ``if`` statement example in the tutorial describes how different ``check_`` producing a child state, to which additional SCT functions can be chained. The ``check_if_else()`` function scanned the code for an ``if`` statement, and broke it into three parts: a ``test``, the ``body`` and the ``orelse`` part; the former two were dived into with the SCT functions ``check_test`` and ``check_body``. Notice that the naming is consistent: the ``test`` part that ``check_if_else()`` surfaces can be inspected with ``check_test()``. -The ``body`` part that ``check_if_else()`` unearths can be inspected with ``check_ifs``. +The ``body`` part that ``check_if_else()`` unearths can be inspected with ``check_body``. Similar to how ``if`` statements has a ``check_if_else`` associated with it, all other compound statements have corresponding ``check_`` functions to perform this action of looking up a statement, @@ -25,282 +25,27 @@ and chopping it up into its constituents that can be inspected with ``check_ 0: - print("x is strictly positive") - - # sct - Ex().check_if_else().multi( - check_test().has_code(/x\s+>\s+0/), # chain A - check_body().check_function('print', 0).check_args('value').has_equal_value() # chain B - ) - -It checks whether the student specified an ``if`` statement, but the check of the ``test`` part is not very robust. -The SCT would not accept a submission that contains ``0 < x`` in the test. - -To increase robustness, pythonwhat features functionality to rerun pieces of the student and solution code, -and then check the resulting value, printed output, or errors they produce. - -Consider the same solution, an example submission, and a better SCT: - -.. code:: - - # solution - x = 4 - if x > 0: - print("x is strictly positive") - - # sct - Ex().check_if_else().multi( - check_test().multi( - set_env(x = -1).has_equal_value(), # chain A1 - set_env(x = 1).has_equal_value(), # chain A2 - set_env(x = 0).has_equal_value() # chain A3 - ), - check_body().check_function('print', 0).check_args('value').has_equal_value() # chain B - ) - - # example student submission - x = 4 - if x >= 0: - print("x is strictly positive") - - -In this SCT, `chain A` has been made more advanced: - -- As explained previously, ``check_test()`` zooms on the 'test portions' of the if statement in both student (``x >= 0``) and solution code (``x > 0``) -- Instead of ``has_code()``, we are using a series of ``set_env().has_equal_value()`` calls. - ``set_env()`` will temporarily override the value of ``x`` in the student/solution process, - and ``has_equal_value()`` will re-execute the pieces of code that are zoomed in on, and compare the results. - - + chain A1 re-executes ``x >= 0`` in the student process and ``x > 0`` in the solution process when ``x`` is equal to ``-1``, and checks if the results are the same, which they are: they both evaluate to ``False``. - + chain A2 re-executes ``x >= 0`` in the student process and ``x > 0`` in the solution process when ``x`` is equal to ``1``, and checks if the results are the same, which they are: they both evaluate to ``True``. - + chain A3 re-executes ``x >= 0`` in the student process and ``x > 0`` in the solution process when ``x`` is equal to ``1``, and checks if the results are the same. - They are not: the student expression evaluates to ``False``, while the solution expressione evaluates to ``True``. - -With this example student submission, the SCT will fail and pythonwhat will automatically generate a meaningful feedback message. -However, because we are not merely string-matching the SCT allows for much more ways of (correctly solving) the problem that would be hard to cater for with ``has_code()``: - -.. code:: - - # example student submission (passing) - x = 4 - if x > 0: - print("x is strictly positive") - - # example student submission (passing) - x = 4 - if 0 < x: - print("x is strictly positive") - -.. note:: - - Notice that ``has_equal_value()`` is also used in the context of checking objects and function arguments. - When checking objects, ``has_equal_value()`` is executing the expression ```` and comparing the result. - When checking function arguments, ``has_equal_value()`` executes the expression that specifies an argument. - They are different applications of the same concept: zooming in on a part of the student/solution submission, - rerunning the expressions and comparing the results. - - -``check_for_loop()`` -~~~~~~~~~~~~~~~~~~~~ - -The following example checks whether the student properly iterates over a dictionary and does the appropriate printouts: - -.. code:: - - # solution - my_dict = {'a': 1, 'b': 2} - for key, value in my_dict.items(): - print(key + " - " + str(value)) - - # sct - Ex().check_object('my_dict').has_equal_value() - Ex().check_for_loop().multi( - check_iter().has_equal_value(), - check_body().multi( - set_context('a', 1).has_equal_output(), - set_context('b', 2).has_equal_output() - ) - ) - -Unlike the ``if`` statement, the ``for`` loop introduces two context values, ``key`` and ``value``. -pythonwhat treats them different from regular variables like ``x`` in the previous example to be robust to students using different names for these context variables, -Similar to ``set_env()``, you can now use ``set_context()`` to temporarily override the values of these context variables. -Next, you can use ``has_equal_x()`` like before to rerun the body of the for loop for different situations. - -- The ``check_object()`` chain verifies that ``my_dict`` is properly initialized. -- ``check_for_loop()`` zooms in on the ``for`` loop, and makes its parts available for further checking. -- ``check_iter()`` zooms in on the iterator part of the for loop, ``my_dict.items()`` in the solution. - - + ``has_equal_value()`` re-executes the expressions specified by student and solution and compares their results. - -- ``check_body()`` zooms in on the body part of the for loop, ``print(key + " - " + str(value))``: - - + Similar to ``set_env()``, we now use ``set_context()`` to temporarily override the values of the context variables ``key`` and ``value``, in this order. - Notice that the context values are not specified by name, this is on purpose. - + ``has_equal_output()`` re-executes the entire for loop body and captures the output this generates. It does this for both the student and solution body, and checks if the outputs are equal. - -Because pythonwhat treats context values differently from normal variables and we're not specifying the variables by name in ``set_context()``, -we can make the SCT robust against submissions that code the correct logic, but use different names for the context values. -Consider the following student submissions that would also pass the SCT: - -.. code:: - - # passing submission 1 - my_dict = {'a': 1, 'b': 2} - for k, v in my_dict.items(): - print(k + " - " + str(v)) - - # passing submission 2 - my_dict = {'a': 1, 'b': 2} - for first, second in my_dict.items(): - mess = first + " - " + str(second) - print(mess) - - -``check_function_def()`` -~~~~~~~~~~~~~~~~~~~~~~~~ - -The following example checks whether students correctly defined their own function: - -.. code:: - - # solution - def shout_echo(word1, echo=1): - echo_word = word1 * echo - shout_words = echo_word + '!!!' - return shout_words - - # sct - Ex().check_function_def('shout_echo').check_correct( - multi( - check_call("f('hey', 3)").has_equal_value(), - check_call("f('hi', 2)").has_equal_value(), - check_call("f('hi')").has_equal_value() - ), - check_body().set_context('test', 1).multi( - has_equal_value(name = 'echo_word'), - has_equal_value(name = 'shout_words') - ) - ) - -Here: - -- ``check_function_def()`` zooms in on the function definition of ``shout_echo`` in both student and solution code (and process) -- ``check_correct()`` is used to - + First check whether the function gives the correct result when called in different ways (through ``check_call()``). - + Only if these 'function unit tests' don't pass, `check_correct()` will run the `check_body()` chain that dives deeper into the - function definition body. This chain sets the context variables - ``word1`` and ``echo``, the arguments of the function - to - the values ``'test'`` and ``1`` respectively, again while being agnostic to the actual name of these context variables. - -Notice how ``check_correct()`` is used to great effect here: why check the function definition internals if the I/O of the function works fine? -Because of this construct, all the following submissions will pass the SCT: - -.. code:: - - # passing submission 1 - def shout_echo(w, e=1): - ew = w * e - return ew + '!!!' - - # passing submission 2 - def shout_echo(a, b=1): - return a * b + '!!!' - -elif statements -~~~~~~~~~~~~~~~ - -In Python, when an if-else statement has an ``elif`` clause, it is held in the `orelse` part. -In this sense, an if-elif-else statement is represented by python as nested if-elses. More specifically, this if-else statement: - -.. code:: - - if x: - print(x) - elif y: - print(y) - else: - print('none') - -Is syntactically equivalent to: - -.. code:: - - if x: - print(x) - else: - if y: - print(y) - else: - print('none') - -The second representation has to be followed when writing the corresponding SCT: - -.. code:: - - Ex().check_if_else() \ - .check_orelse().check_if_else() \ - .check_orelse().has_equal_output() - -Class definition -~~~~~~~~~~~~~~~~ - -Suppose you want to check whether a class was defined correctly: - -.. code:: - - -The following SCT would verify this: - -.. code:: - - check_class_def('MyInt').multi( - check_bases(0).has_equal_ast(), - check_body().check_function_def('__init__').multi( - check_args('self'), - check_args('i'), - check_body().set_context(i = 2).multi( - check_function('super', signature=False), - check_function('super.__init__').check_args(0).has_equal_value() - ) - ) - ) - -- ``check_class_def()`` looks for the class definition itself. -- With ``check_bases()``, you can zoom in on the different basse classes that the class definition inherits from. -- With ``check_body()``, you zoom in on the class body, after which you can use other functions such - as ``check_function_def()`` to look for class methods. -- Of course, just like for other examples, you can use ``check_correct()`` where necessary, - e.g. to verify whether class methods give the right behavior with ``check_call()`` - before diving into the body of the method itself. - -Crazy combo -~~~~~~~~~~~ - -Suppose you want to check whether a function definition containing a for loop was coded correctly. Here's an example: - -.. code:: - - # solution def counter(lst, key): count = 0 for l in lst: count += l[key] return count - # sct that robustly checks this +The following SCT would robustly verify this: + +.. code:: + Ex().check_function_def('counter').check_correct( multi( check_call("f([{'a': 1}], 'a')").has_equal_value(), @@ -314,7 +59,7 @@ Suppose you want to check whether a function definition containing a for loop wa Some notes about this SCT: -- ``check_correct()`` is again used so the body is not further checked if calling the function in different ways produces the same value in both student and solution process. +- ``check_correct()`` is used so the body is not further checked if calling the function in different ways produces the same value in both student and solution process. - ``set_context()`` is used twice. Once to set the context variables introduced by the function definition, and once to set the context variable introducted by the for loop. - ``set_env()`` had to be used to initialize ``count`` to a variable that was scoped only to the function definition. @@ -406,3 +151,9 @@ The table below summarizes all checks that pythonwhat supports to test compound | | | | | | | | +------------------------+------------------------------------------------------+-------------------+ +|check_class_def('f') | .. code:: | | +| | | | +| | class KLS(BASES[0], BASES[1]): | | +| | BODY | | +| | | | ++------------------------+------------------------------------------------------+-------------------+ diff --git a/docs/articles/checking_imports.rst b/docs/articles/checking_imports.rst deleted file mode 100644 index aa7f1dbf..00000000 --- a/docs/articles/checking_imports.rst +++ /dev/null @@ -1,40 +0,0 @@ -Checking imports ----------------- - -Python features many ways to import packages. All of these different methods revolve around the ``import``, ``from`` and ``as`` keywords. -``has_import()`` provides a robust way to check whether a student correctly imported a certain package. - -Take this example to check whether a studnet imported ``matplotlib.pyplot`` as ``plt``: - -.. code:: - - # solution - import matplotlib.pyplot as plt - - # sct - Ex().has_import("matplotlib.pyplot") - - # passing submissions - import matplotlib.pyplot as plt - from matplotlib import pyplot as plt - import matplotlib.pyplot as pltttt - - # failing submissions - import matplotlib as mpl - -By default, ``has_import()`` allows for different ways of aliasing the imported package or function. If you want to make sure the correct alias was used to refer to the package or function that was imported, set ``same_as=True``. - -.. code:: - - # solution - import matplotlib.pyplot as plt - - # sct - Ex().has_import("matplotlib.pyplot", same_as=True) - - # passing submissions - import matplotlib.pyplot as plt - from matplotlib import pyplot as plt - - # failing submissions - import matplotlib.pyplot as pltttt diff --git a/docs/articles/checking_objects.rst b/docs/articles/checking_objects.rst index 1335e745..70e04477 100644 --- a/docs/articles/checking_objects.rst +++ b/docs/articles/checking_objects.rst @@ -74,7 +74,7 @@ You can use the `check_keys()` function to 'zoom in' on a particular key in a di my_df = pd.DataFrame({"a": [1, 2, 3], "b": [4, 5, 6]}) # sct - Ex().check_df("my_df").check_key("a").has_equal_value() + Ex().check_df("my_df").check_keys("a").has_equal_value() # passing submissions my_df = pd.DataFrame({"a": [1, 1 + 1, 3], "b": [4, 5, 6]}) diff --git a/docs/articles/electives.rst b/docs/articles/electives.rst index ea14c36d..2808abe2 100644 --- a/docs/articles/electives.rst +++ b/docs/articles/electives.rst @@ -175,15 +175,6 @@ by name in the SCT above, they may also be given by position.. Ex().check_for_loop().check_body().set_context(0, 'a').has_equal_output() - -Instructor Errors -~~~~~~~~~~~~~~~~~ - -If you are unsure what variables can be set, it's often easiest to take a guess. -When you try to set context values that don't match any target variables in the solution code, -``set_context()`` raises an exception that lists the ones available. - - with_context ============ @@ -216,21 +207,3 @@ This code runs by and replacing step (3) with any sub-tests given as arguments. -fail -==== - -.. autofunction:: pythonwhat.check_logic.fail - -Fails. This function takes a single argument, ``msg``, that is the feedback given to the student. -Note that this would be a terrible idea for grading submissions, but may be useful while writing SCTs. -For example, failing a test will highlight the code as if the previous test/check had failed. - -As a trivial SCT example, - -.. code:: - - Ex().check_for_loop().check_body().fail() # fails boo - -This can also be helpful for debugging SCTs, as it can be used to stop testing as a given point. - - diff --git a/docs/glossary.rst b/docs/glossary.rst index 867386b0..3633ae83 100644 --- a/docs/glossary.rst +++ b/docs/glossary.rst @@ -211,96 +211,36 @@ Check output print("this is weird stuff") print("Thisis weird stuff") -Check import -~~~~~~~~~~~~ +Check Multiple Choice +~~~~~~~~~~~~~~~~~~~~~ .. code:: - # solution - import matplotlib.pyplot as plt + # solution (implicit) + # 3 is the correct answer # sct - Ex().has_import("matplotlib.pyplot") - - # passing submissions - import matplotlib.pyplot as plt - from matplotlib import pyplot as plt - import matplotlib.pyplot as pltttt + Ex().has_chosen(correct = 3, # 1-base indexed + msgs = ["That's someone who makes soups.", + "That's a clown who likes burgers.", + "Correct! Head over to the next exercise!"]) - # failing submissions - import matplotlib as mpl +Check import +~~~~~~~~~~~~ +`See has_import doc `_ Check if statement ~~~~~~~~~~~~~~~~~~ -.. code:: - - # solution - x = 4 - if x > 0: - print("x is strictly positive") - - # sct - Ex().check_if_else().multi( - check_test().multi([ has_equal_value(extra_env = {'x': i}) for i in [4, -1, 0, 1] ]), - check_body().check_function('print', 0).check_args('value').has_equal_value() - ) - - # passing submission - x = 4 - if 0 < x: - print("x is strictly positive") +`See check_if_else doc `_ Check function definition ~~~~~~~~~~~~~~~~~~~~~~~~~ -.. code:: - - # solution - def shout_echo(word1, echo=1): - echo_word = word1 * echo - shout_words = echo_word + '!!!' - return shout_words - - # sct - Ex().check_function_def('shout_echo').check_correct( - multi( - check_call("f('hey', 3)").has_equal_value(), - check_call("f('hi', 2)").has_equal_value(), - check_call("f('hi')").has_equal_value() - ), - check_body().set_context('test', 1).multi( - has_equal_value(name = 'echo_word'), - has_equal_value(name = 'shout_words') - ) - ) +`See check_function_def doc `_ Check list comprehensions ~~~~~~~~~~~~~~~~~~~~~~~~~ -.. code:: - - # solution - L2 = [ i*2 for i in range(0,10) if i>2 ] - - # sct - Ex().check_list_comp().multi( - check_body().has_code('i\*2'), - check_iter().has_equal_value(), - check_ifs(0).multi([has_equal_value(context_vals=[i]) for i in range(0,10)]) - ) - -Check Multiple Choice -~~~~~~~~~~~~~~~~~~~~~ - -.. code:: - - # solution (implicit) - # 3 is the correct answer - - # sct - Ex().has_chosen(correct = 3, # 1-base indexed - msgs = ["That's someone who makes soups.", - "That's a clown who likes burgers.", - "Correct! Head over to the next exercise!"]) \ No newline at end of file +`See check_list_comp doc `_ diff --git a/docs/index.rst b/docs/index.rst index 501766eb..13a18d74 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -21,7 +21,6 @@ The reference docs become useful when you grasp all concepts and want to look up .. toctree:: :maxdepth: 2 - :caption: Reference reference @@ -34,7 +33,6 @@ The reference docs become useful when you grasp all concepts and want to look up articles/checking_function_calls.rst articles/make_your_sct_robust.rst articles/checking_output.rst - articles/checking_imports.rst articles/checking_through_string_matching.rst .. toctree:: diff --git a/docs/reference.rst b/docs/reference.rst index cef0bf19..fb08174a 100644 --- a/docs/reference.rst +++ b/docs/reference.rst @@ -1,10 +1,13 @@ +Reference +========= + .. note:: - - ``check_`` functions produce a child state that 'dives' deeper into a part of the state it was passed. They are typically chained off of for further checking. - - ``has_`` functions always **return the state that they were intially passed** and are used at the 'end' of a chain. + - ``check_`` functions typically 'dive' deeper into a part of the state it was passed. They are typically chained for further checking. + - ``has_`` functions always return the state that they were intially passed and are used at the 'end' of a chain. -SCT building blocks -------------------- +Building blocks +--------------- .. autofunction:: pythonwhat.has_funcs.has_code .. autofunction:: pythonwhat.has_funcs.has_output @@ -16,29 +19,53 @@ SCT building blocks .. autofunction:: pythonwhat.has_funcs.has_equal_error .. autofunction:: pythonwhat.has_funcs.has_equal_ast -Checking objects ----------------- +Combining SCTs +-------------- + +.. autofunction:: pythonwhat.check_logic.multi +.. autofunction:: pythonwhat.check_logic.check_correct +.. autofunction:: pythonwhat.check_logic.check_or +.. autofunction:: pythonwhat.check_logic.check_not + +Objects +------- .. autofunction:: pythonwhat.check_object.check_object .. autofunction:: pythonwhat.check_object.is_instance .. autofunction:: pythonwhat.check_object.check_df .. autofunction:: pythonwhat.check_object.check_keys -Checking function calls and definitions ---------------------------------------- +Function calls +-------------- -.. autofunction:: pythonwhat.has_funcs.has_equal_part_len .. autofunction:: pythonwhat.check_function.check_function -.. autofunction:: pythonwhat.check_funcs.check_call .. autofunction:: pythonwhat.check_funcs.check_args -Combining SCTs --------------- +Function/Class/Lambda definitions +--------------------------------- -.. autofunction:: pythonwhat.check_logic.multi -.. autofunction:: pythonwhat.check_logic.check_correct -.. autofunction:: pythonwhat.check_logic.check_or -.. autofunction:: pythonwhat.check_logic.check_not +.. autofunction:: pythonwhat.check_wrappers.check_function_def +.. autofunction:: pythonwhat.has_funcs.has_equal_part_len +.. autofunction:: pythonwhat.check_funcs.check_call +.. autofunction:: pythonwhat.check_wrappers.check_class_def +.. autofunction:: pythonwhat.check_wrappers.check_lambda_function + +Control flow +------------ + +.. autofunction:: pythonwhat.check_wrappers.check_if_else +.. autofunction:: pythonwhat.check_wrappers.check_try_except +.. autofunction:: pythonwhat.check_wrappers.check_if_exp +.. autofunction:: pythonwhat.check_wrappers.check_with + +Loops +----- + +.. autofunction:: pythonwhat.check_wrappers.check_for_loop +.. autofunction:: pythonwhat.check_wrappers.check_while +.. autofunction:: pythonwhat.check_wrappers.check_list_comp +.. autofunction:: pythonwhat.check_wrappers.check_dict_comp +.. autofunction:: pythonwhat.check_wrappers.check_generator_exp State Management ---------------- @@ -47,3 +74,10 @@ State Management .. autofunction:: pythonwhat.check_logic.disable_highlighting .. autofunction:: pythonwhat.check_logic.set_context .. autofunction:: pythonwhat.check_logic.set_env + +Electives +--------- + +.. autofunction:: pythonwhat.has_funcs.has_chosen +.. autofunction:: pythonwhat.test_exercise.success_msg +.. autofunction:: pythonwhat.check_logic.fail \ No newline at end of file diff --git a/pythonwhat/State.py b/pythonwhat/State.py index cc3cae9a..5e6832fe 100644 --- a/pythonwhat/State.py +++ b/pythonwhat/State.py @@ -212,9 +212,9 @@ def has_different_processes(self): # play it safe (most common) return True - def assert_root(self, fun): + def assert_root(self, fun, extra_msg=""): if self.parent_state is not None: - raise InstructorError("`%s()` should only be called from the root state, `Ex()`." % fun) + raise InstructorError("`%s()` should only be called from the root state, `Ex()`. %s" % (fun, extra_msg)) def assert_is(self, klasses, fun, prev_fun): if self.__class__.__name__ not in klasses: diff --git a/pythonwhat/check_logic.py b/pythonwhat/check_logic.py index 3bb63a69..44f1d9d1 100644 --- a/pythonwhat/check_logic.py +++ b/pythonwhat/check_logic.py @@ -147,7 +147,23 @@ def diagnose_and_check(state=None): # utility functions ----------------------------------------------------------- def fail(msg="", state=None): - """Fail test with message""" + """Fail SCT + + This function takes a single argument, ``msg``, that is the feedback given to the student. + Note that this would be a terrible idea for grading submissions, but may be useful while writing SCTs. + For example, failing a test will highlight the code as if the previous test/check had failed. + + :Example: + + As a trivial SCT example, :: + + Ex().check_for_loop().check_body().fail() + + This can also be helpful for debugging SCTs, as it can be used to stop testing as a given point. + + + +""" rep = Reporter.active_reporter _msg = state.build_message(msg) rep.do_test(Test(Feedback(_msg, state))) @@ -200,8 +216,10 @@ def set_context(*args, state=None, **kwargs): the ``has_equal_x()`` functions. - Note 1: excess args and unmatched kwargs will be unused in the student environment. - - Note 2: positional arguments are more robust to the student using different names for context values. - - Note 3: You have to specify arguments either by position, either by name. A combination is not possible. + - Note 2: When you try to set context values that don't match any target variables in the solution code, + ``set_context()`` raises an exception that lists the ones available. + - Note 3: positional arguments are more robust to the student using different names for context values. + - Note 4: You have to specify arguments either by position, either by name. A combination is not possible. :Example: @@ -291,8 +309,8 @@ def set_env(state = None, **kwargs): # check if condition works with different values of a Ex().check_if_else().check_test().multi( - set_env(a = 3).has_equal_value() - set_env(a = 4).has_equal_value() + set_env(a = 3).has_equal_value(), + set_env(a = 4).has_equal_value(), set_env(a = 5).has_equal_value() ) diff --git a/pythonwhat/check_object.py b/pythonwhat/check_object.py index bb643c59..95ddd881 100644 --- a/pythonwhat/check_object.py +++ b/pythonwhat/check_object.py @@ -44,7 +44,8 @@ def check_object(index, missing_msg=None, expand_msg=None, state=None, typestr=" # Only do the assertion if PYTHONWHAT_V2_ONLY is set to '1' if v2_only(): - state.assert_root('check_object') + extra_msg = "If you want to check the value of an object in e.g. a for loop, use `has_equal_value(name = 'my_obj')` instead." + state.assert_root('check_object', extra_msg=extra_msg) if missing_msg is None: missing_msg = "Did you define the {{typestr}} `{{index}}` without errors?" diff --git a/pythonwhat/check_wrappers.py b/pythonwhat/check_wrappers.py index d9b08d4d..263b2c40 100644 --- a/pythonwhat/check_wrappers.py +++ b/pythonwhat/check_wrappers.py @@ -4,8 +4,9 @@ from pythonwhat.check_function import check_function from pythonwhat.check_has_context import has_context -from functools import partial +from functools import partial, update_wrapper import inspect +from jinja2 import Template __PART_WRAPPERS__ = { 'iter': 'iterable part', @@ -25,18 +26,568 @@ } __NODE_WRAPPERS__ = { - 'list_comp': '{{ordinal}} list comprehension', - 'generator_exp': '{{ordinal}} generator expression', - 'dict_comp': '{{ordinal}} dictionary comprehension', - 'for_loop': '{{ordinal}} for loop', - 'function_def': 'definition of `{{index}}()`', - 'class_def': 'class definition of `{{index}}`', - 'if_exp': '{{ordinal}} if expression', - 'if_else': '{{ordinal}} if statement', - 'lambda_function': '{{ordinal}} lambda function', - 'try_except': '{{ordinal}} try statement', - 'while': '{{ordinal}} `while` loop', - 'with': '{{ordinal}} `with` statement', + 'list_comp': { + 'typestr': '{{ordinal}} list comprehension', + 'docstr': """Check whether a list comprehension was coded and zoom in on it. + + Can be chained with ``check_iter()``, ``check_body()``, and ``check_ifs()``. + + Args: + index: Index of the list comprehension (0-based) + {{typestr}} + {{missing_msg}} + {{expand_msg}} + + :Example: + + Suppose you expect students to create a list ``my_list`` as follows: :: + + my_list = [ i*2 for i in range(0,10) if i>2 ] + + The following SCT would robustly verify this: :: + + Ex().check_correct( + check_object('my_list').has_equal_value(), + check_list_comp().multi( + check_iter().has_equal_value(), + check_body().set_context(4).has_equal_value(), + check_ifs(0).multi( + set_context(0).has_equal_value(), + set_context(3).has_equal_value(), + set_context(5).has_equal_value() + ) + ) + ) + + - With ``check_correct()``, we're making sure that the list comprehension + checking is not executed if ``my_list`` was calculated properly. + - If ``my_list`` is not correct, the 'diagnose' chain will run: ``check_list_comp()`` looks + for the first list comprehension in the student's submission. + - Next, ``check_iter()`` zooms in on the iterator, ``range(0, 10)`` in the case of the solution. + ``has_equal_value()`` verifies whether the expression that the student used evaluates to the + same value as the expression that the solution used. + - ``check_body()`` zooms in on the body, ``i*2`` in the case of the solution. + ``set_context()`` sets the iterator to 4, allowing for the fact that the student used another name instead of ``i`` for this iterator. + ``has_equal_value()`` reruns the body in the student and solution code with the iterator set to 4, and checks if the results are the same. + - ``check_ifs(0)`` zooms in on the first ``if`` of the list comprehension, ``i>2`` in case of the solution. + With a series of ``set_context()`` and ``has_equal_value()``, it is verifies whether this condition evaluates to the same value in student + and solution code for different values of the iterator (`i` in the case of the solution, whatever in the case of the student). + + """, + }, + 'generator_exp': { + 'typestr': '{{ordinal}} generator expression', + 'docstr': """Check whether a generator expression was coded and zoom in on it. + + Can be chained with ``check_iter()``, ``check_body()``, and ``check_ifs()``. + + Args: + index: Index of the generator expression (0-based) + {{typestr}} + {{missing_msg}} + {{expand_msg}} + + :Example: + + Suppose you expect students to create a generator ``my_gen`` as follows: :: + + my_gen = ( i*2 for i in range(0,10) ) + + The following SCT would robustly verify this: :: + + Ex().check_correct( + check_object('my_gen').has_equal_value(), + check_generator_exp().multi( + check_iter().has_equal_value(), + check_body().set_context(4).has_equal_value() + ) + ) + + Have a look at ``check_list_comp`` to understand what's going on; it is very similar. + + """, + }, + 'dict_comp': { + 'typestr': '{{ordinal}} dictionary comprehension', + 'docstr': """Check whether a dictionary comprehension was coded and zoom in on it. + + Can be chained with ``check_key()``, ``check_value()``, and ``check_ifs()``. + + Args: + index: Index of the dictionary comprehension (0-based) + {{typestr}} + {{missing_msg}} + {{expand_msg}} + + :Example: + + Suppose you expect students to create a dictionary ``my_dict`` as follows: :: + + my_dict = { m:len(m) for m in ['a', 'ab', 'abc'] } + + The following SCT would robustly verify this: :: + + Ex().check_correct( + check_object('my_dict').has_equal_value(), + check_dict_comp().multi( + check_iter().has_equal_value(), + check_key().set_context('ab').has_equal_value(), + check_value().set_context('ab').has_equal_value() + ) + ) + + - With ``check_correct()``, we're making sure that the dictionary comprehension + checking is not executed if ``my_dict`` was created properly. + - If ``my_dict`` is not correct, the 'diagnose' chain will run: ``check_dict_comp()`` looks + for the first dictionary comprehension in the student's submission. + - Next, ``check_iter()`` zooms in on the iterator, ``['a', 'ab', 'abc']`` in the case of the solution. + ``has_equal_value()`` verifies whether the expression that the student used evaluates to the + same value as the expression that the solution used. + - ``check_key()`` zooms in on the key of the comprehension, ``m`` in the case of the solution. + ``set_context()`` temporaritly sets the iterator to ``'ab'``, allowing for the fact that the student used another name instead of ``m`` for this iterator. + ``has_equal_value()`` reruns the key expression in the student and solution code with the iterator set to ``'ab'``, and checks if the results are the same. + - ``check_value()`` zooms in on the value of the comprehension, ``len(m)`` in the case of the solution. + ``has_equal_value()`` reruns the value expression in the student and solution code with the iterator set to ``'ab'``, and checks if the results are the same. + + """, + }, + 'for_loop': { + 'typestr': '{{ordinal}} for loop', + 'docstr': """Check whether a for loop was coded and zoom in on it. + + Can be chained with ``check_iter()`` and ``check_body()``. + + Args: + index: Index of the for loop (0-based). + {{typestr}} + {{missing_msg}} + {{expand_msg}} + + :Example: + + Suppose you want a student to iterate over a predefined dictionary ``my_dict`` and do the appropriate printouts: :: + + for key, value in my_dict.items(): + print(key + " - " + str(value)) + + The following SCT would verify this: :: + + Ex().check_for_loop().multi( + check_iter().has_equal_value(), + check_body().multi( + set_context('a', 1).has_equal_output(), + set_context('b', 2).has_equal_output() + ) + ) + + - ``check_for_loop()`` zooms in on the ``for`` loop, and makes its parts available for further checking. + - ``check_iter()`` zooms in on the iterator part of the for loop, ``my_dict.items()`` in the solution. + ``has_equal_value()`` re-executes the expressions specified by student and solution and compares their results. + - ``check_body()`` zooms in on the body part of the for loop, ``print(key + " - " + str(value))``. + For different values of ``key`` and ``value``, the student's body and solution's body are executed again and the printouts are captured and compared to see if they are equal. + + Notice how you do not need to specify the variables by name in ``set_context()``. pythonwhat can figure out the variable names used in both student and solution code, and + can do the verification independent of that. That way, we can make the SCT robust against submissions that code the correct logic, but use different names for the context values. + In other words, the following student submissions that would also pass the SCT: :: + + # passing submission 1 + my_dict = {'a': 1, 'b': 2} + for k, v in my_dict.items(): + print(k + " - " + str(v)) + + # passing submission 2 + my_dict = {'a': 1, 'b': 2} + for first, second in my_dict.items(): + mess = first + " - " + str(second) + print(mess) + + :Example: + + As another example, suppose you want the student to build a list of doubles as follows: :: + + even = [] + for i in range(10): + even.append(2*i) + + The following SCT would robustly verify this: :: + + Ex().check_correct( + check_object('even').has_equal_value(), + check_for_loop().multi( + check_iter().has_equal_value(), + check_body().set_context(2).set_env(even = []).\\ + has_equal_value(name = 'even') + ) + ) + + - ``check_correct()`` makes sure that we do not dive into the ``for`` loop if the array ``even`` is correctly populated in the end. + - If ``even`` was not correctly populated, ``check_for_loop()`` will zoom in on the for loop. + - The ``check_iter()`` chain verifies whether `range(10)` (or something equivalent) was used to iterate over. + - ``check_body()`` zooms in on the body, and reruns the body (``even.append(2*i)`` in the solution) for ``i`` equal to 2, and even temporarily set to an empty array. + Notice how we use ``set_context()`` to robustly set the context value (the student can use a different variable name), while we have to explicitly set ``even`` with ``set_env()``. + Also notice how we use ``has_equal_value(name = 'even')`` instead of the usual ``check_object()``; ``check_object()`` can only be called from the root state ``Ex()``. + + :Example: + + As a follow-up example, suppose you want the student to build a list of doubles of the even numbers only: :: + + even = [] + for i in range(10): + if i % 2 == 0: + even.append(2*i) + + The following SCT would robustly verify this: :: + + Ex().check_correct( + check_object('even').has_equal_value(), + check_for_loop().multi( + check_iter().has_equal_value(), + check_body().check_if_else().multi( + check_test().multi( + set_context(1).has_equal_value(), + set_context(2).has_equal_value() + ), + check_body().set_context(2).\\ + set_env(even = []).has_equal_value(name = 'even') + ) + ) + ) + + """ + }, + 'function_def': { + 'typestr': 'definition of `{{index}}()`', + 'docstr': """Check whether a function was defined and zoom in on it. + + Can be chained with ``check_call()``, ``check_args()`` and ``check_body()``. + + Args: + index: the name of the function definition. + {{typestr}} + {{missing_msg}} + {{expand_msg}} + + :Example: + + Suppose you want a student to create a function ``shout_echo()``: :: + + def shout_echo(word1, echo=1): + echo_word = word1 * echo + shout_words = echo_word + '!!!' + return shout_words + + The following SCT robustly checks this: :: + + Ex().check_function_def('shout_echo').check_correct( + multi( + check_call("f('hey', 3)").has_equal_value(), + check_call("f('hi', 2)").has_equal_value(), + check_call("f('hi')").has_equal_value() + ), + check_body().set_context('test', 1).multi( + has_equal_value(name = 'echo_word'), + has_equal_value(name = 'shout_words') + ) + ) + + Here: + + - ``check_function_def()`` zooms in on the function definition of ``shout_echo`` in both student and solution code (and process). + - ``check_correct()`` is used to + + + First check whether the function gives the correct result when called in different ways (through ``check_call()``). + + Only if these 'function unit tests' don't pass, ``check_correct()`` will run the `check_body()` chain that dives deeper into the + function definition body. This chain sets the context variables - ``word1`` and ``echo``, the arguments of the function - to + the values ``'test'`` and ``1`` respectively, again while being agnostic to the actual name of these context variables. + + Notice how ``check_correct()`` is used to great effect here: why check the function definition internals if the I/O of the function works fine? + Because of this construct, all the following submissions will pass the SCT: :: + + # passing submission 1 + def shout_echo(w, e=1): + ew = w * e + return ew + '!!!' + + # passing submission 2 + def shout_echo(a, b=1): + return a * b + '!!!' + """ + }, + 'class_def': { + 'typestr': 'class definition of `{{index}}`', + 'docstr': """Check whether a class was defined and zoom in on its definition + + Can be chained with ``check_bases()`` and ``check_body()``. + + Args: + index: the name of the function definition. + {{typestr}} + {{missing_msg}} + {{expand_msg}} + + :Example: + + Suppose you want to check whether a class was defined correctly: :: + + class MyInt(int): + def __init__(self, i): + super().__init__(i + 1) + + The following SCT would verify this: :: + + check_class_def('MyInt').multi( + check_bases(0).has_equal_ast(), + check_body().check_function_def('__init__').multi( + check_args('self'), + check_args('i'), + check_body().set_context(i = 2).multi( + check_function('super', signature=False), + check_function('super.__init__').check_args(0).has_equal_value() + ) + ) + ) + + - ``check_class_def()`` looks for the class definition itself. + - With ``check_bases()``, you can zoom in on the different basse classes that the class definition inherits from. + - With ``check_body()``, you zoom in on the class body, after which you can use other functions such + as ``check_function_def()`` to look for class methods. + - Of course, just like for other examples, you can use ``check_correct()`` where necessary, + e.g. to verify whether class methods give the right behavior with ``check_call()`` + before diving into the body of the method itself. + + """ + }, + 'if_exp': { + 'typestr': '{{ordinal}} if expression', + 'docstr': """Check whether an if expression was coded zoom in on it. + + This function works the exact same way as ``check_if_else()``. + """ + }, + 'if_else': { + 'typestr': '{{ordinal}} if statement', + 'docstr': """Check whether an if statement was coded zoom in on it. + + Args: + index: the index of the if statement to look for (0 based) + {{typestr}} + {{missing_msg}} + {{expand_msg}} + + :Example: + + Suppose you want students to print out a message if ``x`` is larger than 0: :: + + x = 4 + if x > 0: + print("x is strictly positive") + + The following SCT would verify that: :: + + Ex().check_if_else().multi( + check_test().multi( + set_env(x = -1).has_equal_value(), + set_env(x = 1).has_equal_value(), + set_env(x = 0).has_equal_value() + ), + check_body().check_function('print', 0).\\ + check_args('value').has_equal_value() + ) + + - ``check_if_else()`` zooms in on the first if statement in the student and solution submission. + - ``check_test()`` zooms in on the 'test' portion of the if statement, ``x > 0`` in case of the solution. + ``has_equal_value()`` reruns this expression and the corresponding expression in the student code for + different values of ``x`` (set with ``set_env()``) and compare there results. + This way, you can robustly verify whether the if test was coded up correctly. If the student + codes up the condition as ``0 < x``, this would also be accepted. + - ``check_body()`` zooms in on the 'body' portion of the if statement, ``print("...")`` in case of the solution. + With a classical ``check_function()`` chain, it is verified whether the if statement contains a + function ``print()`` and whether its argument is set correctly. + + :Example: + + In Python, when an if-else statement has an ``elif`` clause, it is held in the `orelse` part. + In this sense, an if-elif-else statement is represented by python as nested if-elses. + More specifically, this if-else statement: :: + + if x > 0: + print(x) + elif y > 0: + print(y) + else: + print('none') + + Is syntactically equivalent to: :: + + if x > 0: + print(x) + else: + if y > 0: + print(y) + else: + print('none') + + The second representation has to be followed when writing the corresponding SCT: :: + + Ex().check_if_else().multi( + check_test(), # zoom in on x > 0 + check_body(), # zoom in on print(x) + check_orelse().check_if_else().multi( + check_test(), # zoom in on y > 0 + check_body(), # zoom in on print(y) + check_orelse() # zoom in on print('none') + ) + ) + + """ + }, + 'lambda_function': { + 'typestr': '{{ordinal}} lambda function', + 'docstr': """Check whether a lambda function was coded zoom in on it. + + Can be chained with ``check_call()``, ``check_args()`` and ``check_body()``. + + Args: + index: the index of the lambda function (0-based). + {{typestr}} + {{missing_msg}} + {{expand_msg}} + + :Example: + + Suppose you want a student to create a lambda function + that returns the length of an array times two: :: + + lambda x: len(x)*2 + + The following SCT robustly checks this: :: + + Ex().check_lambda_function().check_correct( + multi( + check_call("f([1])").has_equal_value(), + check_call("f([1, 2])").has_equal_value() + ), + check_body().set_context([1, 2, 3]).has_equal_value() + ) + + Here: + + - ``check_lambda_function()`` zooms in on the first lambda function in both student and solution code. + - ``check_correct()`` is used to + + + First check whether the lambda function gives the correct result when called in different ways (through ``check_call()``). + + Only if these 'function unit tests' don't pass, ``check_correct()`` will run the `check_body()` chain that dives deeper into the + lambda function's body. This chain sets the context variable `x`, the argument of the function, to + the values ``[1, 2, 3]``, while being agnostic to the actual name the student used for this context variable. + + Notice how ``check_correct()`` is used to great effect here: why check the function definition internals if the I/O of the function works fine? + Because of this construct, all the following submissions will pass the SCT: :: + + # passing submission 1 + lambda x: len(x) + len(x) + + # passing submission 2 + lambda y, times=2: len(y) * times + """ + }, + 'try_except': { + 'typestr': '{{ordinal}} try statement', + 'docstr': """Check whether a try except statement was coded zoom in on it. + + Can be chained with ``check_body()``, ``check_handlers()``, ``check_orelse()`` and ``check_finalbody()``. + + Args: + index: the index of the try except statement (0-based). + {{typestr}} + {{missing_msg}} + {{expand_msg}} + + :Example: + + Suppose you want to verify whether the student did a `try-except` statement properly: + + do_dangerous_thing = lambda n: n + + try: + x = do_dangerous_thing(n = 4) + except ValueError as e: + x = 'something wrong with inputs' + except: + x = 'something went wrong' + finally: + print('ciao!') + + The following SCT can be used to verify this: :: + + Ex().check_try_except().multi( + check_body().\\ + check_function('do_dangerous_thing').\\ + check_args('n').has_equal_value(), + check_handlers('ValueError').\\ + has_equal_value(name = 'x'), + check_handlers('all').\\ + has_equal_value(name = 'x'), + check_finalbody().\\ + check_function('print').check_args(0).has_equal_value() + ) + + """ + }, + 'while': { + 'typestr': '{{ordinal}} `while` loop', + 'docstr': """Check whether a while loop was coded and zoom in on it. + + Can be chained with ``check_test()``, ``check_body()`` and ``check_orelse()``. + + Args: + index: the index of the while loop to verify (0-based). + {{typestr}} + {{missing_msg}} + {{expand_msg}} + + :Example: + + Suppose you want a student to code a while loop that counts down a counter from 50 until + a multilpe of 11 is found. If it is found, the value should be printed out. :: + + i = 50 + while i % 11 != 0: + i -= 1 + + The following SCT robustly verifies this: :: + + Ex().check_correct( + check_object('i').has_equal_value(), + check_while().multi( + check_test().multi( + set_env(i = 45).has_equal_value(), + set_env(i = 44).has_equal_value() + ), + check_body().set_env(i = 3).has_equal_value(name = 'i') + ) + ) + + - ``check_correct()`` first checks whether the end result of ``i`` is correct. If it is, the entire chain that checks the ``while`` loop is skipped. + - If ``i`` is not correctly calculated, ``check_while_loop()`` zooms in on the while loop. + - ``check_test()`` zooms in on the condition of the ``while`` loop, ``i % 11 != 0`` in the solution, and verifies whether + the expression gives the same results for different values of ``i``, set through ``set_env()``, when comparing student and solution. + - ``check_body()`` zooms in on the body of the ``while`` loop, and ``has_equal_value()`` checks whether rerunning this body + updates ``i`` as expected when ``i`` is temporarily set to 3 with ``set_env()``. + + """ + }, + 'with': { + 'typestr': '{{ordinal}} `with` statement', + 'docstr': """Check whether a with statement was coded zoom in on it. + + Args: + index: the index of the``with`` statement to verify (0-based) + {{typestr}} + {{missing_msg}} + {{expand_msg}} + + """ + }, } scts = {} @@ -48,13 +599,21 @@ # include rest of wrappers for k, v in __PART_WRAPPERS__.items(): + scts['check_'+k] = partial(check_part, k, v) for k, v in __PART_INDEX_WRAPPERS__.items(): scts['check_'+k] = partial(check_part_index, k, part_msg=v) for k, v in __NODE_WRAPPERS__.items(): - scts['check_'+k] = partial(check_node, k+'s', typestr=v) + check_fun = partial(check_node, k+'s', typestr=v['typestr']) + check_fun.__doc__ = Template(v['docstr']).render( + typestr="typestr: If specified, this overrides the standard way of referring to the construct you're zooming in on.", + missing_msg="missing_msg: If specified, this overrides the automatically generated feedback message in case the construct could not be found.", + expand_msg="expand_msg: If specified, this overrides the automatically generated feedback message that is prepended to feedback messages that are thrown further in the SCT chain." + ) + check_fun.__name__ = 'check_'+k + scts[check_fun.__name__] = check_fun for k in ['set_context', 'set_env', 'disable_highlighting', 'check_not', 'check_or', 'check_correct', 'fail', 'override', 'multi']: scts[k] = getattr(check_logic, k) @@ -71,4 +630,6 @@ scts[k] = getattr(check_object, k) scts['has_context'] = has_context -scts['check_function'] = check_function \ No newline at end of file +scts['check_function'] = check_function + +locals().update(scts) \ No newline at end of file diff --git a/pythonwhat/has_funcs.py b/pythonwhat/has_funcs.py index 3e1cdec2..4a735145 100644 --- a/pythonwhat/has_funcs.py +++ b/pythonwhat/has_funcs.py @@ -415,30 +415,52 @@ def has_import(name, state=None): """Checks whether student imported a package or function correctly. + Python features many ways to import packages. + All of these different methods revolve around the ``import``, ``from`` and ``as`` keywords. + ``has_import()`` provides a robust way to check whether a student correctly imported a certain package. + + By default, ``has_import()`` allows for different ways of aliasing the imported package or function. + If you want to make sure the correct alias was used to refer to the package or function that was imported, + set ``same_as=True``. + Args: name (str): the name of the package that has to be checked. same_as (bool): if True, the alias of the package or function has to be the same. Defaults to False. not_imported_msg (str): feedback message when the package is not imported. incorrect_as_msg (str): feedback message if the alias is wrong. - :Example: - Student code:: + Example 1, where aliases don't matter (defaut): :: - import numpy as np - import pandas as pa + # solution + import matplotlib.pyplot as plt - Solution code:: + # sct + Ex().has_import("matplotlib.pyplot") - import numpy as np - import pandas as pd + # passing submissions + import matplotlib.pyplot as plt + from matplotlib import pyplot as plt + import matplotlib.pyplot as pltttt - SCT:: + # failing submissions + import matplotlib as mpl + + Example 2, where the SCT is coded so aliases do matter: :: + + # solution + import matplotlib.pyplot as plt + + # sct + Ex().has_import("matplotlib.pyplot", same_as=True) + + # passing submissions + import matplotlib.pyplot as plt + from matplotlib import pyplot as plt - Ex().has_import("numpy") # pass - Ex().has_import("pandas") # pass - Ex().has_import("pandas", same_as=True) # fail + # failing submissions + import matplotlib.pyplot as pltttt """ @@ -546,7 +568,8 @@ def has_printout(index, print("random"); print(1, 2, 3, 4) """ - state.assert_root('has_printout') + extra_msg = "If you want to check printouts done in e.g. a for loop, you have to use a `check_function('print')` chain instead." + state.assert_root('has_printout', extra_msg=extra_msg) if not_printed_msg is None: not_printed_msg = "Have you used `{{sol_call}}` to do the appropriate printouts?" diff --git a/pythonwhat/test_exercise.py b/pythonwhat/test_exercise.py index 97c23f6c..d6c11526 100644 --- a/pythonwhat/test_exercise.py +++ b/pythonwhat/test_exercise.py @@ -62,7 +62,6 @@ def test_exercise(sct, return rep.build_final_payload() - def success_msg(message): """ Set the succes message of the sct. This message will be the feedback if all tests pass. diff --git a/pythonwhat/test_funcs/test_compound_statement.py b/pythonwhat/test_funcs/test_compound_statement.py index 736d22ec..2784b063 100644 --- a/pythonwhat/test_funcs/test_compound_statement.py +++ b/pythonwhat/test_funcs/test_compound_statement.py @@ -4,7 +4,7 @@ from pythonwhat.check_logic import multi from pythonwhat.has_funcs import has_equal_part_len, has_equal_part, has_equal_value, has_equal_output from pythonwhat.check_has_context import has_context -from functools import partial, update_wrapper +from functools import partial from pythonwhat.Reporter import Reporter from pythonwhat.Test import EqualTest, Test from pythonwhat import utils diff --git a/pythonwhat/test_funcs/test_function.py b/pythonwhat/test_funcs/test_function.py index 07ce6bd7..5cf08a8d 100644 --- a/pythonwhat/test_funcs/test_function.py +++ b/pythonwhat/test_funcs/test_function.py @@ -39,7 +39,6 @@ def test_function(name, index = index - 1 # if root-level (not in compound statement) calls: use has_printout - if name == 'print' and state.parent_state is None and do_eval: try: return has_printout(index=index, not_printed_msg=incorrect_msg, state=state) diff --git a/pythonwhat/utils_ast.py b/pythonwhat/utils_ast.py index fbede1fd..5bd865d3 100644 --- a/pythonwhat/utils_ast.py +++ b/pythonwhat/utils_ast.py @@ -15,7 +15,10 @@ def wrap_in_module(node): return new_node def assert_ast(state, element, fmt_kwargs): - patt = "You are zooming in on the {{part}}, but it is not an AST, so it can't be re-run." + patt = "You are zooming in on the {{part}}, but it is not an AST, so it can't be re-run." + \ + " If this error occurred because of ``check_args()``," + \ + "you may have to refer to your argument differently, e.g. `['args', 0]` or `['kwargs', 'a']`. " + \ + "Read https://pythonwhat.readthedocs.io/en/latest/articles/checking_function_calls.html#signatures for more info." _err_msg = "SCT fails on solution: " _err_msg += state.build_message(patt, fmt_kwargs) # element can also be { 'node': AST } diff --git a/tests/test_author_warnings.py b/tests/test_author_warnings.py index 3bba42e6..de8b034b 100644 --- a/tests/test_author_warnings.py +++ b/tests/test_author_warnings.py @@ -102,7 +102,7 @@ def test_has_printout_on_root(): def test_has_printout_not_on_root(): code = 'for i in range(3): print(i)' s = setup_state(code, code) - with pytest.raises(InstructorError, match=r"`has_printout\(\)` should only be called from the root state, `Ex\(\)`\."): + with pytest.raises(InstructorError, match=r"`has_printout\(\)` should only be called from the root state, `Ex\(\)`\. If you want to check printouts done in e.g. a for loop, you have to use a `check_function\('print'\)` chain instead."): s.check_for_loop().check_body().has_printout(0) def test_has_no_error_not_on_root(): @@ -127,7 +127,7 @@ def test_check_object_not_on_root_v2(): code = 'for i in range(3): x = 1' s = setup_state(code, code) with helper.set_v2_only_env('1'): - with pytest.raises(InstructorError, match=r"`check_object\(\)` should only be called from the root state, `Ex\(\)`\."): + with pytest.raises(InstructorError, match=r"`check_object\(\)` should only be called from the root state, `Ex\(\)`\. If you want to check the value of an object in e.g. a for loop, use `has_equal_value\(name = 'my_obj'\)` instead."): s.check_for_loop().check_body().check_object('x') def test_is_instance_not_on_check_object(): From c4ab6b75930a80d9a552bc5e58cb061cf2a6c1e9 Mon Sep 17 00:00:00 2001 From: Filip Schouwenaars Date: Thu, 27 Sep 2018 16:42:04 +0200 Subject: [PATCH 024/209] feat(docs) - Explain has_equal_value vs has_equal_ast - Get rid of checking output article -> move to ref - Get rid of checking objects article -> move to ref + more examples - Update structure of reference.rst --- docs/articles/checking_function_calls.rst | 25 +++- docs/articles/checking_objects.rst | 119 --------------- docs/articles/checking_output.rst | 69 --------- docs/index.rst | 2 - docs/reference.rst | 42 +++--- pythonwhat/check_funcs.py | 2 +- pythonwhat/check_function.py | 13 +- pythonwhat/check_object.py | 173 ++++++++++++++++++++-- pythonwhat/has_funcs.py | 112 +++++++++++--- 9 files changed, 310 insertions(+), 247 deletions(-) delete mode 100644 docs/articles/checking_objects.rst delete mode 100644 docs/articles/checking_output.rst diff --git a/docs/articles/checking_function_calls.rst b/docs/articles/checking_function_calls.rst index fc1fcb75..c7808a23 100644 --- a/docs/articles/checking_function_calls.rst +++ b/docs/articles/checking_function_calls.rst @@ -72,7 +72,7 @@ Now, the following submissions would fail: - ``val=2.718282; dig=3; round(number=val, dig)`` -- same - ``int_part = 2; dec_part = 0.718282; round(int_part + dec_part, 3)`` -- the string representation of ``int_part + dec_part`` in the student code is compered to ``2.718282`` in the solution code. -As you can see, doing exact string comparison of arguments is not a good idea, as it is very inflexible. +As you can see, doing exact string comparison of arguments is not a good idea here, as it is very inflexible. There are cases, however, where it makes sense to use this, e.g. when there are very big objects passed to functions, and you don't want to spend the processing power to fetch these objects from the student and solution processes. @@ -93,6 +93,27 @@ If the student did not properly call the function, ``check_function()`` will aut No matter how you import the function, you always have to refer to the function with its full name, e.g. ``package.subpackage1.subpackage2.function``. +has_equal_value? has_equal_ast? +=============================== + +In the customizations section above, you could already notice the difference between ``has_equal_value()`` and ``has_equal_ast()`` for checking +whether arguments are correct. The former **reruns** the expression used to specify the argument in both student and solution process +and compares their results, while the latter simply compares the expression's AST representations. Clearly, the former is more robust, but there +are some cases in which ``has_equal_ast()`` can be useful: + +- For better feedback. When using ``has_equal_ast()``, the 'expected x got y' message that is automatically generated when the arguments + don't match up will use the actual expressions used. ``has_equal_value()`` will use string representations of the evaluations of the expressions, + if they make sense, and this is typically less useful. +- To avoid very expensive object comparisons. If you are 100% sure that the object people have to pass as an argument is already correct (because + you checked it earlier in the SCT or because it was already specified in the pre exercise code) and doing an equality check on this object between + student and solution project is likely going to be expensive, then you can safely use ``has_equal_ast()`` to speed things up. +- If you want to save yourself the trouble of building exotic contexts. You'll often find yourself checking function calls in e.g. a for loop. + Typically, these function calls will use objects that were generated inside the loop. To easily unit test the body of a for loop, you'll typically + have to use ``set_context()`` and ``set_env()``. For exotic for loops, this can become tricky, and it might be a quick fix to be a little more + specific about the object names people should use, and just use ``has_equal_ast()`` for the argument comparison. That way, you're bypassing the need + to build up a context in the student/solution process and do object comparisions. + + Signatures ========== @@ -323,7 +344,7 @@ Here: - The second SCT is first checking whether ``df.groupby.mean()`` was called and whether calling it gives the right result. Notice several things: + We describe the entire chain of method calls, leaving out the parentheses and arguments used for method calls in between. - + We use ``sig_from_obj()`` to manually specify a Python expression that ``pythonwhat`` can use to derive the signature from. + + We use ``sig_from_obj()`` to manually specify a Python expression that pythonwhat can use to derive the signature from. If the string you use to describe the function to check evaluates to a method or function in the solution process, like for ``'df.groupby'``, pythonwhat can figure out the signature. However, for ``'df.groupby.mean'`` will `not` evaluate to a method object in the solution process, so we need to manually specify a valid expression that `will` evaluate to a valid signature with ``sig_from_obj()``. diff --git a/docs/articles/checking_objects.rst b/docs/articles/checking_objects.rst deleted file mode 100644 index 70e04477..00000000 --- a/docs/articles/checking_objects.rst +++ /dev/null @@ -1,119 +0,0 @@ -Checking objects ----------------- - -In ``pythonbackend``, both the student's submission as well as the solution code are executed, in separate processes. -``check_object()`` looks at these processes and checks if the referenced object is available in the student process. -Next, you can use ``has_equal_value()`` to check whether the objects in the student and solution process correspond. - -.. note:: - - For more information how DataCamp's coding backends run code, check out `this article `_ - -Basic example -============= - -Consider the following solution, and corresponding SCT: - -.. code:: - - # solution - x = 15 - - # sct option 1 - Ex().check_object("x").has_equal_value() - - # submissions that will pass this sct - x = 10 - x = 12 + 3 - x = 3; x += 12 - - -- ``check_object()`` will check if the variable ``x`` is defined in the student process. -- ``has_equal_value()`` will check whether the value of ``x`` in the solution process is the same as in the student process. - - -.. note:: - - If you only use ``Ex().check_object("x")`` without ``has_equal_value()``, you are only checking whether the object is defined. - -.. caution:: - - ``has_equal_value()`` only looks at **end result** of a variable in the student process. In the example, how the object ``x`` came about in the student's submission, does not matter. - -Checking the type of an object -============================== - -You can use ``is_instance()`` after ``check_object()`` to verify the class of an object: - -.. code:: - - # solution - x = 3 - - # sct - Ex().object("x").is_instance(int) - - # passing submissions - x = 3 - x = 4 - - # failing submissions - x = '3' - x = '4' - - -Checking objects with keys -========================== - -You can use the `check_keys()` function to 'zoom in' on a particular key in a dictionary or a pandas DataFrame: - -.. code:: - - # solution - import pandas as pd - my_df = pd.DataFrame({"a": [1, 2, 3], "b": [4, 5, 6]}) - - # sct - Ex().check_df("my_df").check_keys("a").has_equal_value() - - # passing submissions - my_df = pd.DataFrame({"a": [1, 1 + 1, 3], "b": [4, 5, 6]}) - my_df = pd.DataFrame({"b": [4, 5, 6], "a": [1, 2, 3]}) - -Here `check_df()` does two things: - -+ it checks whether the object ``my_df`` is defined in the student process (``check_object()`` behind the scenes). -+ it checks whether ``my_df`` is an object of type `pandas.DataFrame` (using ``is_instance()`` behind the scene). - -Exotic objects -============== - -pythonwhat compares the objects in the student and solution process with the ``==`` operator. -For basic objects, this ``==`` is operator is properly implemented, so that the objects can be effectively compared. -For more complex objects that are produced by third-party packages, however, it's possible that this equality operator is not implemented in a way you'd expect. -Often, for these object types the ``==`` will compare the actual object instances. - -.. code:: - - # pre exercise code - class Number(): - def __init__(self, n): - self.n = n - - # solution - x = Number(1) - - # sct that won't work - Ex().check_object().has_equal_value() - - # sct - Ex().check_object().has_equal_value(expr_code = 'x.n') - - # submissions that will pass this sct - x = Number(1) - x = Number(2 - 1) - -The basic SCT like in the previous example won't work here. -Notice how we used the ``expr_code`` argument to _override_ which value `has_equal_value()` is checking. -Instead of checking whether `x` corresponds between student and solution process, it's now executing the expression ``x.n`` -and seeing if the result of running this expression in both student and solution process match. diff --git a/docs/articles/checking_output.rst b/docs/articles/checking_output.rst deleted file mode 100644 index c5fa09a8..00000000 --- a/docs/articles/checking_output.rst +++ /dev/null @@ -1,69 +0,0 @@ -Checking output ---------------- - -Checking any output -~~~~~~~~~~~~~~~~~~~ - -Among the student and solution process, the student submission and solution code as a string, -the ``Ex()`` state also contains the output that a student generated with his or her submission. - -With ``has_output()``, you can access this output and match it against a regular or fixed expression. - -As an example, suppose we want a student to print out a sentence: - -.. code:: - - # Print the "This is some ... stuff" - print("This is some weird stuff") - - -The following SCT tests whether the student prints out ``This is some weird stuff``: - -.. code:: - - # Using exact string matching - Ex().has_output("This is some weird stuff", pattern = False) - - # Using a regular expression (more robust) - pattern = True is the default - Ex().has_output(r"This is some \w* stuff", - no_output_msg = "Print out ``This is some ... stuff`` to the output, fill in ``...`` with a word you like.") - - -Checking ``print()`` calls -~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Checking whether the right printouts were done is easy: - -.. code:: - - # solution - x = 4 - print(x) - - # sct - Ex().has_printout(0) - -``has_printout()`` will look for the printout in the solution code that you specified with ``index`` (0 in this case), rerun the ``print()`` call in -the solution process, capture its output, and verify whether the output is present in the output of the student. - -Watch out: ``has_printout()`` will effectively **rerun** the ``print()`` call in the solution process after the entire solution script was executed. -If your solution script updates the value of `x` after executing it, ``has_printout()`` will not work: - -.. code:: - - # solution - x = 4 - print(x) - x = 6 - - # sct that won't work - Ex().has_printout(0) - -In this example, when the ``print(x)`` call is executed, the value of ``x`` will be 6, and pythonwhat will look for the output `'6`' in the output the student generated. -In cases like these, default to using the classical pattern to check function calls: - -.. code:: - - # sct that will work - Ex().check_function('print').check_args(0).has_equal_value()`` - diff --git a/docs/index.rst b/docs/index.rst index 13a18d74..325b8a87 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -29,10 +29,8 @@ The reference docs become useful when you grasp all concepts and want to look up :caption: Basic articles articles/tutorial.rst - articles/checking_objects.rst articles/checking_function_calls.rst articles/make_your_sct_robust.rst - articles/checking_output.rst articles/checking_through_string_matching.rst .. toctree:: diff --git a/docs/reference.rst b/docs/reference.rst index fb08174a..25aabae4 100644 --- a/docs/reference.rst +++ b/docs/reference.rst @@ -6,14 +6,36 @@ Reference - ``check_`` functions typically 'dive' deeper into a part of the state it was passed. They are typically chained for further checking. - ``has_`` functions always return the state that they were intially passed and are used at the 'end' of a chain. -Building blocks ---------------- +Objects +------- + +.. autofunction:: pythonwhat.check_object.check_object +.. autofunction:: pythonwhat.check_object.is_instance +.. autofunction:: pythonwhat.check_object.check_df +.. autofunction:: pythonwhat.check_object.check_keys + +Function calls +-------------- + +.. autofunction:: pythonwhat.check_function.check_function +.. autofunction:: pythonwhat.check_funcs.check_args + +Output +------ -.. autofunction:: pythonwhat.has_funcs.has_code .. autofunction:: pythonwhat.has_funcs.has_output .. autofunction:: pythonwhat.has_funcs.has_printout .. autofunction:: pythonwhat.has_funcs.has_no_error + +Code +---- + +.. autofunction:: pythonwhat.has_funcs.has_code .. autofunction:: pythonwhat.has_funcs.has_import + +has_equal_x +----------- + .. autofunction:: pythonwhat.has_funcs.has_equal_value .. autofunction:: pythonwhat.has_funcs.has_equal_output .. autofunction:: pythonwhat.has_funcs.has_equal_error @@ -27,20 +49,6 @@ Combining SCTs .. autofunction:: pythonwhat.check_logic.check_or .. autofunction:: pythonwhat.check_logic.check_not -Objects -------- - -.. autofunction:: pythonwhat.check_object.check_object -.. autofunction:: pythonwhat.check_object.is_instance -.. autofunction:: pythonwhat.check_object.check_df -.. autofunction:: pythonwhat.check_object.check_keys - -Function calls --------------- - -.. autofunction:: pythonwhat.check_function.check_function -.. autofunction:: pythonwhat.check_funcs.check_args - Function/Class/Lambda definitions --------------------------------- diff --git a/pythonwhat/check_funcs.py b/pythonwhat/check_funcs.py index 3da2c2f3..6280c258 100644 --- a/pythonwhat/check_funcs.py +++ b/pythonwhat/check_funcs.py @@ -380,7 +380,7 @@ def check_call(callstr, argstr = None, expand_msg=None, state=None): callstr (str): call string that specifies how the function should be called, e.g. `f(1, a = 2)`. ``check_call()`` will replace ``f`` with the function/lambda you're targeting. argstr (str): If specified, this overrides the way the function call is refered to in the expand message. - expand_msg (str): If specified, this overrides the expand message. + expand_msg (str): If specified, this overrides any messages that are prepended by previous SCT chains. state (State): state object that is chained from. :Example: diff --git a/pythonwhat/check_function.py b/pythonwhat/check_function.py index 717a0e52..751f803b 100644 --- a/pythonwhat/check_function.py +++ b/pythonwhat/check_function.py @@ -34,8 +34,17 @@ def check_function(name, index=0, state=None): """Check whether a particular function is called. - This function is typically followed by ``check_args()`` to check whether the arguments were - specified correctly. + ``check_function()`` is typically followed by: + + - ``check_args()`` to check whether the arguments were specified. + In turn, ``check_args()`` can be followed by ``has_equal_value()`` or ``has_equal_ast()`` + to assert that the arguments were correctly specified. + - ``has_equal_value()`` to check whether rerunning the function call coded by the student + gives the same result as calling the function call as in the solution. + + Checking function calls is a tricky topic. Please visit the + `dedicated article `_ for more explanation, + edge cases and best practices. Args: name (str): the name of the function to be tested. When checking functions in packages, always diff --git a/pythonwhat/check_object.py b/pythonwhat/check_object.py index 95ddd881..6830e433 100644 --- a/pythonwhat/check_object.py +++ b/pythonwhat/check_object.py @@ -15,30 +15,136 @@ def check_object(index, missing_msg=None, expand_msg=None, state=None, typestr=" Check whether an object is defined in the student's process, and zoom in on its value in both student and solution process to inspect quality (with has_equal_value(). + In ``pythonbackend``, both the student's submission as well as the solution code are executed, in separate processes. + ``check_object()`` looks at these processes and checks if the referenced object is available in the student process. + Next, you can use ``has_equal_value()`` to check whether the objects in the student and solution process correspond. + Args: index (str): the name of the object which value has to be checked. missing_msg (str): feedback message when the object is not defined in the student process. - expand_msg (str): prepending message to put in front. + expand_msg (str): If specified, this overrides any messages that are prepended by previous SCT chains. :Example: + + Suppose you want the student to create a variable ``x``, equal to 15: :: - Student code:: + x = 15 - b = 1 - c = 3 + The following SCT will verify this: :: - Solution code:: + Ex().check_object("x").has_equal_value() - a = 1 - b = 2 - c = 3 + - ``check_object()`` will check if the variable ``x`` is defined in the student process. + - ``has_equal_value()`` will check whether the value of ``x`` in the solution process is the same as in the student process. + + Note that ``has_equal_value()`` only looks at **end result** of a variable in the student process. + In the example, how the object ``x`` came about in the student's submission, does not matter. + This means that all of the following submission will also pass the above SCT: :: - SCT:: + x = 15 + x = 12 + 3 + x = 3; x += 12 + + :Example: + + As the previous example mentioned, ``has_equal_value()`` only looks at the **end result**. If your exercise is + first initializing and object and further down the script is updating the object, you can only look at the final value! + + Suppose you want the student to initialize and populate a list `my_list` as follows: :: + + my_list = [] + for i in range(20): + if i % 3 == 0: + my_list.append(i) + + There is no robust way to verify whether `my_list = [0]` was coded correctly in a separate way. + The best SCT would look something like this: :: + + msg = "Have you correctly initialized `my_list`?" + Ex().check_correct( + check_object('my_list').has_equal_value(), + multi( + # check initialization: [] or list() + check_or( + has_equal_ast(code = "[]", incorrect_msg = msg), + check_function('list') + ), + check_for_loop().multi( + check_iter().has_equal_value(), + check_body().check_if_else().multi( + check_test().multi( + set_context(2).has_equal_value(), + set_context(3).has_equal_value() + ), + check_body().set_context(3).\\ + set_env(my_list = [0]).\\ + has_equal_value(name = 'my_list') + ) + ) + ) + ) + + - ``check_correct()`` is used to robustly check whether ``my_list`` was built correctly. + - If ``my_list`` is not correct, **both** the initialization and the population code are checked. + + :Example: + + Because checking object correctness incorrectly is such a common misconception, we're adding another example: :: + + import pandas as pd + df = pd.DataFrame({'a': [1, 2, 3], 'b': [4, 5, 6]}) + df['c'] = [7, 8, 9] + + The following SCT would be **wrong**, as it does not factor in the possibility that the 'add column ``c``' step could've been wrong: :: + + Ex().check_correct( + check_object('df').has_equal_value(), + check_function('pandas.DataFrame').check_args(0).has_equal_value() + ) + + The following SCT would be better, as it is specific to the steps: :: + + # verify the df = pd.DataFrame(...) step + Ex().check_correct( + check_df('df').multi( + check_keys('a').has_equal_value(), + check_keys('b').has_equal_value() + ), + check_function('pandas.DataFrame').check_args(0).has_equal_value() + ) + + # verify the df['c'] = [...] step + Ex().check_df('df').check_keys('c').has_equal_value() + + :Example: + + pythonwhat compares the objects in the student and solution process with the ``==`` operator. + For basic objects, this ``==`` is operator is properly implemented, so that the objects can be effectively compared. + For more complex objects that are produced by third-party packages, however, it's possible that this equality operator is not implemented in a way you'd expect. + Often, for these object types the ``==`` will compare the actual object instances: :: + + # pre exercise code + class Number(): + def __init__(self, n): + self.n = n - Ex().check_object("a") # fail - Ex().check_object("b") # pass - Ex().check_object("b").has_equal_value() # fail - Ex().check_object("c").has_equal_value() # pass + # solution + x = Number(1) + + # sct that won't work + Ex().check_object().has_equal_value() + + # sct + Ex().check_object().has_equal_value(expr_code = 'x.n') + + # submissions that will pass this sct + x = Number(1) + x = Number(2 - 1) + + The basic SCT like in the previous example will notwork here. + Notice how we used the ``expr_code`` argument to _override_ which value `has_equal_value()` is checking. + Instead of checking whether `x` corresponds between student and solution process, it's now executing the expression ``x.n`` + and seeing if the result of running this expression in both student and solution process match. """ @@ -119,7 +225,45 @@ def is_instance(inst, not_instance_msg=None, state=None): return state def check_df(index, missing_msg=None, not_instance_msg=None, expand_msg=None, state=None): - """Check whether a DataFrame was defined and it is the right type""" + """Check whether a DataFrame was defined and it is the right type + + ``check_df()`` is a combo of ``check_object()`` and ``is_instance()`` that checks whether the specified object exists + and whether the specified object is pandas DataFrame. + + You can continue checking the data frame with ``check_keys()`` function to 'zoom in' on a particular column in the pandas DataFrame: + + Args: + index (str): Name of the data frame to zoom in on. + missing_msg (str): See ``check_object()``. + not_instance_msg (str): See ``is_instance()``. + expand_msg (str): If specified, this overrides any messages that are prepended by previous SCT chains. + + :Example: + + Suppose you want the student to create a DataFrame ``my_df`` with two columns. + The column ``a`` should contain the numbers 1 to 3, + while the contents of column ``b`` can be anything: :: + + import pandas as pd + my_df = pd.DataFrame({"a": [1, 2, 3], "b": ["a", "n", "y"]}) + + The following SCT would robustly check that: :: + + Ex().check_df("my_df").multi( + check_keys("a").has_equal_value(), + check_keys("b") + ) + + - ``check_df()`` checks if ``my_df`` exists (``check_object()`` behind the scenes) and is a DataFrame (``is_instance()``) + - ``check_keys("a")`` zooms in on the column ``a`` of the data frame, and ``has_equal_value()`` checks if the columns correspond between student and solution process. + - ``check_keys("b")`` zooms in on hte column ``b`` of the data frame, but there's no 'equality checking' happening + + The following submissions would pass the SCT above: :: + + my_df = pd.DataFrame({"a": [1, 1 + 1, 3], "b": ["a", "l", "l"]}) + my_df = pd.DataFrame({"a": [1, 2, 3], "b": [4, 5, 6], "c": [7, 8, 9]}) + + """ child = check_object(index, missing_msg=missing_msg, expand_msg=expand_msg, state=state, typestr="pandas DataFrame") is_instance(pd.DataFrame, not_instance_msg=not_instance_msg, state=child) return child @@ -134,6 +278,7 @@ def check_keys(key, missing_msg=None, expand_msg=None, state=None): key (str): Name of the key that the object should have. missing_msg (str): When specified, this overrides the automatically generated message in case the key does not exist. + expand_msg (str): If specified, this overrides any messages that are prepended by previous SCT chains. state (State): The state that is passed in through the SCT chain (don't specify this). :Example: diff --git a/pythonwhat/has_funcs.py b/pythonwhat/has_funcs.py index 4a735145..6b46b73e 100644 --- a/pythonwhat/has_funcs.py +++ b/pythonwhat/has_funcs.py @@ -487,9 +487,12 @@ def has_output(text, pattern=True, no_output_msg=None, state=None): - """Search student output. + """Search student output for a pattern. - Checks if the output contains a (pattern of) text. + Among the student and solution process, the student submission and solution code as a string, + the ``Ex()`` state also contains the output that a student generated with his or her submission. + + With ``has_output()``, you can access this output and match it against a regular or fixed expression. Args: text (str): the text that is searched for @@ -498,15 +501,22 @@ def has_output(text, :Example: - SCT:: + As an example, suppose we want a student to print out a sentence: :: + + # Print the "This is some ... stuff" + print("This is some weird stuff") + + The following SCT tests whether the student prints out ``This is some weird stuff``: :: - Ex().has_output(r'[H|h]i,*\\s+there!') + # Using exact string matching + Ex().has_output("This is some weird stuff", pattern = False) - Submissions:: + # Using a regular expression (more robust) + # pattern = True is the default + msg = "Print out ``This is some ... stuff`` to the output, " + \\ + "fill in ``...`` with a word you like." + Ex().has_output(r"This is some \w* stuff", no_output_msg = msg) - print("Hi, there!") # pass - print("hi there!") # pass - print("Hello there") # fail """ rep = Reporter.active_reporter @@ -529,17 +539,14 @@ def has_printout(index, name=None, copy=False, state=None): - """Check if the output of print() statement in the solution is in the output the student generated. + """Check if the right printouts happened. + + ``has_printout()`` will look for the printout in the solution code that you specified with ``index`` (0 in this case), rerun the ``print()`` call in + the solution process, capture its output, and verify whether the output is present in the output of the student. This is more robust as ``Ex().check_function('print')`` initiated chains as students can use as many printouts as they want, as long as they do the correct one somewhere. - .. note:: - - When zooming in on parts of the student submission (with e.g. ``check_for_loop()``), we are not - zooming in on the piece of the student output that is related to that piece of the student code. - In other words, ``has_printout()`` always considers the entire student output. - Args: index (int): index of the ``print()`` call in the solution whose output you want to search for in the student output. not_printed_msg (str): if specified, this overrides the default message that is generated when the output @@ -552,20 +559,43 @@ def has_printout(index, :Example: - Solution:: + Suppose you want somebody to print out 4: :: print(1, 2, 3, 4) - SCT:: + The following SCT would check that: :: Ex().has_printout(0) - Each of these submissions will pass:: + All of the following SCTs would pass: :: print(1, 2, 3, 4) print('1 2 3 4') print(1, 2, '3 4') print("random"); print(1, 2, 3, 4) + + :Example: + + Watch out: ``has_printout()`` will effectively **rerun** the ``print()`` call in the solution process after the entire solution script was executed. + If your solution script updates the value of `x` after executing it, ``has_printout()`` will not work. + + Suppose you have the following solution: :: + + x = 4 + print(x) + x = 6 + + The following SCT will not work: :: + + Ex().has_printout(0) + + Why? When the ``print(x)`` call is executed, the value of ``x`` will be 6, and pythonwhat will look for the output `'6`' in the output the student generated. + In cases like these, default to using the classical pattern to check function calls. + + The following SCT **will** work: :: + + Ex().check_function('print').check_args(0).has_equal_value() + """ extra_msg = "If you want to check printouts done in e.g. a for loop, you have to use a `check_function('print')` chain instead." @@ -603,12 +633,52 @@ def has_printout(index, def has_no_error(incorrect_msg="Have a look at the console: your code contains an error. Fix it and try again!", state=None): """Check whether the submission did not generate a runtime error. - If all SCTs for an exercise pass, before succeeding pythonwhat will automatically check whether - the student submission generated an error. If you want to verify whether an error was generated - earlier during SCT execution, you can use ``Ex().has_no_error()``. + If all SCTs for an exercise pass, before marking the submission as correct pythonwhat will automatically check whether + the student submission generated an error. This means it is not needed to use ``has_no_error()`` explicitly. + + However, in some cases, using ``has_no_error()`` explicitly somewhere throughout your SCT execution can be helpful: + + - If you want to make sure people didn't write typos when writing a long function name. + - If you want to first verify whether a function actually runs, before checking whether the arguments were specified correctly. + - More generally, if, because of the content, it's instrumental that the script runs without + errors before doing any other verifications. Args: incorrect_msg: if specified, this overrides the default message if the student code generated an error. + + :Example: + + Suppose you're verifying an exercise about model training and validation: :: + + # pre exercise code + import numpy as np + from sklearn.model_selection import train_test_split + from sklearn import datasets + from sklearn import svm + + iris = datasets.load_iris() + iris.data.shape, iris.target.shape + + # solution + X_train, X_test, y_train, y_test = train_test_split( + iris.data, iris.target, test_size=0.4, random_state=0) + + If you want to make sure that ``train_test_split()`` ran without errors, + which would check if the student typed the function without typos and used + sensical arguments, you could use the following SCT: :: + + Ex().has_no_error() + Ex().check_function('sklearn.model_selection.train_test_split').multi( + check_args(['arrays', 0]).has_equal_value(), + check_args(['arrays', 0]).has_equal_value(), + check_args(['options', 'test_size']).has_equal_value(), + check_args(['options', 'random_state']).has_equal_value() + ) + + If, on the other hand, you want to fall back onto pythonwhat's built in behavior, + that checks for an error before marking the exercise as correct, you can simply + leave of the ``has_no_error()`` step. + """ state.assert_root('has_no_error') From d70ba0b59586ab2b53f15844f9e6e08d6426ced3 Mon Sep 17 00:00:00 2001 From: Filip Schouwenaars Date: Fri, 28 Sep 2018 08:00:52 +0200 Subject: [PATCH 025/209] feat(docs): add example on checking args in check_function_def Closes #349 --- pythonwhat/check_wrappers.py | 54 ++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/pythonwhat/check_wrappers.py b/pythonwhat/check_wrappers.py index 263b2c40..6bab3fe5 100644 --- a/pythonwhat/check_wrappers.py +++ b/pythonwhat/check_wrappers.py @@ -311,6 +311,60 @@ def shout_echo(w, e=1): # passing submission 2 def shout_echo(a, b=1): return a * b + '!!!' + + :Example: + + ``check_args()`` is most commonly used in combination with ``check_function()`` + to verify the arguments of function **calls**, but it can also be used + to verify the arguments specified in the signature of a function definition. + + We can extend the SCT for the previous example to explicitly verify the signature: :: + + + msg1 = "Make sure to specify 2 arguments!" + msg2 = "don't specify default arg!" + msg3 = "specify a default arg!" + Ex().check_function_def('shout_echo').check_correct( + multi( + check_call("f('hey', 3)").has_equal_value(), + check_call("f('hi', 2)").has_equal_value(), + check_call("f('hi')").has_equal_value() + ), + multi( + has_equal_part_len("args", unequal_msg=1), + check_args(0).has_equal_part('is_default', msg=msg2), + check_args('word1').has_equal_part('is_default', msg=msg2), + check_args(1).\\ + has_equal_part('is_default', msg=msg3).has_equal_value(), + check_args('echo').\\ + has_equal_part('is_default', msg=msg3).has_equal_value(), + check_body().set_context('test', 1).multi( + has_equal_value(name = 'echo_word'), + has_equal_value(name = 'shout_words') + ) + ) + ) + + - ``has_equal_part_len("args")`` verifies whether student and solution function + definition have the same number of arguments. + - ``check_args(0)`` refers to the first argument in the signature by position, + and the chain checks whether the student did not specify a default as in the solution. + - An alternative for the ``check_args(0)`` chain is to use ``check_args('word1')`` + to refer to the first argument. This is more restrictive, as the requires the + student to use the exact same name. + - ``check_args(1)`` refers to the second argument in the signature by position, + and the chain checks whether the student specified a default, as in the solution, and + whether the value of this default corresponds to the one in the solution. + - The ``check_args('echo')`` chain is a more restrictive alternative for the ``check_args(1)`` + chain. + + Notice that support for verifying arguments is not great yet: + + - A lot of work is needed to verify the number of arguments and whether or not defaults are set. + - You ahve to specify custom messages because pythonwhat doesn't automatically generate messages. + + We are working on it! + """ }, 'class_def': { From dfe6834a1d8031243076268062788f7d9b536c7e Mon Sep 17 00:00:00 2001 From: Filip Schouwenaars Date: Fri, 28 Sep 2018 11:00:41 +0200 Subject: [PATCH 026/209] feat(docs): clean up after review - Improve has_printout docs - Move has_expr docs --- pythonwhat/check_wrappers.py | 2 +- pythonwhat/has_funcs.py | 92 ++++++++++++++++++++---------------- 2 files changed, 53 insertions(+), 41 deletions(-) diff --git a/pythonwhat/check_wrappers.py b/pythonwhat/check_wrappers.py index 6bab3fe5..8a3075df 100644 --- a/pythonwhat/check_wrappers.py +++ b/pythonwhat/check_wrappers.py @@ -361,7 +361,7 @@ def shout_echo(a, b=1): Notice that support for verifying arguments is not great yet: - A lot of work is needed to verify the number of arguments and whether or not defaults are set. - - You ahve to specify custom messages because pythonwhat doesn't automatically generate messages. + - You have to specify custom messages because pythonwhat doesn't automatically generate messages. We are working on it! diff --git a/pythonwhat/has_funcs.py b/pythonwhat/has_funcs.py index 6b46b73e..3c23cac7 100644 --- a/pythonwhat/has_funcs.py +++ b/pythonwhat/has_funcs.py @@ -183,6 +183,41 @@ def parse_tree(tree): DEFAULT_UNDEFINED_NAME_MSG="Running {{'it' if parent['part'] else 'the higlighted expression'}} should define a variable `{{name}}` without errors, but it doesn't." DEFAULT_INCORRECT_NAME_MSG="Are you sure you assigned the correct value to `{{name}}`?" DEFAULT_INCORRECT_EXPR_CODE_MSG="Running the expression `{{expr_code}}` didn't generate the expected result." + +args_string = """ + + Args: + incorrect_msg (str): feedback message if the {0} of the expression in the solution + doesn't match the one of the student. This feedback message will be expanded if it is used + in the context of another check function, like ``check_if_else``. + error_msg (str): feedback message if there was an error when running the targeted student code. + Note that when testing for an error, this message is displayed when none is raised. + undefined_msg (str): feedback message if the ``name`` argument is defined, but a variable + with that name doesn't exist after running the targeted student code. + extra_env (dict): set variables to the extra environment. They will update the student and solution environment in + the active state before the student/solution code in the active state is ran. This argument should contain a + dictionary with the keys the names of the variables you want to set, and the values are the values of these variables. + You can also use ``set_env()`` for this. + context_vals (list): set variables which are bound in a ``for`` loop to certain values. + This argument is only useful when checking a for loop (or list comprehensions). + It contains a list with the values of the bound variables. + You can also use ``set_context()`` for this. + pre_code (str): the code in string form that should be executed before the expression is executed. + This is the ideal place to set a random seed, for example. + expr_code (str): if this argument is set, the expression in the student/solution code will not + be ran. Instead, the given piece of code will be ran in the student as well as the solution environment + and the result will be compared. + name (str): If this is specified, the {0} of running this expression after running the focused expression + is returned, instead of the {0} of the focussed expression in itself. This is typically used to inspect the + {0} of an object after executing the body of e.g. a ``for`` loop. + copy (bool): whether to try to deep copy objects in the environment, such as lists, that could + accidentally be mutated. Disable to speed up SCTs. Disabling may lead to cryptic mutation issues. + func: custom binary function of form f(stu_result, sol_result), for equality testing. + override: If specified, this avoids the execution of the targeted code in the solution process. Instead, it + will compare the {0} of the expression in the student process with the value specified in ``override``. + Typically used in a ``SingleProcessExercise`` or if you want to allow for different solutions other than + the one coded up in the solution. + """ def has_expr(incorrect_msg=None, error_msg=None, undefined_msg=None, @@ -282,43 +317,6 @@ def has_expr(incorrect_msg=None, return state - - -args_string = """ - - Args: - incorrect_msg (str): feedback message if the {0} of the expression in the solution - doesn't match the one of the student. This feedback message will be expanded if it is used - in the context of another check function, like ``check_if_else``. - error_msg (str): feedback message if there was an error when running the targeted student code. - Note that when testing for an error, this message is displayed when none is raised. - undefined_msg (str): feedback message if the ``name`` argument is defined, but a variable - with that name doesn't exist after running the targeted student code. - extra_env (dict): set variables to the extra environment. They will update the student and solution environment in - the active state before the student/solution code in the active state is ran. This argument should contain a - dictionary with the keys the names of the variables you want to set, and the values are the values of these variables. - You can also use ``set_env()`` for this. - context_vals (list): set variables which are bound in a ``for`` loop to certain values. - This argument is only useful when checking a for loop (or list comprehensions). - It contains a list with the values of the bound variables. - You can also use ``set_context()`` for this. - pre_code (str): the code in string form that should be executed before the expression is executed. - This is the ideal place to set a random seed, for example. - expr_code (str): if this argument is set, the expression in the student/solution code will not - be ran. Instead, the given piece of code will be ran in the student as well as the solution environment - and the result will be compared. - name (str): If this is specified, the {0} of running this expression after running the focused expression - is returned, instead of the {0} of the focussed expression in itself. This is typically used to inspect the - {0} of an object after executing the body of e.g. a ``for`` loop. - copy (bool): whether to try to deep copy objects in the environment, such as lists, that could - accidentally be mutated. Disable to speed up SCTs. Disabling may lead to cryptic mutation issues. - func: custom binary function of form f(stu_result, sol_result), for equality testing. - override: If specified, this avoids the execution of the targeted code in the solution process. Instead, it - will compare the {0} of the expression in the student process with the value specified in ``override``. - Typically used in a ``SingleProcessExercise`` or if you want to allow for different solutions other than - the one coded up in the solution. - """ - has_equal_value = partial(has_expr, test = 'value') has_equal_value.__doc__ = """Run targeted student and solution code, and compare returned value. @@ -590,11 +588,25 @@ def has_printout(index, Ex().has_printout(0) Why? When the ``print(x)`` call is executed, the value of ``x`` will be 6, and pythonwhat will look for the output `'6`' in the output the student generated. - In cases like these, default to using the classical pattern to check function calls. + In cases like these, ``has_printout()`` cannot be used. + + :Example: + + Inside a for loop ``has_printout()`` + + Suppose you have the following solution: :: + + for i in range(5): + print(i) + + The following SCT will not work: :: + + Ex().check_for_loop().check_body().has_printout(0) - The following SCT **will** work: :: + The reason is that ``has_printout()`` can only be called from the root state. ``Ex()``. + If you want to check printouts done in e.g. a for loop, you have to use a `check_function('print')` chain instead: :: - Ex().check_function('print').check_args(0).has_equal_value() + Ex().check_for_loop().check_body().set_context(0).check_function('print').check_args(0).has_equal_value() """ From fcc0f828e29c7869be5c9edd5b4485a4e0ee8f17 Mon Sep 17 00:00:00 2001 From: Filip Schouwenaars Date: Fri, 28 Sep 2018 11:22:03 +0200 Subject: [PATCH 027/209] fix(ci): use datacamp for upload to pypi --- .travis.yml | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/.travis.yml b/.travis.yml index 3f28e872..9e9e0743 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,22 +1,17 @@ sudo: false language: python - python: - - "3.5" - +- '3.5' before_install: - - pip install -r requirements.txt - - pip install -e . - +- pip install -r requirements.txt +- pip install -e . script: pytest -m "not compiled" --cov=pythonwhat - after_success: codecov --token=$CODECOV_TOKEN - deploy: provider: pypi - user: machow + user: datacamp password: - secure: Jk24S6GmgT7MPym5ZCE1Rm4jeKISu/cJcEsrmMa/Tl3SQClGqvdgXjfyqEbe+xyEcdx4XWzyDsKT9dST/w6ngn5HmYkw9+JcqROfhANY5z9pTq7vT2wab2EjJcKxFXgMtgmojm1galUCsRhxB45t4nF7t8oDnQvRqAIoCxMJ3XFhdAgOatGMFDzCielEE60gyuEMVMXUoYEcyDIdCeWzdKHpzXTNVPWlKWGro1omGnJ3KwOLHi9Vw+Bmi7KavNGuHOZvouMkcBDJhDBqGE/OMtL4pc17PSgCKHeSxhw5mH+RQAwHAlL1obHahFACyfHbK2q2Wt/rWPkJqsGjPeXZGcgNyzKD+ily0aORXgY7jc9WEp6asWY65LjgiPqAMTspiYuBvioqro+TzKlpfase9UFRXioUwuFvFqLZO47mEgpbrEV5NX8uvCJ0TZs8WJNQ6AecJXl2Ql94aAgSkJ0BJfv9u5m4MSz6Ntdg/I6dXz8C9WgYkuMvSzCJfTdBLbPtzczi+vFvu8sxNlC9Sf+8EB+HC8gerSnPlFBz3Fzi7kvNqRC8/nmZfXNfsLlNNASb+iCpWrIkcS488oGi3sPdd3ESfOi51UMKaFDGoRS53Br38fbLoMWg1pvMXtyvSpIn3XR2racJuIVvd/qyaSG7t//5B2JT86eNlGGWE4RStlI= + secure: PVJrnm2rLmrKkNdcTM4yqfe2v7+dTwu936FCUtDLPIC6inL4Neky9Zs2GpFv9lD9IQ0a7jTdbFp/TzC8klzxDd7LE72YVyZcrafrWXGpXw4WwBozmTsyGLnCXAQ54urMbcwLYw3OR4oUGeoPGOxBsEiQXiPH5Yvwcm9IFuQr98/IRedwqS0udDqWmuvPYfBwK6VSoDhN+jC07B5sdoYfqKlOXkQuleSDJ2XlY8mndbgD3OCpegqcp6ysYj3v80OStJMB0sxMg92q1Ltcn6d5uIFJsYo/K2BfSPP9JANLIicU3AqDIOSinPP9mky915HSQzbqBISIXp6oPcMBcwA+Eju9h3yssAGo1MBGiGQB8Pg46wFiWfPTtceaibjJyfs1nC1SSc7AgP7kaDJhrBRh1XAOhIuyEyrQ5O++7Wvs7kCPYeDQ3VHDsNSVYaVHNP0moGImQKQn/1dKLz9C+tkP4V+GcjLnaj4FfWaAz+CqNqFbxPIJOi8KFIHqWJp/uxIKLGJVjd46vsgsTOl7YuiPQO8hWwqkYR/Vdb9vxvn15SaXI5WCESejo5sg3bI4BQhaIq/3cK00p+BKQHc66rVUezo228kLPDSRZEDfAPyVhTEgAOEVGUPkD7efFMqGhw6WbrnNze6QY2+EsLr57WLNVtJzYgwbOuKeCSxy+JhvXbA= on: tags: true distributions: sdist bdist_wheel From a8311b4bea6e4e0ad5b66ab588ed704e23872bde Mon Sep 17 00:00:00 2001 From: Filip Schouwenaars Date: Fri, 28 Sep 2018 11:24:14 +0200 Subject: [PATCH 028/209] bump version + update CHANGELOG --- CHANGELOG.md | 18 ++++++++++++++++++ pythonwhat/__init__.py | 2 +- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6cc90abb..a6687c1b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,24 @@ All notable changes to the pythonwhat project will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). +## 2.17.2 + +### Improved + +Documentation has been improved significantly (hopefully): + +- Every 'compound statement check' has an example with explanation in the reference now. +- Common usecases have additional examples +- Checking compound statements article has been simplified, limits to context now. +- More helpful message in case check_object or has_printout not called on root state +- More helpful message in case the zoomed in on object is not an AST +- Explain has_equal_value vs has_equal_ast +- Get rid of some articles in favor of more fleshed out reference documentation + +### Fixed + +- CI is now using the `datacamp` account on PyPi + ## 2.17.1 ### Fixed diff --git a/pythonwhat/__init__.py b/pythonwhat/__init__.py index 27c9ea9d..87ea6daf 100644 --- a/pythonwhat/__init__.py +++ b/pythonwhat/__init__.py @@ -1,3 +1,3 @@ -__version__ = '2.17.1' +__version__ = '2.17.2' from .test_exercise import test_exercise, allow_errors From 49b6db54035445621342c2703d81f4d8fd1b8c35 Mon Sep 17 00:00:00 2001 From: Filip Schouwenaars Date: Fri, 28 Sep 2018 11:25:49 +0200 Subject: [PATCH 029/209] fix linebreak --- pythonwhat/has_funcs.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pythonwhat/has_funcs.py b/pythonwhat/has_funcs.py index 3c23cac7..7fe7c93c 100644 --- a/pythonwhat/has_funcs.py +++ b/pythonwhat/has_funcs.py @@ -606,7 +606,9 @@ def has_printout(index, The reason is that ``has_printout()`` can only be called from the root state. ``Ex()``. If you want to check printouts done in e.g. a for loop, you have to use a `check_function('print')` chain instead: :: - Ex().check_for_loop().check_body().set_context(0).check_function('print').check_args(0).has_equal_value() + Ex().check_for_loop().check_body().\\ + set_context(0).check_function('print').\\ + check_args(0).has_equal_value() """ From 67c08b02b42ebf6ebc84838cb75f5ded98db903b Mon Sep 17 00:00:00 2001 From: Filip Schouwenaars Date: Fri, 28 Sep 2018 11:37:20 +0200 Subject: [PATCH 030/209] Revert "fix(ci): use datacamp for upload to pypi" This reverts commit fcc0f828e29c7869be5c9edd5b4485a4e0ee8f17. --- .travis.yml | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/.travis.yml b/.travis.yml index 9e9e0743..3f28e872 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,17 +1,22 @@ sudo: false language: python + python: -- '3.5' + - "3.5" + before_install: -- pip install -r requirements.txt -- pip install -e . + - pip install -r requirements.txt + - pip install -e . + script: pytest -m "not compiled" --cov=pythonwhat + after_success: codecov --token=$CODECOV_TOKEN + deploy: provider: pypi - user: datacamp + user: machow password: - secure: PVJrnm2rLmrKkNdcTM4yqfe2v7+dTwu936FCUtDLPIC6inL4Neky9Zs2GpFv9lD9IQ0a7jTdbFp/TzC8klzxDd7LE72YVyZcrafrWXGpXw4WwBozmTsyGLnCXAQ54urMbcwLYw3OR4oUGeoPGOxBsEiQXiPH5Yvwcm9IFuQr98/IRedwqS0udDqWmuvPYfBwK6VSoDhN+jC07B5sdoYfqKlOXkQuleSDJ2XlY8mndbgD3OCpegqcp6ysYj3v80OStJMB0sxMg92q1Ltcn6d5uIFJsYo/K2BfSPP9JANLIicU3AqDIOSinPP9mky915HSQzbqBISIXp6oPcMBcwA+Eju9h3yssAGo1MBGiGQB8Pg46wFiWfPTtceaibjJyfs1nC1SSc7AgP7kaDJhrBRh1XAOhIuyEyrQ5O++7Wvs7kCPYeDQ3VHDsNSVYaVHNP0moGImQKQn/1dKLz9C+tkP4V+GcjLnaj4FfWaAz+CqNqFbxPIJOi8KFIHqWJp/uxIKLGJVjd46vsgsTOl7YuiPQO8hWwqkYR/Vdb9vxvn15SaXI5WCESejo5sg3bI4BQhaIq/3cK00p+BKQHc66rVUezo228kLPDSRZEDfAPyVhTEgAOEVGUPkD7efFMqGhw6WbrnNze6QY2+EsLr57WLNVtJzYgwbOuKeCSxy+JhvXbA= + secure: Jk24S6GmgT7MPym5ZCE1Rm4jeKISu/cJcEsrmMa/Tl3SQClGqvdgXjfyqEbe+xyEcdx4XWzyDsKT9dST/w6ngn5HmYkw9+JcqROfhANY5z9pTq7vT2wab2EjJcKxFXgMtgmojm1galUCsRhxB45t4nF7t8oDnQvRqAIoCxMJ3XFhdAgOatGMFDzCielEE60gyuEMVMXUoYEcyDIdCeWzdKHpzXTNVPWlKWGro1omGnJ3KwOLHi9Vw+Bmi7KavNGuHOZvouMkcBDJhDBqGE/OMtL4pc17PSgCKHeSxhw5mH+RQAwHAlL1obHahFACyfHbK2q2Wt/rWPkJqsGjPeXZGcgNyzKD+ily0aORXgY7jc9WEp6asWY65LjgiPqAMTspiYuBvioqro+TzKlpfase9UFRXioUwuFvFqLZO47mEgpbrEV5NX8uvCJ0TZs8WJNQ6AecJXl2Ql94aAgSkJ0BJfv9u5m4MSz6Ntdg/I6dXz8C9WgYkuMvSzCJfTdBLbPtzczi+vFvu8sxNlC9Sf+8EB+HC8gerSnPlFBz3Fzi7kvNqRC8/nmZfXNfsLlNNASb+iCpWrIkcS488oGi3sPdd3ESfOi51UMKaFDGoRS53Br38fbLoMWg1pvMXtyvSpIn3XR2racJuIVvd/qyaSG7t//5B2JT86eNlGGWE4RStlI= on: tags: true distributions: sdist bdist_wheel From 121fa4cad4556d504442f16ef097e47cbc90dfd4 Mon Sep 17 00:00:00 2001 From: Jeroen Hermans Date: Mon, 1 Oct 2018 10:53:00 -0400 Subject: [PATCH 031/209] fix(docs): example formatting --- pythonwhat/check_wrappers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pythonwhat/check_wrappers.py b/pythonwhat/check_wrappers.py index 8a3075df..c334924d 100644 --- a/pythonwhat/check_wrappers.py +++ b/pythonwhat/check_wrappers.py @@ -558,7 +558,7 @@ def __init__(self, i): :Example: - Suppose you want to verify whether the student did a `try-except` statement properly: + Suppose you want to verify whether the student did a `try-except` statement properly: :: do_dangerous_thing = lambda n: n From 205beb44e0e091eae05e80f9d00b65168347e803 Mon Sep 17 00:00:00 2001 From: Jeroen Hermans Date: Thu, 18 Oct 2018 12:58:31 +0200 Subject: [PATCH 032/209] Update authoring links --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 1f69dee7..2ccad917 100644 --- a/README.md +++ b/README.md @@ -7,8 +7,8 @@ Verify Python code submissions and auto-generate meaningful feedback messages. Originally developed for Python exercises on DataCamp for so-called Submission Correctness Tests, but can also be used independently. -- New to teaching on DataCamp? Check out https://authoring.datacamp.com. -- To learn what SCTs are and how they work, visit [this article](https://authoring.datacamp.com/courses/exercises/technical-details/sct.html) specifically. +- New to teaching on DataCamp? Check out https://instructor-support.datacamp.com +- To learn what SCTs are and how they work, visit [this article](https://instructor-support.datacamp.com/courses/course-development/submission-correctness-tests) specifically. - For a complete overview of all functionality inside pythonwhat and articles about what to use when, consult https://pythonwhat.readthedocs.io. ## Installation @@ -48,7 +48,7 @@ dir(Ex()._state) # list all elements available in the state object Ex()._state.student_code # access student_code of state object ``` -To learn how to include an SCT in a DataCamp course, visit https://authoring.datacamp.com. +To learn how to include an SCT in a DataCamp course, visit https://instructor-support.datacamp.com. ## Run tests From 1f52c1d7527841571b4bf72dacd01dff650613ce Mon Sep 17 00:00:00 2001 From: Jeroen Hermans Date: Thu, 18 Oct 2018 16:38:43 +0200 Subject: [PATCH 033/209] Update authoring links --- docs/articles/electives.rst | 2 +- docs/articles/processes.rst | 2 +- docs/articles/tutorial.rst | 3 +-- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/docs/articles/electives.rst b/docs/articles/electives.rst index 2808abe2..23acaf33 100644 --- a/docs/articles/electives.rst +++ b/docs/articles/electives.rst @@ -12,7 +12,7 @@ When all tests in an SCT pass, pythonwhat will automatically generate a congratu success_msg("You are a hero when it comes to variable assignment!") -`This article `_ on the authoring docs describes how to write good success messages. +`This article `_ on the authoring docs describes how to write good success messages. Multiple choice exercises diff --git a/docs/articles/processes.rst b/docs/articles/processes.rst index 877818ee..2823b5a4 100644 --- a/docs/articles/processes.rst +++ b/docs/articles/processes.rst @@ -1,7 +1,7 @@ Processes --------- -As explained on the `SCT authoring homepage `_, DataCamp's Python coding backends use uses two separate processes: one process to run the solution code, and one process to run the student's submission. +As explained on the `SCT authoring homepage `_, DataCamp's Python coding backends use uses two separate processes: one process to run the solution code, and one process to run the student's submission. As such, pythonwhat has access to the 'ideal ending scenario' of an exercises, which in turn makes it easier to write SCTs. Instead of having to specify which value an object should be, we can have pythonwhat look into the solution process and compare the object in that process with the object in the student process. diff --git a/docs/articles/tutorial.rst b/docs/articles/tutorial.rst index f0f6f8ce..ba97d5b4 100644 --- a/docs/articles/tutorial.rst +++ b/docs/articles/tutorial.rst @@ -227,6 +227,5 @@ However, when they do make a mistake, you want to be specific about the mistake These seemingly conflicting requirements can be satisfied with ``check_correct()``. It is an **extremely powerful function** that should be used whenever it makes sense. The `Make your SCT robust `_ article is highly recommended reading. -For other guidelines on writing good SCTs, check out the 'How to write good SCTs' section on DataCamp's `general SCT documentation page `_. +For other guidelines on writing good SCTs, check out the 'How to write good SCTs' section on DataCamp's `general SCT documentation page `_. - \ No newline at end of file From 0faed1aef7f4f2a13e3663130ed129631e944ed8 Mon Sep 17 00:00:00 2001 From: Jeroen Hermans Date: Tue, 6 Nov 2018 10:41:16 +0100 Subject: [PATCH 034/209] Add comments --- pythonwhat/check_function.py | 2 +- pythonwhat/check_syntax.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/pythonwhat/check_function.py b/pythonwhat/check_function.py index 751f803b..4cf90f9e 100644 --- a/pythonwhat/check_function.py +++ b/pythonwhat/check_function.py @@ -56,7 +56,7 @@ def check_function(name, index=0, in case the function parameters were not successfully matched. expand_msg (str): If specified, this overrides any messages that are prepended by previous SCT chains. signature (Signature): Normally, check_function() can figure out what the function signature is, - but it might be necessary to use build_sig to manually build a signature and pass this along. + but it might be necessary to use ``sig_from_params()`` to manually build a signature and pass this along. state (State): State object that is passed from the SCT Chain (don't specify this). :Examples: diff --git a/pythonwhat/check_syntax.py b/pythonwhat/check_syntax.py index 3434f513..1b2b7cf8 100644 --- a/pythonwhat/check_syntax.py +++ b/pythonwhat/check_syntax.py @@ -46,7 +46,7 @@ def wrapper(*args, **kwargs): class Chain: def __init__(self, state): self._state = state - self._crnt_sct = None + self._crnt_sct = None # last called SCT self._waiting_on_call = False def _double_attr_error(self): @@ -91,6 +91,7 @@ def __init__(self, stack = None): def __call__(self, *args, **kwargs): if not self._crnt_sct: + # first function in chain state = kwargs.get('state') or args[0] return reduce(lambda s, f: f(state=s), self._stack, state) else: From fa0577bfa8312c4933571850fed61eb62ed8f1db Mon Sep 17 00:00:00 2001 From: Jeroen Hermans Date: Tue, 6 Nov 2018 10:44:54 +0100 Subject: [PATCH 035/209] Clarify F chain --- pythonwhat/check_syntax.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pythonwhat/check_syntax.py b/pythonwhat/check_syntax.py index 1b2b7cf8..0e2f5807 100644 --- a/pythonwhat/check_syntax.py +++ b/pythonwhat/check_syntax.py @@ -84,6 +84,9 @@ def _sct_copy(self, f): return chain class F(Chain): + """ + Chain with deferred State passing + """ def __init__(self, stack = None): self._crnt_sct = None self._stack = [] if stack is None else stack @@ -100,8 +103,7 @@ def __call__(self, *args, **kwargs): @classmethod def _from_func(cls, f): - func_chain = cls() - func_chain._stack.append(f) + func_chain = cls(stack = [f]) return func_chain def Ex(state = None): From cd56b8c545dd143434eaa9452d9717020d66e1a7 Mon Sep 17 00:00:00 2001 From: Jeroen Hermans Date: Wed, 31 Oct 2018 10:40:32 +0100 Subject: [PATCH 036/209] Implement force_diagnose in test_exercise and check_correct --- pythonwhat/State.py | 5 ++++- pythonwhat/check_logic.py | 17 +++++++++++++---- pythonwhat/test_exercise.py | 6 ++++-- tests/helper.py | 2 ++ tests/test_check_logic.py | 16 ++++++++++++++++ 5 files changed, 39 insertions(+), 7 deletions(-) diff --git a/pythonwhat/State.py b/pythonwhat/State.py index 5e6832fe..a5ffe593 100644 --- a/pythonwhat/State.py +++ b/pythonwhat/State.py @@ -49,6 +49,7 @@ def __init__(self, student_parts=None, solution_parts=None, highlight = None, highlighting_disabled = None, messages=None, + force_diagnose = False, **kwargs): # Set basic fields from kwargs @@ -57,6 +58,7 @@ def __init__(self, self.student_parts = student_parts self.solution_parts = solution_parts self.messages = messages if messages else [] + self.force_diagnose = force_diagnose # parse code if didn't happen yet if not hasattr(self, 'student_tree'): @@ -194,7 +196,8 @@ def to_child_state(self, student_subtree=None, solution_subtree=None, highlight = highlight, highlighting_disabled = highlighting_disabled, messages = messages, - parent_state = self) + parent_state = self, + force_diagnose=self.force_diagnose) return(child) def update(self, **kwargs): diff --git a/pythonwhat/check_logic.py b/pythonwhat/check_logic.py index 44f1d9d1..4601fe78 100644 --- a/pythonwhat/check_logic.py +++ b/pythonwhat/check_logic.py @@ -137,12 +137,21 @@ def check_correct(check, diagnose, state=None): ) """ - def diagnose_and_check(state=None): - # use multi twice, since diagnose and check may be lists of tests - multi(diagnose, state=state) + feedback = None + try: multi(check, state=state) + except TestFail as e: + feedback = e.feedback + + try: + multi(diagnose, state=state) + except TestFail as e: + if feedback is not None or state.force_diagnose: + feedback = e.feedback - check_or(diagnose_and_check, check, state=state) + if feedback is not None: + rep = Reporter.active_reporter + rep.do_test(Test(feedback)) # utility functions ----------------------------------------------------------- diff --git a/pythonwhat/test_exercise.py b/pythonwhat/test_exercise.py index d6c11526..e5d61f43 100644 --- a/pythonwhat/test_exercise.py +++ b/pythonwhat/test_exercise.py @@ -14,7 +14,8 @@ def test_exercise(sct, solution_process, raw_student_output, ex_type, - error): + error, + force_diagnose=False): """ Point of interaction with the Python backend. Args: @@ -42,7 +43,8 @@ def test_exercise(sct, pre_exercise_code = check_str(pre_exercise_code), student_process = check_process(student_process), solution_process = check_process(solution_process), - raw_student_output = check_str(raw_student_output) + raw_student_output = check_str(raw_student_output), + force_diagnose = force_diagnose ) State.root_state = state diff --git a/tests/helper.py b/tests/helper.py index 2a96a866..4699e4c4 100644 --- a/tests/helper.py +++ b/tests/helper.py @@ -16,6 +16,7 @@ def run(data, run_code = True): stu_code = data.get("DC_CODE", "") sol_code = data.get("DC_SOLUTION", "") sct = data.get("DC_SCT", "") + force_diagnose = data.get("DC_FORCE_DIAGNOSE", False) class ChDir(object): """ @@ -61,6 +62,7 @@ def __exit__(self, *args): solution_process=sol_process, raw_student_output = raw_stu_output, ex_type = "NormalExercise", + force_diagnose = force_diagnose, error = error) return res diff --git a/tests/test_check_logic.py b/tests/test_check_logic.py index ff1ea096..148c0fa5 100644 --- a/tests/test_check_logic.py +++ b/tests/test_check_logic.py @@ -94,6 +94,22 @@ def test_check_correct(sct, passes, msg): assert output['correct'] == passes if msg: assert output['message'] == msg +@pytest.mark.parametrize('sct, passes, msg', [ + ("Ex().check_correct(has_code('a'), has_code('b'))", False, None), + ("Ex().check_correct(has_code('a'), has_code('c'))", False, None), + ("Ex().check_correct(has_code('b'), has_code('c', not_typed_msg='x'))", False, 'x'), + ("Ex().check_correct(has_code('b', not_typed_msg='x'), has_code('a'))", False, 'x') +]) +def test_check_correct_force_diagnose(sct, passes, msg): + data = { + 'DC_CODE': "'a'", + 'DC_SCT': sct, + 'DC_FORCE_DIAGNOSE': True + } + output = helper.run(data) + assert output['correct'] == passes + if msg: assert output['message'] == msg + @pytest.mark.parametrize('sct, passes, msg', [ ("test_correct(lambda: test_student_typed('a'), lambda: test_student_typed('b'))", True, None), ("test_correct(test_student_typed('a'), test_student_typed('b'))", True, None), From 811a28aac8e96738e5b58e4c373f73d89481ea42 Mon Sep 17 00:00:00 2001 From: Jeroen Hermans Date: Tue, 6 Nov 2018 14:30:03 +0100 Subject: [PATCH 037/209] Bump version, update changelog also update Travis config and vulnerable dependency --- .travis.yml | 15 +++++---------- CHANGELOG.md | 4 ++++ pythonwhat/__init__.py | 2 +- requirements.txt | 2 +- 4 files changed, 11 insertions(+), 12 deletions(-) diff --git a/.travis.yml b/.travis.yml index 3f28e872..a9840af6 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,22 +1,17 @@ sudo: false language: python - python: - - "3.5" - +- '3.5' before_install: - - pip install -r requirements.txt - - pip install -e . - +- pip install -r requirements.txt +- pip install -e . script: pytest -m "not compiled" --cov=pythonwhat - after_success: codecov --token=$CODECOV_TOKEN - deploy: provider: pypi - user: machow + user: datacamp password: - secure: Jk24S6GmgT7MPym5ZCE1Rm4jeKISu/cJcEsrmMa/Tl3SQClGqvdgXjfyqEbe+xyEcdx4XWzyDsKT9dST/w6ngn5HmYkw9+JcqROfhANY5z9pTq7vT2wab2EjJcKxFXgMtgmojm1galUCsRhxB45t4nF7t8oDnQvRqAIoCxMJ3XFhdAgOatGMFDzCielEE60gyuEMVMXUoYEcyDIdCeWzdKHpzXTNVPWlKWGro1omGnJ3KwOLHi9Vw+Bmi7KavNGuHOZvouMkcBDJhDBqGE/OMtL4pc17PSgCKHeSxhw5mH+RQAwHAlL1obHahFACyfHbK2q2Wt/rWPkJqsGjPeXZGcgNyzKD+ily0aORXgY7jc9WEp6asWY65LjgiPqAMTspiYuBvioqro+TzKlpfase9UFRXioUwuFvFqLZO47mEgpbrEV5NX8uvCJ0TZs8WJNQ6AecJXl2Ql94aAgSkJ0BJfv9u5m4MSz6Ntdg/I6dXz8C9WgYkuMvSzCJfTdBLbPtzczi+vFvu8sxNlC9Sf+8EB+HC8gerSnPlFBz3Fzi7kvNqRC8/nmZfXNfsLlNNASb+iCpWrIkcS488oGi3sPdd3ESfOi51UMKaFDGoRS53Br38fbLoMWg1pvMXtyvSpIn3XR2racJuIVvd/qyaSG7t//5B2JT86eNlGGWE4RStlI= + secure: g0lu0u/tDLNxPa0VsmPQhRfPUzA+3EavKljX9goinCbp0o6RWY7LSBEwCqmMcqIlVJ7gL2yMkBC21HsEGkIvkJhoTpWFXVWtbZEUcmyrfviu1HCrOUqsLvy9WOd+h8ZVelsPT4PEuEKtXD8K9qMjRjtiWH9xuoG+LyMPWvO2WGuIxk+z7DNIrPNm48kuC97yKKr2NWXoj9MjhfqPNAizqAiolYdLBwQX/biXQqsGNErSWp1rvojeA27PrtROLFHZO3cni7b3QjmpmaDkAYERfSbfUfom1gcAKQNeM3OfecBVxNsFc8kjhriktQyp9pWGZ44Hn9dmeYVeGfmVOEmIlCppLlQYcRm07QBtOpgN/QqOftAgZBKbd0AXftrxu64G//l9fLXGaW7i9vsqUMc7ttD4FKF5c7Du4+v3i5ouRXnnaMf2bER/FwezPn5tBrAJ2jW96CDYCnUzQzve/NRmjMUSJbNM4YAB5apXq1PyeacLUEPWphZBcurZe0/Gy3ERTDFTMpHC1fS6ciIDnzMLl69HDuCwOfT7Spvj0hjUgDPR650l66rbEG5UHDmymK2r+qsNrmSicahmKOUT8NJ478qP68G1eNemqe2iT6m8dTvMGD5vSqSgPLwrbfX/ECLk4Gx39Xn+Guj1o/TYF8MTMy/CxXIdXXx9P2Z9/4X5Xso= on: tags: true distributions: sdist bdist_wheel diff --git a/CHANGELOG.md b/CHANGELOG.md index a6687c1b..74dfb0cb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ All notable changes to the pythonwhat project will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). +## 2.18.0 + +- Add optional `force_diagnose` parameter to `test_exercise` to force passing the `diagnose` tests in `check_correct`. + ## 2.17.2 ### Improved diff --git a/pythonwhat/__init__.py b/pythonwhat/__init__.py index 87ea6daf..a68a0aaf 100644 --- a/pythonwhat/__init__.py +++ b/pythonwhat/__init__.py @@ -1,3 +1,3 @@ -__version__ = '2.17.2' +__version__ = '2.18.0' from .test_exercise import test_exercise, allow_errors diff --git a/requirements.txt b/requirements.txt index 66071a14..beb1e1f4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -11,7 +11,7 @@ jinja2==2.10 bs4==0.0.1 html5lib==1.0.1 h5py==2.7.1 -requests==2.18.4 +requests==2.20.0 sas7bdat==2.0.7 seaborn==0.8.1 sqlalchemy==1.2.6 From 4ae3eab169a36ba33aa8373c1d1345a54f4eb735 Mon Sep 17 00:00:00 2001 From: Jeroen Hermans Date: Wed, 21 Nov 2018 10:47:45 +0100 Subject: [PATCH 038/209] Fix local exec Execute as module, with globals (which are also locals) https://docs.python.org/3/library/functions.html#exec --- pythonwhat/local.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pythonwhat/local.py b/pythonwhat/local.py index c69a324e..3a1e9be7 100644 --- a/pythonwhat/local.py +++ b/pythonwhat/local.py @@ -7,14 +7,14 @@ from contextlib import redirect_stdout class StubShell(object): - + def __init__(self, init_code = None): self.user_ns = {} if init_code: self.run_code(init_code) - + def run_code(self, code): - exec(code, None, self.user_ns) + exec(code, self.user_ns) class StubProcess(object): From a977f8708b7f42c4c0d55ad8775b609d7251e048 Mon Sep 17 00:00:00 2001 From: Jeroen Hermans Date: Wed, 21 Nov 2018 10:52:09 +0100 Subject: [PATCH 039/209] Bump version, update changelog --- CHANGELOG.md | 8 +++++++- pythonwhat/__init__.py | 2 +- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 74dfb0cb..6ed71871 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,12 @@ All notable changes to the pythonwhat project will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). +## 2.18.1 + +### Fixed + +- Local setup with `setup_state` now works correctly (e.g. when the PEC contains a list comprehension) + ## 2.18.0 - Add optional `force_diagnose` parameter to `test_exercise` to force passing the `diagnose` tests in `check_correct`. @@ -333,4 +339,4 @@ s.check_object('x').has_equal_value() ### Removed -- There is no support for `keep_objs_in_env`, as nobody is using it. \ No newline at end of file +- There is no support for `keep_objs_in_env`, as nobody is using it. diff --git a/pythonwhat/__init__.py b/pythonwhat/__init__.py index a68a0aaf..e5c1c4e3 100644 --- a/pythonwhat/__init__.py +++ b/pythonwhat/__init__.py @@ -1,3 +1,3 @@ -__version__ = '2.18.0' +__version__ = '2.18.1' from .test_exercise import test_exercise, allow_errors From faa6cdb12a5fe9ddca71ab32f5d3873fde61cdf6 Mon Sep 17 00:00:00 2001 From: Jeroen Hermans Date: Wed, 19 Dec 2018 13:12:24 +0100 Subject: [PATCH 040/209] Improve has_equal_ast docs --- pythonwhat/has_funcs.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/pythonwhat/has_funcs.py b/pythonwhat/has_funcs.py index 7fe7c93c..5a9d12c4 100644 --- a/pythonwhat/has_funcs.py +++ b/pythonwhat/has_funcs.py @@ -38,7 +38,7 @@ def verify(part, index): except (KeyError, IndexError): raise InstructorError(_err_msg) - try: + try: verify(state.student_parts[name], index) except (KeyError, IndexError): rep.do_test(Test(Feedback(_msg, state))) @@ -101,6 +101,8 @@ def has_equal_ast(incorrect_msg=None, ``has_equal_ast()`` can be used in two ways: * As a robust version of ``has_code()``. By setting ``code``, you can look for the AST representation of ``code`` in the student's submission. + But be aware that ``a`` and ``a = 1`` won't match, as reading and assigning are not the same in an AST. + Use ``ast.dump(ast.parse(code))`` to see an AST representation of ``code``. * As an expression-based check when using more advanced SCT chain, e.g. to compare the equality of expressions to set function arguments. Args: @@ -252,7 +254,7 @@ def has_expr(incorrect_msg=None, rep = Reporter.active_reporter - get_func = partial(evalCalls[test], + get_func = partial(evalCalls[test], extra_env=extra_env, context_vals=context_vals, pre_code=pre_code, @@ -584,7 +586,7 @@ def has_printout(index, x = 6 The following SCT will not work: :: - + Ex().has_printout(0) Why? When the ``print(x)`` call is executed, the value of ``x`` will be 6, and pythonwhat will look for the output `'6`' in the output the student generated. @@ -592,7 +594,7 @@ def has_printout(index, :Example: - Inside a for loop ``has_printout()`` + Inside a for loop ``has_printout()`` Suppose you have the following solution: :: @@ -600,7 +602,7 @@ def has_printout(index, print(i) The following SCT will not work: :: - + Ex().check_for_loop().check_body().has_printout(0) The reason is that ``has_printout()`` can only be called from the root state. ``Ex()``. @@ -651,7 +653,7 @@ def has_no_error(incorrect_msg="Have a look at the console: your code contains a the student submission generated an error. This means it is not needed to use ``has_no_error()`` explicitly. However, in some cases, using ``has_no_error()`` explicitly somewhere throughout your SCT execution can be helpful: - + - If you want to make sure people didn't write typos when writing a long function name. - If you want to first verify whether a function actually runs, before checking whether the arguments were specified correctly. - More generally, if, because of the content, it's instrumental that the script runs without @@ -680,7 +682,7 @@ def has_no_error(incorrect_msg="Have a look at the console: your code contains a If you want to make sure that ``train_test_split()`` ran without errors, which would check if the student typed the function without typos and used sensical arguments, you could use the following SCT: :: - + Ex().has_no_error() Ex().check_function('sklearn.model_selection.train_test_split').multi( check_args(['arrays', 0]).has_equal_value(), @@ -688,7 +690,7 @@ def has_no_error(incorrect_msg="Have a look at the console: your code contains a check_args(['options', 'test_size']).has_equal_value(), check_args(['options', 'random_state']).has_equal_value() ) - + If, on the other hand, you want to fall back onto pythonwhat's built in behavior, that checks for an error before marking the exercise as correct, you can simply leave of the ``has_no_error()`` step. From c0ec7713598d852cd058cb28531f3940bddb270c Mon Sep 17 00:00:00 2001 From: Jeroen Hermans Date: Wed, 21 Nov 2018 10:53:54 +0100 Subject: [PATCH 041/209] Use raised error information --- pythonwhat/check_function.py | 4 ++-- pythonwhat/tasks.py | 17 +++++++---------- 2 files changed, 9 insertions(+), 12 deletions(-) diff --git a/pythonwhat/check_function.py b/pythonwhat/check_function.py index 4cf90f9e..9187de82 100644 --- a/pythonwhat/check_function.py +++ b/pythonwhat/check_function.py @@ -121,8 +121,8 @@ def check_function(name, index=0, try: sol_sig = get_sig(mapped_name=sol_parts['name'], process=state.solution_process) sol_parts['args'] = bind_args(sol_sig, sol_parts['args']) - except: - raise InstructorError("`check_function()` couldn't match the %s call of `%s` to its signature. " % (get_ord(index + 1), name)) + except Exception as e: + raise InstructorError("`check_function()` couldn't match the %s call of `%s` to its signature:\n%s " % (get_ord(index + 1), name, e)) try: stu_sig = get_sig(mapped_name=stu_parts['name'], process=state.student_process) diff --git a/pythonwhat/tasks.py b/pythonwhat/tasks.py index af320c2e..bb05a575 100644 --- a/pythonwhat/tasks.py +++ b/pythonwhat/tasks.py @@ -111,11 +111,11 @@ def get_signature(name, mapped_name, signature, manual_sigs, env): raise InstructorError('signature error - %s not in builtins' % generic_name) else: raise InstructorError('manual signature not found') - except: + except Exception as e: try: signature = inspect.signature(fun) except: - raise InstructorError('signature error - cannot determine signature') + raise InstructorError(e.args[0] + ' and cannot determine signature') return signature @@ -123,14 +123,11 @@ def get_signature(name, mapped_name, signature, manual_sigs, env): # Get the signature of a function based on an object inside the process @process_task def getSignatureInProcess(name, mapped_name, signature, manual_sigs, process, shell): - try: - return get_signature(name = name, - mapped_name = mapped_name, - signature = signature, - manual_sigs = manual_sigs, - env = get_env(shell.user_ns)) - except: - return None + return get_signature(name = name, + mapped_name = mapped_name, + signature = signature, + manual_sigs = manual_sigs, + env = get_env(shell.user_ns)) @process_task def getSignatureFromObjInProcess(obj_char, process, shell): From ee18a44e9683af7401c6a09bbaa157f5fcfce5e5 Mon Sep 17 00:00:00 2001 From: Jeroen Hermans Date: Mon, 17 Dec 2018 18:55:53 +0100 Subject: [PATCH 042/209] Fix test --- tests/test_author_warnings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_author_warnings.py b/tests/test_author_warnings.py index de8b034b..3c43a1c4 100644 --- a/tests/test_author_warnings.py +++ b/tests/test_author_warnings.py @@ -44,7 +44,7 @@ def test_check_function_2(state): state.check_function('round', 1) def test_check_function_3(state): - with pytest.raises(InstructorError, match=r"`check_function\(\)` couldn't match the first call of `round` to its signature\."): + with pytest.raises(InstructorError, match=r"`check_function\(\)` couldn't match the first call of `round` to its signature:"): sig = Signature([Parameter('wrong', Parameter.KEYWORD_ONLY)]) state.check_function('round', 0, signature=sig) From 303a8e3a5f9cad31932f530c3f465aaca306f9de Mon Sep 17 00:00:00 2001 From: fossabot Date: Tue, 11 Dec 2018 08:06:47 -0800 Subject: [PATCH 043/209] Add license scan report and status Signed-off-by: fossabot --- README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.md b/README.md index 2ccad917..bad2584a 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,7 @@ [![codecov](https://codecov.io/gh/datacamp/pythonwhat/branch/master/graph/badge.svg)](https://codecov.io/gh/datacamp/pythonwhat) [![PyPI version](https://badge.fury.io/py/pythonwhat.svg)](https://badge.fury.io/py/pythonwhat) [![Documentation Status](https://readthedocs.org/projects/pythonwhat/badge/?version=stable)](http://pythonwhat.readthedocs.io/en/stable/?badge=stable) +[![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2Fdatacamp%2Fpythonwhat.svg?type=shield)](https://app.fossa.io/projects/git%2Bgithub.com%2Fdatacamp%2Fpythonwhat?ref=badge_shield) Verify Python code submissions and auto-generate meaningful feedback messages. Originally developed for Python exercises on DataCamp for so-called Submission Correctness Tests, but can also be used independently. @@ -60,3 +61,7 @@ pytest ``` Bugs? Questions? Suggestions? [Create an issue](https://github.com/datacamp/pythonwhat/issues/new), or [contact us](mailto:content-engineering@datacamp.com)! + + +## License +[![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2Fdatacamp%2Fpythonwhat.svg?type=large)](https://app.fossa.io/projects/git%2Bgithub.com%2Fdatacamp%2Fpythonwhat?ref=badge_large) \ No newline at end of file From 2e5040b8366c263844b767eb1f046c7360d56ebe Mon Sep 17 00:00:00 2001 From: Jeroen Hermans Date: Mon, 17 Dec 2018 20:26:47 +0100 Subject: [PATCH 044/209] Improve setup --- MANIFEST.in | 2 ++ requirements.txt | 2 -- setup.py | 54 +++++++++++++++++++++++++------------- tests/test_check_object.py | 5 ++-- 4 files changed, 41 insertions(+), 22 deletions(-) create mode 100644 MANIFEST.in diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 00000000..096dd503 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,2 @@ +include LICENSE +include requirements.txt diff --git a/requirements.txt b/requirements.txt index beb1e1f4..47d57f85 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,10 +12,8 @@ bs4==0.0.1 html5lib==1.0.1 h5py==2.7.1 requests==2.20.0 -sas7bdat==2.0.7 seaborn==0.8.1 sqlalchemy==1.2.6 -tweepy==3.6.0 xlrd==1.1.0 # test-utils deps diff --git a/setup.py b/setup.py index 90aeb25b..8b575558 100644 --- a/setup.py +++ b/setup.py @@ -5,25 +5,43 @@ from os import path from setuptools import setup -_version_re = re.compile(r'__version__\s+=\s+(.*)') +PACKAGE_NAME = "pythonwhat" +REQUIREMENT_NAMES = ["markdown2", "jinja2", "asttokens", "dill", "numpy", "pandas"] -PACKAGE_NAME = 'pythonwhat' HERE = path.abspath(path.dirname(__file__)) -with open(path.join(HERE, 'README.md'), encoding='utf-8') as fp: +VERSION_FILE = path.join(HERE, PACKAGE_NAME, "__init__.py") +REQUIREMENTS_FILE = path.join(HERE, "requirements.txt") +README_FILE = path.join(HERE, "README.md") + +with open(VERSION_FILE, encoding="utf-8") as fp: + _version_re = re.compile(r"__version__\s+=\s+(.*)") + VERSION = str(ast.literal_eval(_version_re.search(fp.read()).group(1))) +with open(REQUIREMENTS_FILE, encoding="utf-8") as fp: + req_txt = fp.read() + _requirements_re_template = r"^({}(?:\s*[<>=]+\s*\S*)?)\s*(?:#.*)?$" + REQUIREMENTS = [ + re.search(_requirements_re_template.format(requirement), req_txt, re.M).group(0) + for requirement in REQUIREMENT_NAMES + ] +with open(README_FILE, encoding="utf-8") as fp: README = fp.read() -with open(path.join(HERE, PACKAGE_NAME, '__init__.py'), 'rb') as fp: - VERSION = str(ast.literal_eval(_version_re.search( - fp.read().decode('utf-8')).group(1))) -setup(name=PACKAGE_NAME, - version=VERSION, - packages=[PACKAGE_NAME, 'pythonwhat.test_funcs'], - install_requires=["dill", "numpy", "pandas", "markdown2", "jinja2", "asttokens>=1.1.10"], - long_description=README, - long_description_content_type='text/markdown', - license='GNU version 3', - author='DataCamp', - author_email='content-engineering@datacamp.com', - maintainer='Filip Schouwenaars', - maintainer_email='filip@datacamp.com', - url='https://github.com/datacamp/pythonwhat') +setup( + name=PACKAGE_NAME, + version=VERSION, + packages=[PACKAGE_NAME, "pythonwhat.test_funcs"], + install_requires=REQUIREMENTS, + description="Submission correctness tests for Python", + long_description=README, + long_description_content_type="text/markdown", + author="Filip Schouwenaars", + author_email="filip@datacamp.com", + maintainer="Jeroen Hermans", + maintainer_email="content-engineering@datacamp.com", + url="https://github.com/datacamp/pythonwhat", + classifiers=[ + "Programming Language :: Python :: 3", + "License :: OSI Approved :: GNU Affero General Public License v3", + "Operating System :: OS Independent", + ], +) diff --git a/tests/test_check_object.py b/tests/test_check_object.py index 1d00eb25..cad7ae77 100644 --- a/tests/test_check_object.py +++ b/tests/test_check_object.py @@ -141,6 +141,7 @@ def test_check_keys_exotic(sct): assert output['correct'] def test_non_dillable(): + # xlrd needed for Excel support code = "xl = pd.ExcelFile('battledeath.xlsx')" res = helper.run({ 'DC_PEC': "import pandas as pd; from urllib.request import urlretrieve; urlretrieve('https://s3.amazonaws.com/assets.datacamp.com/production/course_998/datasets/battledeath.xlsx', 'battledeath.xlsx')", @@ -194,7 +195,7 @@ def test_equality_challenge_2(): "DC_SCT": "Ex().check_object('mat').has_equal_value()" }) assert res['correct'] - + @pytest.mark.parametrize('name, ls, le, cs, ce', [ ('a', 3, 3, 5, 9), ("c", 8, 8, 5, 9), @@ -303,7 +304,7 @@ def diff_assign_data(): df2.columns = ["c", "d"] ''' } - + def test_several_assignments(diff_assign_data): res = helper.run({ **diff_assign_data, From abadc650ac0241646cfd32255689866b93ffe851 Mon Sep 17 00:00:00 2001 From: Jeroen Hermans Date: Mon, 17 Dec 2018 20:33:47 +0100 Subject: [PATCH 045/209] Fix documentation --- pythonwhat/check_wrappers.py | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/pythonwhat/check_wrappers.py b/pythonwhat/check_wrappers.py index c334924d..a9f5f692 100644 --- a/pythonwhat/check_wrappers.py +++ b/pythonwhat/check_wrappers.py @@ -29,7 +29,7 @@ 'list_comp': { 'typestr': '{{ordinal}} list comprehension', 'docstr': """Check whether a list comprehension was coded and zoom in on it. - + Can be chained with ``check_iter()``, ``check_body()``, and ``check_ifs()``. Args: @@ -78,7 +78,7 @@ 'generator_exp': { 'typestr': '{{ordinal}} generator expression', 'docstr': """Check whether a generator expression was coded and zoom in on it. - + Can be chained with ``check_iter()``, ``check_body()``, and ``check_ifs()``. Args: @@ -110,7 +110,7 @@ 'dict_comp': { 'typestr': '{{ordinal}} dictionary comprehension', 'docstr': """Check whether a dictionary comprehension was coded and zoom in on it. - + Can be chained with ``check_key()``, ``check_value()``, and ``check_ifs()``. Args: @@ -156,7 +156,7 @@ 'docstr': """Check whether a for loop was coded and zoom in on it. Can be chained with ``check_iter()`` and ``check_body()``. - + Args: index: Index of the for loop (0-based). {{typestr}} @@ -179,7 +179,7 @@ set_context('b', 2).has_equal_output() ) ) - + - ``check_for_loop()`` zooms in on the ``for`` loop, and makes its parts available for further checking. - ``check_iter()`` zooms in on the iterator part of the for loop, ``my_dict.items()`` in the solution. ``has_equal_value()`` re-executes the expressions specified by student and solution and compares their results. @@ -198,7 +198,7 @@ # passing submission 2 my_dict = {'a': 1, 'b': 2} for first, second in my_dict.items(): - mess = first + " - " + str(second) + mess = first + " - " + str(second) print(mess) :Example: @@ -252,7 +252,7 @@ ) ) ) - + """ }, 'function_def': { @@ -370,7 +370,7 @@ def shout_echo(a, b=1): 'class_def': { 'typestr': 'class definition of `{{index}}`', 'docstr': """Check whether a class was defined and zoom in on its definition - + Can be chained with ``check_bases()`` and ``check_body()``. Args: @@ -389,7 +389,7 @@ def __init__(self, i): The following SCT would verify this: :: - check_class_def('MyInt').multi( + Ex().check_class_def('MyInt').multi( check_bases(0).has_equal_ast(), check_body().check_function_def('__init__').multi( check_args('self'), @@ -442,7 +442,7 @@ def __init__(self, i): check_test().multi( set_env(x = -1).has_equal_value(), set_env(x = 1).has_equal_value(), - set_env(x = 0).has_equal_value() + set_env(x = 0).has_equal_value() ), check_body().check_function('print', 0).\\ check_args('value').has_equal_value() @@ -459,11 +459,11 @@ def __init__(self, i): function ``print()`` and whether its argument is set correctly. :Example: - + In Python, when an if-else statement has an ``elif`` clause, it is held in the `orelse` part. In this sense, an if-elif-else statement is represented by python as nested if-elses. More specifically, this if-else statement: :: - + if x > 0: print(x) elif y > 0: @@ -656,7 +656,7 @@ def __init__(self, i): scts['check_'+k] = partial(check_part, k, v) -for k, v in __PART_INDEX_WRAPPERS__.items(): +for k, v in __PART_INDEX_WRAPPERS__.items(): scts['check_'+k] = partial(check_part_index, k, part_msg=v) for k, v in __NODE_WRAPPERS__.items(): @@ -686,4 +686,4 @@ def __init__(self, i): scts['has_context'] = has_context scts['check_function'] = check_function -locals().update(scts) \ No newline at end of file +locals().update(scts) From 2e011fb6d96806d20e1617a287cec3fa4992375a Mon Sep 17 00:00:00 2001 From: Jeroen Hermans Date: Mon, 17 Dec 2018 20:35:14 +0100 Subject: [PATCH 046/209] Clarify error --- pythonwhat/has_funcs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pythonwhat/has_funcs.py b/pythonwhat/has_funcs.py index 5a9d12c4..03979cbb 100644 --- a/pythonwhat/has_funcs.py +++ b/pythonwhat/has_funcs.py @@ -273,7 +273,7 @@ def has_expr(incorrect_msg=None, env=state.solution_env) if (test == 'error') ^ isinstance(eval_sol, Exception): - raise InstructorError("Evaluating expression raised error in solution process (or not an error if testing for one). " + raise InstructorError("Evaluating expression raised error in solution process (or didn't raise if testing for one). " "Error: {} - {}".format(type(eval_sol), str_sol)) if isinstance(eval_sol, ReprFail): raise InstructorError("Couldn't extract the value for the highlighted expression from the solution process: " + eval_sol.info) From 83fadae7de74940b930f230daf8ba3da6803edc5 Mon Sep 17 00:00:00 2001 From: Jeroen Hermans Date: Tue, 18 Dec 2018 09:42:30 +0100 Subject: [PATCH 047/209] Limit impact of unpicklable object (fix skipping deepcopy on value access) --- pythonwhat/tasks.py | 11 +++++++++-- tests/test_has_expr.py | 26 ++++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/pythonwhat/tasks.py b/pythonwhat/tasks.py index bb05a575..85d1d76f 100644 --- a/pythonwhat/tasks.py +++ b/pythonwhat/tasks.py @@ -318,14 +318,21 @@ def taskRunEval(tree, tree = ast.Expression(tree) # Expression code takes precedence over tree code - if expr_code: code = expr_code - else: code = compile(tree, "