From fe9195fa297a82f074d65f1e6c06d3eaca90392f Mon Sep 17 00:00:00 2001 From: Jeroen Hermans Date: Sun, 8 Sep 2019 22:46:07 +0200 Subject: [PATCH 01/64] Use messaging utils from protowhat --- pythonwhat/checks/check_funcs.py | 2 +- pythonwhat/checks/check_function.py | 2 +- pythonwhat/checks/has_funcs.py | 3 +- .../test_funcs/test_compound_statement.py | 6 +-- pythonwhat/test_funcs/test_object_accessed.py | 4 +- pythonwhat/utils.py | 46 ------------------- tests/test_utils.py | 23 ---------- 7 files changed, 9 insertions(+), 77 deletions(-) diff --git a/pythonwhat/checks/check_funcs.py b/pythonwhat/checks/check_funcs.py index 306d445e..b9e53026 100644 --- a/pythonwhat/checks/check_funcs.py +++ b/pythonwhat/checks/check_funcs.py @@ -2,7 +2,7 @@ from pythonwhat.checks.has_funcs import has_part from protowhat.Feedback import InstructorError from pythonwhat.tasks import setUpNewEnvInProcess, breakDownNewEnvInProcess -from pythonwhat.utils import get_ord +from protowhat.utils_messaging import get_ord from pythonwhat.utils_ast import assert_ast import ast from jinja2 import Template diff --git a/pythonwhat/checks/check_function.py b/pythonwhat/checks/check_function.py index 32ff5b8e..ff82800f 100644 --- a/pythonwhat/checks/check_function.py +++ b/pythonwhat/checks/check_function.py @@ -1,6 +1,6 @@ from pythonwhat.checks.check_funcs import part_to_child from pythonwhat.tasks import getSignatureInProcess -from pythonwhat.utils import get_ord, get_times +from protowhat.utils_messaging import get_ord, get_times from protowhat.Feedback import InstructorError from pythonwhat.parsing import IndexedDict from functools import partial diff --git a/pythonwhat/checks/has_funcs.py b/pythonwhat/checks/has_funcs.py index 796863b7..45139205 100644 --- a/pythonwhat/checks/has_funcs.py +++ b/pythonwhat/checks/has_funcs.py @@ -1,3 +1,4 @@ +from protowhat.utils_messaging import get_ord from pythonwhat.tasks import ( getResultInProcess, getOutputInProcess, @@ -670,7 +671,7 @@ def has_printout( except (KeyError, IndexError): raise InstructorError( "`has_printout({})` couldn't find the {} print call in your solution.".format( - index, utils.get_ord(index + 1) + index, get_ord(index + 1) ) ) diff --git a/pythonwhat/test_funcs/test_compound_statement.py b/pythonwhat/test_funcs/test_compound_statement.py index a41d8f37..c4572583 100644 --- a/pythonwhat/test_funcs/test_compound_statement.py +++ b/pythonwhat/test_funcs/test_compound_statement.py @@ -1,3 +1,4 @@ +from protowhat.utils_messaging import get_ord from protowhat.sct_syntax import link_to_state from pythonwhat.checks.check_funcs import ( check_node, @@ -15,7 +16,6 @@ ) from pythonwhat.checks.check_has_context import has_context from functools import partial -from pythonwhat import utils # this is done by the chain for v2 # it's only needed when a new state is created and (possibly) used elsewhere @@ -515,7 +515,7 @@ def check_context(state): state, "context", i, - "%s context" % utils.get_ord(i + 1), + "%s context" % get_ord(i + 1), missing_msg=MSG_NUM_CTXT2, ) @@ -600,4 +600,4 @@ def test_comp( # test that ifs are same length has_equal_part_len(child, "ifs", insufficient_ifs_msg) # test individual ifs - multi(check_part_index(child, "ifs", i, utils.get_ord(i + 1) + " if"), if_test) + multi(check_part_index(child, "ifs", i, get_ord(i + 1) + " if"), if_test) diff --git a/pythonwhat/test_funcs/test_object_accessed.py b/pythonwhat/test_funcs/test_object_accessed.py index a9ad7474..f7436440 100644 --- a/pythonwhat/test_funcs/test_object_accessed.py +++ b/pythonwhat/test_funcs/test_object_accessed.py @@ -1,5 +1,5 @@ +from protowhat.utils_messaging import get_times from pythonwhat.Test import BiggerTest -import pythonwhat.utils def test_object_accessed(state, name, times=1, not_accessed_msg=None): @@ -46,7 +46,7 @@ def test_object_accessed(state, name, times=1, not_accessed_msg=None): if name.startswith(full_name): stud_name = name.replace(full_name, orig) - add = " at least %s" % pythonwhat.utils.get_times(times) if times > 1 else "" + add = " at least %s" % get_times(times) if times > 1 else "" not_accessed_msg = "Have you accessed `%s`%s?" % (stud_name, add) # name should be contained inside the student_object_accesses. diff --git a/pythonwhat/utils.py b/pythonwhat/utils.py index 963c1fe7..7a36f7de 100644 --- a/pythonwhat/utils.py +++ b/pythonwhat/utils.py @@ -17,52 +17,6 @@ def shorten_str(text, to_chars=100): return text -def get_ord(num): - assert num != 0, "use strictly positive numbers in get_ord()" - nums = { - 1: "first", - 2: "second", - 3: "third", - 4: "fourth", - 5: "fifth", - 6: "sixth", - 7: "seventh", - 8: "eight", - 9: "nineth", - 10: "tenth", - } - if num in nums: - return nums[num] - else: - return "%dth" % num - - -def get_times(num): - nums = {1: "once", 2: "twice"} - if num in nums: - return nums[num] - else: - return "%s times" % get_num(num) - - -def get_num(num): - nums = { - 0: "no", - 1: "one", - 2: "two", - 3: "three", - 4: "four", - 5: "five", - 6: "six", - 7: "seven", - 8: "eight", - } - if num in nums: - return nums[num] - else: - return str(num) - - def copy_env(env): mutableTypes = (tuple, list, dict) # One list comprehension to filter list. Might need some cleaning, but it diff --git a/tests/test_utils.py b/tests/test_utils.py index 37a684ba..e69de29b 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -1,23 +0,0 @@ -import pytest -from pythonwhat import utils - - -@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 - - -@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 - - -@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 From dd49f7b450062547e3c35c2437a91fd2c0234830 Mon Sep 17 00:00:00 2001 From: Jeroen Hermans Date: Mon, 13 Jan 2020 13:43:59 +0100 Subject: [PATCH 02/64] Use parameters_arg decorator --- pythonwhat/State.py | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/pythonwhat/State.py b/pythonwhat/State.py index 4c321587..e5877a47 100644 --- a/pythonwhat/State.py +++ b/pythonwhat/State.py @@ -1,4 +1,5 @@ from functools import partialmethod +from protowhat.utils import parameters_attr from pythonwhat.parsing import ( TargetVars, FunctionParser, @@ -35,6 +36,7 @@ def __len__(self): return len(self._items) +@parameters_attr class State(ProtoState): """State of the SCT environment. @@ -75,11 +77,9 @@ def __init__( solution_env=Context(), ): args = locals().copy() - self.params = list() for k, v in args.items(): if k != "self": - self.params.append(k) setattr(self, k, v) self.messages = messages if messages else [] @@ -113,14 +113,16 @@ def to_child(self, append_message="", node_name="", **kwargs): student tree and solution tree. This is necessary when testing if statements or for loops for example. """ - bad_pars = set(kwargs) - set(self.params) - if bad_pars: - raise ValueError("Invalid init params for State: %s" % ", ".join(bad_pars)) + bad_parameters = set(kwargs) - set(self.parameters) + if bad_parameters: + raise ValueError( + "Invalid init parameters for State: %s" % ", ".join(bad_parameters) + ) base_kwargs = { attr: getattr(self, attr) - for attr in self.params - if attr not in ["highlight"] + for attr in self.parameters + if hasattr(self, attr) and attr not in ["ast_dispatcher", "highlight"] } if not isinstance(append_message, dict): @@ -162,11 +164,11 @@ def update_context(name): init_kwargs = {**base_kwargs, **kwargs} child = klass(**init_kwargs) - extra_attrs = set(vars(self)) - set(self.params) + extra_attrs = set(vars(self)) - set(self.parameters) for attr in extra_attrs: # don't copy attrs set on new instances in init # the cached manual_sigs is passed - if attr not in {"params", "ast_dispatcher", "converters"}: + if attr not in {"ast_dispatcher", "converters"}: setattr(child, attr, getattr(self, attr)) return child From 59ab54034475ea8f705546605a6063ae9b1b3d77 Mon Sep 17 00:00:00 2001 From: Jeroen Hermans Date: Fri, 26 Jul 2019 17:07:00 +0200 Subject: [PATCH 03/64] Rename F to LazyChain in code --- pythonwhat/sct_syntax.py | 8 ++++++-- pythonwhat/test_exercise.py | 2 +- tests/test_check_files.py | 8 ++++---- tests/test_spec.py | 12 ++++++------ 4 files changed, 17 insertions(+), 13 deletions(-) diff --git a/pythonwhat/sct_syntax.py b/pythonwhat/sct_syntax.py index 71451e64..fb36e132 100644 --- a/pythonwhat/sct_syntax.py +++ b/pythonwhat/sct_syntax.py @@ -1,4 +1,8 @@ -from protowhat.sct_syntax import Chain as ProtoChain, F as ProtoF, state_dec_gen +from protowhat.sct_syntax import ( + Chain as ProtoChain, + LazyChain as ProtoLazyChain, + state_dec_gen, +) from pythonwhat.checks.check_wrappers import scts from pythonwhat.State import State from pythonwhat.probe import Node, Probe, TEST_NAMES @@ -35,7 +39,7 @@ def __init__(self, state, attr_scts=sct_dict): super().__init__(state, attr_scts) -class F(ProtoF): +class LazyChain(ProtoLazyChain): def __init__(self, stack=None, attr_scts=sct_dict): super().__init__(stack, attr_scts) diff --git a/pythonwhat/test_exercise.py b/pythonwhat/test_exercise.py index bf96b6a4..4d27bcc0 100644 --- a/pythonwhat/test_exercise.py +++ b/pythonwhat/test_exercise.py @@ -90,7 +90,7 @@ def prep_context(): "from inspect import Parameter as param", "from pythonwhat.signatures import sig_from_params, sig_from_obj", "from pythonwhat.State import set_converter", - "from pythonwhat.sct_syntax import F, Ex", + "from pythonwhat.sct_syntax import LazyChain as F, Ex", ] [exec(line, None, cntxt) for line in imports] diff --git a/tests/test_check_files.py b/tests/test_check_files.py index 2bc0d7fc..214abfe7 100644 --- a/tests/test_check_files.py +++ b/tests/test_check_files.py @@ -3,7 +3,7 @@ import pytest import tests.helper as helper -from protowhat.sct_syntax import F +from protowhat.sct_syntax import LazyChain from pythonwhat.local import ChDir from protowhat.Test import TestFail as TF @@ -68,9 +68,9 @@ def test_file_existence_syntax(temp_py_file): assert expected_content in file_chain._state.student_code with helper.verify_sct(True): - file_chain = chain >> F(attr_scts={"check_file": cf.check_file}).check_file( - temp_py_file.name - ) + file_chain = chain >> LazyChain( + attr_scts={"check_file": cf.check_file} + ).check_file(temp_py_file.name) assert expected_content in file_chain._state.student_code diff --git a/tests/test_spec.py b/tests/test_spec.py index c6ee9d26..54222660 100644 --- a/tests/test_spec.py +++ b/tests/test_spec.py @@ -289,7 +289,7 @@ def test_override(k, code): # Test SCT Ex syntax (copied from sqlwhat) ----------------------------------- -from pythonwhat.sct_syntax import Ex, F, state_dec +from pythonwhat.sct_syntax import Ex, LazyChain, state_dec @pytest.fixture @@ -299,12 +299,12 @@ def addx(): @pytest.fixture def f(): - return F._from_func(lambda state: state + "b") + return LazyChain._from_func(lambda state: state + "b") @pytest.fixture def f2(): - return F._from_func(lambda state: state + "c") + return LazyChain._from_func(lambda state: state + "c") def test_f_from_func(f): @@ -312,11 +312,11 @@ def test_f_from_func(f): def test_f_sct_copy_kw(addx): - assert F()._sct_copy(addx)(x="x")("state") == "statex" + assert LazyChain()._sct_copy(addx)(x="x")("state") == "statex" def test_f_sct_copy_pos(addx): - assert F()._sct_copy(addx)("x")("state") == "statex" + assert LazyChain()._sct_copy(addx)("x")("state") == "statex" def test_ex_sct_copy_kw(addx): @@ -346,7 +346,7 @@ def test_f_add_f(f, f2): def test_f_from_state_dec(addx): dec_addx = state_dec(addx) f = dec_addx(x="x") - isinstance(f, F) + isinstance(f, LazyChain) assert f("state") == "statex" From 018dc8d543834c8396701a2e2c26d95a90a95213 Mon Sep 17 00:00:00 2001 From: Jeroen Hermans Date: Thu, 15 Aug 2019 22:59:12 +0200 Subject: [PATCH 04/64] Improve variable name --- pythonwhat/local.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pythonwhat/local.py b/pythonwhat/local.py index fec46391..0c0febbd 100644 --- a/pythonwhat/local.py +++ b/pythonwhat/local.py @@ -159,14 +159,14 @@ def run_single_process(pec, code, pid=None, mode="simple"): if mode == "stub": # no isolation process = StubProcess(init_code=pec, pid=pid) - raw_stu_output, error = run_code(process.shell.run_code, code) + raw_output, error = run_code(process.shell.run_code, code) elif mode == "simple": # no advanced functionality process = SimpleProcess(pid) process.start() _ = process.executeTask(TaskCaptureOutput(pec)) - raw_stu_output, error = process.executeTask(TaskCaptureOutput(code)) + raw_output, error = process.executeTask(TaskCaptureOutput(code)) elif mode == "full" and BACKEND_AVAILABLE: # slow @@ -178,13 +178,13 @@ def run_single_process(pec, code, pid=None, mode="simple"): output, raw_output = process.executeTask( TaskCaptureFullOutput((code,), "script.py", None, silent=True) ) - raw_stu_output = raw_output["output_stream"] + raw_output = raw_output["output_stream"] error = raw_output["error"] else: raise ValueError("Invalid mode") - return process, raw_stu_output, error + return process, raw_output, error def run_exercise(pec, sol_code, stu_code, sol_wd=None, stu_wd=None, **kwargs): From b07930045cc32c4add17f712abc5d72dee389946 Mon Sep 17 00:00:00 2001 From: Jeroen Hermans Date: Thu, 15 Aug 2019 23:01:08 +0200 Subject: [PATCH 05/64] Update to new Chain classes --- pythonwhat/sct_syntax.py | 21 ++++++++------------- tests/helper.py | 2 +- tests/test_author_warnings.py | 2 +- tests/test_check_files.py | 5 ++--- tests/test_spec.py | 27 ++++++++++++++++----------- tests/test_v2_only.py | 2 +- 6 files changed, 29 insertions(+), 30 deletions(-) diff --git a/pythonwhat/sct_syntax.py b/pythonwhat/sct_syntax.py index fb36e132..7cbb62ea 100644 --- a/pythonwhat/sct_syntax.py +++ b/pythonwhat/sct_syntax.py @@ -1,6 +1,7 @@ from protowhat.sct_syntax import ( - Chain as ProtoChain, - LazyChain as ProtoLazyChain, + Chain, + EagerChain, + LazyChain, state_dec_gen, ) from pythonwhat.checks.check_wrappers import scts @@ -31,21 +32,13 @@ def wrapper(*args, **kwargs): return wrapper -state_dec = state_dec_gen(State, sct_dict) +state_dec = state_dec_gen(State) - -class Chain(ProtoChain): - def __init__(self, state, attr_scts=sct_dict): - super().__init__(state, attr_scts) - - -class LazyChain(ProtoLazyChain): - def __init__(self, stack=None, attr_scts=sct_dict): - super().__init__(stack, attr_scts) +assert LazyChain # todo: __all__? def Ex(state=None): - return Chain(state or State.root_state) + return EagerChain(state=state or State.root_state) if include_v1(): @@ -65,5 +58,7 @@ def Ex(state=None): for k in ["test_or", "test_correct"]: sct_dict[k] = multi_dec(getattr(test_funcs, k)) +Chain.register_scts(sct_dict) + # Prepare check_funcs to be used alone (e.g. test = check_with().check_body()) v2_check_functions = {k: state_dec(v) for k, v in scts.items()} diff --git a/tests/helper.py b/tests/helper.py index 32ec3e70..c315022e 100644 --- a/tests/helper.py +++ b/tests/helper.py @@ -7,8 +7,8 @@ from pythonwhat.local import StubProcess, run_exercise, ChDir, WorkerProcess from contextlib import contextmanager from protowhat.Test import TestFail as TF +from protowhat.sct_syntax import Chain from pythonwhat.test_exercise import test_exercise -from pythonwhat.sct_syntax import Chain import pytest import tempfile diff --git a/tests/test_author_warnings.py b/tests/test_author_warnings.py index ab722c73..915bb460 100644 --- a/tests/test_author_warnings.py +++ b/tests/test_author_warnings.py @@ -24,7 +24,7 @@ def test_converter_err(): def test_check_syntax_double_getattr(): s = setup_state() - with pytest.raises(AttributeError, match=r"Did you forget to call a statement"): + with pytest.raises(AttributeError, match=r"Expected a call of"): s.check_list_comp.check_body() diff --git a/tests/test_check_files.py b/tests/test_check_files.py index 214abfe7..e68b4331 100644 --- a/tests/test_check_files.py +++ b/tests/test_check_files.py @@ -67,10 +67,9 @@ def test_file_existence_syntax(temp_py_file): file_chain = chain.check_file(temp_py_file.name) assert expected_content in file_chain._state.student_code + LazyChain.register_scts({"check_file": cf.check_file}) with helper.verify_sct(True): - file_chain = chain >> LazyChain( - attr_scts={"check_file": cf.check_file} - ).check_file(temp_py_file.name) + file_chain = chain >> LazyChain().check_file(temp_py_file.name) assert expected_content in file_chain._state.student_code diff --git a/tests/test_spec.py b/tests/test_spec.py index 54222660..521e64ce 100644 --- a/tests/test_spec.py +++ b/tests/test_spec.py @@ -289,7 +289,8 @@ def test_override(k, code): # Test SCT Ex syntax (copied from sqlwhat) ----------------------------------- -from pythonwhat.sct_syntax import Ex, LazyChain, state_dec +from protowhat.sct_syntax import ChainExtender +from pythonwhat.sct_syntax import Ex, EagerChain, LazyChain, state_dec @pytest.fixture @@ -299,12 +300,12 @@ def addx(): @pytest.fixture def f(): - return LazyChain._from_func(lambda state: state + "b") + return LazyChain._from_func(lambda state, b: state + b, kwargs={"b": "b"}) @pytest.fixture def f2(): - return LazyChain._from_func(lambda state: state + "c") + return LazyChain._from_func(lambda state, c: state + c, kwargs={"c": "c"}) def test_f_from_func(f): @@ -312,23 +313,23 @@ def test_f_from_func(f): def test_f_sct_copy_kw(addx): - assert LazyChain()._sct_copy(addx)(x="x")("state") == "statex" + assert LazyChain((addx, (), {"x": "x"}))("state") == "statex" def test_f_sct_copy_pos(addx): - assert LazyChain()._sct_copy(addx)("x")("state") == "statex" + assert LazyChain((addx, ("x",), {}))("state") == "statex" def test_ex_sct_copy_kw(addx): - assert Ex("state")._sct_copy(addx)(x="x")._state == "statex" + assert EagerChain((addx, (), {"x": "x"}), state="state")._state == "statex" def test_ex_sct_copy_pos(addx): - assert Ex("state")._sct_copy(addx)("x")._state == "statex" + assert EagerChain((addx, ("x",), {}), state="state")._state == "statex" def test_f_2_funcs(f, addx): - g = f._sct_copy(addx) + g = ChainExtender(f, addx) assert g(x="x")("a") == "abx" @@ -352,15 +353,19 @@ def test_f_from_state_dec(addx): @pytest.fixture def ex(): - return Ex("state")._sct_copy(lambda state, x: state + x)("x") + return ChainExtender(Ex("state"), lambda state, x: state + x)("x") def test_ex_add_f(ex, f): - (ex >> f)._state = "statexb" + assert (ex >> f)._state == "statexb" + + +def test_ex_add_f_add_f(ex, f, f2): + assert (ex >> (f >> f2))._state == "statexbc" def test_ex_add_unary(ex): - (ex >> (lambda state: state + "b"))._state == "statexb" + assert (ex >> (lambda state: state + "b"))._state == "statexb" def test_ex_add_ex_err(ex): diff --git a/tests/test_v2_only.py b/tests/test_v2_only.py index 6c3de211..02527b63 100644 --- a/tests/test_v2_only.py +++ b/tests/test_v2_only.py @@ -5,7 +5,7 @@ def relooooad(): import pythonwhat.sct_syntax - + pythonwhat.sct_syntax.Chain.registered_scts = {} importlib.reload(pythonwhat.sct_syntax) From 5cc93cd6aa575c52b2e4c1c42b894004efe6d0b5 Mon Sep 17 00:00:00 2001 From: Jeroen Hermans Date: Mon, 2 Sep 2019 12:51:49 +0200 Subject: [PATCH 06/64] Extract ChainedCall --- pythonwhat/sct_syntax.py | 2 +- tests/test_check_files.py | 2 +- tests/test_spec.py | 17 ++++++++++------- tests/test_v2_only.py | 2 +- 4 files changed, 13 insertions(+), 10 deletions(-) diff --git a/pythonwhat/sct_syntax.py b/pythonwhat/sct_syntax.py index 7cbb62ea..4235eac2 100644 --- a/pythonwhat/sct_syntax.py +++ b/pythonwhat/sct_syntax.py @@ -58,7 +58,7 @@ def Ex(state=None): for k in ["test_or", "test_correct"]: sct_dict[k] = multi_dec(getattr(test_funcs, k)) -Chain.register_scts(sct_dict) +Chain.register_functions(sct_dict) # Prepare check_funcs to be used alone (e.g. test = check_with().check_body()) v2_check_functions = {k: state_dec(v) for k, v in scts.items()} diff --git a/tests/test_check_files.py b/tests/test_check_files.py index e68b4331..082ef1da 100644 --- a/tests/test_check_files.py +++ b/tests/test_check_files.py @@ -67,7 +67,7 @@ def test_file_existence_syntax(temp_py_file): file_chain = chain.check_file(temp_py_file.name) assert expected_content in file_chain._state.student_code - LazyChain.register_scts({"check_file": cf.check_file}) + LazyChain.register_functions({"check_file": cf.check_file}) with helper.verify_sct(True): file_chain = chain >> LazyChain().check_file(temp_py_file.name) assert expected_content in file_chain._state.student_code diff --git a/tests/test_spec.py b/tests/test_spec.py index 521e64ce..fe05b711 100644 --- a/tests/test_spec.py +++ b/tests/test_spec.py @@ -289,7 +289,7 @@ def test_override(k, code): # Test SCT Ex syntax (copied from sqlwhat) ----------------------------------- -from protowhat.sct_syntax import ChainExtender +from protowhat.sct_syntax import ChainExtender, ChainedCall from pythonwhat.sct_syntax import Ex, EagerChain, LazyChain, state_dec @@ -300,12 +300,12 @@ def addx(): @pytest.fixture def f(): - return LazyChain._from_func(lambda state, b: state + b, kwargs={"b": "b"}) + return LazyChain(ChainedCall(lambda state, b: state + b, kwargs={"b": "b"})) @pytest.fixture def f2(): - return LazyChain._from_func(lambda state, c: state + c, kwargs={"c": "c"}) + return LazyChain(ChainedCall(lambda state, c: state + c, kwargs={"c": "c"})) def test_f_from_func(f): @@ -313,19 +313,22 @@ def test_f_from_func(f): def test_f_sct_copy_kw(addx): - assert LazyChain((addx, (), {"x": "x"}))("state") == "statex" + assert LazyChain(ChainedCall(addx, kwargs={"x": "x"}))("state") == "statex" def test_f_sct_copy_pos(addx): - assert LazyChain((addx, ("x",), {}))("state") == "statex" + assert LazyChain(ChainedCall(addx, ("x",)))("state") == "statex" def test_ex_sct_copy_kw(addx): - assert EagerChain((addx, (), {"x": "x"}), state="state")._state == "statex" + assert ( + EagerChain(ChainedCall(addx, kwargs={"x": "x"}), state="state")._state + == "statex" + ) def test_ex_sct_copy_pos(addx): - assert EagerChain((addx, ("x",), {}), state="state")._state == "statex" + assert EagerChain(ChainedCall(addx, ("x",)), state="state")._state == "statex" def test_f_2_funcs(f, addx): diff --git a/tests/test_v2_only.py b/tests/test_v2_only.py index 02527b63..d7f7f377 100644 --- a/tests/test_v2_only.py +++ b/tests/test_v2_only.py @@ -5,7 +5,7 @@ def relooooad(): import pythonwhat.sct_syntax - pythonwhat.sct_syntax.Chain.registered_scts = {} + pythonwhat.sct_syntax.Chain.registered_functions = {} importlib.reload(pythonwhat.sct_syntax) From 4c092734ce80bfbf585f9b02b83c983ae8de48a8 Mon Sep 17 00:00:00 2001 From: Jeroen Hermans Date: Sun, 22 Sep 2019 23:21:08 +0200 Subject: [PATCH 07/64] Update to latest protowhat --- pythonwhat/State.py | 68 +++++----- pythonwhat/checks/check_funcs.py | 38 +++--- pythonwhat/checks/check_function.py | 45 +++---- pythonwhat/checks/check_has_context.py | 26 ++-- pythonwhat/checks/check_logic.py | 15 ++- pythonwhat/checks/check_object.py | 28 ++-- pythonwhat/checks/check_wrappers.py | 2 +- pythonwhat/checks/has_funcs.py | 122 ++++++++++-------- pythonwhat/feedback.py | 19 +++ pythonwhat/local.py | 2 +- pythonwhat/reporter.py | 5 - pythonwhat/tasks.py | 15 ++- pythonwhat/test_exercise.py | 20 +-- pythonwhat/test_funcs/test_function.py | 11 +- pythonwhat/test_funcs/test_object_accessed.py | 6 +- pythonwhat/test_funcs/utils.py | 35 ++--- pythonwhat/utils_ast.py | 17 +-- tests/helper.py | 2 +- tests/test_author_warnings.py | 2 +- tests/test_check_files.py | 2 +- tests/test_check_function.py | 3 +- tests/test_debug.py | 13 +- tests/test_has_expr.py | 2 +- tests/test_has_printout.py | 2 +- tests/test_messaging.py | 2 +- tests/test_spec.py | 2 +- tests/test_state.py | 5 +- 27 files changed, 286 insertions(+), 223 deletions(-) create mode 100644 pythonwhat/feedback.py delete mode 100644 pythonwhat/reporter.py diff --git a/pythonwhat/State.py b/pythonwhat/State.py index e5877a47..11eee239 100644 --- a/pythonwhat/State.py +++ b/pythonwhat/State.py @@ -1,5 +1,7 @@ from functools import partialmethod from protowhat.utils import parameters_attr +from protowhat.Feedback import FeedbackComponent +from pythonwhat.feedback import Feedback from pythonwhat.parsing import ( TargetVars, FunctionParser, @@ -8,7 +10,7 @@ ) from protowhat.State import State as ProtoState from protowhat.selectors import DispatcherInterface -from protowhat.Feedback import InstructorError +from protowhat.failure import debugger from pythonwhat import signatures from pythonwhat.converters import get_manual_converters from collections.abc import Mapping @@ -50,6 +52,8 @@ class State(ProtoState): """ + feedback_cls = Feedback + def __init__( self, student_code, @@ -62,8 +66,9 @@ def __init__( reporter, force_diagnose=False, highlight=None, + highlight_offset=None, highlighting_disabled=None, - messages=None, + feedback_context=None, creator=None, student_ast=None, solution_ast=None, @@ -77,13 +82,12 @@ def __init__( solution_env=Context(), ): args = locals().copy() + self.debug = False for k, v in args.items(): if k != "self": setattr(self, k, v) - self.messages = messages if messages else [] - self.ast_dispatcher = self.get_dispatcher() # Parse solution and student code @@ -91,7 +95,8 @@ def __init__( if isinstance(self.student_code, str) and student_ast is None: self.student_ast = self.parse(student_code) if isinstance(self.solution_code, str) and solution_ast is None: - self.solution_ast = self.parse(solution_code, test=False) + with debugger(self): + self.solution_ast = self.parse(solution_code) if highlight is None: # todo: check parent_state? (move check to reporting?) self.highlight = self.student_ast @@ -106,7 +111,7 @@ def get_manual_sigs(self): return self.manual_sigs - def to_child(self, append_message="", node_name="", **kwargs): + def to_child(self, append_message=None, node_name="", **kwargs): """Dive into nested tree. Set the current state as a state with a subtree of this syntax tree as @@ -125,9 +130,10 @@ def to_child(self, append_message="", node_name="", **kwargs): if hasattr(self, attr) and attr not in ["ast_dispatcher", "highlight"] } - if not isinstance(append_message, dict): - append_message = {"msg": append_message, "kwargs": {}} - kwargs["messages"] = [*self.messages, append_message] + if append_message and not isinstance(append_message, FeedbackComponent): + append_message = FeedbackComponent(append_message) + kwargs["feedback_context"] = append_message + kwargs["creator"] = {"type": "to_child", "args": {"state": self}} def update_kwarg(name, func): kwargs[name] = func(kwargs[name]) @@ -185,27 +191,30 @@ def has_different_processes(self): def assert_execution_root(self, fun, extra_msg=""): if not (self.is_root or self.is_creator_type("run")): - raise InstructorError( - "`%s()` should only be called focusing on a full script, following `Ex()` or `run()`. %s" - % (fun, extra_msg) - ) + with debugger(self): + self.report( + "`%s()` should only be called focusing on a full script, following `Ex()` or `run()`. %s" + % (fun, extra_msg) + ) def is_creator_type(self, type): return self.creator and self.creator.get("type") == type 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])) - ) + with debugger(self): + self.report( + "`%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])) - ) + with debugger(self): + self.report( + "`%s()` should not be called on %s." + % (fun, " or ".join(["`%s()`" % pf for pf in prev_fun])) + ) def parse_external(self, code): res = (None, None) @@ -237,17 +246,17 @@ def parse_internal(self, code): try: return self.ast_dispatcher.parse(code) except Exception as e: - raise InstructorError( + self.report( "Something went wrong when parsing the solution code: %s" % str(e) ) - def parse(self, text, test=True): - if test: - parse_method = self.parse_external - token_attr = "student_ast_tokens" - else: + def parse(self, text): + if self.debug: parse_method = self.parse_internal token_attr = "solution_ast_tokens" + else: + parse_method = self.parse_external + token_attr = "student_ast_tokens" tokens, ast = parse_method(text) setattr(self, token_attr, tokens) @@ -258,9 +267,8 @@ def get_dispatcher(self): try: return Dispatcher(self.pre_exercise_code) except Exception as e: - raise InstructorError( - "Something went wrong when parsing the PEC: %s" % str(e) - ) + with debugger(self): + self.report("Something went wrong when parsing the PEC: %s" % str(e)) class Dispatcher(DispatcherInterface): diff --git a/pythonwhat/checks/check_funcs.py b/pythonwhat/checks/check_funcs.py index b9e53026..be618aa4 100644 --- a/pythonwhat/checks/check_funcs.py +++ b/pythonwhat/checks/check_funcs.py @@ -1,6 +1,7 @@ +from protowhat.Feedback import FeedbackComponent from pythonwhat.checks.check_logic import multi from pythonwhat.checks.has_funcs import has_part -from protowhat.Feedback import InstructorError +from protowhat.failure import debugger from pythonwhat.tasks import setUpNewEnvInProcess, breakDownNewEnvInProcess from protowhat.utils_messaging import get_ord from pythonwhat.utils_ast import assert_ast @@ -14,7 +15,7 @@ def render(template, kwargs): def part_to_child(stu_part, sol_part, append_message, state, node_name=None): # stu_part and sol_part will be accessible on all templates - append_message["kwargs"].update({"stu_part": stu_part, "sol_part": sol_part}) + append_message.kwargs.update({"stu_part": stu_part, "sol_part": sol_part}) # if the parts are dictionaries, use to deck out child state if all(isinstance(p, dict) for p in [stu_part, sol_part]): @@ -51,14 +52,14 @@ def check_part(state, name, part_msg, missing_msg=None, expand_msg=None): if not part_msg: part_msg = name - append_message = {"msg": expand_msg, "kwargs": {"part": part_msg}} + append_message = FeedbackComponent(expand_msg, {"part": part_msg}) - has_part(state, name, missing_msg, append_message["kwargs"]) + has_part(state, name, missing_msg, append_message.kwargs) stu_part = state.student_parts[name] sol_part = state.solution_parts[name] - assert_ast(state, sol_part, append_message["kwargs"]) + assert_ast(state, sol_part, append_message.kwargs) return part_to_child(stu_part, sol_part, append_message, state) @@ -83,7 +84,7 @@ def check_part_index(state, name, index, part_msg, missing_msg=None, expand_msg= fmt_kwargs = {"index": index, "ordinal": ordinal} fmt_kwargs.update(part=render(part_msg, fmt_kwargs)) - append_message = {"msg": expand_msg, "kwargs": fmt_kwargs} + append_message = FeedbackComponent(expand_msg, fmt_kwargs) # check there are enough parts for index has_part(state, name, missing_msg, fmt_kwargs, index) @@ -130,14 +131,13 @@ def check_node( try: stu_out[index] except (KeyError, IndexError): # TODO comment errors - _msg = state.build_message(missing_msg, fmt_kwargs) - state.report(_msg) + state.report(missing_msg, fmt_kwargs) # get node at index stu_part = stu_out[index] sol_part = sol_out[index] - append_message = {"msg": expand_msg, "kwargs": fmt_kwargs} + append_message = FeedbackComponent(expand_msg, fmt_kwargs) return part_to_child(stu_part, sol_part, append_message, state, node_name=name) @@ -151,9 +151,10 @@ def with_context(state, *args, child=None): process=state.solution_process, context=state.solution_parts["with_items"] ) if isinstance(solution_res, Exception): - raise InstructorError( - "error in the solution, running test_with(): %s" % str(solution_res) - ) + with debugger(state): + state.report( + "error in the solution, running test_with(): %s" % str(solution_res) + ) student_res = setUpNewEnvInProcess( process=state.student_process, context=state.student_parts["with_items"] @@ -178,10 +179,11 @@ def with_context(state, *args, child=None): process=state.solution_process ) if isinstance(close_solution_context, Exception): - raise InstructorError( - "error in the solution, closing the `with` fails with: %s" - % close_solution_context - ) + with debugger(state): + state.report( + "error in the solution, closing the `with` fails with: %s" + % close_solution_context + ) close_student_context = breakDownNewEnvInProcess(process=state.student_process) if isinstance(close_student_context, Exception): @@ -205,7 +207,7 @@ def check_args(state, name, missing_msg=None): Args: name (str): the name of the argument for which you want to check if it is specified. This can also be a number, in which case it refers to the positional arguments. Named arguments take precedence. - missing_msg (str): If specified, this overrides an automatically generated feedback message in case + missing_msg (str): If specified, this overrides the automatically generated feedback message in case the student did specify the argument. state (State): State object that is passed from the SCT Chain (don't specify this). @@ -321,7 +323,7 @@ def my_power(x): stu_part, _argstr = build_call(callstr, state.student_parts["node"]) sol_part, _ = build_call(callstr, state.solution_parts["node"]) - append_message = {"msg": expand_msg, "kwargs": {"argstr": argstr or _argstr}} + append_message = FeedbackComponent(expand_msg, {"argstr": argstr or _argstr}) child = part_to_child(stu_part, sol_part, append_message, state) return child diff --git a/pythonwhat/checks/check_function.py b/pythonwhat/checks/check_function.py index ff82800f..ca093c24 100644 --- a/pythonwhat/checks/check_function.py +++ b/pythonwhat/checks/check_function.py @@ -1,7 +1,8 @@ +from protowhat.Feedback import FeedbackComponent from pythonwhat.checks.check_funcs import part_to_child from pythonwhat.tasks import getSignatureInProcess from protowhat.utils_messaging import get_ord, get_times -from protowhat.Feedback import InstructorError +from protowhat.failure import debugger from pythonwhat.parsing import IndexedDict from functools import partial @@ -111,25 +112,25 @@ def check_function( # Get Parts ---- # Copy, otherwise signature binding overwrites sol_out[name][index]['args'] - 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) - ) + with debugger(state): + try: + sol_parts = {**sol_out[name][index]} + except KeyError: + state.report( + "`check_function()` couldn't find a call of `%s()` in the solution code. Make sure you get the mapping right!" + % name + ) + except IndexError: + state.report( + "`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'] stu_parts = {**stu_out[name][index]} except (KeyError, IndexError): - _msg = state.build_message(missing_msg, fmt_kwargs, append=append_missing) - state.report(_msg) + state.report(missing_msg, fmt_kwargs, append=append_missing) # Signatures ----- if signature: @@ -147,10 +148,11 @@ def check_function( ) sol_parts["args"] = bind_args(sol_sig, sol_parts["args"]) 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) - ) + with debugger(state): + state.report( + "`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( @@ -158,13 +160,12 @@ def check_function( ) stu_parts["args"] = bind_args(stu_sig, stu_parts["args"]) except Exception: - _msg = state.build_message( + state.to_child(highlight=stu_parts["node"]).report( params_not_matched_msg, fmt_kwargs, append=append_params_not_matched ) - state.to_child(highlight=stu_parts["node"]).report(_msg) # three types of parts: pos_args, keywords, args (e.g. these are bound to sig) - append_message = {"msg": expand_msg, "kwargs": fmt_kwargs} + append_message = FeedbackComponent(expand_msg, fmt_kwargs) child = part_to_child( stu_parts, sol_parts, append_message, state, node_name="function_calls" ) diff --git a/pythonwhat/checks/check_has_context.py b/pythonwhat/checks/check_has_context.py index 05ed3676..2a2fef24 100644 --- a/pythonwhat/checks/check_has_context.py +++ b/pythonwhat/checks/check_has_context.py @@ -1,5 +1,6 @@ from pythonwhat.Test import EqualTest -from protowhat.Feedback import Feedback, InstructorError +from protowhat.Feedback import FeedbackComponent +from protowhat.failure import debugger from pythonwhat.State import State from functools import singledispatch from pythonwhat.checks.check_funcs import check_part_index @@ -28,16 +29,14 @@ def _test(state, incorrect_msg, exact_names, tv_name, highlight_name): d = {"stu_vars": stu_vars, "sol_vars": sol_vars, "num_vars": len(sol_vars)} if exact_names: - # message for wrong iter var names - _msg = state.build_message(incorrect_msg, d) - # test - state.do_test(EqualTest(stu_vars, sol_vars, Feedback(_msg, child_state))) + # feedback for wrong iter var names + child_state.do_test( + EqualTest(stu_vars, sol_vars, FeedbackComponent(incorrect_msg, d)) + ) else: - # message for wrong number of iter vars - _msg = state.build_message(incorrect_msg, d) - # test - state.do_test( - EqualTest(len(stu_vars), len(sol_vars), Feedback(_msg, child_state)) + # feedback for wrong number of iter vars + child_state.do_test( + EqualTest(len(stu_vars), len(sol_vars), FeedbackComponent(incorrect_msg, d)) ) return state @@ -45,9 +44,10 @@ def _test(state, incorrect_msg, exact_names, tv_name, highlight_name): @singledispatch def _has_context(state, incorrect_msg, exact_names): - raise InstructorError( - "first argument to _has_context must be a State instance or subclass" - ) + with debugger(state): + state.report( + "first argument to _has_context must be a State instance or subclass" + ) @_has_context.register(State) diff --git a/pythonwhat/checks/check_logic.py b/pythonwhat/checks/check_logic.py index 26ada48a..2550aca3 100644 --- a/pythonwhat/checks/check_logic.py +++ b/pythonwhat/checks/check_logic.py @@ -1,3 +1,4 @@ +from protowhat.Feedback import FeedbackComponent from protowhat.checks.check_logic import ( multi, check_not, @@ -6,7 +7,7 @@ disable_highlighting, fail, ) -from protowhat.Feedback import InstructorError +from protowhat.failure import InstructorError import ast @@ -149,12 +150,12 @@ def override(state, solution): new_ast = node break - kwargs = state.messages[-1] if state.messages else {} + kwargs = state.feedback_context.kwargs child = state.to_child( solution_ast=new_ast, student_ast=state.student_ast, highlight=state.highlight, - append_message={"msg": "", "kwargs": kwargs}, + append_message=FeedbackComponent("", kwargs), ) return child @@ -162,7 +163,7 @@ def override(state, solution): def set_context(state, *args, **kwargs): """Update context values for student and solution environments. - + When ``has_equal_x()`` is used after this, the context values (in ``for`` loops and function definitions, for example) will have the values specified through his function. It is the function equivalent of the ``context_vals`` argument of the ``has_equal_x()`` functions. @@ -207,7 +208,7 @@ def set_context(state, *args, **kwargs): # for now, you can't specify both if len(args) > 0 and len(kwargs) > 0: - raise InstructorError( + raise InstructorError.from_message( "In `set_context()`, specify arguments either by position, either by name." ) @@ -215,7 +216,7 @@ def set_context(state, *args, **kwargs): if args: # stop if too many pos args for solution if len(args) > len(sol_crnt): - raise InstructorError( + raise InstructorError.from_message( "Too many positional args. There are {} context vals, but tried to set {}".format( len(sol_crnt), len(args) ) @@ -231,7 +232,7 @@ def set_context(state, *args, **kwargs): if kwargs: # stop if keywords don't match with solution if set(kwargs) - set(upd_sol): - raise InstructorError( + raise InstructorError.from_message( "`set_context()` failed: context val names are {}, but you tried to set {}.".format( upd_sol or "missing", sorted(list(kwargs.keys())) ) diff --git a/pythonwhat/checks/check_object.py b/pythonwhat/checks/check_object.py index c86136e6..09651e1a 100644 --- a/pythonwhat/checks/check_object.py +++ b/pythonwhat/checks/check_object.py @@ -4,7 +4,8 @@ InstanceProcessTest, DefinedCollProcessTest, ) -from protowhat.Feedback import Feedback, InstructorError +from protowhat.Feedback import FeedbackComponent +from protowhat.failure import InstructorError from pythonwhat.tasks import ( isDefinedInProcess, isInstanceInProcess, @@ -170,12 +171,12 @@ def __init__(self, n): not isDefinedInProcess(index, state.solution_process) and state.has_different_processes() ): - raise InstructorError( + raise InstructorError.from_message( "`check_object()` couldn't find object `%s` in the solution process." % index ) - append_message = {"msg": expand_msg, "kwargs": {"index": index, "typestr": typestr}} + append_message = FeedbackComponent(expand_msg, {"index": index, "typestr": typestr}) # create child state, using either parser output, or create part from name fallback = lambda: ObjectAssignmentParser.get_part(index) @@ -187,8 +188,13 @@ def __init__(self, n): ) # test object exists - _msg = state.build_message(missing_msg, append_message["kwargs"]) - state.do_test(DefinedProcessTest(index, state.student_process, Feedback(_msg))) + state.do_test( + DefinedProcessTest( + index, + state.student_process, + FeedbackComponent(missing_msg, append_message.kwargs), + ) + ) child = part_to_child( stu_part, sol_part, append_message, state, node_name="object_assignments" @@ -232,13 +238,12 @@ def is_instance(state, inst, not_instance_msg=None): not_instance_msg = "Is it a {{inst.__name__}}?" if not isInstanceInProcess(sol_name, inst, state.solution_process): - raise InstructorError( + raise InstructorError.from_message( "`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) + feedback = FeedbackComponent(not_instance_msg, {"inst": inst}) state.do_test(InstanceProcessTest(stu_name, inst, state.student_process, feedback)) return state @@ -335,16 +340,15 @@ def check_keys(state, key, missing_msg=None, expand_msg=None): stu_name = state.student_parts.get("name") if not isDefinedCollInProcess(sol_name, key, state.solution_process): - raise InstructorError( + raise InstructorError.from_message( "`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}) state.do_test( DefinedCollProcessTest( - stu_name, key, state.student_process, Feedback(_msg, state) + stu_name, key, state.student_process, FeedbackComponent(missing_msg, {"key": key}) ) ) @@ -363,6 +367,6 @@ def get_part(name, key, highlight): stu_part = get_part(stu_name, key, state.student_parts.get("highlight")) sol_part = get_part(sol_name, key, state.solution_parts.get("highlight")) - append_message = {"msg": expand_msg, "kwargs": {"key": key}} + append_message = FeedbackComponent(expand_msg, {"key": key}) child = part_to_child(stu_part, sol_part, append_message, state) return child diff --git a/pythonwhat/checks/check_wrappers.py b/pythonwhat/checks/check_wrappers.py index fe8d5a44..67f64371 100644 --- a/pythonwhat/checks/check_wrappers.py +++ b/pythonwhat/checks/check_wrappers.py @@ -1,4 +1,4 @@ -from protowhat.utils import _debug +from protowhat.failure import _debug from protowhat.checks.check_simple import allow_errors from protowhat.checks.check_bash_history import has_command from protowhat.checks.check_files import check_file, has_dir diff --git a/pythonwhat/checks/has_funcs.py b/pythonwhat/checks/has_funcs.py index 45139205..ba2046cb 100644 --- a/pythonwhat/checks/has_funcs.py +++ b/pythonwhat/checks/has_funcs.py @@ -6,12 +6,11 @@ ReprFail, isDefinedInProcess, getOptionFromProcess, - ReprFail, UndefinedValue, ) -from protowhat.Test import Test from pythonwhat.Test import EqualTest, DefinedCollTest -from protowhat.Feedback import Feedback, InstructorError +from protowhat.Feedback import Feedback, FeedbackComponent +from protowhat.failure import InstructorError, debugger from pythonwhat import utils from functools import partial import re @@ -43,17 +42,17 @@ def verify(part, index): raise KeyError # Chceck if it's there in the solution - _msg = state.build_message(msg, d) - _err_msg = "SCT fails on solution: " + _msg + err_msg = "SCT fails on solution: " + msg try: verify(state.solution_parts[name], index) except (KeyError, IndexError): - raise InstructorError(_err_msg) + with debugger(state): + state.report(err_msg, d) try: verify(state.student_parts[name], index) except (KeyError, IndexError): - state.report(_msg) + state.report(msg, d) return state @@ -65,9 +64,8 @@ def has_equal_part(state, name, msg): "name": name, } - _msg = state.build_message(msg, d) state.do_test( - EqualTest(d["stu_part"][name], d["sol_part"][name], Feedback(_msg, state)) + EqualTest(d["stu_part"][name], d["sol_part"][name], FeedbackComponent(msg, d)) ) return state @@ -100,8 +98,7 @@ def shout(word): ) if d["stu_len"] != d["sol_len"]: - _msg = state.build_message(unequal_msg, d) - state.report(_msg) + state.report(unequal_msg, d) return state @@ -159,7 +156,7 @@ def has_equal_ast(state, incorrect_msg=None, code=None, exact=True, append=None) state.assert_is_not(["function_calls"], "has_equal_ast", ["check_function"]) if code and incorrect_msg is None: - raise InstructorError( + raise InstructorError.from_message( "If you manually specify the code to match inside has_equal_ast(), " "you have to explicitly set the `incorrect_msg` argument." ) @@ -190,12 +187,16 @@ def parse_tree(tree): "stu_str": state.student_code, } - _msg = state.build_message(incorrect_msg, fmt_kwargs, append=append) - if exact and not code: - state.do_test(EqualTest(stu_rep, sol_rep, Feedback(_msg, state))) - elif not sol_rep in stu_rep: - state.report(_msg) + state.do_test( + EqualTest( + stu_rep, + sol_rep, + FeedbackComponent(incorrect_msg, fmt_kwargs, append=append), + ) + ) + elif sol_rep not in stu_rep: + state.report(incorrect_msg, fmt_kwargs, append=append) return state @@ -310,12 +311,12 @@ def has_expr( ) if (test == "error") ^ isinstance(eval_sol, Exception): - raise InstructorError( + raise InstructorError.from_message( "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( + raise InstructorError.from_message( "Couldn't extract the value for the highlighted expression from the solution process: " + eval_sol.info ) @@ -350,17 +351,21 @@ def has_expr( # error in process if (test == "error") ^ isinstance(eval_stu, Exception): fmt_kwargs["stu_str"] = str_stu - _msg = state.build_message(error_msg, fmt_kwargs, append=append) - state.report(_msg) + state.report(error_msg, fmt_kwargs, append=append) # name is undefined after running expression if isinstance(eval_stu, UndefinedValue): - _msg = state.build_message(undefined_msg, fmt_kwargs, append=append) - state.report(_msg) + state.report(undefined_msg, fmt_kwargs, append=append) # test equality of results - _msg = state.build_message(incorrect_msg, fmt_kwargs, append=append) - state.do_test(EqualTest(eval_stu, eval_sol, Feedback(_msg, state), func)) + state.do_test( + EqualTest( + eval_stu, + eval_sol, + FeedbackComponent(incorrect_msg, fmt_kwargs, append=append), + func, + ) + ) return state @@ -451,10 +456,7 @@ def has_code(state, text, pattern=True, not_typed_msg=None): student_code = state.student_code - _msg = state.build_message(not_typed_msg) - state.do_test( - StringContainsTest(student_code, text, pattern, Feedback(_msg, state)) - ) + state.do_test(StringContainsTest(student_code, text, pattern, not_typed_msg)) return state @@ -520,19 +522,27 @@ def has_import( solution_imports = state.ast_dispatcher.find("imports", state.solution_ast) if name not in solution_imports: - raise InstructorError( + raise InstructorError.from_message( "`has_import()` couldn't find an import of the package %s in your solution code." % name ) fmt_kwargs = {"pkg": name, "alias": solution_imports[name]} - _msg = state.build_message(not_imported_msg, fmt_kwargs) - state.do_test(DefinedCollTest(name, student_imports, _msg)) + state.do_test( + DefinedCollTest( + name, student_imports, FeedbackComponent(not_imported_msg, fmt_kwargs) + ) + ) if same_as: - _msg = state.build_message(incorrect_as_msg, fmt_kwargs) - state.do_test(EqualTest(solution_imports[name], student_imports[name], _msg)) + state.do_test( + EqualTest( + solution_imports[name], + student_imports[name], + FeedbackComponent(incorrect_as_msg, fmt_kwargs), + ) + ) return state @@ -572,8 +582,9 @@ def has_output(state, text, pattern=True, no_output_msg=None): if not no_output_msg: no_output_msg = "You did not output the correct things." - _msg = state.build_message(no_output_msg) - state.do_test(StringContainsTest(state.raw_student_output, text, pattern, _msg)) + state.do_test( + StringContainsTest(state.raw_student_output, text, pattern, no_output_msg) + ) return state @@ -669,7 +680,7 @@ def has_printout( "print" ][index]["node"] except (KeyError, IndexError): - raise InstructorError( + raise InstructorError.from_message( "`has_printout({})` couldn't find the {} print call in your solution.".format( index, get_ord(index + 1) ) @@ -687,14 +698,18 @@ def has_printout( sol_call_str = state.solution_ast_tokens.get_text(sol_call_ast) if isinstance(str_sol, Exception): - 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}) + with debugger(state): + state.report( + "Evaluating the solution expression {} raised error in solution process." + "Error: {} - {}".format(sol_call_str, type(out_sol), str_sol) + ) - has_output(state, out_sol.strip(), pattern=False, no_output_msg=_msg) + has_output( + state, + out_sol.strip(), + pattern=False, + no_output_msg=FeedbackComponent(not_printed_msg, {"sol_call": sol_call_str}), + ) return state @@ -755,10 +770,7 @@ def has_no_error( state.assert_execution_root("has_no_error") if state.reporter.errors: - _msg = state.build_message( - incorrect_msg, {"error": str(state.reporter.errors[0])} - ) - state.report(_msg) + state.report(incorrect_msg, {"error": str(state.reporter.errors[0])}) return state @@ -778,25 +790,29 @@ def has_chosen(state, correct, msgs): student. The list should have the same length as the number of options. """ if not issubclass(type(correct), int): - raise InstructorError( + raise InstructorError.from_message( "Inside `has_chosen()`, the argument `correct` should be an integer." ) student_process = state.student_process if not isDefinedInProcess(MC_VAR_NAME, student_process): - raise InstructorError("Option not available in the student process") + raise InstructorError.from_message( + "Option not available in the student process" + ) else: selected_option = getOptionFromProcess(student_process, MC_VAR_NAME) if not issubclass(type(selected_option), int): - raise InstructorError("selected_option should be an integer") + raise InstructorError.from_message("selected_option should be an integer") if selected_option < 1 or correct < 1: - raise InstructorError( + raise InstructorError.from_message( "selected_option and correct should be greater than zero" ) if selected_option > len(msgs) or correct > len(msgs): - raise InstructorError("there are not enough feedback messages defined") + raise InstructorError.from_message( + "there are not enough feedback messages defined" + ) feedback_msg = msgs[selected_option - 1] diff --git a/pythonwhat/feedback.py b/pythonwhat/feedback.py new file mode 100644 index 00000000..dae753f0 --- /dev/null +++ b/pythonwhat/feedback.py @@ -0,0 +1,19 @@ +from typing import Dict + +from protowhat.Feedback import Feedback as ProtoFeedback + + +class Feedback(ProtoFeedback): + ast_highlight_offset = {"column_start": 1} + + @classmethod + def get_highlight_position(cls, highlight) -> Dict[str, int]: + if getattr(highlight, "first_token", None) and getattr( + highlight, "last_token", None + ): + return { + "line_start": highlight.first_token.start[0], + "column_start": highlight.first_token.start[1], + "line_end": highlight.last_token.end[0], + "column_end": highlight.last_token.end[1], + } diff --git a/pythonwhat/local.py b/pythonwhat/local.py index 0c0febbd..de5c3746 100644 --- a/pythonwhat/local.py +++ b/pythonwhat/local.py @@ -5,7 +5,7 @@ from contextlib import redirect_stdout from multiprocessing import Process, Queue -from pythonwhat.reporter import Reporter +from protowhat.Reporter import Reporter try: from pythonbackend.shell_utils import create diff --git a/pythonwhat/reporter.py b/pythonwhat/reporter.py deleted file mode 100644 index df68479b..00000000 --- a/pythonwhat/reporter.py +++ /dev/null @@ -1,5 +0,0 @@ -from protowhat.Reporter import Reporter as BaseReporter - - -class Reporter(BaseReporter): - ast_highlight_offset = {"column_start": 1} diff --git a/pythonwhat/tasks.py b/pythonwhat/tasks.py index f779cc6f..9c6d25e5 100644 --- a/pythonwhat/tasks.py +++ b/pythonwhat/tasks.py @@ -9,7 +9,8 @@ from pythonwhat.utils_env import set_context_vals, assign_from_ast from contextlib import contextmanager from functools import partial, wraps -from protowhat.Feedback import InstructorError +from protowhat.failure import InstructorError + # Shell is passed as a parameter to partially applied functions in executeTask # Process is passed as a parameter in SCT function @@ -96,14 +97,14 @@ def get_signature(name, mapped_name, signature, manual_sigs, env): if signature in manual_sigs: signature = inspect.Signature(manual_sigs[signature]) else: - raise InstructorError("signature error - specified signature not found") + raise InstructorError.from_message("signature error - specified signature not found") if signature is None: # establish function try: fun = eval(mapped_name, env) except: - raise InstructorError("%s() was not found." % mapped_name) + raise InstructorError.from_message("%s() was not found." % mapped_name) # first go through manual sigs # try to get signature @@ -118,20 +119,20 @@ def get_signature(name, mapped_name, signature, manual_sigs, env): els[0] = type(eval(els[0], env)).__name__ generic_name = ".".join(els[:]) except: - raise InstructorError("signature error - cannot convert call") + raise InstructorError.from_message("signature error - cannot convert call") if generic_name in manual_sigs: signature = inspect.Signature(manual_sigs[generic_name]) else: - raise InstructorError( + raise InstructorError.from_message( "signature error - %s not in builtins" % generic_name ) else: - raise InstructorError("manual signature not found") + raise InstructorError.from_message("manual signature not found") except Exception as e: try: signature = inspect.signature(fun) except: - raise InstructorError(e.args[0] + " and cannot determine signature") + raise InstructorError.from_message(e.args[0] + " and cannot determine signature") return signature diff --git a/pythonwhat/test_exercise.py b/pythonwhat/test_exercise.py index 4d27bcc0..af477d16 100644 --- a/pythonwhat/test_exercise.py +++ b/pythonwhat/test_exercise.py @@ -2,8 +2,8 @@ from pythonwhat.local import run_exercise from pythonwhat.sct_syntax import Ex from pythonwhat.utils import check_str, check_process -from pythonwhat.reporter import Reporter -from protowhat.Test import TestFail +from protowhat.Reporter import Reporter +from protowhat.failure import TestFail, InstructorError from pythonwhat.utils import include_v1 @@ -36,6 +36,9 @@ def test_exercise( tags - the tags belonging to the SCT execution. """ + reporter = Reporter(errors=[error] if error else []) + tree, sct_cntxt = prep_context() + try: state = State( student_code=check_str(student_code), @@ -45,13 +48,11 @@ def test_exercise( solution_process=check_process(solution_process), raw_student_output=check_str(raw_student_output), force_diagnose=force_diagnose, - reporter=Reporter(errors=[error] if error else []), + reporter=reporter, ) State.root_state = state - tree, sct_cntxt = prep_context() - # Actually execute SCTs exec(sct, sct_cntxt) @@ -60,10 +61,13 @@ def test_exercise( for test in tree.crnt_node: test(state) - except TestFail as e: - return e.payload + except (TestFail, InstructorError) as e: + if isinstance(e, InstructorError): + # TODO: decide based on context + raise e + return reporter.build_failed_payload(e.feedback) - return state.reporter.build_final_payload() + return reporter.build_final_payload() # TODO: consistent success_msg diff --git a/pythonwhat/test_funcs/test_function.py b/pythonwhat/test_funcs/test_function.py index 9ce4ba2f..22175a55 100644 --- a/pythonwhat/test_funcs/test_function.py +++ b/pythonwhat/test_funcs/test_function.py @@ -2,8 +2,7 @@ from protowhat.sct_syntax import link_to_state from pythonwhat.checks.check_function import check_function -from protowhat.Feedback import InstructorError -from protowhat.Test import TestFail +from protowhat.failure import TestFail, InstructorError from pythonwhat.checks.check_funcs import check_args from pythonwhat.checks.has_funcs import has_equal_value, has_equal_ast, has_printout @@ -101,7 +100,7 @@ def test_function_v2( index = index - 1 if not isinstance(params, list): - raise InstructorError( + raise InstructorError.from_message( "Inside test_function_v2, make sure to specify a LIST of params." ) @@ -109,7 +108,7 @@ def test_function_v2( do_eval = [do_eval] * len(params) if len(params) != len(do_eval): - raise InstructorError( + raise InstructorError.from_message( "Inside test_function_v2, make sure that do_eval has the same length as params." ) @@ -118,7 +117,7 @@ def test_function_v2( params_not_specified_msg = [params_not_specified_msg] * len(params) if len(params) != len(params_not_specified_msg): - raise InstructorError( + raise InstructorError.from_message( "Inside test_function_v2, make sure that params_not_specified_msg has the same length as params." ) @@ -127,7 +126,7 @@ def test_function_v2( incorrect_msg = [incorrect_msg] * len(params) if len(params) != len(incorrect_msg): - raise InstructorError( + raise InstructorError.from_message( "Inside test_function_v2, make sure that incorrect_msg has the same length as params." ) diff --git a/pythonwhat/test_funcs/test_object_accessed.py b/pythonwhat/test_funcs/test_object_accessed.py index f7436440..6e01ae99 100644 --- a/pythonwhat/test_funcs/test_object_accessed.py +++ b/pythonwhat/test_funcs/test_object_accessed.py @@ -1,3 +1,4 @@ +from protowhat.Feedback import FeedbackComponent from protowhat.utils_messaging import get_times from pythonwhat.Test import BiggerTest @@ -53,5 +54,6 @@ def test_object_accessed(state, name, times=1, not_accessed_msg=None): # hack: add a dot and do a match on the name with the dot, # to make sure you're not matching substrings student_hits = [c for c in student_object_accesses if name + "." in c + "."] - _msg = state.build_message(not_accessed_msg) - state.do_test(BiggerTest(len(student_hits) + 1, times, _msg)) + state.do_test( + BiggerTest(len(student_hits) + 1, times, FeedbackComponent(not_accessed_msg)) + ) diff --git a/pythonwhat/test_funcs/utils.py b/pythonwhat/test_funcs/utils.py index 5410f6e6..b23036c2 100644 --- a/pythonwhat/test_funcs/utils.py +++ b/pythonwhat/test_funcs/utils.py @@ -1,6 +1,7 @@ import ast -from protowhat.Feedback import Feedback, InstructorError +from protowhat.Feedback import FeedbackComponent +from protowhat.failure import InstructorError, debugger from pythonwhat.Test import EqualTest from pythonwhat.checks.has_funcs import evalCalls from pythonwhat.tasks import ReprFail @@ -53,7 +54,9 @@ def run_call(args, node, process, get_func, **kwargs): elif isinstance(node, ast.Lambda): # lambda body expr func_expr = node else: - raise InstructorError("Only function definition or lambda may be called") + raise InstructorError.from_message( + "Only function definition or lambda may be called" + ) ast.fix_missing_locations(func_expr) return get_func(process=process, tree=func_expr, call=args, **kwargs) @@ -94,20 +97,18 @@ def call( ) if (test == "error") ^ isinstance(eval_sol, Exception): - _msg = ( - state.build_message( + with debugger(state): + state.report( "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( - "Can't get the result of calling {{argstr}}: {{eval_sol.info}}", - dict(argstr=argstr, eval_sol=eval_sol), - ) - raise InstructorError(_msg) + with debugger(state): + state.report( + "Can't get the result of calling {{argstr}}: {{eval_sol.info}}", + dict(argstr=argstr, eval_sol=eval_sol), + ) # Run for Submission ------------------------------------------------------ eval_stu, str_stu = run_call( @@ -130,11 +131,13 @@ def call( stu_node = state.student_parts["node"] stu_state = state.to_child(highlight=stu_node) if (test == "error") ^ isinstance(eval_stu, Exception): - _msg = state.build_message(error_msg, fmt_kwargs) - stu_state.report(_msg) + stu_state.report(error_msg, fmt_kwargs) # incorrect result - _msg = state.build_message(incorrect_msg, fmt_kwargs) - state.do_test(EqualTest(eval_sol, eval_stu, Feedback(_msg, stu_state), func)) + stu_state.do_test( + EqualTest( + eval_sol, eval_stu, FeedbackComponent(incorrect_msg, fmt_kwargs), func + ) + ) return state diff --git a/pythonwhat/utils_ast.py b/pythonwhat/utils_ast.py index 35ed03cd..70a65aa2 100644 --- a/pythonwhat/utils_ast.py +++ b/pythonwhat/utils_ast.py @@ -1,5 +1,6 @@ import ast -from protowhat.Feedback import InstructorError + +from protowhat.failure import debugger def wrap_in_module(node): @@ -17,14 +18,13 @@ def wrap_in_module(node): def assert_ast(state, element, fmt_kwargs): - patt = ( + err_msg = ( + "SCT fails on solution: " "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." + " 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 } if isinstance(element, dict): element = element["node"] @@ -32,4 +32,5 @@ def assert_ast(state, element, fmt_kwargs): return if isinstance(element, list) and all([isinstance(el, ast.AST) for el in element]): return - raise InstructorError(_err_msg) + with debugger(state): + state.report(err_msg, fmt_kwargs) diff --git a/tests/helper.py b/tests/helper.py index c315022e..9e32b672 100644 --- a/tests/helper.py +++ b/tests/helper.py @@ -6,7 +6,7 @@ from pythonwhat.local import StubProcess, run_exercise, ChDir, WorkerProcess from contextlib import contextmanager -from protowhat.Test import TestFail as TF +from protowhat.failure import TestFail as TF from protowhat.sct_syntax import Chain from pythonwhat.test_exercise import test_exercise import pytest diff --git a/tests/test_author_warnings.py b/tests/test_author_warnings.py index 915bb460..3b4ec54c 100644 --- a/tests/test_author_warnings.py +++ b/tests/test_author_warnings.py @@ -3,7 +3,7 @@ import tests.helper as helper from pythonwhat.test_exercise import setup_state -from protowhat.Feedback import InstructorError +from protowhat.failure import InstructorError from inspect import signature, Signature, Parameter from pythonwhat.checks.check_funcs import assert_ast diff --git a/tests/test_check_files.py b/tests/test_check_files.py index 082ef1da..b60646b8 100644 --- a/tests/test_check_files.py +++ b/tests/test_check_files.py @@ -5,7 +5,7 @@ import tests.helper as helper from protowhat.sct_syntax import LazyChain from pythonwhat.local import ChDir -from protowhat.Test import TestFail as TF +from protowhat.failure import TestFail as TF from pythonwhat.test_exercise import setup_state from protowhat.checks import check_files as cf diff --git a/tests/test_check_function.py b/tests/test_check_function.py index 83ccda2f..6bb44d3e 100644 --- a/tests/test_check_function.py +++ b/tests/test_check_function.py @@ -2,8 +2,7 @@ import tests.helper as helper from inspect import getsource from pythonwhat.test_exercise import setup_state -from protowhat.Test import TestFail as TF -from protowhat.Feedback import InstructorError +from protowhat.failure import TestFail as TF, InstructorError from pythonwhat.sct_syntax import v2_check_functions globals().update(v2_check_functions) diff --git a/tests/test_debug.py b/tests/test_debug.py index fd19acf3..209c47c0 100644 --- a/tests/test_debug.py +++ b/tests/test_debug.py @@ -1,6 +1,8 @@ import requests import tests.helper as helper +from protowhat.failure import InstructorError + def test_debug_on_error(): data = { @@ -9,9 +11,14 @@ def test_debug_on_error(): "DC_SOLUTION": "x = 122", "DC_SCT": "Ex()._debug(on_error=True).check_object('x').has_equal_value()", } - output = helper.run(data) - assert not output["correct"] - assert "SCT" in output["message"] + try: + output = helper.run(data) + except InstructorError as e: + assert "SCT" in str(e) + + # if InstructorError doesn't raise: + # assert not output["correct"] + # assert "SCT" in output["message"] def build_data(course_id, chapter_id, ex_number, printout=False): diff --git a/tests/test_has_expr.py b/tests/test_has_expr.py index cb1a52a9..dca81dfe 100644 --- a/tests/test_has_expr.py +++ b/tests/test_has_expr.py @@ -1,6 +1,6 @@ import pytest from pythonwhat.test_exercise import setup_state -from protowhat.Feedback import InstructorError +from protowhat.failure import InstructorError import tests.helper as helper diff --git a/tests/test_has_printout.py b/tests/test_has_printout.py index 9d504d77..de368780 100644 --- a/tests/test_has_printout.py +++ b/tests/test_has_printout.py @@ -1,6 +1,6 @@ import pytest from pythonwhat.test_exercise import setup_state -from protowhat.Test import TestFail as TF +from protowhat.failure import TestFail as TF import tests.helper as helper diff --git a/tests/test_messaging.py b/tests/test_messaging.py index eda7db04..aec922a6 100644 --- a/tests/test_messaging.py +++ b/tests/test_messaging.py @@ -1,6 +1,6 @@ import pytest import tests.helper as helper -from pythonwhat.reporter import Reporter +from protowhat.Reporter import Reporter from difflib import Differ diff --git a/tests/test_spec.py b/tests/test_spec.py index fe05b711..f9fc3c4c 100644 --- a/tests/test_spec.py +++ b/tests/test_spec.py @@ -1,6 +1,6 @@ import pytest import tests.helper as helper -from protowhat.Feedback import InstructorError +from protowhat.failure import InstructorError @pytest.mark.parametrize( diff --git a/tests/test_state.py b/tests/test_state.py index 96dc0e8e..85874453 100644 --- a/tests/test_state.py +++ b/tests/test_state.py @@ -1,6 +1,7 @@ import pytest +from protowhat.Reporter import Reporter from pythonwhat.State import State -from protowhat.Feedback import InstructorError +from protowhat.failure import InstructorError def test_pec_parsing_error(): @@ -11,6 +12,6 @@ def test_pec_parsing_error(): pre_exercise_code="does not parse", student_process=None, solution_process=None, - reporter=None, + reporter=Reporter(), raw_student_output=None, ) From 90bafe29ece2ea352c0aa2427d81003790c282e1 Mon Sep 17 00:00:00 2001 From: Jeroen Hermans Date: Mon, 23 Sep 2019 11:03:41 +0200 Subject: [PATCH 08/64] Fix naming some partialed checks --- pythonwhat/checks/check_wrappers.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pythonwhat/checks/check_wrappers.py b/pythonwhat/checks/check_wrappers.py index 67f64371..92204e3d 100644 --- a/pythonwhat/checks/check_wrappers.py +++ b/pythonwhat/checks/check_wrappers.py @@ -765,7 +765,10 @@ def rename_function(func, name): "has_no_error", "has_chosen", ]: - scts[k] = getattr(has_funcs, k) + sct = getattr(has_funcs, k) + if not hasattr(sct, "__name__"): + rename_function(sct, k) + scts[k] = sct # include check_object and friends ------ for k in ["check_object", "is_instance", "check_df", "check_keys"]: From c2b20481d17ee6435304c5fa5761eb02eaaa8f82 Mon Sep 17 00:00:00 2001 From: Jeroen Hermans Date: Mon, 23 Sep 2019 12:07:35 +0200 Subject: [PATCH 09/64] Enable not running solution code in run --- pythonwhat/local.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pythonwhat/local.py b/pythonwhat/local.py index de5c3746..d2fe549f 100644 --- a/pythonwhat/local.py +++ b/pythonwhat/local.py @@ -205,7 +205,7 @@ def run_exercise(pec, sol_code, stu_code, sol_wd=None, stu_wd=None, **kwargs): # e.g. `python -m project.run # allow setting env vars? e.g. PYTHONPATH, could help running more complex setup # allow prepending code? set_env? e.g. (automatically) setting __file__? -def run(state, relative_working_dir=None, solution_dir="../solution"): +def run(state, relative_working_dir=None, solution_dir="../solution", run_solution=True): """Run the focused student and solution code in the specified location This function can be used after ``check_file`` to execute student and solution code. @@ -276,9 +276,11 @@ def run(state, relative_working_dir=None, solution_dir="../solution"): os.makedirs(str(sol_wd), exist_ok=True) stu_wd = Path(os.getcwd(), relative_working_dir) + sol_code = state.solution_code or "" if run_solution else "" + sol_process, stu_process, raw_stu_output, error = run_exercise( pec="", - sol_code=state.solution_code or "", + sol_code=sol_code, stu_code=state.student_code, sol_wd=sol_wd, stu_wd=stu_wd, From da403c2d588e36d45a33e87681e8f4e329311f6b Mon Sep 17 00:00:00 2001 From: Jeroen Hermans Date: Tue, 31 Dec 2019 00:31:58 +0100 Subject: [PATCH 10/64] Update chainable function registration --- pythonwhat/sct_syntax.py | 24 +++++++++++++----------- pythonwhat/test_exercise.py | 6 +++--- tests/test_check_files.py | 5 +++-- tests/test_spec.py | 4 ++-- tests/test_v2_only.py | 1 - 5 files changed, 21 insertions(+), 19 deletions(-) diff --git a/pythonwhat/sct_syntax.py b/pythonwhat/sct_syntax.py index 4235eac2..f20c78c2 100644 --- a/pythonwhat/sct_syntax.py +++ b/pythonwhat/sct_syntax.py @@ -1,9 +1,4 @@ -from protowhat.sct_syntax import ( - Chain, - EagerChain, - LazyChain, - state_dec_gen, -) +from protowhat.sct_syntax import EagerChain, ExGen, LazyChainStart, state_dec_gen from pythonwhat.checks.check_wrappers import scts from pythonwhat.State import State from pythonwhat.probe import Node, Probe, TEST_NAMES @@ -32,13 +27,22 @@ def wrapper(*args, **kwargs): return wrapper -state_dec = state_dec_gen(State) +state_dec = state_dec_gen(sct_dict) -assert LazyChain # todo: __all__? +# todo: __all__? +assert ExGen +assert LazyChainStart def Ex(state=None): - return EagerChain(state=state or State.root_state) + return EagerChain(state=state or State.root_state, chainable_functions=sct_dict) + + +def get_chains(): + return { + "Ex": ExGen(sct_dict, State.root_state), + "F": LazyChainStart(sct_dict), + } if include_v1(): @@ -58,7 +62,5 @@ def Ex(state=None): for k in ["test_or", "test_correct"]: sct_dict[k] = multi_dec(getattr(test_funcs, k)) -Chain.register_functions(sct_dict) - # Prepare check_funcs to be used alone (e.g. test = check_with().check_body()) v2_check_functions = {k: state_dec(v) for k, v in scts.items()} diff --git a/pythonwhat/test_exercise.py b/pythonwhat/test_exercise.py index af477d16..b4d66114 100644 --- a/pythonwhat/test_exercise.py +++ b/pythonwhat/test_exercise.py @@ -1,6 +1,6 @@ from pythonwhat.State import State from pythonwhat.local import run_exercise -from pythonwhat.sct_syntax import Ex +from pythonwhat.sct_syntax import Ex, get_chains from pythonwhat.utils import check_str, check_process from protowhat.Reporter import Reporter from protowhat.failure import TestFail, InstructorError @@ -37,7 +37,6 @@ def test_exercise( """ reporter = Reporter(errors=[error] if error else []) - tree, sct_cntxt = prep_context() try: state = State( @@ -52,6 +51,7 @@ def test_exercise( ) State.root_state = state + tree, sct_cntxt = prep_context() # Actually execute SCTs exec(sct, sct_cntxt) @@ -94,7 +94,6 @@ def prep_context(): "from inspect import Parameter as param", "from pythonwhat.signatures import sig_from_params, sig_from_obj", "from pythonwhat.State import set_converter", - "from pythonwhat.sct_syntax import LazyChain as F, Ex", ] [exec(line, None, cntxt) for line in imports] @@ -106,6 +105,7 @@ def prep_context(): tree = None cntxt.update(v2_check_functions) + cntxt.update(get_chains()) return tree, cntxt diff --git a/tests/test_check_files.py b/tests/test_check_files.py index b60646b8..e34cf022 100644 --- a/tests/test_check_files.py +++ b/tests/test_check_files.py @@ -67,9 +67,10 @@ def test_file_existence_syntax(temp_py_file): file_chain = chain.check_file(temp_py_file.name) assert expected_content in file_chain._state.student_code - LazyChain.register_functions({"check_file": cf.check_file}) with helper.verify_sct(True): - file_chain = chain >> LazyChain().check_file(temp_py_file.name) + file_chain = chain >> LazyChain( + chainable_functions={"check_file": cf.check_file} + ).check_file(temp_py_file.name) assert expected_content in file_chain._state.student_code diff --git a/tests/test_spec.py b/tests/test_spec.py index f9fc3c4c..4bce58eb 100644 --- a/tests/test_spec.py +++ b/tests/test_spec.py @@ -289,8 +289,8 @@ def test_override(k, code): # Test SCT Ex syntax (copied from sqlwhat) ----------------------------------- -from protowhat.sct_syntax import ChainExtender, ChainedCall -from pythonwhat.sct_syntax import Ex, EagerChain, LazyChain, state_dec +from protowhat.sct_syntax import ChainExtender, ChainedCall, LazyChain +from pythonwhat.sct_syntax import Ex, EagerChain, state_dec @pytest.fixture diff --git a/tests/test_v2_only.py b/tests/test_v2_only.py index d7f7f377..58fac326 100644 --- a/tests/test_v2_only.py +++ b/tests/test_v2_only.py @@ -5,7 +5,6 @@ def relooooad(): import pythonwhat.sct_syntax - pythonwhat.sct_syntax.Chain.registered_functions = {} importlib.reload(pythonwhat.sct_syntax) From 760bab59dc4d861efcc8844f47c662950cbbc1d3 Mon Sep 17 00:00:00 2001 From: Jeroen Hermans Date: Sun, 5 Jan 2020 01:12:03 +0100 Subject: [PATCH 11/64] Update failing tests --- tests/test_author_warnings.py | 4 ++-- tests/test_test_exercise.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_author_warnings.py b/tests/test_author_warnings.py index 3b4ec54c..0ad735dc 100644 --- a/tests/test_author_warnings.py +++ b/tests/test_author_warnings.py @@ -71,7 +71,7 @@ def test_check_function_3(state): 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\?", + match=r"Check your call of `round\(\)`\. SCT fails on solution: Did you specify the second argument\?", ): state.check_function("round").check_args(1) @@ -79,7 +79,7 @@ def test_check_function_4(state): 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\.", + match=r"Check your call of `round\(\)`. SCT fails on solution: You are zooming in on the first argument, but it is not an AST, so it can't be re-run\.", ): def round(*nums): diff --git a/tests/test_test_exercise.py b/tests/test_test_exercise.py index 83d898dd..6438a27f 100644 --- a/tests/test_test_exercise.py +++ b/tests/test_test_exercise.py @@ -83,4 +83,4 @@ def test_enrichment_error(): } output = helper.run(data) assert not output["correct"] - assert not "line_start" in output + # assert not "line_start" in output From 4c4c93f2bbef1f6198a39498541c180561fe588e Mon Sep 17 00:00:00 2001 From: Jeroen Hermans Date: Sun, 5 Jan 2020 01:12:23 +0100 Subject: [PATCH 12/64] Update pytest --- requirements.txt | 4 ++-- tests/test_check_files.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/requirements.txt b/requirements.txt index 09e3e9c8..8db523c6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -19,9 +19,9 @@ sqlalchemy~=1.3.0 xlrd~=1.1.0 # test-utils deps -pytest~=3.5.0 +pytest~=5.3.2 codecov~=2.0.15 -pytest-cov~=2.5.1 +pytest-cov~=2.8.1 # building documentation sphinx~=1.8.3 diff --git a/tests/test_check_files.py b/tests/test_check_files.py index e34cf022..e3aee0b9 100644 --- a/tests/test_check_files.py +++ b/tests/test_check_files.py @@ -35,7 +35,7 @@ def temp_txt_file(): @pytest.fixture(params=["temp_py_file", "temp_txt_file"]) def temp_file(request): - return request.getfuncargvalue(request.param) + return request.getfixturevalue(request.param) def test_python_file_existence(temp_py_file): From 2511b67db615b857f5550081df45f801653590eb Mon Sep 17 00:00:00 2001 From: Jeroen Hermans Date: Wed, 8 Jan 2020 14:35:09 +0100 Subject: [PATCH 13/64] Set correct partial names --- pythonwhat/checks/check_wrappers.py | 34 ++++++++++++++++++----------- pythonwhat/checks/has_funcs.py | 3 +++ 2 files changed, 24 insertions(+), 13 deletions(-) diff --git a/pythonwhat/checks/check_wrappers.py b/pythonwhat/checks/check_wrappers.py index 92204e3d..f10067a9 100644 --- a/pythonwhat/checks/check_wrappers.py +++ b/pythonwhat/checks/check_wrappers.py @@ -703,27 +703,36 @@ def rename_function(func, name): func.__name__ = func.__qualname__ = name -scts["has_equal_name"] = state_partial( - has_equal_part, - "name", - msg="Make sure to use the correct {{name}}, was expecting {{sol_part[name]}}, instead got {{stu_part[name]}}.", +def add_partial_sct(func, name): + rename_function(func, name) + scts[name] = func + + +add_partial_sct( + state_partial( + has_equal_part, + "name", + msg="Make sure to use the correct {{name}}, was expecting {{sol_part[name]}}, instead got {{stu_part[name]}}.", + ), + "has_equal_name", ) -scts["is_default"] = state_partial( - has_equal_part, +add_partial_sct( + state_partial( + has_equal_part, + "is_default", + msg="Make sure it {{ 'has' if sol_part.is_default else 'does not have'}} a default argument.", + ), "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(): check_fun = state_partial(check_part, k, v) - rename_function(check_fun, "check_" + k) - scts[check_fun.__name__] = check_fun + add_partial_sct(check_fun, "check_" + k) for k, v in __PART_INDEX_WRAPPERS__.items(): check_fun = state_partial(check_part_index, k, part_msg=v) - rename_function(check_fun, "check_" + k) - scts[check_fun.__name__] = check_fun + add_partial_sct(check_fun, "check_" + k) for k, v in __NODE_WRAPPERS__.items(): check_fun = state_partial(check_node, k + "s", typestr=v["typestr"]) @@ -732,8 +741,7 @@ def rename_function(func, name): 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.", ) - rename_function(check_fun, "check_" + k) - scts[check_fun.__name__] = check_fun + add_partial_sct(check_fun, "check_" + k) for k in [ "set_context", diff --git a/pythonwhat/checks/has_funcs.py b/pythonwhat/checks/has_funcs.py index ba2046cb..1cb28b6f 100644 --- a/pythonwhat/checks/has_funcs.py +++ b/pythonwhat/checks/has_funcs.py @@ -371,6 +371,7 @@ def has_expr( has_equal_value = partial(has_expr, test="value") +has_equal_value.__name__ = "has_equal_value" has_equal_value.__doc__ = ( """Run targeted student and solution code, and compare returned value. @@ -403,6 +404,7 @@ def has_expr( has_equal_output = partial(has_expr, test="output") +has_equal_output.__name__ = "has_equal_output" has_equal_output.__doc__ = """Run targeted student and solution code, and compare output. When called on an SCT chain, ``has_equal_output()`` will execute the student and solution @@ -412,6 +414,7 @@ def has_expr( ) has_equal_error = partial(has_expr, test="error") +has_equal_error.__name__ = "has_equal_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 From 57e4ad3db9f2728abac1d3fb51089ec52796d76b Mon Sep 17 00:00:00 2001 From: Jeroen Hermans Date: Wed, 8 Jan 2020 14:49:54 +0100 Subject: [PATCH 14/64] Simplify except in test_exercise --- pythonwhat/test_exercise.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pythonwhat/test_exercise.py b/pythonwhat/test_exercise.py index b4d66114..21423e4b 100644 --- a/pythonwhat/test_exercise.py +++ b/pythonwhat/test_exercise.py @@ -3,7 +3,7 @@ from pythonwhat.sct_syntax import Ex, get_chains from pythonwhat.utils import check_str, check_process from protowhat.Reporter import Reporter -from protowhat.failure import TestFail, InstructorError +from protowhat.failure import Failure, InstructorError from pythonwhat.utils import include_v1 @@ -61,7 +61,7 @@ def test_exercise( for test in tree.crnt_node: test(state) - except (TestFail, InstructorError) as e: + except Failure as e: if isinstance(e, InstructorError): # TODO: decide based on context raise e From f0935e55d1dc2bbb61c072b20dd94b63d730ed7d Mon Sep 17 00:00:00 2001 From: Jeroen Hermans Date: Mon, 13 Jan 2020 13:50:01 +0100 Subject: [PATCH 15/64] Order imports --- pythonwhat/State.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/pythonwhat/State.py b/pythonwhat/State.py index 11eee239..86a0f426 100644 --- a/pythonwhat/State.py +++ b/pythonwhat/State.py @@ -1,6 +1,15 @@ +import asttokens + from functools import partialmethod -from protowhat.utils import parameters_attr +from collections.abc import Mapping + +from protowhat.failure import debugger from protowhat.Feedback import FeedbackComponent +from protowhat.selectors import DispatcherInterface +from protowhat.State import State as ProtoState +from protowhat.utils import parameters_attr +from pythonwhat import signatures +from pythonwhat.converters import get_manual_converters from pythonwhat.feedback import Feedback from pythonwhat.parsing import ( TargetVars, @@ -8,13 +17,6 @@ ObjectAccessParser, parser_dict, ) -from protowhat.State import State as ProtoState -from protowhat.selectors import DispatcherInterface -from protowhat.failure import debugger -from pythonwhat import signatures -from pythonwhat.converters import get_manual_converters -from collections.abc import Mapping -import asttokens from pythonwhat.utils_ast import wrap_in_module From 8f21dad7b281c04142f18e00c0958c485666d509 Mon Sep 17 00:00:00 2001 From: Jeroen Hermans Date: Mon, 20 Jan 2020 15:00:27 +0100 Subject: [PATCH 16/64] Add defensive fallback --- pythonwhat/checks/check_logic.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pythonwhat/checks/check_logic.py b/pythonwhat/checks/check_logic.py index 2550aca3..e9b86ab5 100644 --- a/pythonwhat/checks/check_logic.py +++ b/pythonwhat/checks/check_logic.py @@ -111,7 +111,7 @@ str(fail.__doc__) + """ :Example: - + As a trivial SCT example, :: Ex().check_for_loop().check_body().fail() @@ -150,7 +150,7 @@ def override(state, solution): new_ast = node break - kwargs = state.feedback_context.kwargs + kwargs = state.feedback_context.kwargs if state.feedback_context else {} child = state.to_child( solution_ast=new_ast, student_ast=state.student_ast, From 4aa4a1a810e4252566b03a651b9c30b7abdf4632 Mon Sep 17 00:00:00 2001 From: Jeroen Hermans Date: Mon, 20 Jan 2020 15:03:00 +0100 Subject: [PATCH 17/64] Prevent type error for incorrect usage --- pythonwhat/checks/has_funcs.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pythonwhat/checks/has_funcs.py b/pythonwhat/checks/has_funcs.py index 1cb28b6f..2ffff43a 100644 --- a/pythonwhat/checks/has_funcs.py +++ b/pythonwhat/checks/has_funcs.py @@ -41,12 +41,13 @@ def verify(part, index): if part is None: raise KeyError - # Chceck if it's there in the solution - err_msg = "SCT fails on solution: " + msg + # TODO: instructor error if msg is not str + # Check if it's there in the solution try: verify(state.solution_parts[name], index) except (KeyError, IndexError): with debugger(state): + err_msg = "SCT fails on solution: {}".format(msg) state.report(err_msg, d) try: From 3de9cd45e85f707f0344344e25aea72fe271fc38 Mon Sep 17 00:00:00 2001 From: Jeroen Hermans Date: Mon, 20 Jan 2020 15:32:22 +0100 Subject: [PATCH 18/64] Fix converter related dill error dilling a custom converter function causes globals to be converted the root_state attribute on ChainStart instances includes things that can't be dilled --- pythonwhat/sct_syntax.py | 6 +++++- pythonwhat/test_exercise.py | 4 +++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/pythonwhat/sct_syntax.py b/pythonwhat/sct_syntax.py index f20c78c2..df846ee7 100644 --- a/pythonwhat/sct_syntax.py +++ b/pythonwhat/sct_syntax.py @@ -1,4 +1,4 @@ -from protowhat.sct_syntax import EagerChain, ExGen, LazyChainStart, state_dec_gen +from protowhat.sct_syntax import EagerChain, ExGen, LazyChainStart, state_dec_gen, LazyChain from pythonwhat.checks.check_wrappers import scts from pythonwhat.State import State from pythonwhat.probe import Node, Probe, TEST_NAMES @@ -38,6 +38,10 @@ def Ex(state=None): return EagerChain(state=state or State.root_state, chainable_functions=sct_dict) +def F(): + return LazyChain(chainable_functions=sct_dict) + + def get_chains(): return { "Ex": ExGen(sct_dict, State.root_state), diff --git a/pythonwhat/test_exercise.py b/pythonwhat/test_exercise.py index 21423e4b..9bce76e2 100644 --- a/pythonwhat/test_exercise.py +++ b/pythonwhat/test_exercise.py @@ -94,6 +94,7 @@ def prep_context(): "from inspect import Parameter as param", "from pythonwhat.signatures import sig_from_params, sig_from_obj", "from pythonwhat.State import set_converter", + "from pythonwhat.sct_syntax import F, Ex" ] [exec(line, None, cntxt) for line in imports] @@ -105,7 +106,8 @@ def prep_context(): tree = None cntxt.update(v2_check_functions) - cntxt.update(get_chains()) + # TODO: ChainStart instances cause errors when dill tries to pass manual converter functions + # cntxt.update(get_chains()) return tree, cntxt From 223ec8aaf205b3367e365c8d87faddc7b7c73cce Mon Sep 17 00:00:00 2001 From: Jeroen Hermans Date: Tue, 28 Jan 2020 18:21:53 +0100 Subject: [PATCH 19/64] Update protowhat --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 8db523c6..6ab6afa6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ # pythonwhat deps -protowhat~=1.11.0 +protowhat~=2.0.1 asttokens~=1.1.10 dill~=0.2.7.1 markdown2~=2.3.7 From 760efc5844a1ca389cfa5c91d12224dd9336d5c5 Mon Sep 17 00:00:00 2001 From: Jeroen Hermans Date: Tue, 28 Jan 2020 19:51:03 +0100 Subject: [PATCH 20/64] Bump version, update changelog --- CHANGELOG.md | 4 ++++ pythonwhat/__init__.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index faa15589..dce4ac63 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.23.0 + +- Update to protowhat v2 (embedding xwhats, `prepare_validation` helper, for checking bash history, autodebug) + ## 2.22.0 - Add support for replacing the placeholder `__focus__` with the focused code in the `expr_code` argument diff --git a/pythonwhat/__init__.py b/pythonwhat/__init__.py index ee28d955..442cce44 100644 --- a/pythonwhat/__init__.py +++ b/pythonwhat/__init__.py @@ -1,3 +1,3 @@ -__version__ = "2.22.0" +__version__ = "2.23.0" from .test_exercise import test_exercise, allow_errors From cac72267efa098716b458c51487acaf12fa058fa Mon Sep 17 00:00:00 2001 From: James O'Reilly Date: Wed, 8 Jul 2020 15:47:02 +0200 Subject: [PATCH 21/64] fix has_expr string formatting --- pythonwhat/checks/has_funcs.py | 19 ++++++++++--- pythonwhat/utils.py | 6 ++--- tests/test_messaging.py | 49 +++++++++++++++++++++++++++++++++- 3 files changed, 65 insertions(+), 9 deletions(-) diff --git a/pythonwhat/checks/has_funcs.py b/pythonwhat/checks/has_funcs.py index 2ffff43a..509e6214 100644 --- a/pythonwhat/checks/has_funcs.py +++ b/pythonwhat/checks/has_funcs.py @@ -339,13 +339,24 @@ def has_expr( "expr_code": expr_code, } - fmt_kwargs["stu_eval"] = utils.shorten_str(str(eval_stu)) - fmt_kwargs["sol_eval"] = utils.shorten_str(str(eval_sol)) + fmt_kwargs["stu_eval"] = str(eval_stu) + fmt_kwargs["sol_eval"] = str(eval_sol) + + # wrap in quotes if eval_sol or eval_stu are strings + if test == "value": + if isinstance(eval_stu, str): + fmt_kwargs["stu_eval"] = '\'{}\''.format(fmt_kwargs["stu_eval"]) + if isinstance(eval_sol, str): + fmt_kwargs["sol_eval"] = '\'{}\''.format(fmt_kwargs["sol_eval"]) + + # check if student or solution evaluations are too long or contain newlines if incorrect_msg == DEFAULT_INCORRECT_MSG and ( - fmt_kwargs["stu_eval"] is None - or fmt_kwargs["sol_eval"] is None + utils.unshowable_string(fmt_kwargs["stu_eval"]) + or utils.unshowable_string(fmt_kwargs["sol_eval"]) or fmt_kwargs["stu_eval"] == fmt_kwargs["sol_eval"] ): + fmt_kwargs["stu_eval"] = None + fmt_kwargs["sol_eval"] = None incorrect_msg = "Expected something different." # tests --- diff --git a/pythonwhat/utils.py b/pythonwhat/utils.py index 7a36f7de..f4e9dc67 100644 --- a/pythonwhat/utils.py +++ b/pythonwhat/utils.py @@ -11,10 +11,8 @@ 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 +def unshowable_string(text): + return "\n" in text or len(text) > 50 def copy_env(env): diff --git a/tests/test_messaging.py b/tests/test_messaging.py index aec922a6..cb20346b 100644 --- a/tests/test_messaging.py +++ b/tests/test_messaging.py @@ -532,7 +532,7 @@ def test_check_call(stu, patt): [ ( "echo_word = (lambda word1, echo: word1 * echo * 2)", - "Check the first lambda function. To verify it, we reran it with the arguments `('test', 2)`. Expected `testtest`, but got `testtesttesttest`.", + "Check the first lambda function. To verify it, we reran it with the arguments `('test', 2)`. Expected `'testtest'`, but got `'testtesttesttest'`.", ) ], ) @@ -695,6 +695,53 @@ def test_has_equal_x_2(stu, patt, cols, cole): assert lines(output, cols, cole) +def test_has_equal_value_wrap_string(): + sol = """print(' , ')""" + stu = """print(', ')""" + sct = """Ex().check_function('print', index=0, signature=False).check_args(0).has_equal_value(copy = False)""" + output = helper.run( + { + "DC_CODE": stu, + "DC_SOLUTION": sol, + "DC_SCT": sct, + } + ) + assert not output["correct"] + assert output["message"] == "Check your call of print(). Did you correctly specify the first argument? Expected ' , ', but got ', '." # nopep8 + + +## Testing output edge cases ------------------------------------------------- + + +def test_has_equal_value_dont_wrap_newline(): + sol = """print('\\n')""" + stu = """print('text')""" + sct = """Ex().check_function('print', index=0, signature=False).check_args(0).has_equal_value()""" + output = helper.run( + { + "DC_CODE": stu, + "DC_SOLUTION": sol, + "DC_SCT": sct, + } + ) + assert not output["correct"] + assert output["message"] == "Check your call of print(). Did you correctly specify the first argument? Expected something different." # nopep8 + + +def test_has_equal_value_dont_wrap_too_long(): + sol = """print('short text')""" + stu = """print('This text is longer than 50 characters if I copy it 3 times. This text is longer than 50 characters if I copy it 3 times. This text is longer than 50 characters if I copy it 3 times.')""" # nopep8 + sct = """Ex().check_function('print', index=0, signature=False).check_args(0).has_equal_value()""" + output = helper.run( + { + "DC_CODE": stu, + "DC_SOLUTION": sol, + "DC_SCT": sct, + } + ) + assert not output["correct"] + assert output["message"] == "Check your call of print(). Did you correctly specify the first argument? Expected something different." # nopep8 + ## Check has no error --------------------------------------------------------- From 411afc65a514ccc4d013fdc3a73c14f0bd42fe6c Mon Sep 17 00:00:00 2001 From: James O'Reilly Date: Wed, 29 Jul 2020 10:27:41 +0200 Subject: [PATCH 22/64] Bump version and changelog --- CHANGELOG.md | 4 ++++ pythonwhat/__init__.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dce4ac63..3e4af1c8 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.23.1 + +- Fix string formatting in has_expr(). Strings will now have quotes in error messages. Leading and trailing whitespace is no longer removed. + ## 2.23.0 - Update to protowhat v2 (embedding xwhats, `prepare_validation` helper, for checking bash history, autodebug) diff --git a/pythonwhat/__init__.py b/pythonwhat/__init__.py index 442cce44..e30b3e2a 100644 --- a/pythonwhat/__init__.py +++ b/pythonwhat/__init__.py @@ -1,3 +1,3 @@ -__version__ = "2.23.0" +__version__ = "2.23.1" from .test_exercise import test_exercise, allow_errors From b0f1a4e9e4bd052fc7c1c6164fedead36e009fa0 Mon Sep 17 00:00:00 2001 From: James O'Reilly Date: Fri, 31 Jul 2020 12:31:27 +0200 Subject: [PATCH 23/64] Updated feedback message for student submissions which are too long. --- pythonwhat/checks/has_funcs.py | 18 +++++++++++------- pythonwhat/utils.py | 8 ++++++-- tests/test_messaging.py | 4 ++-- 3 files changed, 19 insertions(+), 11 deletions(-) diff --git a/pythonwhat/checks/has_funcs.py b/pythonwhat/checks/has_funcs.py index 509e6214..0e6fca51 100644 --- a/pythonwhat/checks/has_funcs.py +++ b/pythonwhat/checks/has_funcs.py @@ -349,15 +349,19 @@ def has_expr( if isinstance(eval_sol, str): fmt_kwargs["sol_eval"] = '\'{}\''.format(fmt_kwargs["sol_eval"]) + # reformat student evaluation string if it is too long or contains newlines + if incorrect_msg == DEFAULT_INCORRECT_MSG: + fmt_kwargs["stu_eval"] = utils.shorten_string(fmt_kwargs["stu_eval"]) + # check if student or solution evaluations are too long or contain newlines if incorrect_msg == DEFAULT_INCORRECT_MSG and ( - utils.unshowable_string(fmt_kwargs["stu_eval"]) - or utils.unshowable_string(fmt_kwargs["sol_eval"]) - or fmt_kwargs["stu_eval"] == fmt_kwargs["sol_eval"] - ): - fmt_kwargs["stu_eval"] = None - fmt_kwargs["sol_eval"] = None - incorrect_msg = "Expected something different." + len(fmt_kwargs["sol_eval"]) > 50 or + utils.has_newline(fmt_kwargs["stu_eval"]) or + utils.has_newline(fmt_kwargs["sol_eval"]) or + fmt_kwargs["stu_eval"] == fmt_kwargs["sol_eval"]): + fmt_kwargs["stu_eval"] = None + fmt_kwargs["sol_eval"] = None + incorrect_msg = "Expected something different." # tests --- # error in process diff --git a/pythonwhat/utils.py b/pythonwhat/utils.py index f4e9dc67..0e9cb3af 100644 --- a/pythonwhat/utils.py +++ b/pythonwhat/utils.py @@ -10,9 +10,13 @@ def include_v1(): def v2_only(): return not include_v1() +def shorten_string(text): + if len(text) > 50: + text = text[0:45] + "..." + return text -def unshowable_string(text): - return "\n" in text or len(text) > 50 +def has_newline(text): + return "\n" in text def copy_env(env): diff --git a/tests/test_messaging.py b/tests/test_messaging.py index cb20346b..dd2bd02a 100644 --- a/tests/test_messaging.py +++ b/tests/test_messaging.py @@ -728,7 +728,7 @@ def test_has_equal_value_dont_wrap_newline(): assert output["message"] == "Check your call of print(). Did you correctly specify the first argument? Expected something different." # nopep8 -def test_has_equal_value_dont_wrap_too_long(): +def test_has_equal_value_shorten_too_long(): sol = """print('short text')""" stu = """print('This text is longer than 50 characters if I copy it 3 times. This text is longer than 50 characters if I copy it 3 times. This text is longer than 50 characters if I copy it 3 times.')""" # nopep8 sct = """Ex().check_function('print', index=0, signature=False).check_args(0).has_equal_value()""" @@ -740,7 +740,7 @@ def test_has_equal_value_dont_wrap_too_long(): } ) assert not output["correct"] - assert output["message"] == "Check your call of print(). Did you correctly specify the first argument? Expected something different." # nopep8 + assert output["message"] == "Check your call of print(). Did you correctly specify the first argument? Expected 'short text', but got 'This text is longer than 50 characters if I ...." # nopep8 ## Check has no error --------------------------------------------------------- From 59e209f59ecc429620795a8da126a36ac1d46a9b Mon Sep 17 00:00:00 2001 From: James O'Reilly Date: Mon, 3 Aug 2020 15:54:22 +0200 Subject: [PATCH 24/64] Updated to pythonwhat v2.23.2 --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3e4af1c8..7738b79e 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.23.2 + +- Update behaviour of has_expr() default feedback message. If the student's evaluation is too long, it is now shortened and an ellipsis is added. + ## 2.23.1 - Fix string formatting in has_expr(). Strings will now have quotes in error messages. Leading and trailing whitespace is no longer removed. From 36e43b6aa188b8f25d3775d6335e412141a062c0 Mon Sep 17 00:00:00 2001 From: James O'Reilly Date: Mon, 3 Aug 2020 17:21:06 +0200 Subject: [PATCH 25/64] Updated has_expr feedback logic and added test --- pythonwhat/checks/has_funcs.py | 11 +++++------ tests/test_messaging.py | 17 ++++++++++++++++- 2 files changed, 21 insertions(+), 7 deletions(-) diff --git a/pythonwhat/checks/has_funcs.py b/pythonwhat/checks/has_funcs.py index 0e6fca51..41fbded1 100644 --- a/pythonwhat/checks/has_funcs.py +++ b/pythonwhat/checks/has_funcs.py @@ -349,9 +349,8 @@ def has_expr( if isinstance(eval_sol, str): fmt_kwargs["sol_eval"] = '\'{}\''.format(fmt_kwargs["sol_eval"]) - # reformat student evaluation string if it is too long or contains newlines - if incorrect_msg == DEFAULT_INCORRECT_MSG: - fmt_kwargs["stu_eval"] = utils.shorten_string(fmt_kwargs["stu_eval"]) + # reformat student evaluation string if it is too long + fmt_kwargs["stu_eval"] = utils.shorten_string(fmt_kwargs["stu_eval"]) # check if student or solution evaluations are too long or contain newlines if incorrect_msg == DEFAULT_INCORRECT_MSG and ( @@ -359,9 +358,9 @@ def has_expr( utils.has_newline(fmt_kwargs["stu_eval"]) or utils.has_newline(fmt_kwargs["sol_eval"]) or fmt_kwargs["stu_eval"] == fmt_kwargs["sol_eval"]): - fmt_kwargs["stu_eval"] = None - fmt_kwargs["sol_eval"] = None - incorrect_msg = "Expected something different." + fmt_kwargs["stu_eval"] = None + fmt_kwargs["sol_eval"] = None + incorrect_msg = "Expected something different." # tests --- # error in process diff --git a/tests/test_messaging.py b/tests/test_messaging.py index dd2bd02a..b51ae2a5 100644 --- a/tests/test_messaging.py +++ b/tests/test_messaging.py @@ -728,7 +728,7 @@ def test_has_equal_value_dont_wrap_newline(): assert output["message"] == "Check your call of print(). Did you correctly specify the first argument? Expected something different." # nopep8 -def test_has_equal_value_shorten_too_long(): +def test_has_equal_value_shorten_submission(): sol = """print('short text')""" stu = """print('This text is longer than 50 characters if I copy it 3 times. This text is longer than 50 characters if I copy it 3 times. This text is longer than 50 characters if I copy it 3 times.')""" # nopep8 sct = """Ex().check_function('print', index=0, signature=False).check_args(0).has_equal_value()""" @@ -742,6 +742,21 @@ def test_has_equal_value_shorten_too_long(): assert not output["correct"] assert output["message"] == "Check your call of print(). Did you correctly specify the first argument? Expected 'short text', but got 'This text is longer than 50 characters if I ...." # nopep8 + +def test_has_equal_value_dont_shorten_solution(): + sol = """print('This solution is really really really really really really really really long!')""" + stu = """print('short text')""" # nopep8 + sct = """Ex().check_function('print', index=0, signature=False).check_args(0).has_equal_value()""" + output = helper.run( + { + "DC_CODE": stu, + "DC_SOLUTION": sol, + "DC_SCT": sct, + } + ) + assert not output["correct"] + assert output["message"] == "Check your call of print(). Did you correctly specify the first argument? Expected something different." # nopep8 + ## Check has no error --------------------------------------------------------- From 0ad5fea146b117984c2b51f58d00989bee870980 Mon Sep 17 00:00:00 2001 From: Nuno Rafael Rocha Date: Thu, 15 Oct 2020 10:27:47 +0100 Subject: [PATCH 26/64] fix: typo on highlighted word --- pythonwhat/checks/has_funcs.py | 6 +++--- tests/test_messaging.py | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pythonwhat/checks/has_funcs.py b/pythonwhat/checks/has_funcs.py index 41fbded1..0ccfaaa4 100644 --- a/pythonwhat/checks/has_funcs.py +++ b/pythonwhat/checks/has_funcs.py @@ -203,9 +203,9 @@ def parse_tree(tree): 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_ERROR_MSG = "Running {{'it' if parent['part'] else 'the highlighted expression'}} generated an error: `{{stu_str}}`." +DEFAULT_ERROR_MSG_INV = "Running {{'it' if parent['part'] else 'the highlighted expression'}} didn't generate an error, but it should!" +DEFAULT_UNDEFINED_NAME_MSG = "Running {{'it' if parent['part'] else 'the highlighted 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}}`?" ) diff --git a/tests/test_messaging.py b/tests/test_messaging.py index b51ae2a5..914a5a72 100644 --- a/tests/test_messaging.py +++ b/tests/test_messaging.py @@ -501,11 +501,11 @@ def test(a, b): print(a + b) return a + b """, - "Check the definition of `test()`. To verify it, we reran `test(3, 1)`. Running the higlighted expression generated an error: `wrong`.", + "Check the definition of `test()`. To verify it, we reran `test(3, 1)`. Running the highlighted expression generated an error: `wrong`.", ), ( "def test(a, b): print(int(a) + int(b)); return int(a) + int(b)", - 'Check the definition of `test()`. To verify it, we reran `test(1, "2")`. Running the higlighted expression didn\'t generate an error, but it should!', + 'Check the definition of `test()`. To verify it, we reran `test(1, "2")`. Running the highlighted expression didn\'t generate an error, but it should!', ), ], ) From e5a59a515edc1074066972eb277c3c8643e95780 Mon Sep 17 00:00:00 2001 From: TimSangster Date: Fri, 3 Sep 2021 10:02:31 +0200 Subject: [PATCH 27/64] chore: fix test_debug using blocked endpoint --- tests/test_debug.py | 15 +- tests/test_debug_exercises.json | 259 ++++++++++++++++++++++++++++++++ 2 files changed, 265 insertions(+), 9 deletions(-) create mode 100644 tests/test_debug_exercises.json diff --git a/tests/test_debug.py b/tests/test_debug.py index 209c47c0..92cd2bb3 100644 --- a/tests/test_debug.py +++ b/tests/test_debug.py @@ -1,5 +1,5 @@ -import requests import tests.helper as helper +import json from protowhat.failure import InstructorError @@ -21,13 +21,10 @@ def test_debug_on_error(): # assert "SCT" in output["message"] -def build_data(course_id, chapter_id, ex_number, printout=False): - url = "https://www.datacamp.com/api/courses/{course_id}/chapters/{chapter_id}/exercises.json".format( - course_id=course_id, chapter_id=chapter_id - ) - resp = requests.get(url) - assert resp.status_code == 200 - ex = resp.json()[ex_number - 1] +def build_data(ex_number, printout=False): + # "https://www.datacamp.com/api/courses/735/chapters/1842/exercises.json" + with open('tests/test_debug_exercises.json') as exercises_json: + ex = json.load(exercises_json)[ex_number - 1] pec = ex.get("pre_exercise_code", "") sol = ex.get("solution", "") @@ -60,6 +57,6 @@ def test_normal_pass(): def test_dc_exercise(): - data = build_data(735, 1842, 2) + data = build_data(2) output = helper.run(data) assert output["correct"] diff --git a/tests/test_debug_exercises.json b/tests/test_debug_exercises.json new file mode 100644 index 00000000..f1d30816 --- /dev/null +++ b/tests/test_debug_exercises.json @@ -0,0 +1,259 @@ +[ + { + "id": 14251, + "type": "VideoExercise", + "assignment": null, + "title": "Hello Python!", + "sample_code": "", + "instructions": null, + "number": 1, + "sct": "", + "pre_exercise_code": "", + "solution": "", + "hint": null, + "attachments": null, + "xp": 50, + "possible_answers": [], + "feedbacks": [], + "question": "", + "video_link": null, + "video_hls": null, + "aspect_ratio": 56.25, + "projector_key": "course_735_d8fcd4c930027fa4e1c3870c7e7e0ff1" + }, + { + "id": 14023, + "type": "NormalExercise", + "assignment": "

In the Python script on the right, you can type Python code to solve the exercises. If you hit Run Code or Submit Answer, your python script (script.py) is executed and the output is shown in the IPython Shell. Submit Answer checks whether your submission is correct and gives you feedback.

\n

You can hit Run Code and Submit Answer as often as you want. If you're stuck, you can click Get Hint, and ultimately Get Solution.

\n

You can also use the IPython Shell interactively by simply typing commands and hitting Enter. When you work in the shell directly, your code will not be checked for correctness so it is a great way to experiment.

", + "title": "The Python Interface", + "sample_code": "# Example, do not modify!\nprint(5 / 8)\n\n# Print the sum of 7 and 10\n", + "instructions": "
    \n
  • Experiment in the IPython Shell; type 5 / 8, for example.
  • \n
  • Add another line of code to the Python script on the top-right (not in the Shell): print(7 + 10).
  • \n
  • Hit Submit Answer to execute the Python script and receive feedback.
  • \n
", + "number": 2, + "sct": "Ex().has_printout(1, not_printed_msg = \"__JINJA__:Have you used `{{sol_call}}` to print out the sum of 7 and 10?\")\nsuccess_msg(\"Great! On to the next one!\")", + "pre_exercise_code": "", + "solution": "# Example, do not modify!\nprint(5 / 8)\n\n# Put code below here\nprint(7 + 10)", + "hint": "

Simply add print(7 + 10) in the script on the top-right (not in the Shell) and hit 'Submit Answer'.

", + "attachments": null, + "xp": 100, + "possible_answers": [], + "feedbacks": [], + "question": "" + }, + { + "id": 14024, + "type": "MultipleChoiceExercise", + "assignment": "

Python is a pretty versatile language. For which applications can you use Python?

", + "title": "When to use Python?", + "sample_code": "", + "instructions": null, + "number": 3, + "sct": "msg1 = \"Incorrect. Python can do simple and quick calculations, but it is much more than that!\"\nmsg2 = \"Incorrect. There is a very popular framework to build database-driven websites (Django), but Python can do much more.\"\nmsg3 = \"Incorrect. Python is a powerful tool to do data analysis, but you can also use it for other ends.\"\nmsg4 = \"Correct! Python is an extremely versatile language.\"\nEx().has_chosen(4, [msg1, msg2, msg3, msg4])", + "pre_exercise_code": "", + "solution": "", + "hint": "

Hugo mentioned in the video that Python can be used to build practically any piece of software.

", + "attachments": null, + "xp": 50, + "possible_answers": [ + "You want to do some quick calculations.", + "For your new business, you want to develop a database-driven website.", + "Your boss asks you to clean and analyze the results of the latest satisfaction survey.", + "All of the above." + ], + "feedbacks": [], + "question": "" + }, + { + "id": 14025, + "type": "NormalExercise", + "assignment": "

Something that Hugo didn't mention in his videos is that you can add comments to your Python scripts. Comments are important to make sure that you and others can understand what your code is about.

\n

To add comments to your Python script, you can use the # tag. These comments are not run as Python code, so they will not influence your result. As an example, take the comment in the editor, # Division; it is completely ignored during execution.

", + "title": "Any comments?", + "sample_code": "# Division\nprint(5 / 8)\n\n\nprint(7 + 10)", + "instructions": "

Above the print(7 + 10), add the comment

\n
# Addition\n
", + "number": 4, + "sct": "Ex().has_code(\"#\\s*(\\w+)[\\s.!?]*print\\s*\\(\\s*7\", not_typed_msg = \"Make sure to add the comment right before `print(7 + 10)`.\")\nsuccess_msg(\"Great!\")", + "pre_exercise_code": "", + "solution": "# Division\nprint(5 / 8)\n\n# Addition\nprint(7 + 10)", + "hint": "

For this exercise you only have to add one line of comments. It won't run as Python code. Add # Addition right above print(7 + 10).

", + "attachments": null, + "xp": 100, + "possible_answers": [], + "feedbacks": [], + "question": "" + }, + { + "id": 14026, + "type": "NormalExercise", + "assignment": "

Python is perfectly suited to do basic calculations. Apart from addition, subtraction, multiplication and division, there is also support for more advanced operations such as:

\n
    \n
  • Exponentiation: **. This operator raises the number to its left to the power of the number to its right. For example 4**2 will give 16.
  • \n
  • Modulo: %. This operator returns the remainder of the division of the number to the left by the number on its right. For example 18 % 7 equals 4.
  • \n
\n

The code in the script gives some examples.

", + "title": "Python as a calculator", + "sample_code": "# Addition, subtraction\nprint(5 + 5)\nprint(5 - 5)\n\n# Multiplication, division, modulo, and exponentiation\nprint(3 * 5)\nprint(10 / 2)\nprint(18 % 7)\nprint(4 ** 2)\n\n# How much is your $100 worth after 7 years?\n", + "instructions": "

Suppose you have $100, which you can invest with a 10% return each year. After one year, it's \\(100 \\times 1.1 = 110\\) dollars, and after two years it's \\(100 \\times 1.1 \\times 1.1 = 121\\). Add code to calculate how much money you end up with after 7 years, and print the result.

", + "number": 5, + "sct": "Ex().has_printout(6, not_printed_msg = \"Have you used `print(100 * 1.1 ** 7)` to print out the result of your calculations?\")\nsuccess_msg(\"Time for another video!\")", + "pre_exercise_code": "", + "solution": "# Addition, subtraction\nprint(5 + 5)\nprint(5 - 5)\n\n# Multiplication, division, modulo, and exponentiation\nprint(3 * 5)\nprint(10 / 2)\nprint(18 % 7)\nprint(4 ** 2)\n\n# How much is your $100 worth after 7 years?\nprint(100 * 1.1 ** 7)", + "hint": "

After two years you have \\(100 \\times 1.1 \\times 1.1 = 100 \\times 1.1^2\\). How much do you have after 7 years than? Use * and **.

", + "attachments": null, + "xp": 100, + "possible_answers": [], + "feedbacks": [], + "question": "" + }, + { + "id": 972033, + "type": "VideoExercise", + "assignment": null, + "title": "Variables and Types", + "sample_code": "", + "instructions": null, + "number": 6, + "sct": "", + "pre_exercise_code": "", + "solution": "", + "hint": null, + "attachments": null, + "xp": 50, + "possible_answers": [], + "feedbacks": [], + "question": "", + "video_link": null, + "video_hls": null, + "aspect_ratio": 56.25, + "projector_key": "course_735_433dcfcfedaee070cbf440491c402e3b" + }, + { + "id": 14043, + "type": "NormalExercise", + "assignment": "

In Python, a variable allows you to refer to a value with a name. To create a variable use =, like this example:

\n
x = 5\n
\n

You can now use the name of this variable, x, instead of the actual value, 5.

\n

Remember, = in Python means assignment, it doesn't test equality!

", + "title": "Variable Assignment", + "sample_code": "# Create a variable savings\n\n\n# Print out savings\n", + "instructions": "
    \n
  • Create a variable savings with the value 100.
  • \n
  • Check out this variable by typing print(savings) in the script.
  • \n
", + "number": 7, + "sct": "Ex().check_object(\"savings\").has_equal_value(incorrect_msg=\"Assign `100` to the variable `savings`.\")\nEx().has_printout(0, not_printed_msg = \"Print out `savings`, the variable you created, with `print(savings)`.\")\nsuccess_msg(\"Great! Let's try to do some calculations with this variable now!\")", + "pre_exercise_code": "", + "solution": "# Create a variable savings\nsavings = 100\n\n# Print out savings\nprint(savings)", + "hint": "
    \n
  • Type savings = 100 to create the variable savings.
  • \n
  • After creating the variable savings, you can type print(savings).
  • \n
", + "attachments": null, + "xp": 100, + "possible_answers": [], + "feedbacks": [], + "question": "" + }, + { + "id": 13726, + "type": "NormalExercise", + "assignment": "

Remember how you calculated the money you ended up with after 7 years of investing $100? You did something like this:

\n
100 * 1.1 ** 7\n
\n

Instead of calculating with the actual values, you can use variables instead. The savings variable you've created in the previous exercise represents the $100 you started with. It's up to you to create a new variable to represent 1.1 and then redo the calculations!

", + "title": "Calculations with variables", + "sample_code": "# Create a variable savings\nsavings = 100\n\n# Create a variable growth_multiplier\n\n\n# Calculate result\n\n\n# Print out result\n", + "instructions": "
    \n
  • Create a variable growth_multiplier, equal to 1.1.
  • \n
  • Create a variable, result, equal to the amount of money you saved after 7 years.
  • \n
  • Print out the value of result.
  • \n
", + "number": 8, + "sct": "Ex().check_object(\"savings\", missing_msg=\"The variable `savings` was defined for you, don't remove it!\").has_equal_value(incorrect_msg=\"The variable `savings` should be `100`, like it was defined for you.\"),\nEx().check_object(\"growth_multiplier\").has_equal_value(incorrect_msg=\"Did you assign the correct value to `growth_multiplier`?\")\nEx().check_correct(\n check_object(\"result\").has_equal_value(incorrect_msg=\"Have you used `*` and `**` to calculate `result`?\"),\n multi(\n has_code(\"savings\\s*\\*\\s*\\(*\\s*growth_multiplier\", not_typed_msg = \"Did you multiply `savings` by `growth_multiplier ** 7`?\"), \n has_code(\"growth_multiplier\\s*\\*\\*\\s*7\", not_typed_msg = \"Did you raise `growth_multiplier` to the power of `7` using `**`?\") \n )\n)\n\nEx().has_printout(0, not_printed_msg=\"Remember to print out `result` at the end of your script.\")\nsuccess_msg(\"Great!\")", + "pre_exercise_code": "", + "solution": "# Create a variable savings\nsavings = 100\n\n# Create a variable growth_multiplier\ngrowth_multiplier = 1.1\n\n# Calculate result\nresult = savings * growth_multiplier ** 7\n\n# Print out result\nprint(result)", + "hint": "
    \n
  • To create the variable growth_multiplier, use growth_multiplier = 1.1.
  • \n
  • In the example code block of the assignment, replace 100 with savings and 1.1 with growth_multiplier: savings * growth_multiplier ** 7.
  • \n
  • Use the print() function to print the value of a variable.
  • \n
", + "attachments": null, + "xp": 100, + "possible_answers": [], + "feedbacks": [], + "question": "" + }, + { + "id": 14044, + "type": "NormalExercise", + "assignment": "

In the previous exercise, you worked with two Python data types:

\n
    \n
  • int, or integer: a number without a fractional part. savings, with the value 100, is an example of an integer.
  • \n
  • float, or floating point: a number that has both an integer and fractional part, separated by a point. growth_multiplier, with the value 1.1, is an example of a float.
  • \n
\n

Next to numerical data types, there are two other very common data types:

\n
    \n
  • str, or string: a type to represent text. You can use single or double quotes to build a string.
  • \n
  • bool, or boolean: a type to represent logical values. Can only be True or False (the capitalization is important!).
  • \n
", + "title": "Other variable types", + "sample_code": "# Create a variable desc\n\n\n# Create a variable profitable\n", + "instructions": "
    \n
  • Create a new string, desc, with the value \"compound interest\".
  • \n
  • Create a new boolean, profitable, with the value True.
  • \n
", + "number": 9, + "sct": "Ex().check_object(\"desc\").has_equal_value()\nEx().check_object(\"profitable\").has_equal_value()\nsuccess_msg(\"Nice!\")", + "pre_exercise_code": "", + "solution": "# Create a variable desc\ndesc = \"compound interest\"\n\n# Create a variable profitable\nprofitable = True", + "hint": "
    \n
  • To create a variable in Python, use =. Make sure to wrap your string in single or double quotes.
  • \n
  • Only two boolean values exist in Python: True and False. TRUE, true, FALSE, false and other versions will not be accepted.
  • \n
", + "attachments": null, + "xp": 100, + "possible_answers": [], + "feedbacks": [], + "question": "" + }, + { + "id": 14347, + "type": "MultipleChoiceExercise", + "assignment": "

To find out the type of a value or a variable that refers to that value, you can use the type() function. Suppose you've defined a variable a, but you forgot the type of this variable. To determine the type of a, simply execute:

\n
type(a)\n
\n

We already went ahead and created three variables: a, b and c. You can use the IPython shell to discover their type. Which of the following options is correct?

", + "title": "Guess the type", + "sample_code": "", + "instructions": null, + "number": 10, + "sct": "msg1 = \"The type of `a` is not `int`. Try out `type(a)` and see for yourself.\"\nmsg2 = \"`b` is not a `bool`, it's a `str`! The fact that `True` is wrapped in double quotes makes it a string.\"\nmsg3 = \"Correcto perfecto!\"\nmsg4 = \"None of the variable's types is correct here. Try `type(a)` and see what type this variable is.\"\nEx().has_chosen(3,[msg1, msg2, msg3, msg4])", + "pre_exercise_code": "a = 100*1.1**7\nb = \"True\"\nc = False", + "solution": "", + "hint": "

Use type(a), type(b) and type(c) inside the IPython Shell to find out about the variables' types.

", + "attachments": null, + "xp": 50, + "possible_answers": [ + "a is of type int, b is of type str, c is of type bool", + "a is of type float, b is of type bool, c is of type str", + "a is of type float, b is of type str, c is of type bool", + "a is of type int, b is of type bool, c is of type str" + ], + "feedbacks": [], + "question": "" + }, + { + "id": 14046, + "type": "NormalExercise", + "assignment": "

Hugo mentioned that different types behave differently in Python.

\n

When you sum two strings, for example, you'll get different behavior than when you sum two integers or two booleans.

\n

In the script some variables with different types have already been created. It's up to you to use them.

", + "title": "Operations with other types", + "sample_code": "savings = 100\ngrowth_multiplier = 1.1\ndesc = \"compound interest\"\n\n# Assign product of growth_multiplier and savings to year1\n\n\n# Print the type of year1\n\n\n# Assign sum of desc and desc to doubledesc\n\n\n# Print out doubledesc\n", + "instructions": "
    \n
  • Calculate the product of savings and growth_multiplier. Store the result in year1.
  • \n
  • What do you think the resulting type will be? Find out by printing out the type of year1.
  • \n
  • Calculate the sum of desc and desc and store the result in a new variable doubledesc.
  • \n
  • Print out doubledesc. Did you expect this?
  • \n
", + "number": 11, + "sct": "# predefined\nmsg = \"You don't have to change or remove the predefined variables.\"\nobjs = [\"savings\", \"growth_multiplier\", \"desc\", \"year1\"]\nEx().multi(\n check_object('savings', missing_msg=msg).has_equal_value(incorrect_msg=msg),\n check_object('growth_multiplier', missing_msg=msg).has_equal_value(incorrect_msg=msg),\n check_object('desc', missing_msg=msg).has_equal_value(incorrect_msg=msg),\n check_object('year1', missing_msg=msg).has_equal_value(incorrect_msg=msg)\n)\n\n# check year1 and printout\nEx().multi(\n check_object(\"year1\").has_equal_value(incorrect_msg=\"Multiply `savings` and `growth_multiplier` to create the `year1` variable.\"),\n has_printout(0, not_printed_msg = \"__JINJA__:Use `{{sol_call}}` to print out the type of `year1`.\")\n)\n\n# check doubledesc and prinout\nEx().multi(\n check_object(\"doubledesc\").has_equal_value(incorrect_msg = \"Have you stored the result of `desc + desc` in `doubledesc`?\"),\n has_printout(1, not_printed_msg = \"Don't forget to print out `doubledesc`.\")\n)\n\nsuccess_msg(\"Nice. Notice how `desc + desc` causes `\\\"compound interest\\\"` and `\\\"compound interest\\\"` to be pasted together.\")", + "pre_exercise_code": "", + "solution": "savings = 100\ngrowth_multiplier = 1.1\ndesc = \"compound interest\"\n\n# Assign product of savings and growth_multiplier to year1\nyear1 = savings * growth_multiplier\n\n# Print the type of year1\nprint(type(year1))\n\n# Assign sum of desc and desc to doubledesc\ndoubledesc = desc + desc\n\n# Print out doubledesc\nprint(doubledesc)", + "hint": "
    \n
  • Assign growth_multiplier * savings to a new variable, year1.
  • \n
  • To print the type of a variable x, use print(type(x)).
  • \n
  • Assign desc + desc to a new variable, doubledesc.
  • \n
  • To print a variable x, write print(x) in the script.
  • \n
", + "attachments": null, + "xp": 100, + "possible_answers": [], + "feedbacks": [], + "question": "" + }, + { + "id": 14047, + "type": "NormalExercise", + "assignment": "

Using the + operator to paste together two strings can be very useful in building custom messages.

\n

Suppose, for example, that you've calculated the return of your investment and want to summarize the results in a string. Assuming the integer savings and float result are defined, you can try something like this:

\n
print(\"I started with $\" + savings + \" and now have $\" + result + \". Awesome!\")\n
\n

This will not work, though, as you cannot simply sum strings and integers/floats.

\n

To fix the error, you'll need to explicitly convert the types of your variables. More specifically, you'll need str(), to convert a value into a string. str(savings), for example, will convert the integer savings to a string.

\n

Similar functions such as int(), float() and bool() will help you convert Python values into any type.

", + "title": "Type conversion", + "sample_code": "# Definition of savings and result\nsavings = 100\nresult = 100 * 1.10 ** 7\n\n# Fix the printout\nprint(\"I started with $\" + savings + \" and now have $\" + result + \". Awesome!\")\n\n# Definition of pi_string\npi_string = \"3.1415926\"\n\n# Convert pi_string into float: pi_float\n", + "instructions": "
    \n
  • Hit Run Code to run the code. Try to understand the error message.
  • \n
  • Fix the code such that the printout runs without errors; use the function str() to convert the variables to strings.
  • \n
  • Convert the variable pi_string to a float and store this float as a new variable, pi_float.
  • \n
", + "number": 12, + "sct": "\n# ensure predefined values are unmodified\nmsg = \"You don't have to change or remove the predefined variables.\"\nEx().multi(\n check_object(\"savings\", missing_msg=msg).has_equal_value(incorrect_msg=msg),\n check_object(\"result\", missing_msg=msg).has_equal_value(incorrect_msg=msg)\n)\n\nEx().check_correct(\n has_printout(0),\n multi(\n check_function(\"str\", 0).check_args(0).has_equal_value(incorrect_msg=\"Inside the `print()` command, make sure to convert `savings` into a string with `str(savings)`.\"),\n check_function(\"str\", 1).check_args(0).has_equal_value(incorrect_msg=\"Inside the `print()` command, make sure to convert `result` into a string `str(result)`.\")\n )\n)\n\n# check pi_float\nEx().check_correct(\n check_object(\"pi_float\").has_equal_value(),\n multi(\n check_object(\"pi_string\").has_equal_value(),\n check_function(\"float\", missing_msg = \"In order to convert `pi_string` to a float, be sure to use the `float()` function.\").has_equal_value(incorrect_msg=\"Use `float(pi_string) to create the variable `pi_float`.\")\n )\n)\n\nsuccess_msg(\"Great! You have a profit of around $95; that's pretty awesome indeed!\")", + "pre_exercise_code": "", + "solution": "# Definition of savings and result\nsavings = 100\nresult = 100 * 1.10 ** 7\n\n# Fix the printout\nprint(\"I started with $\" + str(savings) + \" and now have $\" + str(result) + \". Awesome!\")\n\n# Definition of pi_string\npi_string = \"3.1415926\"\n\n# Convert pi_string into float: pi_float\npi_float = float(pi_string)", + "hint": "
    \n
  • You should use str() twice!
  • \n
  • Use float() on pi_string and store the result in pi_float.
  • \n
", + "attachments": null, + "xp": 100, + "possible_answers": [], + "feedbacks": [], + "question": "" + }, + { + "id": 14253, + "type": "MultipleChoiceExercise", + "assignment": "

Now that you know something more about combining different sources of information, have a look at the four Python expressions below.\nWhich one of these will throw an error? You can always copy and paste this code in the IPython Shell to find out!

", + "title": "Can Python handle everything?", + "sample_code": "", + "instructions": null, + "number": 13, + "sct": "msg1 = \"Incorrect, this command runs perfectly fine.\"\nmsg2 = \"It's perfectly possible to 'multiply strings' in Python...\"\nmsg3 = \"Correct! Because you're not converting `2` to a string with [str()](https://docs.python.org/3/library/functions.html#func-str), this will give an error.\"\nmsg4 = \"`True + False` doesn't error out. Feel free to try it in the console to confirm!\"\nEx().has_chosen(3, [msg1, msg2, msg3, msg4])", + "pre_exercise_code": "", + "solution": "", + "hint": "

Copy and paste the different expressions into the IPython Shell and try to figure out which one throws an error.

", + "attachments": null, + "xp": 50, + "possible_answers": [ + "\"I can add integers, like \" + str(5) + \" to strings.\"", + "\"I said \" + (\"Hey \" * 2) + \"Hey!\"", + "\"The correct answer to this multiple choice exercise is answer number \" + 2", + "True + False" + ], + "feedbacks": [], + "question": "" + } +] \ No newline at end of file From b4ad485a85075056a274f1848f940c4757800de0 Mon Sep 17 00:00:00 2001 From: TimSangster Date: Fri, 3 Sep 2021 12:41:43 +0200 Subject: [PATCH 28/64] chore: bump python to 3.9, update dependencies --- .travis.yml | 6 +++--- README.md | 6 +++--- requirements.txt | 40 ++++++++++++++++++++-------------------- 3 files changed, 26 insertions(+), 26 deletions(-) diff --git a/.travis.yml b/.travis.yml index b829f5a1..54a9d1fb 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,10 +1,10 @@ sudo: false language: python python: -- '3.5' +- '3.9' before_install: -- pip install -r requirements.txt -- pip install -e . +- pip3.9 install -r requirements.txt +- pip3.9 install -e . script: pytest -m "not compiled" --cov=pythonwhat after_success: codecov --token=$CODECOV_TOKEN deploy: diff --git a/README.md b/README.md index 08511592..f60f0ef7 100644 --- a/README.md +++ b/README.md @@ -54,9 +54,9 @@ To learn how to include an SCT in a DataCamp course, visit https://instructor-su ## Run tests ```bash -pyenv local 3.5.2 -pip install -r requirements.txt -pip install -e . +pyenv local 3.9.6 +pip3.9 install -r requirements.txt +pip3.9 install -e . pytest ``` diff --git a/requirements.txt b/requirements.txt index 6ab6afa6..359fb6ab 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,30 +1,30 @@ # pythonwhat deps -protowhat~=2.0.1 -asttokens~=1.1.10 -dill~=0.2.7.1 -markdown2~=2.3.7 -jinja2~=2.10 +protowhat~=2.1.0 +asttokens~=2.0.5 +dill~=0.3.4 +markdown2~=2.3.10 +jinja2~=2.11.3 -numpy~=1.14.2 -pandas~=0.22.0 +numpy~=1.19.5 +pandas~=1.3.2 # test deps -scipy~=1.0.1 +scipy~=1.7.1 bs4~=0.0.1 -html5lib~=1.0.1 -h5py~=2.7.1 -requests~=2.20.0 -seaborn~=0.8.1 -sqlalchemy~=1.3.0 -xlrd~=1.1.0 +html5lib~=1.1 +h5py~=3.1.0 +requests~=2.26.0 +seaborn~=0.11.2 +sqlalchemy~=1.4.23 +xlrd~=2.0.1 # test-utils deps -pytest~=5.3.2 -codecov~=2.0.15 -pytest-cov~=2.8.1 +pytest~=6.2.5 +codecov~=2.1.12 +pytest-cov~=2.12.1 # building documentation -sphinx~=1.8.3 -sphinx_rtd_theme~=0.3.1 -sphinx-jinja~=1.1.0 +sphinx~=4.1.2 +sphinx_rtd_theme~=0.5.2 +sphinx-jinja~=1.1.1 sphinxprettysearchresults~=0.3.5 From 6e45c9f46a75b7dfe4a6fe02433989f9ae4b3cf4 Mon Sep 17 00:00:00 2001 From: TimSangster Date: Fri, 3 Sep 2021 15:22:03 +0200 Subject: [PATCH 29/64] chore: remove codecov --- .travis.yml | 1 - README.md | 1 - requirements.txt | 1 - 3 files changed, 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index 54a9d1fb..194ed22d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,7 +6,6 @@ before_install: - pip3.9 install -r requirements.txt - pip3.9 install -e . script: pytest -m "not compiled" --cov=pythonwhat -after_success: codecov --token=$CODECOV_TOKEN deploy: provider: pypi user: datacamp diff --git a/README.md b/README.md index f60f0ef7..a63083cc 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,6 @@ # pythonwhat [![Build Status](https://travis-ci.org/datacamp/pythonwhat.svg?branch=master)](https://travis-ci.org/datacamp/pythonwhat) -[![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) diff --git a/requirements.txt b/requirements.txt index 359fb6ab..2bf70ddd 100644 --- a/requirements.txt +++ b/requirements.txt @@ -20,7 +20,6 @@ xlrd~=2.0.1 # test-utils deps pytest~=6.2.5 -codecov~=2.1.12 pytest-cov~=2.12.1 # building documentation From cf0bfb7d67b16fe9519d8695094ed730775a2d30 Mon Sep 17 00:00:00 2001 From: TimSangster Date: Tue, 7 Sep 2021 14:00:24 +0200 Subject: [PATCH 30/64] chore: update calls to ast.Module It is now needed to pass a second argument for the type_ignores argument https://bugs.python.org/issue35894 --- pythonwhat/utils_ast.py | 2 +- pythonwhat/utils_env.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pythonwhat/utils_ast.py b/pythonwhat/utils_ast.py index 70a65aa2..e9f301e1 100644 --- a/pythonwhat/utils_ast.py +++ b/pythonwhat/utils_ast.py @@ -4,7 +4,7 @@ def wrap_in_module(node): - new_node = ast.Module(node) + new_node = ast.Module(node, []) if isinstance(node, list): if len(node) > 0: new_node.first_token = node[0].first_token diff --git a/pythonwhat/utils_env.py b/pythonwhat/utils_env.py index eb037fb0..4b4601ad 100644 --- a/pythonwhat/utils_env.py +++ b/pythonwhat/utils_env.py @@ -10,7 +10,7 @@ def assign_from_ast(node, expr): """ if isinstance(expr, str): expr = ast.Name(id=expr, ctx=ast.Load()) - mod = ast.Module([ast.Assign(targets=[node], value=expr)]) + mod = ast.Module([ast.Assign(targets=[node], value=expr)], []) ast.fix_missing_locations(mod) return compile(mod, "", "exec") From 9a71b0bc7cca7ebc273dd58869decbb6ddbbc4df Mon Sep 17 00:00:00 2001 From: TimSangster Date: Tue, 7 Sep 2021 17:58:08 +0200 Subject: [PATCH 31/64] chore: fix updated xl lib, className for pandas ExcelFile --- pythonwhat/converters.py | 1 + requirements.txt | 1 + tests/test_check_object.py | 2 +- 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/pythonwhat/converters.py b/pythonwhat/converters.py index b990c451..16d48ba2 100644 --- a/pythonwhat/converters.py +++ b/pythonwhat/converters.py @@ -6,6 +6,7 @@ def get_manual_converters(): converters = { "pandas.io.excel.ExcelFile": lambda x: x.io, + "pandas.io.excel._base.ExcelFile": lambda x: x.io, "builtins.dict_keys": lambda x: sorted(x), "builtins.dict_items": lambda x: sorted(x), "bs4.BeautifulSoup": lambda x: str(x), diff --git a/requirements.txt b/requirements.txt index 2bf70ddd..c497129d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -17,6 +17,7 @@ requests~=2.26.0 seaborn~=0.11.2 sqlalchemy~=1.4.23 xlrd~=2.0.1 +openpyxl~=3.0.7 # test-utils deps pytest~=6.2.5 diff --git a/tests/test_check_object.py b/tests/test_check_object.py index 1537c05d..8138972a 100644 --- a/tests/test_check_object.py +++ b/tests/test_check_object.py @@ -180,7 +180,7 @@ def test_check_keys_exotic(sct): def test_non_dillable(): - # xlrd needed for Excel support + # xlrd and openpyxl needed for Excel support code = "xl = pd.ExcelFile('battledeath.xlsx')" res = helper.run( { From a4af0b5e24996a2f827065401e9fd4cf07543028 Mon Sep 17 00:00:00 2001 From: TimSangster Date: Fri, 10 Sep 2021 16:51:11 +0200 Subject: [PATCH 32/64] chore: update incorrect tests --- tests/test_check_function.py | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/tests/test_check_function.py b/tests/test_check_function.py index 6bb44d3e..ff902fc8 100644 --- a/tests/test_check_function.py +++ b/tests/test_check_function.py @@ -212,6 +212,7 @@ def check_function_multiple_times(): # Methods --------------------------------------------------------------------- +# Manually create a signature for functions that need self passed def test_method_1(): code = "df.groupby('b').sum()" s = setup_state( @@ -221,26 +222,29 @@ def test_method_1(): ) helper.passes(s.check_function("df.groupby").check_args(0).has_equal_value()) helper.passes(s.check_function("df.groupby.sum", signature=False)) - from pythonwhat.signatures import sig_from_obj - import pandas as pd + import inspect + from inspect import Parameter as param + manual_sig = inspect.Signature([ + param("x", param.POSITIONAL_OR_KEYWORD, default=None), + param("axis", param.POSITIONAL_OR_KEYWORD, default=None) + ]) helper.passes( - s.check_function("df.groupby.sum", signature=sig_from_obj(pd.Series.sum)) + s.check_function("df.groupby.sum", signature=manual_sig) ) def test_method_2(): - code = "df[df.b == 'x'].a.sum()" + code = "print('a')" s = setup_state( sol_code=code, stu_code=code, - pec="import pandas as pd; df = pd.DataFrame({'a': [1, 2, 3], 'b': ['x', 'x', 'y']})", + pec="", ) - helper.passes(s.check_function("df.a.sum", signature=False)) - from pythonwhat.signatures import sig_from_obj - import pandas as pd + helper.passes(s.check_function("print", signature=False)) - helper.passes(s.check_function("df.a.sum", signature=sig_from_obj(pd.Series.sum))) + from pythonwhat.signatures import sig_from_obj + helper.passes(s.check_function("print", signature=sig_from_obj('print'))) from pythonwhat.signatures import sig_from_params, param From 53e88c53fdad31b85705af8472f0830e9901a905 Mon Sep 17 00:00:00 2001 From: TimSangster Date: Tue, 14 Sep 2021 11:29:43 +0200 Subject: [PATCH 33/64] chore: fix warnings declare pytest markers update assert_frame_equal, assert_series_equal import remove array_equal comparison because elementwise comparison can fail here (DeprecationWarning: elementwise comparison failed; this will raise an error in the future.) update incorrect is not comparison --- pytest.ini | 3 +++ pythonwhat/Test.py | 9 ++++----- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/pytest.ini b/pytest.ini index 015f8591..724b2ae5 100644 --- a/pytest.ini +++ b/pytest.ini @@ -1,3 +1,6 @@ [pytest] testpaths = tests/ addopts =-m "not compiled" +markers = + slow + compiled diff --git a/pythonwhat/Test.py b/pythonwhat/Test.py index 50c20f5c..2852462a 100644 --- a/pythonwhat/Test.py +++ b/pythonwhat/Test.py @@ -1,6 +1,7 @@ import re import numpy as np import pandas as pd +from pandas.testing import assert_frame_equal, assert_series_equal from pythonwhat.tasks import * from protowhat.Test import Test @@ -115,8 +116,6 @@ def is_equal(x, y): # Types of errors don't matter (this is debatable) return str(x) == str(y) if areinstance(x, y, (np.ndarray, dict, list, tuple)): - if np.array_equal(x, y): - return True np.testing.assert_equal(x, y) return True elif areinstance(x, y, (map, filter)): @@ -124,12 +123,12 @@ def is_equal(x, y): elif areinstance(x, y, (pd.DataFrame,)): if x.equals(y): return True - pd.util.testing.assert_frame_equal(x, y) + assert_frame_equal(x, y) return True elif areinstance(x, y, (pd.Series,)): if x.equals(y): return True - pd.util.testing.assert_series_equal(x, y) + assert_series_equal(x, y) return True else: return x == y @@ -185,4 +184,4 @@ def test(self): if self.pattern: self.result = re.search(self.search_string, self.string) is not None else: - self.result = self.string.find(self.search_string) is not -1 + self.result = self.string.find(self.search_string) != -1 From 16071dbe90413d72f7e3f442d39d7bac9acda2a2 Mon Sep 17 00:00:00 2001 From: TimSangster Date: Tue, 14 Sep 2021 12:02:17 +0200 Subject: [PATCH 34/64] chore: temporarily skip test until travisci supports more recent python3.9 version --- tests/test_test_with.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_test_with.py b/tests/test_test_with.py index 6e4719aa..f34bd589 100644 --- a/tests/test_test_with.py +++ b/tests/test_test_with.py @@ -244,6 +244,7 @@ def test_test_with_3(sct, passes, patt, lines): helper.with_line_info(res, *lines) +@pytest.mark.skip def test_test_with_destructuring(): code = """ with A() as (one, *others): From 928c519987ff31271fe3a36594d5a4db087f4100 Mon Sep 17 00:00:00 2001 From: TimSangster Date: Thu, 16 Sep 2021 10:24:25 +0200 Subject: [PATCH 35/64] chore: update pypi password --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 194ed22d..8e686742 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,7 +10,7 @@ deploy: provider: pypi user: datacamp password: - secure: oyhm/R427TtmjJFMlvxQAR3UK5CA3vRSQd7CTtGTkL3W2zoZsPI7p3XbJn4CwC1f1XPdAbfN/Km035Fn4NlVHwJT54NTGkcy8GPV8whxEvWptiA1XWO6uwIqHh7rF/cVhX21AYXWAYkFq0Y0w1+q3OK75czMBrclwn9t+m111uoEUdZyowI5EVXbTHfzpNrRY3wA+tVWdz3Yd4IIC5foFsMP28j/5JCuMWXT1Knf53ZaoTSpyoEllAe/8sP/3K6/adgiwz5vL6kTFtSeuX0EQ6uvlXWyP4x8APNcnfVKxEC16iwakOgVzhw4n3uMsQ6tgo11w+A2crJU1N8KxdZtG8gAw42jWSZMmB3SHHx0NQhi71SvrjYXwDwe8bOvRFDHiR+2WQzoYC4WJUyn4WYpScazkQpeqYdoOTBnAr1VNzZHOQNwejX5KpLP15oXj6ima/lqefQWnIRixeBkJibSTdRl2HlvdPzP75DSjg8eA0Xt8/ad2oWV4om9zIUwAkJrnzih3ZsnshSqVZcxpsRDZvV2LXz39JzlNECqxfLGF+Oy6M8QQXnALzYuHhs/BgzPlLdQw8QLwHPqvv38LNfrWo36jR3j9UYu1/45LZauIEJuLGCyPclT1PDnAtxH3SGCEi8aAODzAi2qptbBHxJHjsRcVeMx6Oiz4AmEuxT4QRU= + secure: JrQnpSi03zbBXpRPYydm1K3539UYsn8tefJtWOm6mnoSl0/zeFU7s5vsVCu5LngmM3O1GzpfVQCh9BUC/1u+oDr7NpQ6zASBbNkwyb9sWj2eH/6fOBkLPhOIBspBTDr+LYOAvZC4+fr/7tb7OtiMzTKeAlRJRkj1LUmEbD8sbBdn5msDfY14AmhtTfA4MibsBlF3uzIDgpy4HaIJRw/YgZXcasB83Mxv5g242FHrCNUmbWJkaiSb+CRwcLqpCYg5B1hNLKA61IBi4Cpk6iM2XsP0LTk+TmS6hPZi3Qbdm6knkhhElbmKyXvANFrswrd4HbR8tRMBcOR/APlLqdVm6aKXF0E20rkcpVEdseSssJ/O+q0EiRe3033nupNseLNmQcQdGeOAoOIOkcKMySrM4eUK2XjZ4M+Uw9arZ+creMiIhzMYOoMnCrkXlyQkyz5X2qDW+HyTgl0aLUaOKGNaoJ7UJ78sGihx34tFaxI9sB6InZ3wF/lj1KbvQG66gMjIRVZ20AqLKWvUYCED6DzgPQ+T5IVG4GDezUSV8J/XZGDUnkN1z0zeQZo3UTinicriRzYgpzYhs4iZ1fXo4pw26xk69SQZauxr5jYrTWTygKvyPK6Qnpq7e4EXGYDQHNVevyYFic/kAvP+Aofly/38iK59AjBiKGeUr7SYTdag9N4= on: tags: true distributions: sdist bdist_wheel From 82c27f20eb0ebde39200101e1343a0dba28069a8 Mon Sep 17 00:00:00 2001 From: TimSangster Date: Thu, 16 Sep 2021 10:55:23 +0200 Subject: [PATCH 36/64] chore: bump version, update changelog --- CHANGELOG.md | 5 +++++ pythonwhat/__init__.py | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7738b79e..cd1ab6fc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,11 @@ 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.24.0 + +- Support Python version 3.9 +- Update dependencies to up-to-date versions + ## 2.23.2 - Update behaviour of has_expr() default feedback message. If the student's evaluation is too long, it is now shortened and an ellipsis is added. diff --git a/pythonwhat/__init__.py b/pythonwhat/__init__.py index e30b3e2a..3679b143 100644 --- a/pythonwhat/__init__.py +++ b/pythonwhat/__init__.py @@ -1,3 +1,3 @@ -__version__ = "2.23.1" +__version__ = "2.24.0" from .test_exercise import test_exercise, allow_errors From 5ee3121da3be8158ee0a91f99d8216344571e78c Mon Sep 17 00:00:00 2001 From: TimSangster Date: Thu, 16 Sep 2021 11:18:21 +0200 Subject: [PATCH 37/64] chore: update pypi password --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 8e686742..2add2bd2 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,7 +10,7 @@ deploy: provider: pypi user: datacamp password: - secure: JrQnpSi03zbBXpRPYydm1K3539UYsn8tefJtWOm6mnoSl0/zeFU7s5vsVCu5LngmM3O1GzpfVQCh9BUC/1u+oDr7NpQ6zASBbNkwyb9sWj2eH/6fOBkLPhOIBspBTDr+LYOAvZC4+fr/7tb7OtiMzTKeAlRJRkj1LUmEbD8sbBdn5msDfY14AmhtTfA4MibsBlF3uzIDgpy4HaIJRw/YgZXcasB83Mxv5g242FHrCNUmbWJkaiSb+CRwcLqpCYg5B1hNLKA61IBi4Cpk6iM2XsP0LTk+TmS6hPZi3Qbdm6knkhhElbmKyXvANFrswrd4HbR8tRMBcOR/APlLqdVm6aKXF0E20rkcpVEdseSssJ/O+q0EiRe3033nupNseLNmQcQdGeOAoOIOkcKMySrM4eUK2XjZ4M+Uw9arZ+creMiIhzMYOoMnCrkXlyQkyz5X2qDW+HyTgl0aLUaOKGNaoJ7UJ78sGihx34tFaxI9sB6InZ3wF/lj1KbvQG66gMjIRVZ20AqLKWvUYCED6DzgPQ+T5IVG4GDezUSV8J/XZGDUnkN1z0zeQZo3UTinicriRzYgpzYhs4iZ1fXo4pw26xk69SQZauxr5jYrTWTygKvyPK6Qnpq7e4EXGYDQHNVevyYFic/kAvP+Aofly/38iK59AjBiKGeUr7SYTdag9N4= + secure: YSxFsCIO8OW/8NyVtVPjvWceAscKcjX3PpDqU8/GwP6TkpE/ORZbc2Gc5zZz6arq/zormtGvv85fxc+mub9GqRdavPtNDaeq1QgzDIWfXOHAoQ5jWBwfXQz/MvP9YS3yk5uex1usga/nlm1Q6Am7RwYnttpNv3TRRJbDDMOc9wzYa+lHYZBlvaispewgusz6uXzeO3M1Aj+KwNSfwMU8yAJNhFGG5mk329yv8EGKMXO9nuTYOLm3JF1E1aiZD0bcVDETvTGq9GyWQG+Od3zvXDpQW0B0rHgRmXCH7AVdHIAxletYf7jEhFmNh+kn8yXxtcnuRo6E90HMGPGU91ErF5NDWpOlCTbdSNj2oGHDZFWiIw717RBWMz7MyfV8aeseL6HKNu+jnDPm+1ANSpzI3V+DUexsKynPDzRT2kZsM5jSxyuncx8oNsmaqt9eRL9vOhzD4tBP6oRI8tPtQMB/4zXv12ihFYc5fYARRgx5jlAxqwYfwvsdF49Sna9XGiubQeJOPs1tDni/eXZrr7ey0Mj8r7eJyxqXcsXqYq1F4jDyijubGcYJyhPXU9F64vlPel/Kvg7Z5c0XOu/6yptoVaapSM3v7NYk65hvGq+GBtX4IexwaZousOVeHMHavXVU5HL4+t7UrFxLKnsZIGZtRVT2nuUV5P/fcUjeF0uw70A= on: tags: true distributions: sdist bdist_wheel From edd74f758f168783f30f8e8525cdf23b864e3fc1 Mon Sep 17 00:00:00 2001 From: TimSangster Date: Thu, 16 Sep 2021 11:18:21 +0200 Subject: [PATCH 38/64] chore: move ci to circleci --- .circleci/config.yml | 40 ++++++++++++++++++++++++++++++++++++++++ .travis.yml | 18 ------------------ 2 files changed, 40 insertions(+), 18 deletions(-) create mode 100644 .circleci/config.yml delete mode 100644 .travis.yml diff --git a/.circleci/config.yml b/.circleci/config.yml new file mode 100644 index 00000000..f0bfc4e5 --- /dev/null +++ b/.circleci/config.yml @@ -0,0 +1,40 @@ +version: 2.1 + +orbs: + python: circleci/python@1.2 + +jobs: + build-and-test: + docker: + - image: cimg/python:3.9 + steps: + - checkout + - python/install-packages: + pkg-manager: pip + - run: + name: Run tests + command: pytest -m "not compiled" --cov=pythonwhat + publish: + docker: + - image: cimg/python:3.9 + steps: + - checkout + - run: + command: | + python setup.py sdist bdist_wheel + pip install pipenv + pipenv install twine + pipenv run twine upload --verbose --repository pypi dist/* + +workflows: + build: + jobs: + - build-and-test + - publish: + requires: + - build-and-test + filters: + tags: + only: /^.*#v\d+\.\d+\.\d+(-rc\.\d+)?$/ + branches: + ignore: /.*/ diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 2add2bd2..00000000 --- a/.travis.yml +++ /dev/null @@ -1,18 +0,0 @@ -sudo: false -language: python -python: -- '3.9' -before_install: -- pip3.9 install -r requirements.txt -- pip3.9 install -e . -script: pytest -m "not compiled" --cov=pythonwhat -deploy: - provider: pypi - user: datacamp - password: - secure: YSxFsCIO8OW/8NyVtVPjvWceAscKcjX3PpDqU8/GwP6TkpE/ORZbc2Gc5zZz6arq/zormtGvv85fxc+mub9GqRdavPtNDaeq1QgzDIWfXOHAoQ5jWBwfXQz/MvP9YS3yk5uex1usga/nlm1Q6Am7RwYnttpNv3TRRJbDDMOc9wzYa+lHYZBlvaispewgusz6uXzeO3M1Aj+KwNSfwMU8yAJNhFGG5mk329yv8EGKMXO9nuTYOLm3JF1E1aiZD0bcVDETvTGq9GyWQG+Od3zvXDpQW0B0rHgRmXCH7AVdHIAxletYf7jEhFmNh+kn8yXxtcnuRo6E90HMGPGU91ErF5NDWpOlCTbdSNj2oGHDZFWiIw717RBWMz7MyfV8aeseL6HKNu+jnDPm+1ANSpzI3V+DUexsKynPDzRT2kZsM5jSxyuncx8oNsmaqt9eRL9vOhzD4tBP6oRI8tPtQMB/4zXv12ihFYc5fYARRgx5jlAxqwYfwvsdF49Sna9XGiubQeJOPs1tDni/eXZrr7ey0Mj8r7eJyxqXcsXqYq1F4jDyijubGcYJyhPXU9F64vlPel/Kvg7Z5c0XOu/6yptoVaapSM3v7NYk65hvGq+GBtX4IexwaZousOVeHMHavXVU5HL4+t7UrFxLKnsZIGZtRVT2nuUV5P/fcUjeF0uw70A= - on: - tags: true - distributions: sdist bdist_wheel - repo: datacamp/pythonwhat - skip_upload_docs: true From 2242933dbe93e320cc11123455a62f07e19388e3 Mon Sep 17 00:00:00 2001 From: Ewald Date: Tue, 1 Feb 2022 11:08:22 +0100 Subject: [PATCH 39/64] chore: bump version --- pythonwhat/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pythonwhat/__init__.py b/pythonwhat/__init__.py index 3679b143..ee49a479 100644 --- a/pythonwhat/__init__.py +++ b/pythonwhat/__init__.py @@ -1,3 +1,3 @@ -__version__ = "2.24.0" +__version__ = "2.24.1" from .test_exercise import test_exercise, allow_errors From 21bdc32a2705915e84212706d294f9381b6b41de Mon Sep 17 00:00:00 2001 From: Ewald Date: Tue, 1 Feb 2022 11:19:29 +0100 Subject: [PATCH 40/64] fix: circleci config tag regex --- .circleci/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index f0bfc4e5..602ee7d5 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -35,6 +35,6 @@ workflows: - build-and-test filters: tags: - only: /^.*#v\d+\.\d+\.\d+(-rc\.\d+)?$/ + only: /^v\d+\.\d+\.\d+(-rc\.\d+)?$/ branches: ignore: /.*/ From c07a1c2fa2f4bba9a70e25e9d5aeaeb2070cfa7d Mon Sep 17 00:00:00 2001 From: Ewald Date: Wed, 2 Feb 2022 13:32:52 +0100 Subject: [PATCH 41/64] fix: ci config regex Adding the tag filter to the build-and-test job because it is required for the publish job and it won't run otherwise. Adapted the regex to not include rc tags. --- .circleci/config.yml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 602ee7d5..80b32bed 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -29,12 +29,15 @@ jobs: workflows: build: jobs: - - build-and-test + - build-and-test: + filters: + tags: + only: /^v\d+\.\d+\.\d+$/ - publish: requires: - build-and-test filters: tags: - only: /^v\d+\.\d+\.\d+(-rc\.\d+)?$/ + only: /^v\d+\.\d+\.\d+$/ branches: ignore: /.*/ From 0c4aef4f2acb159ae62444e12dd4a1b74d6abdc1 Mon Sep 17 00:00:00 2001 From: Bogdan Floris Date: Thu, 29 Dec 2022 18:50:09 +0200 Subject: [PATCH 42/64] feat: add multi line code formatting for has_equal_ast --- pythonwhat/__init__.py | 2 +- pythonwhat/checks/has_funcs.py | 35 +++++++++++++++------- pythonwhat/utils.py | 15 ++++++++++ requirements.txt | 7 +++-- tests/test_spec.py | 54 ++++++++++++++++++++++++++++++++++ 5 files changed, 99 insertions(+), 14 deletions(-) diff --git a/pythonwhat/__init__.py b/pythonwhat/__init__.py index ee49a479..5809a0fe 100644 --- a/pythonwhat/__init__.py +++ b/pythonwhat/__init__.py @@ -1,3 +1,3 @@ -__version__ = "2.24.1" +__version__ = "2.24.2" from .test_exercise import test_exercise, allow_errors diff --git a/pythonwhat/checks/has_funcs.py b/pythonwhat/checks/has_funcs.py index 0ccfaaa4..004b72b2 100644 --- a/pythonwhat/checks/has_funcs.py +++ b/pythonwhat/checks/has_funcs.py @@ -167,7 +167,11 @@ def has_equal_ast(state, incorrect_msg=None, code=None, exact=True, append=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 = "Expected `{{sol_str}}`, but got `{{stu_str}}`." + incorrect_msg = ( + "Expected \n```\n{{sol_str}}\n```\n, but got \n```\n{{stu_str}}\n```\n" + if utils.is_multiline_code(state.student_code, state.solution_code) + else "Expected `{{sol_str}}`, but got `{{stu_str}}`." + ) def parse_tree(tree): # get contents of module.body if only 1 element @@ -183,10 +187,18 @@ def parse_tree(tree): stu_rep = parse_tree(state.student_ast) sol_rep = parse_tree(state.solution_ast if not code else ast.parse(code)) - fmt_kwargs = { - "sol_str": state.solution_code if not code else code, - "stu_str": state.student_code, - } + if utils.is_multiline_code(state.student_code, state.solution_code): + fmt_kwargs = { + "sol_str": utils.format_code(state.solution_code) + if not code + else utils.format_code(code), + "stu_str": utils.format_code(state.student_code), + } + else: + fmt_kwargs = { + "sol_str": state.solution_code if not code else code, + "stu_str": state.student_code, + } if exact and not code: state.do_test( @@ -345,19 +357,20 @@ def has_expr( # wrap in quotes if eval_sol or eval_stu are strings if test == "value": if isinstance(eval_stu, str): - fmt_kwargs["stu_eval"] = '\'{}\''.format(fmt_kwargs["stu_eval"]) + fmt_kwargs["stu_eval"] = "'{}'".format(fmt_kwargs["stu_eval"]) if isinstance(eval_sol, str): - fmt_kwargs["sol_eval"] = '\'{}\''.format(fmt_kwargs["sol_eval"]) + fmt_kwargs["sol_eval"] = "'{}'".format(fmt_kwargs["sol_eval"]) # reformat student evaluation string if it is too long fmt_kwargs["stu_eval"] = utils.shorten_string(fmt_kwargs["stu_eval"]) # check if student or solution evaluations are too long or contain newlines if incorrect_msg == DEFAULT_INCORRECT_MSG and ( - len(fmt_kwargs["sol_eval"]) > 50 or - utils.has_newline(fmt_kwargs["stu_eval"]) or - utils.has_newline(fmt_kwargs["sol_eval"]) or - fmt_kwargs["stu_eval"] == fmt_kwargs["sol_eval"]): + len(fmt_kwargs["sol_eval"]) > 50 + or utils.has_newline(fmt_kwargs["stu_eval"]) + or utils.has_newline(fmt_kwargs["sol_eval"]) + or fmt_kwargs["stu_eval"] == fmt_kwargs["sol_eval"] + ): fmt_kwargs["stu_eval"] = None fmt_kwargs["sol_eval"] = None incorrect_msg = "Expected something different." diff --git a/pythonwhat/utils.py b/pythonwhat/utils.py index 0e9cb3af..f25010be 100644 --- a/pythonwhat/utils.py +++ b/pythonwhat/utils.py @@ -1,6 +1,19 @@ from types import ModuleType import copy import os +import black + + +def format_code(text): + mode = black.FileMode() + try: + return black.format_file_contents(text, fast=True, mode=mode) + except black.NothingChanged: + return text + + +def is_multiline_code(stu_code: str, sol_code: str) -> bool: + return has_newline(stu_code) or has_newline(sol_code) def include_v1(): @@ -10,11 +23,13 @@ def include_v1(): def v2_only(): return not include_v1() + def shorten_string(text): if len(text) > 50: text = text[0:45] + "..." return text + def has_newline(text): return "\n" in text diff --git a/requirements.txt b/requirements.txt index c497129d..4f813d83 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,9 +1,12 @@ # pythonwhat deps -protowhat~=2.1.0 +protowhat~=2.1.3 asttokens~=2.0.5 dill~=0.3.4 -markdown2~=2.3.10 +markdown2~=2.4.6 jinja2~=2.11.3 +markupsafe==2.0.1 +black==19.10b0 +Pygments==2.13.0 numpy~=1.19.5 pandas~=1.3.2 diff --git a/tests/test_spec.py b/tests/test_spec.py index 4bce58eb..04db0fe7 100644 --- a/tests/test_spec.py +++ b/tests/test_spec.py @@ -137,6 +137,60 @@ def test_has_equal_ast_simple_pass(data): assert sct_payload["correct"] +def test_has_equal_ast_formatting(data): + data["DC_CODE"] = """ +def car_wash(env): + car_wash_num = 0 + while True: + car_wash_num += 2 + + # Get the current simulation time and clock-in the process time + yield env.timeout(5) + +a = 2 +b = 3 +""" + data["DC_SOLUTION"] = """ +def car_wash(env): + car_wash_num = 0 + while True: + car_wash_num += 1 + + # Get the current simulation time and clock-in the process time + yield env.timeout(5) + +a = 2 +b = 3 +""" + data["DC_SCT"] = """ +Ex().check_function_def("car_wash").multi( + check_body().check_while().has_equal_ast() +) +""" + sct_payload = helper.run(data) + assert not sct_payload["correct"] + incorrect_msg = """Did you correctly specify the body? Check the first while loop. Expected + +
while True:
+    car_wash_num += 1
+
+    # Get the current simulation time and clock-in the process time
+    yield env.timeout(5)
+
+
+ +, but got + +
while True:
+    car_wash_num += 2
+
+    # Get the current simulation time and clock-in the process time
+    yield env.timeout(5)
+
+
""" + assert sct_payload["message"] == incorrect_msg + + def test_has_equal_ast_simple_fail(data): data["DC_SCT"] = "Ex().has_equal_ast()" failing_submission(data) From 3d47e3922ae4f194025b1fbb08514f009657cd49 Mon Sep 17 00:00:00 2001 From: Bogdan Floris Date: Thu, 5 Jan 2023 12:40:22 +0200 Subject: [PATCH 43/64] fix: catch black formatting error --- pythonwhat/utils.py | 2 +- tests/test_spec.py | 63 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+), 1 deletion(-) diff --git a/pythonwhat/utils.py b/pythonwhat/utils.py index f25010be..31b22fe1 100644 --- a/pythonwhat/utils.py +++ b/pythonwhat/utils.py @@ -8,7 +8,7 @@ def format_code(text): mode = black.FileMode() try: return black.format_file_contents(text, fast=True, mode=mode) - except black.NothingChanged: + except (black.NothingChanged, black.InvalidInput, black.CannotSplit): return text diff --git a/tests/test_spec.py b/tests/test_spec.py index 04db0fe7..299fc441 100644 --- a/tests/test_spec.py +++ b/tests/test_spec.py @@ -191,6 +191,69 @@ def car_wash(env): assert sct_payload["message"] == incorrect_msg +def test_black_formatting_errors_catch(data): + data["DC_CODE"] = """ +# Define a function called plot_timeseries +def plot_timeseries(axes, x, y, color, xlabel, ylabel): + + # Plot the inputs x,y in the provided color + axes.plot(x, y, color=color) + + # Set the x-axis label + axes.set_xlabel(xlabel) + + # Set the y-axis label + axes.set_ylabel(ylabel, color=color) + + # Set the colors tick params for y-axis + axes.tick_params('y', colors=color) +""" + data["DC_SOLUTION"] = """ +# Define a function called plot_timeseries +def plot_timeseries(axes, x, y, color, xlabel, ylabel): + + # Plot the inputs x,y in the provided color + axes.plot(x, y, color=color) + + # Set the x-axis label + axes.set_xlabel(xlabel) + + # Set the y-axis label + axes.set_ylabel(ylabel, color=color) + + # Set the colors tick params for y-axis + axes.tick_params('y', colors=color) +""" + data["DC_SCT"] = """ +msg1 = "Did you plot the x and y in the provided color?" +msg2 = "Did you set the x-axis label?" +msg3 = "Did you set the y-axis label?" +msg4 = "Did you set the colors tick params for y-axis?" + +Ex().check_function_def("plot_timeseries").check_body().multi( + check_or( + has_equal_ast(msg1, "axes.plot(x, y, color=color)", exact = False), + has_equal_ast(msg1, "axes.plot(x, y, c=color)", exact = False) + ), + check_or( + has_equal_ast(msg2, "axes.set_xlabel(xlabel)", exact = False), + has_equal_ast(msg2, "axes.set_xlabel(xlabel=xlabel)", exact = False) + ), + check_or( + has_equal_ast(msg3, "axes.set_ylabel(ylabel, color=color)", exact = False), + has_equal_ast(msg3, "axes.set_ylabel(ylabel, c=color)", exact = False), + has_equal_ast(msg3, "axes.set_ylabel(ylabel=ylabel, color=color)", exact = False), + has_equal_ast(msg3, "axes.set_ylabel(ylabel=ylabel, c=color)", exact = False) + ), + has_equal_ast(msg4, "axes.tick_params('y', colors=color)", exact = False), +) + +success_msg("Very good. Next, let's use this function!") +""" + sct_payload = helper.run(data) + assert sct_payload["correct"] + + def test_has_equal_ast_simple_fail(data): data["DC_SCT"] = "Ex().has_equal_ast()" failing_submission(data) From bbc3c369059b522bf33b58f786fefc1b1569dee2 Mon Sep 17 00:00:00 2001 From: Bogdan Floris Date: Thu, 5 Jan 2023 12:45:30 +0200 Subject: [PATCH 44/64] chore: bump version --- pythonwhat/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pythonwhat/__init__.py b/pythonwhat/__init__.py index 5809a0fe..551fcbec 100644 --- a/pythonwhat/__init__.py +++ b/pythonwhat/__init__.py @@ -1,3 +1,3 @@ -__version__ = "2.24.2" +__version__ = "2.24.3" from .test_exercise import test_exercise, allow_errors From bc3c1eba4435ee83424fcfb4c82ec17c8c80a67d Mon Sep 17 00:00:00 2001 From: Bogdan Floris Date: Fri, 6 Jan 2023 10:32:00 +0200 Subject: [PATCH 45/64] fix: catch IndentationError --- pythonwhat/__init__.py | 2 +- pythonwhat/utils.py | 4 ++-- tests/test_spec.py | 2 -- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/pythonwhat/__init__.py b/pythonwhat/__init__.py index 551fcbec..d2ac3ca9 100644 --- a/pythonwhat/__init__.py +++ b/pythonwhat/__init__.py @@ -1,3 +1,3 @@ -__version__ = "2.24.3" +__version__ = "2.24.4" from .test_exercise import test_exercise, allow_errors diff --git a/pythonwhat/utils.py b/pythonwhat/utils.py index 31b22fe1..bc33d6ca 100644 --- a/pythonwhat/utils.py +++ b/pythonwhat/utils.py @@ -7,8 +7,8 @@ def format_code(text): mode = black.FileMode() try: - return black.format_file_contents(text, fast=True, mode=mode) - except (black.NothingChanged, black.InvalidInput, black.CannotSplit): + return black.format_file_contents(text, fast=True, mode=mode).rstrip() + except (black.NothingChanged, black.InvalidInput, IndentationError): return text diff --git a/tests/test_spec.py b/tests/test_spec.py index 299fc441..310ff764 100644 --- a/tests/test_spec.py +++ b/tests/test_spec.py @@ -176,7 +176,6 @@ def car_wash(env): # Get the current simulation time and clock-in the process time yield env.timeout(5) - , but got @@ -186,7 +185,6 @@ def car_wash(env): # Get the current simulation time and clock-in the process time yield env.timeout(5) - """ assert sct_payload["message"] == incorrect_msg From 8bb63dd88f3126863bb8cb471157956607f7b4db Mon Sep 17 00:00:00 2001 From: James Addison Date: Wed, 31 Jan 2024 15:18:04 +0000 Subject: [PATCH 46/64] docs: cleanup: remove redundant sphinxprettysearchresults Sphinx extension This extension served as a workaround for non-ideal search result formatting in Sphinx versions earlier than v2.0.0 and is no longer required. --- docs/conf.py | 1 - requirements.txt | 1 - 2 files changed, 2 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 46ddd4cd..bace3095 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -53,7 +53,6 @@ "sphinx.ext.autodoc", "sphinx.ext.napoleon", "sphinxcontrib.jinja", - "sphinxprettysearchresults", ] # Add any paths that contain templates here, relative to this directory. diff --git a/requirements.txt b/requirements.txt index 4f813d83..966dccd1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -30,4 +30,3 @@ pytest-cov~=2.12.1 sphinx~=4.1.2 sphinx_rtd_theme~=0.5.2 sphinx-jinja~=1.1.1 -sphinxprettysearchresults~=0.3.5 From e0df02858684622483f363620bd0fde2ab4db9ab Mon Sep 17 00:00:00 2001 From: Rik Bauwens Date: Sat, 7 Sep 2024 10:29:45 +0200 Subject: [PATCH 47/64] chore: split and update test and docs dependencies --- .circleci/config.yml | 1 + README.md | 2 +- requirements-docs.txt | 5 +++++ requirements-test.txt | 16 ++++++++++++++++ requirements.txt | 34 +++++++--------------------------- 5 files changed, 30 insertions(+), 28 deletions(-) create mode 100644 requirements-docs.txt create mode 100644 requirements-test.txt diff --git a/.circleci/config.yml b/.circleci/config.yml index 80b32bed..f968f675 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -10,6 +10,7 @@ jobs: steps: - checkout - python/install-packages: + pip-dependency-file: requirements-test.txt pkg-manager: pip - run: name: Run tests diff --git a/README.md b/README.md index a63083cc..7e07f0fe 100644 --- a/README.md +++ b/README.md @@ -54,7 +54,7 @@ To learn how to include an SCT in a DataCamp course, visit https://instructor-su ```bash pyenv local 3.9.6 -pip3.9 install -r requirements.txt +pip3.9 install -r requirements-test.txt pip3.9 install -e . pytest ``` diff --git a/requirements-docs.txt b/requirements-docs.txt new file mode 100644 index 00000000..7788bc32 --- /dev/null +++ b/requirements-docs.txt @@ -0,0 +1,5 @@ +-r requirements.txt + +sphinx~=4.1.2 +sphinx_rtd_theme~=0.5.2 +sphinx-jinja~=1.1.1 diff --git a/requirements-test.txt b/requirements-test.txt new file mode 100644 index 00000000..c1a63d96 --- /dev/null +++ b/requirements-test.txt @@ -0,0 +1,16 @@ +-r requirements.txt + +# test deps +scipy~=1.13.1 +bs4~=0.0.1 +html5lib~=1.1 +# h5py~=3.1.0 +requests~=2.26.0 +seaborn~=0.11.2 +sqlalchemy~=1.4.23 +xlrd~=2.0.1 +openpyxl~=3.0.7 + +# test-utils deps +pytest~=6.2.5 +pytest-cov~=2.12.1 diff --git a/requirements.txt b/requirements.txt index 966dccd1..4536e838 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,32 +1,12 @@ # pythonwhat deps -protowhat~=2.1.3 -asttokens~=2.0.5 +protowhat~=2.2.0 +asttokens~=2.4.1 dill~=0.3.4 -markdown2~=2.4.6 -jinja2~=2.11.3 -markupsafe==2.0.1 +markdown2~=2.4.13 +jinja2~=3.1.3 +markupsafe==2.1.5 black==19.10b0 Pygments==2.13.0 -numpy~=1.19.5 -pandas~=1.3.2 - -# test deps -scipy~=1.7.1 -bs4~=0.0.1 -html5lib~=1.1 -h5py~=3.1.0 -requests~=2.26.0 -seaborn~=0.11.2 -sqlalchemy~=1.4.23 -xlrd~=2.0.1 -openpyxl~=3.0.7 - -# test-utils deps -pytest~=6.2.5 -pytest-cov~=2.12.1 - -# building documentation -sphinx~=4.1.2 -sphinx_rtd_theme~=0.5.2 -sphinx-jinja~=1.1.1 +numpy~=1.26.0 +pandas~=1.5.3 From 90793f9d4c25edaec959799359d137ad459f9b47 Mon Sep 17 00:00:00 2001 From: Rik Bauwens Date: Mon, 9 Sep 2024 13:10:17 +0200 Subject: [PATCH 48/64] chore: bump version --- pythonwhat/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pythonwhat/__init__.py b/pythonwhat/__init__.py index d2ac3ca9..b225565e 100644 --- a/pythonwhat/__init__.py +++ b/pythonwhat/__init__.py @@ -1,3 +1,3 @@ -__version__ = "2.24.4" +__version__ = "2.25.0" from .test_exercise import test_exercise, allow_errors From 50973f92409c0e0c4204e2ef0cab3d838a6e38ef Mon Sep 17 00:00:00 2001 From: Rik Bauwens Date: Sat, 7 Sep 2024 11:46:47 +0200 Subject: [PATCH 49/64] chore: fix typo's, use raw strings, update varnames --- pythonwhat/checks/has_funcs.py | 2 +- pythonwhat/sct_syntax.py | 2 +- pythonwhat/tasks.py | 2 +- pythonwhat/test_exercise.py | 4 ++-- pythonwhat/utils.py | 3 +-- tests/test_has_output.py | 18 +++++++++--------- 6 files changed, 15 insertions(+), 16 deletions(-) diff --git a/pythonwhat/checks/has_funcs.py b/pythonwhat/checks/has_funcs.py index 004b72b2..870e1069 100644 --- a/pythonwhat/checks/has_funcs.py +++ b/pythonwhat/checks/has_funcs.py @@ -579,7 +579,7 @@ def has_import( def has_output(state, text, pattern=True, no_output_msg=None): - """Search student output for a pattern. + r"""Search student output for a pattern. 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. diff --git a/pythonwhat/sct_syntax.py b/pythonwhat/sct_syntax.py index df846ee7..2ff2af6c 100644 --- a/pythonwhat/sct_syntax.py +++ b/pythonwhat/sct_syntax.py @@ -19,7 +19,7 @@ def wrapper(*args, **kwargs): args[0] if len(args) == 1 and isinstance(args[0], (list, tuple)) else args ) for arg in args: - if isinstance(arg, Node) and arg.parent.name is "root": + if isinstance(arg, Node) and arg.parent.name == "root": arg.parent.remove_child(arg) arg.update_child_calls() return f(*args, **kwargs) diff --git a/pythonwhat/tasks.py b/pythonwhat/tasks.py index 9c6d25e5..35c87ace 100644 --- a/pythonwhat/tasks.py +++ b/pythonwhat/tasks.py @@ -370,7 +370,7 @@ def taskRunEval( Args: tree (ast): current focused ast, used to get code to execute process: manages shell (see local.py) - shell: link to to get process namespace from execution up until now + shell: link to get process namespace from execution up until now env: update value in focused code by name extra_env: variables to be replaced in focused code by name from extra_env in has_expr context: sum of set_context in sct chain diff --git a/pythonwhat/test_exercise.py b/pythonwhat/test_exercise.py index 9bce76e2..68d6da8b 100644 --- a/pythonwhat/test_exercise.py +++ b/pythonwhat/test_exercise.py @@ -22,7 +22,7 @@ def test_exercise( """ Point of interaction with the Python backend. Args: - sct (str): The solution corectness test as a string of code. + sct (str): The solution correctness test as a string of code. student_code (str): The code which is entered by the student. solution_code (str): The code which is in the solution. pre_exercise_code (str): The code which is executed pre exercise. @@ -73,7 +73,7 @@ def test_exercise( # TODO: consistent success_msg def success_msg(message): """ - Set the succes message of the sct. This message will be the feedback if all tests pass. + Set the success message of the sct. This message will be the feedback if all tests pass. Args: message (str): A string containing the feedback message. """ diff --git a/pythonwhat/utils.py b/pythonwhat/utils.py index bc33d6ca..1f9c3378 100644 --- a/pythonwhat/utils.py +++ b/pythonwhat/utils.py @@ -1,10 +1,9 @@ from types import ModuleType import copy import os -import black - def format_code(text): + import black mode = black.FileMode() try: return black.format_file_contents(text, fast=True, mode=mode).rstrip() diff --git a/tests/test_has_output.py b/tests/test_has_output.py index 96f01591..a0cd76e3 100644 --- a/tests/test_has_output.py +++ b/tests/test_has_output.py @@ -4,42 +4,42 @@ @pytest.mark.parametrize( - "stu, passes", + "student_code, passes", [ ('print("Hi, there!")', True), ('print("hi there!")', True), ('print("Hello there")', False), ], ) -def test_has_output_basic(stu, passes): - s = setup_state(stu, "") +def test_has_output_basic(student_code, passes): + s = setup_state(student_code, "") with helper.verify_sct(passes): s.has_output(r"[H|h]i,*\s+there!") @pytest.mark.parametrize( - "stu, passes", + "student_code, passes", [ ('print("Hi, there!")', True), ('print("hi there!")', False), ('print("Hello there")', False), ], ) -def test_has_output_pattern(stu, passes): - s = setup_state(stu, "") +def test_has_output_pattern(student_code, passes): + s = setup_state(student_code, "") with helper.verify_sct(passes): s.has_output("Hi, there!", pattern=False) @pytest.mark.parametrize( - "stu, passes", + "student_code, passes", [ ('print("Hi, there!")', True), ('print("hi there!")', True), ('print("Hello there")', False), ], ) -def test_test_output_contains(stu, passes): - s = setup_state(stu, "") +def test_test_output_contains(student_code, passes): + s = setup_state(student_code, "") with helper.verify_sct(passes): s.test_output_contains(r"[H|h]i,*\s+there!") From 030fd312e20a0354ca6cc383e99301adc9147099 Mon Sep 17 00:00:00 2001 From: Rik Bauwens Date: Mon, 9 Sep 2024 16:25:45 +0200 Subject: [PATCH 50/64] chore: org-global-context --- .circleci/config.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.circleci/config.yml b/.circleci/config.yml index f968f675..968df0b9 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -31,10 +31,12 @@ workflows: build: jobs: - build-and-test: + context: org-global filters: tags: only: /^v\d+\.\d+\.\d+$/ - publish: + context: org-global requires: - build-and-test filters: From 16e8837c1a0c472066b072646ec5c58fe93ed361 Mon Sep 17 00:00:00 2001 From: Rik Bauwens Date: Wed, 11 Sep 2024 16:47:08 +0200 Subject: [PATCH 51/64] chore: fix failing test --- requirements-test.txt | 1 + tests/helper.py | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-test.txt b/requirements-test.txt index c1a63d96..74e73265 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -10,6 +10,7 @@ seaborn~=0.11.2 sqlalchemy~=1.4.23 xlrd~=2.0.1 openpyxl~=3.0.7 +h5py~=3.11.0 # test-utils deps pytest~=6.2.5 diff --git a/tests/helper.py b/tests/helper.py index 9e32b672..d4dad846 100644 --- a/tests/helper.py +++ b/tests/helper.py @@ -51,7 +51,6 @@ def in_temp_dir(): def run(data, run_code=True): - pec = data.get("DC_PEC", "") stu_code = data.get("DC_CODE", "") sol_code = data.get("DC_SOLUTION", "") From c3bfa0411a52a2035e43cdc2061856537d5bf1db Mon Sep 17 00:00:00 2001 From: Rik Bauwens Date: Wed, 27 Nov 2024 12:12:05 +0100 Subject: [PATCH 52/64] feat: lazy import pandas --- pythonwhat/Test.py | 4 ++-- pythonwhat/checks/check_object.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pythonwhat/Test.py b/pythonwhat/Test.py index 2852462a..ad517811 100644 --- a/pythonwhat/Test.py +++ b/pythonwhat/Test.py @@ -1,7 +1,5 @@ import re import numpy as np -import pandas as pd -from pandas.testing import assert_frame_equal, assert_series_equal from pythonwhat.tasks import * from protowhat.Test import Test @@ -111,6 +109,8 @@ def areinstance(x, y, tuple_of_classes): # First try to the faster equality functions. If these don't pass, # Run the assertions that are typically slower. def is_equal(x, y): + import pandas as pd + from pandas.testing import assert_frame_equal, assert_series_equal try: if areinstance(x, y, (Exception,)): # Types of errors don't matter (this is debatable) diff --git a/pythonwhat/checks/check_object.py b/pythonwhat/checks/check_object.py index 09651e1a..7f54e9ac 100644 --- a/pythonwhat/checks/check_object.py +++ b/pythonwhat/checks/check_object.py @@ -13,7 +13,6 @@ ) from pythonwhat.checks.check_funcs import part_to_child from pythonwhat.utils import v2_only -import pandas as pd import ast @@ -289,6 +288,7 @@ def check_df(state, index, missing_msg=None, not_instance_msg=None, expand_msg=N my_df = pd.DataFrame({"a": [1, 2, 3], "b": [4, 5, 6], "c": [7, 8, 9]}) """ + import pandas as pd child = check_object( state, index, From d0d00254488275a045828d92ff21d5d9625527be Mon Sep 17 00:00:00 2001 From: Rik Bauwens Date: Wed, 27 Nov 2024 12:14:42 +0100 Subject: [PATCH 53/64] chore: bump version --- pythonwhat/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pythonwhat/__init__.py b/pythonwhat/__init__.py index b225565e..46780396 100644 --- a/pythonwhat/__init__.py +++ b/pythonwhat/__init__.py @@ -1,3 +1,3 @@ -__version__ = "2.25.0" +__version__ = "2.26.0" from .test_exercise import test_exercise, allow_errors From 96aada465951bde482a1f2443afc077bf1d18b67 Mon Sep 17 00:00:00 2001 From: Rik Bauwens Date: Wed, 27 Nov 2024 13:41:40 +0100 Subject: [PATCH 54/64] feat: lazy import numpy --- pythonwhat/Test.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pythonwhat/Test.py b/pythonwhat/Test.py index ad517811..353e9daf 100644 --- a/pythonwhat/Test.py +++ b/pythonwhat/Test.py @@ -1,5 +1,4 @@ import re -import numpy as np from pythonwhat.tasks import * from protowhat.Test import Test @@ -95,6 +94,7 @@ def test(self): """ Perform the actual test. result is set to False if the objects differ, True otherwise. """ + import numpy as np self.result = np.array(self.func(self.obj1, self.obj2)).all() @@ -111,6 +111,7 @@ def areinstance(x, y, tuple_of_classes): def is_equal(x, y): import pandas as pd from pandas.testing import assert_frame_equal, assert_series_equal + import numpy as np try: if areinstance(x, y, (Exception,)): # Types of errors don't matter (this is debatable) From 5571da25876daaeac848ce40a4b838203eab31f2 Mon Sep 17 00:00:00 2001 From: Rik Bauwens Date: Wed, 27 Nov 2024 13:49:54 +0100 Subject: [PATCH 55/64] chore: bump version --- pythonwhat/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pythonwhat/__init__.py b/pythonwhat/__init__.py index 46780396..81c925a6 100644 --- a/pythonwhat/__init__.py +++ b/pythonwhat/__init__.py @@ -1,3 +1,3 @@ -__version__ = "2.26.0" +__version__ = "2.27.0" from .test_exercise import test_exercise, allow_errors From c6c524bc9ea0a11997960797f3cbd59bdc8f5d66 Mon Sep 17 00:00:00 2001 From: Rik Bauwens Date: Mon, 30 Dec 2024 13:02:10 +0100 Subject: [PATCH 56/64] feat: delay pandas / numpy imports --- pythonwhat/Test.py | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/pythonwhat/Test.py b/pythonwhat/Test.py index 353e9daf..9126ac9f 100644 --- a/pythonwhat/Test.py +++ b/pythonwhat/Test.py @@ -104,18 +104,30 @@ def test(self): def areinstance(x, y, tuple_of_classes): return isinstance(x, tuple_of_classes) and isinstance(y, tuple_of_classes) +def is_primitive(x): + return isinstance(x, (str, int, float, bool, type(None))) + +def is_collection_of_primitives(x): + return isinstance(x, (list, tuple, set)) and all(is_primitive(element) for element in x) # For equality of ndarrays, list, dicts, pd Series and pd DataFrames: # First try to the faster equality functions. If these don't pass, # Run the assertions that are typically slower. def is_equal(x, y): - import pandas as pd - from pandas.testing import assert_frame_equal, assert_series_equal - import numpy as np try: + if areinstance(x, y, (str, int, float, bool, type(None))): + return x == y + if is_collection_of_primitives(x): + return x == y if areinstance(x, y, (Exception,)): # Types of errors don't matter (this is debatable) return str(x) == str(y) + + # Delay importing pandas / numpy until absolutely necessary. This is important for performance in Pyodide. + import pandas as pd + from pandas.testing import assert_frame_equal, assert_series_equal + import numpy as np + if areinstance(x, y, (np.ndarray, dict, list, tuple)): np.testing.assert_equal(x, y) return True From ffe19df961cf4e261cc42905c0fb877f36bd17b2 Mon Sep 17 00:00:00 2001 From: Rik Bauwens Date: Mon, 30 Dec 2024 13:06:03 +0100 Subject: [PATCH 57/64] chore: bump version --- pythonwhat/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pythonwhat/__init__.py b/pythonwhat/__init__.py index 81c925a6..2cb3bd02 100644 --- a/pythonwhat/__init__.py +++ b/pythonwhat/__init__.py @@ -1,3 +1,3 @@ -__version__ = "2.27.0" +__version__ = "2.28.0" from .test_exercise import test_exercise, allow_errors From 26f8576d2512475e839affec5d548a7f43002ceb Mon Sep 17 00:00:00 2001 From: Ricardo Lopes Date: Mon, 24 Mar 2025 15:00:04 +0000 Subject: [PATCH 58/64] feat: drop direct dependency on numpy and pandas --- pythonwhat/Test.py | 78 +++++++++++++++++++++++++++++++------------ requirements-test.txt | 4 +++ requirements.txt | 3 -- setup.py | 3 +- 4 files changed, 62 insertions(+), 26 deletions(-) diff --git a/pythonwhat/Test.py b/pythonwhat/Test.py index 9126ac9f..47c0703c 100644 --- a/pythonwhat/Test.py +++ b/pythonwhat/Test.py @@ -94,8 +94,14 @@ def test(self): """ Perform the actual test. result is set to False if the objects differ, True otherwise. """ - import numpy as np - self.result = np.array(self.func(self.obj1, self.obj2)).all() + result = self.func(self.obj1, self.obj2) + + try: + import numpy as np + + self.result = np.array(result).all() + except ImportError: + self.result = result # Helpers for testing equality @@ -117,34 +123,64 @@ def is_equal(x, y): try: if areinstance(x, y, (str, int, float, bool, type(None))): return x == y + if is_collection_of_primitives(x): return x == y + if areinstance(x, y, (Exception,)): # Types of errors don't matter (this is debatable) return str(x) == str(y) + if areinstance(x, y, (list, tuple,)): + if len(x) != len(y): + return False + return all(is_equal(x_element, y_element) for x_element, y_element in zip(x, y)) + + if areinstance(x, y, (map, filter,)): + x_list, y_list = list(x), list(y) + if len(x_list) != len(y_list): + return False + return all(is_equal(x_element, y_element) for x_element, y_element in zip(x_list, y_list)) + + if areinstance(x, y, (set,)): + return is_equal(sorted(x), sorted(y)) + + if areinstance(x, y, (dict,)): + if x.keys() != y.keys(): + return False + return all(is_equal(x[key], y[key]) for key in x) + # Delay importing pandas / numpy until absolutely necessary. This is important for performance in Pyodide. - import pandas as pd - from pandas.testing import assert_frame_equal, assert_series_equal - import numpy as np - - if areinstance(x, y, (np.ndarray, dict, list, tuple)): - np.testing.assert_equal(x, y) - return True - elif areinstance(x, y, (map, filter)): - return np.array_equal(list(x), list(y)) - elif areinstance(x, y, (pd.DataFrame,)): - if x.equals(y): + # Also, assume they may not be available, as Pyodide won't install them unless they are needed. + try: + import numpy as np + + if areinstance(x, y, (np.ndarray,)): + np.testing.assert_equal(x, y) return True - assert_frame_equal(x, y) - return True - elif areinstance(x, y, (pd.Series,)): - if x.equals(y): + except ImportError: + if areinstance(x, y, (np.ndarray,)): + raise RuntimeError("NumPy is required for comparing NumPy objects.") + + try: + import pandas as pd + from pandas.testing import assert_frame_equal, assert_series_equal + + if areinstance(x, y, (pd.DataFrame,)): + if x.equals(y): + return True + assert_frame_equal(x, y) return True - assert_series_equal(x, y) - return True - else: - return x == y + elif areinstance(x, y, (pd.Series,)): + if x.equals(y): + return True + assert_series_equal(x, y) + return True + except ImportError: + if areinstance(x, y, (pd.DataFrame, pd.Series)): + raise RuntimeError("pandas is required for comparing pandas objects.") + + return x == y except Exception: return False diff --git a/requirements-test.txt b/requirements-test.txt index 74e73265..dc834ee5 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -15,3 +15,7 @@ h5py~=3.11.0 # test-utils deps pytest~=6.2.5 pytest-cov~=2.12.1 + +# not included in requirements.txt +numpy~=1.26.0 +pandas~=1.5.3 diff --git a/requirements.txt b/requirements.txt index 4536e838..1a20938a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,6 +7,3 @@ jinja2~=3.1.3 markupsafe==2.1.5 black==19.10b0 Pygments==2.13.0 - -numpy~=1.26.0 -pandas~=1.5.3 diff --git a/setup.py b/setup.py index 19a3ad90..9c48423c 100644 --- a/setup.py +++ b/setup.py @@ -7,7 +7,6 @@ PACKAGE_NAME = "pythonwhat" REQUIREMENT_NAMES = ["protowhat", "markdown2", "jinja2", "asttokens", "dill"] -PEER_REQUIREMENTS = ["numpy", "pandas"] HERE = path.abspath(path.dirname(__file__)) VERSION_FILE = path.join(HERE, PACKAGE_NAME, "__init__.py") @@ -23,7 +22,7 @@ REQUIREMENTS = [ re.search(_requirements_re_template.format(requirement), req_txt, re.M).group(0) for requirement in REQUIREMENT_NAMES - ] + PEER_REQUIREMENTS + ] with open(README_FILE, encoding="utf-8") as fp: README = fp.read() From da38df00a5bea279d474a2a2debf01ecd9a4a189 Mon Sep 17 00:00:00 2001 From: Ricardo Lopes Date: Fri, 28 Mar 2025 14:40:27 +0000 Subject: [PATCH 59/64] chore: bump version --- pythonwhat/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pythonwhat/__init__.py b/pythonwhat/__init__.py index 2cb3bd02..70c36b33 100644 --- a/pythonwhat/__init__.py +++ b/pythonwhat/__init__.py @@ -1,3 +1,3 @@ -__version__ = "2.28.0" +__version__ = "2.29.0" from .test_exercise import test_exercise, allow_errors From e09b09b8e48ff2486238072a3b4481c81f2f3d33 Mon Sep 17 00:00:00 2001 From: Ricardo Lopes Date: Tue, 15 Apr 2025 17:25:08 +0100 Subject: [PATCH 60/64] Add support for Python 3.12 --- .circleci/config.yml | 2 +- README.md | 7 +++---- pythonwhat/checks/check_object.py | 2 +- pythonwhat/local.py | 4 ++-- requirements-test.txt | 19 +++++++++---------- requirements.txt | 4 +--- tests/test_check_function.py | 10 +++++----- 7 files changed, 22 insertions(+), 26 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 968df0b9..acb1e62a 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -6,7 +6,7 @@ orbs: jobs: build-and-test: docker: - - image: cimg/python:3.9 + - image: cimg/python:3.12 steps: - checkout - python/install-packages: diff --git a/README.md b/README.md index 7e07f0fe..f50b0e0b 100644 --- a/README.md +++ b/README.md @@ -53,9 +53,9 @@ To learn how to include an SCT in a DataCamp course, visit https://instructor-su ## Run tests ```bash -pyenv local 3.9.6 -pip3.9 install -r requirements-test.txt -pip3.9 install -e . +pyenv local 3.12.7 +pip3.12 install -r requirements-test.txt +pip3.12 install -e . pytest ``` @@ -63,7 +63,6 @@ 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) diff --git a/pythonwhat/checks/check_object.py b/pythonwhat/checks/check_object.py index 7f54e9ac..2b0b8856 100644 --- a/pythonwhat/checks/check_object.py +++ b/pythonwhat/checks/check_object.py @@ -354,7 +354,7 @@ def check_keys(state, key, missing_msg=None, expand_msg=None): def get_part(name, key, highlight): if isinstance(key, str): - slice_val = ast.Str(s=key) + slice_val = ast.Constant(value=key) else: slice_val = ast.parse(str(key)).body[0].value expr = ast.Subscript( diff --git a/pythonwhat/local.py b/pythonwhat/local.py index d2fe549f..7dee518e 100644 --- a/pythonwhat/local.py +++ b/pythonwhat/local.py @@ -29,7 +29,7 @@ def run_code(self, code): class StubProcess: def __init__(self, init_code=None, pid=None): self.shell = StubShell(init_code) - self._identity = (pid,) if pid else (random.randint(0, 1e12),) + self._identity = (pid,) if pid else (random.randint(0, int(1e12)),) def executeTask(self, task): return task(self.shell) @@ -73,7 +73,7 @@ def __init__(self, pid=None): ) # when parent process is killed, sub/childprocess get also killed self.instances.append(self) # used to detect single process exercise - self._identity = (pid,) if pid else (random.randint(0, 1e12),) + self._identity = (pid,) if pid else (random.randint(0, int(1e12)),) def get_shell(self): return create({}) diff --git a/requirements-test.txt b/requirements-test.txt index dc834ee5..6bc0e2d7 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -1,21 +1,20 @@ -r requirements.txt # test deps -scipy~=1.13.1 +scipy~=1.14.1 bs4~=0.0.1 html5lib~=1.1 -# h5py~=3.1.0 -requests~=2.26.0 +requests~=2.31.0 seaborn~=0.11.2 -sqlalchemy~=1.4.23 +sqlalchemy~=2.0.29 xlrd~=2.0.1 -openpyxl~=3.0.7 -h5py~=3.11.0 +openpyxl~=3.1.0 +h5py~=3.12.1 # test-utils deps -pytest~=6.2.5 -pytest-cov~=2.12.1 +pytest~=8.1.1 +pytest-cov~=6.1.1 # not included in requirements.txt -numpy~=1.26.0 -pandas~=1.5.3 +numpy~=2.0.2 +pandas~=2.2.3 diff --git a/requirements.txt b/requirements.txt index 1a20938a..5437ccbe 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,8 +2,6 @@ protowhat~=2.2.0 asttokens~=2.4.1 dill~=0.3.4 -markdown2~=2.4.13 +markdown2~=2.5.3 jinja2~=3.1.3 -markupsafe==2.1.5 black==19.10b0 -Pygments==2.13.0 diff --git a/tests/test_check_function.py b/tests/test_check_function.py index ff902fc8..e778211c 100644 --- a/tests/test_check_function.py +++ b/tests/test_check_function.py @@ -92,11 +92,11 @@ def my_fun(a, b, *args, **kwargs): pass ][0]["args"] sig = signature(my_fun) bound_args = bind_args(sig, args) - assert bound_args["a"]["node"].n == 1 - assert bound_args["b"]["node"].n == 2 - assert bound_args["args"][0]["node"].n == 3 - assert bound_args["args"][1]["node"].n == 4 - assert bound_args["kwargs"]["c"]["node"].n == 5 + assert bound_args["a"]["node"].value == 1 + assert bound_args["b"]["node"].value == 2 + assert bound_args["args"][0]["node"].value == 3 + assert bound_args["args"][1]["node"].value == 4 + assert bound_args["kwargs"]["c"]["node"].value == 5 @pytest.mark.parametrize("argspec", [["args", 0], ["args", 1], ["kwargs", "c"]]) From 027e396255278edcc688263ddf8353cea6499743 Mon Sep 17 00:00:00 2001 From: Ricardo Lopes Date: Wed, 30 Apr 2025 15:22:39 +0100 Subject: [PATCH 61/64] Update protowhat --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 5437ccbe..328869cf 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ # pythonwhat deps -protowhat~=2.2.0 +protowhat~=2.3.1 asttokens~=2.4.1 dill~=0.3.4 markdown2~=2.5.3 From b8b255b7d750529f461bacdecd631c13e69cbd0e Mon Sep 17 00:00:00 2001 From: Ricardo Lopes Date: Wed, 30 Apr 2025 15:38:28 +0100 Subject: [PATCH 62/64] Bump version --- pythonwhat/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pythonwhat/__init__.py b/pythonwhat/__init__.py index 70c36b33..52f25fd4 100644 --- a/pythonwhat/__init__.py +++ b/pythonwhat/__init__.py @@ -1,3 +1,3 @@ -__version__ = "2.29.0" +__version__ = "2.30.0" from .test_exercise import test_exercise, allow_errors From 9fbbeb39da425dd5709cdaaf32dad3447a226569 Mon Sep 17 00:00:00 2001 From: Ricardo Lopes Date: Fri, 23 May 2025 14:17:00 +0100 Subject: [PATCH 63/64] Fix typed signature validation --- pythonwhat/tasks.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pythonwhat/tasks.py b/pythonwhat/tasks.py index 35c87ace..3516ff7b 100644 --- a/pythonwhat/tasks.py +++ b/pythonwhat/tasks.py @@ -134,7 +134,8 @@ def get_signature(name, mapped_name, signature, manual_sigs, env): except: raise InstructorError.from_message(e.args[0] + " and cannot determine signature") - return signature + params = [param.replace(annotation=inspect._empty) for param in signature.parameters.values()] + return signature.replace(parameters=params) # Get the signature of a function based on an object inside the process From b2ce04c9c8dd0e4d3bbac1f312cfd42d92aa3734 Mon Sep 17 00:00:00 2001 From: Ricardo Lopes Date: Mon, 26 May 2025 09:44:14 +0100 Subject: [PATCH 64/64] Fix dill issue on Python3.12 --- pythonwhat/__init__.py | 2 +- requirements.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pythonwhat/__init__.py b/pythonwhat/__init__.py index 52f25fd4..47b6d8fd 100644 --- a/pythonwhat/__init__.py +++ b/pythonwhat/__init__.py @@ -1,3 +1,3 @@ -__version__ = "2.30.0" +__version__ = "2.30.1" from .test_exercise import test_exercise, allow_errors diff --git a/requirements.txt b/requirements.txt index 328869cf..c7103e83 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ # pythonwhat deps protowhat~=2.3.1 asttokens~=2.4.1 -dill~=0.3.4 +dill~=0.4.0 markdown2~=2.5.3 jinja2~=3.1.3 black==19.10b0