From faa6cdb12a5fe9ddca71ab32f5d3873fde61cdf6 Mon Sep 17 00:00:00 2001 From: Jeroen Hermans Date: Wed, 19 Dec 2018 13:12:24 +0100 Subject: [PATCH 001/170] Improve has_equal_ast docs --- pythonwhat/has_funcs.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/pythonwhat/has_funcs.py b/pythonwhat/has_funcs.py index 7fe7c93c..5a9d12c4 100644 --- a/pythonwhat/has_funcs.py +++ b/pythonwhat/has_funcs.py @@ -38,7 +38,7 @@ def verify(part, index): except (KeyError, IndexError): raise InstructorError(_err_msg) - try: + try: verify(state.student_parts[name], index) except (KeyError, IndexError): rep.do_test(Test(Feedback(_msg, state))) @@ -101,6 +101,8 @@ def has_equal_ast(incorrect_msg=None, ``has_equal_ast()`` can be used in two ways: * As a robust version of ``has_code()``. By setting ``code``, you can look for the AST representation of ``code`` in the student's submission. + But be aware that ``a`` and ``a = 1`` won't match, as reading and assigning are not the same in an AST. + Use ``ast.dump(ast.parse(code))`` to see an AST representation of ``code``. * As an expression-based check when using more advanced SCT chain, e.g. to compare the equality of expressions to set function arguments. Args: @@ -252,7 +254,7 @@ def has_expr(incorrect_msg=None, rep = Reporter.active_reporter - get_func = partial(evalCalls[test], + get_func = partial(evalCalls[test], extra_env=extra_env, context_vals=context_vals, pre_code=pre_code, @@ -584,7 +586,7 @@ def has_printout(index, x = 6 The following SCT will not work: :: - + Ex().has_printout(0) Why? When the ``print(x)`` call is executed, the value of ``x`` will be 6, and pythonwhat will look for the output `'6`' in the output the student generated. @@ -592,7 +594,7 @@ def has_printout(index, :Example: - Inside a for loop ``has_printout()`` + Inside a for loop ``has_printout()`` Suppose you have the following solution: :: @@ -600,7 +602,7 @@ def has_printout(index, print(i) The following SCT will not work: :: - + Ex().check_for_loop().check_body().has_printout(0) The reason is that ``has_printout()`` can only be called from the root state. ``Ex()``. @@ -651,7 +653,7 @@ def has_no_error(incorrect_msg="Have a look at the console: your code contains a the student submission generated an error. This means it is not needed to use ``has_no_error()`` explicitly. However, in some cases, using ``has_no_error()`` explicitly somewhere throughout your SCT execution can be helpful: - + - If you want to make sure people didn't write typos when writing a long function name. - If you want to first verify whether a function actually runs, before checking whether the arguments were specified correctly. - More generally, if, because of the content, it's instrumental that the script runs without @@ -680,7 +682,7 @@ def has_no_error(incorrect_msg="Have a look at the console: your code contains a If you want to make sure that ``train_test_split()`` ran without errors, which would check if the student typed the function without typos and used sensical arguments, you could use the following SCT: :: - + Ex().has_no_error() Ex().check_function('sklearn.model_selection.train_test_split').multi( check_args(['arrays', 0]).has_equal_value(), @@ -688,7 +690,7 @@ def has_no_error(incorrect_msg="Have a look at the console: your code contains a check_args(['options', 'test_size']).has_equal_value(), check_args(['options', 'random_state']).has_equal_value() ) - + If, on the other hand, you want to fall back onto pythonwhat's built in behavior, that checks for an error before marking the exercise as correct, you can simply leave of the ``has_no_error()`` step. From c0ec7713598d852cd058cb28531f3940bddb270c Mon Sep 17 00:00:00 2001 From: Jeroen Hermans Date: Wed, 21 Nov 2018 10:53:54 +0100 Subject: [PATCH 002/170] Use raised error information --- pythonwhat/check_function.py | 4 ++-- pythonwhat/tasks.py | 17 +++++++---------- 2 files changed, 9 insertions(+), 12 deletions(-) diff --git a/pythonwhat/check_function.py b/pythonwhat/check_function.py index 4cf90f9e..9187de82 100644 --- a/pythonwhat/check_function.py +++ b/pythonwhat/check_function.py @@ -121,8 +121,8 @@ def check_function(name, index=0, try: sol_sig = get_sig(mapped_name=sol_parts['name'], process=state.solution_process) sol_parts['args'] = bind_args(sol_sig, sol_parts['args']) - except: - raise InstructorError("`check_function()` couldn't match the %s call of `%s` to its signature. " % (get_ord(index + 1), name)) + except Exception as e: + raise InstructorError("`check_function()` couldn't match the %s call of `%s` to its signature:\n%s " % (get_ord(index + 1), name, e)) try: stu_sig = get_sig(mapped_name=stu_parts['name'], process=state.student_process) diff --git a/pythonwhat/tasks.py b/pythonwhat/tasks.py index af320c2e..bb05a575 100644 --- a/pythonwhat/tasks.py +++ b/pythonwhat/tasks.py @@ -111,11 +111,11 @@ def get_signature(name, mapped_name, signature, manual_sigs, env): raise InstructorError('signature error - %s not in builtins' % generic_name) else: raise InstructorError('manual signature not found') - except: + except Exception as e: try: signature = inspect.signature(fun) except: - raise InstructorError('signature error - cannot determine signature') + raise InstructorError(e.args[0] + ' and cannot determine signature') return signature @@ -123,14 +123,11 @@ def get_signature(name, mapped_name, signature, manual_sigs, env): # Get the signature of a function based on an object inside the process @process_task def getSignatureInProcess(name, mapped_name, signature, manual_sigs, process, shell): - try: - return get_signature(name = name, - mapped_name = mapped_name, - signature = signature, - manual_sigs = manual_sigs, - env = get_env(shell.user_ns)) - except: - return None + return get_signature(name = name, + mapped_name = mapped_name, + signature = signature, + manual_sigs = manual_sigs, + env = get_env(shell.user_ns)) @process_task def getSignatureFromObjInProcess(obj_char, process, shell): From ee18a44e9683af7401c6a09bbaa157f5fcfce5e5 Mon Sep 17 00:00:00 2001 From: Jeroen Hermans Date: Mon, 17 Dec 2018 18:55:53 +0100 Subject: [PATCH 003/170] Fix test --- tests/test_author_warnings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_author_warnings.py b/tests/test_author_warnings.py index de8b034b..3c43a1c4 100644 --- a/tests/test_author_warnings.py +++ b/tests/test_author_warnings.py @@ -44,7 +44,7 @@ def test_check_function_2(state): state.check_function('round', 1) def test_check_function_3(state): - with pytest.raises(InstructorError, match=r"`check_function\(\)` couldn't match the first call of `round` to its signature\."): + with pytest.raises(InstructorError, match=r"`check_function\(\)` couldn't match the first call of `round` to its signature:"): sig = Signature([Parameter('wrong', Parameter.KEYWORD_ONLY)]) state.check_function('round', 0, signature=sig) From 303a8e3a5f9cad31932f530c3f465aaca306f9de Mon Sep 17 00:00:00 2001 From: fossabot Date: Tue, 11 Dec 2018 08:06:47 -0800 Subject: [PATCH 004/170] Add license scan report and status Signed-off-by: fossabot --- README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.md b/README.md index 2ccad917..bad2584a 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,7 @@ [![codecov](https://codecov.io/gh/datacamp/pythonwhat/branch/master/graph/badge.svg)](https://codecov.io/gh/datacamp/pythonwhat) [![PyPI version](https://badge.fury.io/py/pythonwhat.svg)](https://badge.fury.io/py/pythonwhat) [![Documentation Status](https://readthedocs.org/projects/pythonwhat/badge/?version=stable)](http://pythonwhat.readthedocs.io/en/stable/?badge=stable) +[![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2Fdatacamp%2Fpythonwhat.svg?type=shield)](https://app.fossa.io/projects/git%2Bgithub.com%2Fdatacamp%2Fpythonwhat?ref=badge_shield) Verify Python code submissions and auto-generate meaningful feedback messages. Originally developed for Python exercises on DataCamp for so-called Submission Correctness Tests, but can also be used independently. @@ -60,3 +61,7 @@ pytest ``` Bugs? Questions? Suggestions? [Create an issue](https://github.com/datacamp/pythonwhat/issues/new), or [contact us](mailto:content-engineering@datacamp.com)! + + +## License +[![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2Fdatacamp%2Fpythonwhat.svg?type=large)](https://app.fossa.io/projects/git%2Bgithub.com%2Fdatacamp%2Fpythonwhat?ref=badge_large) \ No newline at end of file From 2e5040b8366c263844b767eb1f046c7360d56ebe Mon Sep 17 00:00:00 2001 From: Jeroen Hermans Date: Mon, 17 Dec 2018 20:26:47 +0100 Subject: [PATCH 005/170] Improve setup --- MANIFEST.in | 2 ++ requirements.txt | 2 -- setup.py | 54 +++++++++++++++++++++++++------------- tests/test_check_object.py | 5 ++-- 4 files changed, 41 insertions(+), 22 deletions(-) create mode 100644 MANIFEST.in diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 00000000..096dd503 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,2 @@ +include LICENSE +include requirements.txt diff --git a/requirements.txt b/requirements.txt index beb1e1f4..47d57f85 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,10 +12,8 @@ bs4==0.0.1 html5lib==1.0.1 h5py==2.7.1 requests==2.20.0 -sas7bdat==2.0.7 seaborn==0.8.1 sqlalchemy==1.2.6 -tweepy==3.6.0 xlrd==1.1.0 # test-utils deps diff --git a/setup.py b/setup.py index 90aeb25b..8b575558 100644 --- a/setup.py +++ b/setup.py @@ -5,25 +5,43 @@ from os import path from setuptools import setup -_version_re = re.compile(r'__version__\s+=\s+(.*)') +PACKAGE_NAME = "pythonwhat" +REQUIREMENT_NAMES = ["markdown2", "jinja2", "asttokens", "dill", "numpy", "pandas"] -PACKAGE_NAME = 'pythonwhat' HERE = path.abspath(path.dirname(__file__)) -with open(path.join(HERE, 'README.md'), encoding='utf-8') as fp: +VERSION_FILE = path.join(HERE, PACKAGE_NAME, "__init__.py") +REQUIREMENTS_FILE = path.join(HERE, "requirements.txt") +README_FILE = path.join(HERE, "README.md") + +with open(VERSION_FILE, encoding="utf-8") as fp: + _version_re = re.compile(r"__version__\s+=\s+(.*)") + VERSION = str(ast.literal_eval(_version_re.search(fp.read()).group(1))) +with open(REQUIREMENTS_FILE, encoding="utf-8") as fp: + req_txt = fp.read() + _requirements_re_template = r"^({}(?:\s*[<>=]+\s*\S*)?)\s*(?:#.*)?$" + REQUIREMENTS = [ + re.search(_requirements_re_template.format(requirement), req_txt, re.M).group(0) + for requirement in REQUIREMENT_NAMES + ] +with open(README_FILE, encoding="utf-8") as fp: README = fp.read() -with open(path.join(HERE, PACKAGE_NAME, '__init__.py'), 'rb') as fp: - VERSION = str(ast.literal_eval(_version_re.search( - fp.read().decode('utf-8')).group(1))) -setup(name=PACKAGE_NAME, - version=VERSION, - packages=[PACKAGE_NAME, 'pythonwhat.test_funcs'], - install_requires=["dill", "numpy", "pandas", "markdown2", "jinja2", "asttokens>=1.1.10"], - long_description=README, - long_description_content_type='text/markdown', - license='GNU version 3', - author='DataCamp', - author_email='content-engineering@datacamp.com', - maintainer='Filip Schouwenaars', - maintainer_email='filip@datacamp.com', - url='https://github.com/datacamp/pythonwhat') +setup( + name=PACKAGE_NAME, + version=VERSION, + packages=[PACKAGE_NAME, "pythonwhat.test_funcs"], + install_requires=REQUIREMENTS, + description="Submission correctness tests for Python", + long_description=README, + long_description_content_type="text/markdown", + author="Filip Schouwenaars", + author_email="filip@datacamp.com", + maintainer="Jeroen Hermans", + maintainer_email="content-engineering@datacamp.com", + url="https://github.com/datacamp/pythonwhat", + classifiers=[ + "Programming Language :: Python :: 3", + "License :: OSI Approved :: GNU Affero General Public License v3", + "Operating System :: OS Independent", + ], +) diff --git a/tests/test_check_object.py b/tests/test_check_object.py index 1d00eb25..cad7ae77 100644 --- a/tests/test_check_object.py +++ b/tests/test_check_object.py @@ -141,6 +141,7 @@ def test_check_keys_exotic(sct): assert output['correct'] def test_non_dillable(): + # xlrd needed for Excel support code = "xl = pd.ExcelFile('battledeath.xlsx')" res = helper.run({ 'DC_PEC': "import pandas as pd; from urllib.request import urlretrieve; urlretrieve('https://s3.amazonaws.com/assets.datacamp.com/production/course_998/datasets/battledeath.xlsx', 'battledeath.xlsx')", @@ -194,7 +195,7 @@ def test_equality_challenge_2(): "DC_SCT": "Ex().check_object('mat').has_equal_value()" }) assert res['correct'] - + @pytest.mark.parametrize('name, ls, le, cs, ce', [ ('a', 3, 3, 5, 9), ("c", 8, 8, 5, 9), @@ -303,7 +304,7 @@ def diff_assign_data(): df2.columns = ["c", "d"] ''' } - + def test_several_assignments(diff_assign_data): res = helper.run({ **diff_assign_data, From abadc650ac0241646cfd32255689866b93ffe851 Mon Sep 17 00:00:00 2001 From: Jeroen Hermans Date: Mon, 17 Dec 2018 20:33:47 +0100 Subject: [PATCH 006/170] Fix documentation --- pythonwhat/check_wrappers.py | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/pythonwhat/check_wrappers.py b/pythonwhat/check_wrappers.py index c334924d..a9f5f692 100644 --- a/pythonwhat/check_wrappers.py +++ b/pythonwhat/check_wrappers.py @@ -29,7 +29,7 @@ 'list_comp': { 'typestr': '{{ordinal}} list comprehension', 'docstr': """Check whether a list comprehension was coded and zoom in on it. - + Can be chained with ``check_iter()``, ``check_body()``, and ``check_ifs()``. Args: @@ -78,7 +78,7 @@ 'generator_exp': { 'typestr': '{{ordinal}} generator expression', 'docstr': """Check whether a generator expression was coded and zoom in on it. - + Can be chained with ``check_iter()``, ``check_body()``, and ``check_ifs()``. Args: @@ -110,7 +110,7 @@ 'dict_comp': { 'typestr': '{{ordinal}} dictionary comprehension', 'docstr': """Check whether a dictionary comprehension was coded and zoom in on it. - + Can be chained with ``check_key()``, ``check_value()``, and ``check_ifs()``. Args: @@ -156,7 +156,7 @@ 'docstr': """Check whether a for loop was coded and zoom in on it. Can be chained with ``check_iter()`` and ``check_body()``. - + Args: index: Index of the for loop (0-based). {{typestr}} @@ -179,7 +179,7 @@ set_context('b', 2).has_equal_output() ) ) - + - ``check_for_loop()`` zooms in on the ``for`` loop, and makes its parts available for further checking. - ``check_iter()`` zooms in on the iterator part of the for loop, ``my_dict.items()`` in the solution. ``has_equal_value()`` re-executes the expressions specified by student and solution and compares their results. @@ -198,7 +198,7 @@ # passing submission 2 my_dict = {'a': 1, 'b': 2} for first, second in my_dict.items(): - mess = first + " - " + str(second) + mess = first + " - " + str(second) print(mess) :Example: @@ -252,7 +252,7 @@ ) ) ) - + """ }, 'function_def': { @@ -370,7 +370,7 @@ def shout_echo(a, b=1): 'class_def': { 'typestr': 'class definition of `{{index}}`', 'docstr': """Check whether a class was defined and zoom in on its definition - + Can be chained with ``check_bases()`` and ``check_body()``. Args: @@ -389,7 +389,7 @@ def __init__(self, i): The following SCT would verify this: :: - check_class_def('MyInt').multi( + Ex().check_class_def('MyInt').multi( check_bases(0).has_equal_ast(), check_body().check_function_def('__init__').multi( check_args('self'), @@ -442,7 +442,7 @@ def __init__(self, i): check_test().multi( set_env(x = -1).has_equal_value(), set_env(x = 1).has_equal_value(), - set_env(x = 0).has_equal_value() + set_env(x = 0).has_equal_value() ), check_body().check_function('print', 0).\\ check_args('value').has_equal_value() @@ -459,11 +459,11 @@ def __init__(self, i): function ``print()`` and whether its argument is set correctly. :Example: - + In Python, when an if-else statement has an ``elif`` clause, it is held in the `orelse` part. In this sense, an if-elif-else statement is represented by python as nested if-elses. More specifically, this if-else statement: :: - + if x > 0: print(x) elif y > 0: @@ -656,7 +656,7 @@ def __init__(self, i): scts['check_'+k] = partial(check_part, k, v) -for k, v in __PART_INDEX_WRAPPERS__.items(): +for k, v in __PART_INDEX_WRAPPERS__.items(): scts['check_'+k] = partial(check_part_index, k, part_msg=v) for k, v in __NODE_WRAPPERS__.items(): @@ -686,4 +686,4 @@ def __init__(self, i): scts['has_context'] = has_context scts['check_function'] = check_function -locals().update(scts) \ No newline at end of file +locals().update(scts) From 2e011fb6d96806d20e1617a287cec3fa4992375a Mon Sep 17 00:00:00 2001 From: Jeroen Hermans Date: Mon, 17 Dec 2018 20:35:14 +0100 Subject: [PATCH 007/170] Clarify error --- pythonwhat/has_funcs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pythonwhat/has_funcs.py b/pythonwhat/has_funcs.py index 5a9d12c4..03979cbb 100644 --- a/pythonwhat/has_funcs.py +++ b/pythonwhat/has_funcs.py @@ -273,7 +273,7 @@ def has_expr(incorrect_msg=None, env=state.solution_env) if (test == 'error') ^ isinstance(eval_sol, Exception): - raise InstructorError("Evaluating expression raised error in solution process (or not an error if testing for one). " + raise InstructorError("Evaluating expression raised error in solution process (or didn't raise if testing for one). " "Error: {} - {}".format(type(eval_sol), str_sol)) if isinstance(eval_sol, ReprFail): raise InstructorError("Couldn't extract the value for the highlighted expression from the solution process: " + eval_sol.info) From 83fadae7de74940b930f230daf8ba3da6803edc5 Mon Sep 17 00:00:00 2001 From: Jeroen Hermans Date: Tue, 18 Dec 2018 09:42:30 +0100 Subject: [PATCH 008/170] Limit impact of unpicklable object (fix skipping deepcopy on value access) --- pythonwhat/tasks.py | 11 +++++++++-- tests/test_has_expr.py | 26 ++++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/pythonwhat/tasks.py b/pythonwhat/tasks.py index bb05a575..85d1d76f 100644 --- a/pythonwhat/tasks.py +++ b/pythonwhat/tasks.py @@ -318,14 +318,21 @@ def taskRunEval(tree, tree = ast.Expression(tree) # Expression code takes precedence over tree code - if expr_code: code = expr_code - else: code = compile(tree, "