From 12cd54d0602621e6f095df4f05122a903359c74b Mon Sep 17 00:00:00 2001 From: Chris Chudzicki Date: Sun, 30 Jun 2019 17:14:28 -0400 Subject: [PATCH 01/14] move get_number_of_args to own file, write a py3-specific version I've also re-worked the Python 2 version so that it should now work for Python 3. But the Python-3 only version is much simpler. Also, the Python 2 version raises deprecation warnings in Python 3 (for getargspec). --- mitxgraders/helpers/get_number_of_args.py | 146 ++++++++++++++++++++++ mitxgraders/helpers/validatorfuncs.py | 134 ++------------------ 2 files changed, 158 insertions(+), 122 deletions(-) create mode 100644 mitxgraders/helpers/get_number_of_args.py diff --git a/mitxgraders/helpers/get_number_of_args.py b/mitxgraders/helpers/get_number_of_args.py new file mode 100644 index 00000000..c4bd3639 --- /dev/null +++ b/mitxgraders/helpers/get_number_of_args.py @@ -0,0 +1,146 @@ +import inspect +import six + +def get_builtin_positional_args(obj): + """ + Get the number of position arguments on a built-in function by inspecting + its docstring. (Built-in functions cannot be inspected by inspect.inspect.getargspec.) + + NOTE: + - works in Python 3, but intended for Python 2 + + >>> pow.__doc__ # doctest: +ELLIPSIS + 'pow(x, y[, z]) -> number... + >>> get_builtin_positional_args(pow) + 2 + """ + # Built-in functions cannot be inspected by + # inspect.inspect.getargspec. We have to try and parse + # the __doc__ attribute of the function. + docstr = obj.__doc__ + if docstr: + items = docstr.split('\n') + if items: + func_descr = items[0] + s = func_descr.replace(obj.__name__, '') + idx1 = s.find('(') + idx_default = s.find('[') + idx2 = s.find(')') if idx_default == -1 else idx_default + if idx1 != -1 and idx2 != -1 and (idx2 > idx1+1): + argstring = s[idx1+1:idx2] + # This gets the argument string + # Count the number of commas! + return argstring.count(",") + 1 + return 0 # pragma: no cover + +def get_number_of_args_py2(callable_obj): + """ + Get number of positional arguments of function or callable object. + + NOTES: + - Seems to work in Python 3, but is based on inspect.getargspec which + raises a DeprecationWarning in Python 3. + - in Python 3, use the much simpler signature-based + get_number_of_args_py3 funciton instead. + """ + + if inspect.isbuiltin(callable_obj): + # Built-in function + func = callable_obj + return get_builtin_positional_args(func) + elif hasattr(callable_obj, "nin"): + # Matches RandomFunction or numpy ufunc + return callable_obj.nin + else: + if inspect.isfunction(callable_obj) or inspect.ismethod(callable_obj): + # Assume object is a function + func = callable_obj + # see https://docs.python.org/2/library/inspect.html#inspect.inspect.getargspec + # defaults might be None, or something weird for Mock functions + args, _, _, defaults = inspect.getargspec(func) + else: + # Callable object + func = callable_obj.__call__ + args, _, _, defaults = inspect.getargspec(func) + + try: + num_args = len(args) - len(defaults) + except TypeError: + num_args = len(args) + + # If func is a bound method, remove one argument + # (in Python 2.7, unbound methods have __self__ = None) + try: + if func.__self__ is not None: + num_args += -1 + except AttributeError: + pass + + return num_args + +def get_number_of_args_py3(callable_obj): + """ + Get number of positional arguments of function or callable object. + + NOTES: + - based on inspect.signature + - in Python 2, use getargspec-based get_number_of_args_py3 instead + """ + params = inspect.signature(callable_obj).parameters + empty = inspect.Parameter.empty + return sum([params[key].default == empty for key in params]) + +def get_number_of_args(callable_obj): + """ + Get number of positional arguments of function or callable object. + + Examples + ======== + + Works for simple functions: + >>> def f(x, y): + ... return x + y + >>> get_number_of_args(f) + 2 + + Positional arguments only: + >>> def f(x, y, z=5): + ... return x + y + >>> get_number_of_args(f) + 2 + + Works with bound and unbound object methods + >>> class Foo: + ... def do_stuff(self, x, y, z): + ... return x*y*z + >>> get_number_of_args(Foo.do_stuff) # unbound, is NOT automatically passed self as argument + 4 + >>> foo = Foo() + >>> get_number_of_args(foo.do_stuff) # bound, is automatically passed self as argument + 3 + + Works for bound and unbound callable objects + >>> class Bar: + ... def __call__(self, x, y): + ... return x + y + >>> get_number_of_args(Bar) # unbound, is NOT automatically passed self as argument + 3 + >>> bar = Bar() + >>> get_number_of_args(bar) # bound, is automatically passed self as argument + 2 + + Works on built-in functions (assuming their docstring is correct) + >>> import math + >>> get_number_of_args(math.sin) + 1 + + Works on numpy ufuncs + >>> import numpy as np + >>> get_number_of_args(np.sin) + 1 + + Works on RandomFunctions (tested in unit tests due to circular imports) + """ + if six.PY2: + return get_number_of_args_py2(callable_obj) + return get_number_of_args_py3(callable_obj) diff --git a/mitxgraders/helpers/validatorfuncs.py b/mitxgraders/helpers/validatorfuncs.py index c734e81c..ee98ed76 100644 --- a/mitxgraders/helpers/validatorfuncs.py +++ b/mitxgraders/helpers/validatorfuncs.py @@ -5,11 +5,11 @@ """ from __future__ import print_function, division, absolute_import, unicode_literals -import six from numbers import Number -from inspect import getargspec, isbuiltin +import six from voluptuous import All, Range, NotIn, Invalid, Schema, Any, Required, Length, truth, Coerce from mitxgraders.helpers.compatibility import ensure_text +from mitxgraders.helpers.get_number_of_args import get_number_of_args def Positive(thetype): """Demand a positive number type""" @@ -171,120 +171,6 @@ def is_callable(obj): """Returns true if obj is callable""" return callable(obj) -def get_builtin_positional_args(obj): - """ - Get the number of position arguments on a built-in function by inspecting - its docstring. (Built-in functions cannot be inspected by inspect.getargspec.) - - >>> pow.__doc__ # doctest: +ELLIPSIS - 'pow(x, y[, z]) -> number... - >>> get_builtin_positional_args(pow) - 2 - """ - # Built-in functions cannot be inspected by - # inspect.getargspec. We have to try and parse - # the __doc__ attribute of the function. - docstr = obj.__doc__ - if docstr: - items = docstr.split('\n') - if items: - func_descr = items[0] - s = func_descr.replace(obj.__name__, '') - idx1 = s.find('(') - idx_default = s.find('[') - idx2 = s.find(')') if idx_default == -1 else idx_default - if idx1 != -1 and idx2 != -1 and (idx2 > idx1+1): - argstring = s[idx1+1:idx2] - # This gets the argument string - # Count the number of commas! - return argstring.count(",") + 1 - return 0 # pragma: no cover - -def get_number_of_args(callable_obj): - """ - Get number of positional arguments of function or callable object. - - Examples - ======== - - Works for simple functions: - >>> def f(x, y): - ... return x + y - >>> get_number_of_args(f) - 2 - - Positional arguments only: - >>> def f(x, y, z=5): - ... return x + y - >>> get_number_of_args(f) - 2 - - Works with bound and unbound object methods - >>> class Foo: - ... def do_stuff(self, x, y, z): - ... return x*y*z - >>> get_number_of_args(Foo.do_stuff) # unbound, is NOT automatically passed self as argument - 4 - >>> foo = Foo() - >>> get_number_of_args(foo.do_stuff) # bound, is automatically passed self as argument - 3 - - Works for bound and unbound callable objects - >>> class Bar: - ... def __call__(self, x, y): - ... return x + y - >>> get_number_of_args(Bar) # unbound, is NOT automatically passed self as argument - 3 - >>> bar = Bar() - >>> get_number_of_args(bar) # bound, is automatically passed self as argument - 2 - - Works on built-in functions (assuming their docstring is correct) - >>> import math - >>> get_number_of_args(math.sin) - 1 - - Works on numpy ufuncs - >>> import numpy as np - >>> get_number_of_args(np.sin) - 1 - - Works on RandomFunctions (tested in unit tests due to circular imports) - """ - if isbuiltin(callable_obj): - # Built-in function - func = callable_obj - return get_builtin_positional_args(func) - elif hasattr(callable_obj, "nin"): - # Matches RandomFunction or numpy ufunc - return callable_obj.nin - else: - try: - # Assume object is a function - func = callable_obj - # see https://docs.python.org/2/library/inspect.html#inspect.getargspec - # defaults might be None, or something weird for Mock functions - args, _, _, defaults = getargspec(func) - except TypeError: - # Callable object - func = callable_obj.__call__ - args, _, _, defaults = getargspec(func) - - try: - num_args = len(args) - len(defaults) - except TypeError: - num_args = len(args) - - # If func is a bound method, remove one argument - # (in Python 2.7, unbound methods have __self__ = None) - try: - if func.__self__ is not None: - num_args += -1 - except AttributeError: - pass - - return num_args - def is_callable_with_args(num_args): """ Validates that a function is callable and takes num_args arguments @@ -293,9 +179,11 @@ def is_callable_with_args(num_args): >>> def func(x, y): return x + y >>> is_callable_with_args(2)(func) == func True - >>> is_callable_with_args(3)(func) == func # doctest: +ELLIPSIS - Traceback (most recent call last): - Invalid: Expected function... to have 3 arguments, instead it has 2 + >>> try: # doctest: +ELLIPSIS + ... is_callable_with_args(3)(func) == func + ... except Invalid as error: + ... print(error) + Expected function ... to have 3 arguments, instead it has 2 Callable objects work, too: >>> class Foo: @@ -304,9 +192,11 @@ def is_callable_with_args(num_args): >>> foo = Foo() >>> is_callable_with_args(1)(foo) == foo True - >>> is_callable_with_args(1)(Foo) # doctest: +ELLIPSIS - Traceback (most recent call last): - Invalid: Expected function... to have 1 arguments, instead it has 2 + >>> try: # doctest: +ELLIPSIS + ... is_callable_with_args(1)(Foo) + ... except Invalid as error: + ... print(error) + Expected function... to have 1 arguments, instead it has 2 """ def _validate(func): # first, check that the function is callable From 89618458d39cd51646fe1842a0931c4b2b5fe599 Mon Sep 17 00:00:00 2001 From: Chris Chudzicki Date: Mon, 1 Jul 2019 11:22:05 -0400 Subject: [PATCH 02/14] convert doctest tracebacks to try/except --- .../matrix_grader/matrix_grader.md | 36 +++++++++++-------- docs/index.md | 10 +++--- 2 files changed, 28 insertions(+), 18 deletions(-) diff --git a/docs/grading_math/matrix_grader/matrix_grader.md b/docs/grading_math/matrix_grader/matrix_grader.md index 9e8a7d9a..31628caa 100644 --- a/docs/grading_math/matrix_grader/matrix_grader.md +++ b/docs/grading_math/matrix_grader/matrix_grader.md @@ -164,9 +164,11 @@ Vectors are distinct from single-row matrices and single-column matrices, and ca >>> vec = MathArray([1, 2, 3]) >>> row = MathArray([[1, 2, 3]]) >>> col = MathArray([[1], [2], [3]]) ->>> vec + row # raises error -Traceback (most recent call last): -MathArrayShapeError: Cannot add/subtract a vector of length 3 with a matrix of shape (rows: 1, cols: 3). +>>> try: +... vec + row # raises error +... except StudentFacingError as error: +... print(error) +Cannot add/subtract a vector of length 3 with a matrix of shape (rows: 1, cols: 3). >>> A = MathArray([[1, 2, 3], [4, 5, 6]]) >>> A * vec # matrix * vector @@ -200,20 +202,26 @@ Some sample error messages: | `'A^2'` | No | Cannot raise a non-square matrix to powers. | | `'B^2'` | Yes | n/a | - + diff --git a/docs/index.md b/docs/index.md index 9fa333dc..12c84d79 100644 --- a/docs/index.md +++ b/docs/index.md @@ -131,10 +131,12 @@ The options passed to a grading class undergo extensive validation and graders w A few error messages serve only as warnings. For example, if you attempt to configure a `FormulaGrader` with `pi` as a variable, you will receive a warning: ```pycon ->>> from mitxgraders import FormulaGrader ->>> grader = FormulaGrader(variables=['pi']) -Traceback (most recent call last): -ConfigError: Warning: 'variables' contains entries 'pi' which will override default values. If you intend to override defaults, you may suppress this warning by adding 'suppress_warnings=True' to the grader configuration. +>>> from mitxgraders import * +>>> try: +... grader = FormulaGrader(variables=['pi']) +... except ConfigError as error: +... print(error) +Warning: 'variables' contains entries 'pi' which will override default values. If you intend to override defaults, you may suppress this warning by adding 'suppress_warnings=True' to the grader configuration. ``` From 8133c68fdac75f8f91ee5a7cd63f08ee8cbc282d Mon Sep 17 00:00:00 2001 From: Chris Chudzicki Date: Mon, 1 Jul 2019 15:38:57 -0400 Subject: [PATCH 03/14] set numpy printer to legacy when running tests --- conftest.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 conftest.py diff --git a/conftest.py b/conftest.py new file mode 100644 index 00000000..98548384 --- /dev/null +++ b/conftest.py @@ -0,0 +1,15 @@ +import numpy as np + +try: + # Prior to version 1.13, numpy added an extra space before floats when printing arrays + # We use 1.6 for Python 2 and 1.16 for Python 3, so the printing difference + # causes problems for doctests. + # + # Setting the printer to legacy 1.13 combined with the doctest directive + # NORMALIZE_WHITESPACE is fixes the issue. + np.set_printoptions(legacy='1.13') + body = "# Setting numpy to print in legacy mode" + msg = "{header}\n{body}\n{footer}".format(header='#'*40, footer='#'*40, body=body) + print(msg) +except TypeError: + pass From ab35ae76b52144a48aa61320fb9fc1d3f1336268 Mon Sep 17 00:00:00 2001 From: Chris Chudzicki Date: Mon, 1 Jul 2019 15:39:34 -0400 Subject: [PATCH 04/14] fix some more doctests --- mitxgraders/comparers/comparers.py | 32 +++--- mitxgraders/formulagrader/formulagrader.py | 107 +++++++++++---------- mitxgraders/helpers/calc/expressions.py | 101 ++++++++++--------- mitxgraders/helpers/calc/mathfuncs.py | 28 +++--- 4 files changed, 150 insertions(+), 118 deletions(-) diff --git a/mitxgraders/comparers/comparers.py b/mitxgraders/comparers/comparers.py index a97442d2..ffafdd15 100644 --- a/mitxgraders/comparers/comparers.py +++ b/mitxgraders/comparers/comparers.py @@ -93,9 +93,11 @@ def between_comparer(comparer_params_eval, student_eval, utils): True Input must be real: - >>> grader(None, '5e8+2e6*i')['ok'] - Traceback (most recent call last): - InputTypeError: Input must be real. + >>> try: + ... grader(None, '5e8+2e6*i')['ok'] + ... except InputTypeError as error: + ... print(error) + Input must be real. """ start, stop = comparer_params_eval @@ -232,9 +234,11 @@ def vector_span_comparer(comparer_params_eval, student_eval, utils): True Input shape is validated: - >>> grader(None, '5') - Traceback (most recent call last): - InputTypeError: Expected answer to be a vector, but input is a scalar + >>> try: + ... grader(None, '5') + ... except InputTypeError as error: + ... print(error) + Expected answer to be a vector, but input is a scalar Multiple vectors can be provided: >>> grader = MatrixGrader( @@ -261,9 +265,11 @@ def vector_span_comparer(comparer_params_eval, student_eval, utils): ... 'comparer': vector_span_comparer ... }, ... ) - >>> grader(None, '[1, 2, 3]') # doctest: +ELLIPSIS - Traceback (most recent call last): - StudentFacingError: Problem Configuration Error: ...to equal-length vectors + >>> try: + ... grader(None, '[1, 2, 3]') # doctest: +ELLIPSIS + ... except StudentFacingError as error: + ... print(error) + Problem Configuration Error: ...to equal-length vectors """ # Validate the comparer params @@ -337,9 +343,11 @@ def vector_phase_comparer(comparer_params_eval, student_eval, utils): ... 'comparer': vector_phase_comparer ... }, ... ) - >>> grader(None, '[1, 2, 3]') # doctest: +ELLIPSIS - Traceback (most recent call last): - StudentFacingError: Problem Configuration Error: ...to a single vector. + >>> try: + ... grader(None, '[1, 2, 3]') # doctest: +ELLIPSIS + ... except StudentFacingError as error: + ... print(error) + Problem Configuration Error: ...to a single vector. """ # Validate that author comparer_params evaluate to a single vector if not len(comparer_params_eval) == 1 and is_vector(comparer_params_eval[0]): diff --git a/mitxgraders/formulagrader/formulagrader.py b/mitxgraders/formulagrader/formulagrader.py index 74a55d71..da760396 100644 --- a/mitxgraders/formulagrader/formulagrader.py +++ b/mitxgraders/formulagrader/formulagrader.py @@ -72,13 +72,14 @@ def validate_forbidden_strings_not_used(expr, forbidden_strings, forbidden_msg): True Fails validation if any forbidden string is used: - >>> validate_forbidden_strings_not_used( - ... 'sin(x+x)', - ... ['*x', '+ x', '- x'], - ... 'A forbidden string was used!' - ... ) - Traceback (most recent call last): - InvalidInput: A forbidden string was used! + >>> try: + ... validate_forbidden_strings_not_used( + ... 'sin(x+x)', + ... ['*x', '+ x', '- x'], + ... 'A forbidden string was used!') + ... except InvalidInput as error: + ... print(error) + A forbidden string was used! """ stripped_expr = expr.replace(' ', '') for forbidden in forbidden_strings: @@ -101,14 +102,15 @@ def validate_only_permitted_functions_used(used_funcs, permitted_functions): ... set(['f', 'g', 'sin', 'cos']) ... ) True - >>> validate_only_permitted_functions_used( - ... set(['f', 'Sin', 'h']), - ... set(['f', 'g', 'sin', 'cos']) - ... ) - Traceback (most recent call last): - InvalidInput: Invalid Input: function(s) 'h', 'Sin' not permitted in answer + >>> try: + ... validate_only_permitted_functions_used( + ... set(['f', 'Sin', 'h']), + ... set(['f', 'g', 'sin', 'cos'])) + ... except InvalidInput as error: + ... print(error) + Invalid Input: function(s) 'Sin', 'h' not permitted in answer """ - used_not_permitted = [f for f in used_funcs if f not in permitted_functions] + used_not_permitted = sorted([f for f in used_funcs if f not in permitted_functions]) if used_not_permitted: func_names = ", ".join(["'{f}'".format(f=f) for f in used_not_permitted]) message = "Invalid Input: function(s) {} not permitted in answer".format(func_names) @@ -167,14 +169,15 @@ def get_permitted_functions(default_funcs, whitelist, blacklist, always_allowed) Blacklist and whitelist cannot be simultaneously used: >>> default_funcs = {'sin': None, 'cos': None, 'tan': None} >>> always_allowed = {'f1': None, 'f2': None} - >>> get_permitted_functions( - ... default_funcs, - ... ['sin'], - ... ['cos'], - ... always_allowed - ... ) - Traceback (most recent call last): - ValueError: whitelist and blacklist cannot both be non-empty + >>> try: + ... get_permitted_functions( + ... default_funcs, + ... ['sin'], + ... ['cos'], + ... always_allowed) + ... except ValueError as error: + ... print(error) + whitelist and blacklist cannot both be non-empty """ # should never trigger except in doctest above, # Grader's config validation should raise an error first @@ -200,12 +203,13 @@ def validate_required_functions_used(used_funcs, required_funcs): ... ['cos', 'f'] ... ) True - >>> validate_required_functions_used( - ... ['sin', 'cos', 'F', 'g'], - ... ['cos', 'f'] - ... ) - Traceback (most recent call last): - InvalidInput: Invalid Input: Answer must contain the function f + >>> try: + ... validate_required_functions_used( + ... ['sin', 'cos', 'F', 'g'], + ... ['cos', 'f']) + ... except InvalidInput as error: + ... print(error) + Invalid Input: Answer must contain the function f """ for func in required_funcs: if func not in used_funcs: @@ -262,21 +266,25 @@ def validate_no_collisions(config, keys): Duplicate entries raise a ConfigError: >>> keys = ['variables', 'user_constants', 'numbered_vars'] - >>> validate_no_collisions({ - ... 'variables':['a', 'b', 'c', 'x', 'y'], - ... 'user_constants':{'x': 5, 'y': 10}, - ... 'numbered_vars':['phi', 'psi'] - ... }, keys) - Traceback (most recent call last): - ConfigError: 'user_constants' and 'variables' contain duplicate entries: ['x', 'y'] - - >>> validate_no_collisions({ - ... 'variables':['a', 'psi', 'phi', 'X', 'Y'], - ... 'user_constants':{'x': 5, 'y': 10}, - ... 'numbered_vars':['phi', 'psi'] - ... }, keys) - Traceback (most recent call last): - ConfigError: 'numbered_vars' and 'variables' contain duplicate entries: ['phi', 'psi'] + >>> try: + ... validate_no_collisions({ + ... 'variables':['a', 'b', 'c', 'x', 'y'], + ... 'user_constants':{'x': 5, 'y': 10}, + ... 'numbered_vars':['phi', 'psi'] + ... }, keys) + ... except ConfigError as error: + ... print(error) + 'user_constants' and 'variables' contain duplicate entries: ['x', 'y'] + + >>> try: + ... validate_no_collisions({ + ... 'variables':['a', 'psi', 'phi', 'X', 'Y'], + ... 'user_constants':{'x': 5, 'y': 10}, + ... 'numbered_vars':['phi', 'psi'] + ... }, keys) + ... except ConfigError as error: + ... print(error) + 'numbered_vars' and 'variables' contain duplicate entries: ['phi', 'psi'] Without duplicates, return True >>> validate_no_collisions({ @@ -308,13 +316,12 @@ def warn_if_override(config, key, defaults): ===== >>> config = {'vars': ['a', 'b', 'cat', 'psi', 'pi']} - >>> warn_if_override( - ... config, - ... 'vars', - ... {'cat': 1, 'pi': 2} - ... ) # doctest: +ELLIPSIS - Traceback (most recent call last): - ConfigError: Warning: 'vars' contains entries 'cat', 'pi' ... + >>> defaults = {'cat': 1, 'pi': 2} + >>> try: + ... warn_if_override(config, 'vars', defaults) # doctest: +ELLIPSIS + ... except ConfigError as error: + ... print(error) + Warning: 'vars' contains entries 'cat', 'pi' ... >>> config = {'vars': ['a', 'b', 'cat', 'psi', 'pi'], 'suppress_warnings': True} >>> warn_if_override( diff --git a/mitxgraders/helpers/calc/expressions.py b/mitxgraders/helpers/calc/expressions.py index 427b7342..4b869e31 100644 --- a/mitxgraders/helpers/calc/expressions.py +++ b/mitxgraders/helpers/calc/expressions.py @@ -116,9 +116,11 @@ class BracketValidator(object): >>> BV = BracketValidator >>> expr = '1 + ( ( x + 1 )^2 + ( + [T_{1' - >>> BV.validate(expr) # doctest: +NORMALIZE_WHITESPACE - Traceback (most recent call last): - UnbalancedBrackets: Invalid Input: + >>> try: + ... BV.validate(expr)# doctest: +NORMALIZE_WHITESPACE + ... except UnbalancedBrackets as error: + ... print(error) + Invalid Input: 1 curly brace was opened without being closed (highlighted below) 2 parentheses were opened without being closed (highlighted below) 1 square bracket was opened without being closed (highlighted below) @@ -269,11 +271,8 @@ def cast_np_numeric_as_builtin(obj, map_across_lists=False): >>> z = 3 + 2j >>> z128 = np.complex128(z) >>> examples = [x, x64, y, y64, z, z128] - >>> [ - ... type(cast_np_numeric_as_builtin(example)) - ... for example in examples - ... ] == [float, float, int, int, complex, complex] - True + >>> [type(cast_np_numeric_as_builtin(ex)).__name__ for ex in examples] + ['float', 'float', 'int', 'int', 'complex', 'complex'] Leaves MathArrays alone: >>> from mitxgraders.helpers.calc.math_array import MathArray @@ -284,8 +283,8 @@ def cast_np_numeric_as_builtin(obj, map_across_lists=False): Optionally, map across a list: >>> target = [np.float64(1.0), np.float64(2.0)] >>> result = cast_np_numeric_as_builtin(target, map_across_lists=True) - >>> [type(item) for item in result] - [, ] + >>> [type(item).__name__ for item in result] + ['float', 'float'] """ if isinstance(obj, np.number): @@ -803,9 +802,11 @@ def eval_function(parse_result, functions): validate that the correct number of arguments are passed: >>> def h(x, y): return x + y - >>> MathExpression.eval_function(['h', [1, 2, 3]], {"h": h}) - Traceback (most recent call last): - ArgumentError: Wrong number of arguments passed to h(...): Expected 2 inputs, but received 3. + >>> try: + ... MathExpression.eval_function(['h', [1, 2, 3]], {"h": h}) + ... except ArgumentError as error: + ... print(error) + Wrong number of arguments passed to h(...): Expected 2 inputs, but received 3. However, if the function to be evaluated has a truthy 'validated' property, we assume it does its own validation and we do not check the @@ -817,9 +818,11 @@ def eval_function(parse_result, functions): ... raise StudentFacingError('I need two inputs!') ... return args[0]*args[1] >>> g.validated = True - >>> MathExpression.eval_function(['g', [1]], {"g": g}) - Traceback (most recent call last): - StudentFacingError: I need two inputs! + >>> try: + ... MathExpression.eval_function(['g', [1]], {"g": g}) + ... except StudentFacingError as error: + ... print(error) + I need two inputs! """ # Obtain the function and arguments name, args = parse_result @@ -897,16 +900,16 @@ def eval_array(parse_result, metadata_dict): 2 >>> metadata_dict = { 'max_array_dim_used': 0 } - >>> MathExpression.eval_array([ - ... [1, 2], + >>> MathExpression.eval_array([ # doctest: +NORMALIZE_WHITESPACE + ... [1 , 2], ... [3, 4] ... ], metadata_dict) - MathArray([[1, 2], - [3, 4]]) + MathArray([[1, 2], + [3, 4]]) In practice, this is called recursively: >>> metadata_dict = { 'max_array_dim_used': 0 } - >>> MathExpression.eval_array([ + >>> MathExpression.eval_array([ # doctest: +NORMALIZE_WHITESPACE ... MathExpression.eval_array([1, 2, 3], metadata_dict), ... MathExpression.eval_array([4, 5, 6], metadata_dict) ... ], metadata_dict) @@ -917,7 +920,7 @@ def eval_array(parse_result, metadata_dict): One complex entry will convert everything to complex: >>> metadata_dict = { 'max_array_dim_used': 0 } - >>> MathExpression.eval_array([ + >>> MathExpression.eval_array([ # doctest: +NORMALIZE_WHITESPACE ... MathExpression.eval_array([1, 2j, 3], metadata_dict), ... MathExpression.eval_array([4, 5, 6], metadata_dict) ... ], metadata_dict) @@ -926,20 +929,24 @@ def eval_array(parse_result, metadata_dict): We try to detect shape errors: >>> metadata_dict = { 'max_array_dim_used': 0 } - >>> MathExpression.eval_array([ # doctest: +ELLIPSIS - ... MathExpression.eval_array([1, 2, 3], metadata_dict), - ... 4 - ... ], metadata_dict) - Traceback (most recent call last): - UnableToParse: Unable to parse vector/matrix. If you're trying ... + >>> try: # doctest: +ELLIPSIS + ... MathExpression.eval_array([ + ... MathExpression.eval_array([1, 2, 3], metadata_dict), + ... 4 + ... ], metadata_dict) + ... except UnableToParse as error: + ... print(error) + Unable to parse vector/matrix. If you're trying ... >>> metadata_dict = { 'max_array_dim_used': 0 } - >>> MathExpression.eval_array([ # doctest: +ELLIPSIS - ... 2.0, - ... MathExpression.eval_array([1, 2, 3], metadata_dict), - ... 4 - ... ], metadata_dict) - Traceback (most recent call last): - UnableToParse: Unable to parse vector/matrix. If you're trying ... + >>> try: # doctest: +ELLIPSIS + ... MathExpression.eval_array([ + ... 2.0, + ... MathExpression.eval_array([1, 2, 3], metadata_dict), + ... 4 + ... ], metadata_dict) + ... except UnableToParse as error: + ... print(error) + Unable to parse vector/matrix. If you're trying ... """ shape_message = ("Unable to parse vector/matrix. If you're trying to " "enter a matrix, this is most likely caused by an " @@ -1061,9 +1068,11 @@ def eval_product(parse_result): ===== >>> MathExpression.eval_product([2,"*",3,"/",4]) 1.5 - >>> MathExpression.eval_product([2,"*",3,"+",4]) - Traceback (most recent call last): - CalcError: Unexpected symbol + in eval_product + >>> try: + ... MathExpression.eval_product([2,"*",3,"+",4]) + ... except CalcError as error: + ... print(error) + Unexpected symbol + in eval_product """ double_vector_mult_has_occured = False triple_vector_mult_error = CalcError( @@ -1109,9 +1118,11 @@ def eval_sum(parse_result): 1 >>> MathExpression.eval_sum(["+",2,"+",3,"-",4]) 1 - >>> MathExpression.eval_sum(["+",2,"*",3,"-",4]) - Traceback (most recent call last): - CalcError: Unexpected symbol * in eval_sum + >>> try: + ... MathExpression.eval_sum(["+",2,"*",3,"-",4]) + ... except CalcError as error: + ... print(error) + Unexpected symbol * in eval_sum """ data = parse_result[:] result = data.pop(0) @@ -1182,9 +1193,11 @@ def evaluator(formula, nan Submissions that generate infinities will raise an error: - >>> evaluator("inf", variables={'inf': float('inf')})[0] # doctest: +ELLIPSIS - Traceback (most recent call last): - CalcOverflowError: Numerical overflow occurred. Does your expression generate ... + >>> try: # doctest: +ELLIPSIS + ... evaluator("inf", variables={'inf': float('inf')})[0] + ... except CalcOverflowError as error: + ... print(error) + Numerical overflow occurred. Does your expression generate ... Unless you specify that infinity is ok: >>> evaluator("inf", variables={'inf': float('inf')}, allow_inf=True)[0] diff --git a/mitxgraders/helpers/calc/mathfuncs.py b/mitxgraders/helpers/calc/mathfuncs.py index 864bf610..198fde6a 100644 --- a/mitxgraders/helpers/calc/mathfuncs.py +++ b/mitxgraders/helpers/calc/mathfuncs.py @@ -128,7 +128,7 @@ def real(z): >>> isinstance(real(2+3j), float) True - Can be used with arrays, too: + Can be used with arrays, too: # doctest: +NORMALIZE_WHITESPACE >>> real(np.array([1+10j, 2+20j, 3+30j])) array([ 1., 2., 3.]) """ @@ -190,9 +190,11 @@ def factorial(z): True Throws errors at poles: - >>> factorial(-2) # doctest: +ELLIPSIS - Traceback (most recent call last): - FunctionEvalError: Error evaluating factorial() or fact() in input... + >>> try: # doctest: +ELLIPSIS + ... factorial(-2) + ... except FunctionEvalError as error: + ... print(error) + Error evaluating factorial() or fact() in input... """ try: @@ -442,18 +444,20 @@ def is_nearly_zero(x, tolerance, reference=None): False Works for arrays, too: - >>> x = np.array([[1, 1], [1, -1]])/10 - >>> round(np.linalg.norm(x), 6) - 0.2 - >>> is_nearly_zero(x, '5%', reference=10) + >>> x = np.array([[1, 1], [0, -1]]) + >>> np.linalg.norm(x) # doctest: +ELLIPSIS + 1.732050... + >>> is_nearly_zero(x, '18%', reference=10) True - >>> is_nearly_zero(x, '1.5%', reference=10) + >>> is_nearly_zero(x, '17%', reference=10) False A ValueError is raised when percentage tolerance is used without reference: - >>> is_nearly_zero(0.4, '3%') - Traceback (most recent call last): - ValueError: When tolerance is a percentage, reference must not be None. + >>> try: + ... is_nearly_zero(0.4, '3%') + ... except ValueError as error: + ... print(error) + When tolerance is a percentage, reference must not be None. """ # When used within graders, tolerance has already been # validated as a Number or PercentageString From 223ecdaf40e02e0e176ff83732d627462a557457 Mon Sep 17 00:00:00 2001 From: Chris Chudzicki Date: Mon, 1 Jul 2019 15:55:19 -0400 Subject: [PATCH 05/14] fix more doctests, including all remaining tracebacks --> try/except --- mitxgraders/helpers/calc/specify_domain.py | 76 +++++++++++++--------- mitxgraders/helpers/validatorfuncs.py | 11 ++-- 2 files changed, 54 insertions(+), 33 deletions(-) diff --git a/mitxgraders/helpers/calc/specify_domain.py b/mitxgraders/helpers/calc/specify_domain.py index 53fc9f32..707152e3 100644 --- a/mitxgraders/helpers/calc/specify_domain.py +++ b/mitxgraders/helpers/calc/specify_domain.py @@ -49,12 +49,16 @@ def number_validator(obj): True Provides a useful error message: - >>> number_validator(MathArray([1, 2, 3])) - Traceback (most recent call last): - Invalid: received a vector of length 3, expected a scalar - >>> number_validator([1, 2, 3]) - Traceback (most recent call last): - Invalid: received a list, expected a scalar + >>> try: + ... number_validator(MathArray([1, 2, 3])) + ... except Invalid as error: + ... print(error) + received a vector of length 3, expected a scalar + >>> try: + ... number_validator([1, 2, 3]) + ... except Invalid as error: + ... print(error) + received a list, expected a scalar """ if isinstance(obj, Number): @@ -82,33 +86,41 @@ def make_shape_validator(shape): MathArray([1, 2, 3, 4]) Provides useful error messages if obj is a number or MathArray: - >>> validate_vec4(MathArray([[1, 2, 3], [4, 5, 6]])) - Traceback (most recent call last): - Invalid: received a matrix of shape (rows: 2, cols: 3), expected a vector of length 4 - >>> validate_vec4(5) - Traceback (most recent call last): - Invalid: received a scalar, expected a vector of length 4 + >>> try: + ... validate_vec4(MathArray([[1, 2, 3], [4, 5, 6]])) + ... except Invalid as error: + ... print(error) + received a matrix of shape (rows: 2, cols: 3), expected a vector of length 4 + >>> try: + ... validate_vec4(5) + ... except Invalid as error: + ... print(error) + received a scalar, expected a vector of length 4 Fallback error message shows Python type: - >>> validate_vec4({}) - Traceback (most recent call last): - Invalid: received a dict, expected a vector of length 4 + >>> try: + ... validate_vec4({}) + ... except Invalid as error: + ... print(error) + received a dict, expected a vector of length 4 Instead of specifying a tuple shape, you can specify 'square' to demand square matrices of any dimension. >>> validate_square = make_shape_validator('square') >>> square2 = MathArray([[1, 2], [3, 4]]) >>> square3 = MathArray([[1, 2, 3], [4, 5, 6], [7, 8, 9]]) - >>> validate_square(square2) + >>> validate_square(square2) # doctest: +NORMALIZE_WHITESPACE MathArray([[1, 2], [3, 4]]) - >>> validate_square(square3) + >>> validate_square(square3) # doctest: +NORMALIZE_WHITESPACE MathArray([[1, 2, 3], [4, 5, 6], [7, 8, 9]]) - >>> validate_square(MathArray([1, 2, 3, 4])) - Traceback (most recent call last): - Invalid: received a vector of length 4, expected a square matrix + >>> try: + ... validate_square(MathArray([1, 2, 3, 4])) + ... except Invalid as error: + ... print(error) + received a vector of length 4, expected a square matrix """ def shape_validator(obj): if isinstance(obj, MathArray): @@ -167,14 +179,18 @@ class SpecifyDomain(ObjectWithSchema): If inputs are bad, student-facing ArgumentShapeErrors are thrown: >>> a = MathArray([2, -1, 3]) >>> b = MathArray([-1, 4]) - >>> cross(a, b) # doctest: +ELLIPSIS - Traceback (most recent call last): - ArgumentShapeError: There was an error evaluating function cross(...) + >>> try: + ... cross(a, b) + ... except ArgumentShapeError as error: + ... print(error) + There was an error evaluating function cross(...) 1st input is ok: received a vector of length 3 as expected 2nd input has an error: received a vector of length 2, expected a vector of length 3 - >>> cross(a) - Traceback (most recent call last): - ArgumentError: Wrong number of arguments passed to cross(...): Expected 2 inputs, but received 1. + >>> try: + ... cross(a) + ... except ArgumentError as error: + ... print(error) + Wrong number of arguments passed to cross(...): Expected 2 inputs, but received 1. To specify that an input should be a an array of specific size, use a list or tuple for that shape value. Below, [3, 2] specifies a 3 by 2 matrix (the tuple @@ -184,9 +200,11 @@ class SpecifyDomain(ObjectWithSchema): ... def f(x, y, z): ... pass # implement complicated stuff here >>> square_mat = MathArray([[1, 2], [3, 4]]) - >>> f(1, 2, 3, square_mat) # doctest: +ELLIPSIS - Traceback (most recent call last): - ArgumentShapeError: There was an error evaluating function f(...) + >>> try: + ... f(1, 2, 3, square_mat) + ... except ArgumentShapeError as error: + ... print(error) + There was an error evaluating function f(...) 1st input is ok: received a scalar as expected 2nd input has an error: received a scalar, expected a matrix of shape (rows: 3, cols: 2) 3rd input has an error: received a scalar, expected a vector of length 2 diff --git a/mitxgraders/helpers/validatorfuncs.py b/mitxgraders/helpers/validatorfuncs.py index ee98ed76..6aeea1b0 100644 --- a/mitxgraders/helpers/validatorfuncs.py +++ b/mitxgraders/helpers/validatorfuncs.py @@ -232,13 +232,16 @@ def is_shape_specification(min_dim=1, max_dim=None): Valid inputs are standardized to tuples: >>> vec_or_mat = Schema(is_shape_specification(min_dim=1, max_dim=2)) - >>> map(vec_or_mat, [3, (3,), [3], (4, 2), [4, 2] ]) + >>> valid_examples = [3, (3,), [3], (4, 2), [4, 2] ] + >>> [vec_or_mat(item) for item in valid_examples] [(3,), (3,), (3,), (4, 2), (4, 2)] Invalid inputs raise a useful error: - >>> vec_or_mat(0) # doctest: +ELLIPSIS - Traceback (most recent call last): - MultipleInvalid: expected shape specification to be a positive integer,... + >>> try: # doctest: +ELLIPSIS + ... vec_or_mat(0) + ... except Invalid as error: + ... print(error) + expected shape specification to be a positive integer,... """ msg = ('expected shape specification to be a positive integer, or a ' From 81b1eb8048152e1180851db811518c174447185d Mon Sep 17 00:00:00 2001 From: Chris Chudzicki Date: Mon, 1 Jul 2019 21:04:18 -0400 Subject: [PATCH 06/14] fix some doctests, change somme iterators to lists zip, map, filter, .keys(), .items(), and .values() return single-use iterators in Python 3, but lists in Python 2. I've changed all instances of these iterators to explicit lists unless the instance is immediately consumed (i.e., not given a reference for reuse). --- mitxgraders/formulagrader/formulagrader.py | 2 +- mitxgraders/helpers/calc/math_array.py | 8 ++++---- mitxgraders/plugins/integralgrader.py | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/mitxgraders/formulagrader/formulagrader.py b/mitxgraders/formulagrader/formulagrader.py index da760396..89813d7e 100644 --- a/mitxgraders/formulagrader/formulagrader.py +++ b/mitxgraders/formulagrader/formulagrader.py @@ -709,7 +709,7 @@ def gen_var_and_func_samples(self, answer, student_input, sibling_formulas): self.suffixes, self.constants) - func_samples = gen_symbols_samples(self.random_funcs.keys(), + func_samples = gen_symbols_samples(list(self.random_funcs.keys()), self.config['samples'], self.random_funcs, self.functions, diff --git a/mitxgraders/helpers/calc/math_array.py b/mitxgraders/helpers/calc/math_array.py index 6dc80dc1..bd5e44a6 100644 --- a/mitxgraders/helpers/calc/math_array.py +++ b/mitxgraders/helpers/calc/math_array.py @@ -16,8 +16,8 @@ def is_number_zero(value): """ Tests whether a value is the scalar number 0. - >>> map(is_number_zero, [0, 0.0, 0j]) - [True, True, True] + >>> is_number_zero(0), is_number_zero(0.0), is_number_zero(0) + (True, True, True) >>> is_number_zero(np.matrix([0, 0, 0])) False """ @@ -434,10 +434,10 @@ def identity(n): Usage: - >>> identity(2) + >>> identity(2) # doctest: +NORMALIZE_WHITESPACE MathArray([[ 1., 0.], [ 0., 1.]]) - >>> identity(3) + >>> identity(3) # doctest: +NORMALIZE_WHITESPACE MathArray([[ 1., 0., 0.], [ 0., 1., 0.], [ 0., 0., 1.]]) diff --git a/mitxgraders/plugins/integralgrader.py b/mitxgraders/plugins/integralgrader.py index 7c0d617c..16551b6e 100644 --- a/mitxgraders/plugins/integralgrader.py +++ b/mitxgraders/plugins/integralgrader.py @@ -378,7 +378,7 @@ def raw_check(self, answer, cleaned_input): self.config['sample_from'], self.functions, {}, self.constants) - func_samples = gen_symbols_samples(self.random_funcs.keys(), + func_samples = gen_symbols_samples(list(self.random_funcs.keys()), self.config['samples'], self.random_funcs, self.functions, {}, {} ) From e5602cb554be28e706a914473d6fddfbe6fbfcb0 Mon Sep 17 00:00:00 2001 From: Chris Chudzicki Date: Mon, 1 Jul 2019 21:10:07 -0400 Subject: [PATCH 07/14] for Python 3, import reload from importlib --- tests/test_zip.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests/test_zip.py b/tests/test_zip.py index 301a37ae..82d7e183 100644 --- a/tests/test_zip.py +++ b/tests/test_zip.py @@ -8,6 +8,13 @@ import pytest import mitxgraders +try: + # for Python 3 + from importlib import reload +except ImportError: + # reload is builtin in Python 2 + pass + @pytest.fixture() def loadzip(): """pytest fixture to dynamically load the library from python_lib.zip""" From 2540490feee6380d454360ae3aa99cbe20b99ea0 Mon Sep 17 00:00:00 2001 From: Chris Chudzicki Date: Mon, 1 Jul 2019 21:15:30 -0400 Subject: [PATCH 08/14] change another dict.items() to list(dict.items()) Even though this is consumed immediately in a for-loop, it needs to be a list not a generator since its dictionary is being modified. --- mitxgraders/sampling.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mitxgraders/sampling.py b/mitxgraders/sampling.py index e17328e4..8c90c060 100644 --- a/mitxgraders/sampling.py +++ b/mitxgraders/sampling.py @@ -655,7 +655,7 @@ def gen_symbols_samples(symbols, samples, sample_from, functions, suffixes, cons } while unevaluated_dependents: progress_made = False - for symbol, dependencies in unevaluated_dependents.items(): + for symbol, dependencies in list(unevaluated_dependents.items()): if is_subset(dependencies, sample_dict): sample_dict[symbol] = sample_from[symbol].compute_sample( sample_dict, functions, suffixes) From a0c98373091f07d40110c2249e140cdb21280954 Mon Sep 17 00:00:00 2001 From: Chris Chudzicki Date: Mon, 1 Jul 2019 21:21:34 -0400 Subject: [PATCH 09/14] handle numpy ufuncs in get_number_of_args_py3 --- mitxgraders/helpers/get_number_of_args.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/mitxgraders/helpers/get_number_of_args.py b/mitxgraders/helpers/get_number_of_args.py index c4bd3639..fe3bada0 100644 --- a/mitxgraders/helpers/get_number_of_args.py +++ b/mitxgraders/helpers/get_number_of_args.py @@ -86,6 +86,11 @@ def get_number_of_args_py3(callable_obj): - based on inspect.signature - in Python 2, use getargspec-based get_number_of_args_py3 instead """ + if hasattr(callable_obj, "nin"): + # Matches RandomFunction or numpy ufunc + # Sadly, even Py3's inspect.signature can't handle numpy ufunc... + return callable_obj.nin + params = inspect.signature(callable_obj).parameters empty = inspect.Parameter.empty return sum([params[key].default == empty for key in params]) From c47ef55212242fa1d56e0f65a1b4788989779f0d Mon Sep 17 00:00:00 2001 From: Chris Chudzicki Date: Mon, 1 Jul 2019 22:02:42 -0400 Subject: [PATCH 10/14] make mock compatible with py2 and py3 --- tests/formulagrader/test_formulagrader.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/tests/formulagrader/test_formulagrader.py b/tests/formulagrader/test_formulagrader.py index 1a34ea09..675e7d9c 100644 --- a/tests/formulagrader/test_formulagrader.py +++ b/tests/formulagrader/test_formulagrader.py @@ -446,10 +446,18 @@ def is_coterminal_and_large(comparer_params, student_input, utils): reduced = student_input % (360) return utils.within_tolerance(answer, reduced) and student_input > min_value - mocked = mock.Mock(side_effect=is_coterminal_and_large, - # The next two kwargs ensure that the Mock behaves nicely for inspect.getargspec - spec=is_coterminal_and_large, - func_code=is_coterminal_and_large.func_code,) + mocked = (mock.create_autospec(is_coterminal_and_large, + side_effect=is_coterminal_and_large) + if six.PY3 else + mock.Mock(side_effect=is_coterminal_and_large, + # The next two kwargs ensure that the Mock behaves nicely + # for inspect.getargspec in Python 2 + # but this does NOT work in Python 3, where create_autospec + # is needed + spec=is_coterminal_and_large, + func_code=is_coterminal_and_large.__code__) + ) + grader = FormulaGrader( answers={ From b00a70ad3e061841b0bc5a71acda3e7f297301b3 Mon Sep 17 00:00:00 2001 From: Chris Chudzicki Date: Mon, 1 Jul 2019 22:37:04 -0400 Subject: [PATCH 11/14] make get_number_of_args raise an error in Py2 if used with constructor When used on class constructors: ```python class Foo(object): def __call__(self, x): return x ``` get_number_of_args_py2 previously returned the number of arguments in Foo.__call__, which was incorrect, since calling Foo(...) would run the __init__ method! --- mitxgraders/helpers/get_number_of_args.py | 36 ++++++++++++++++++----- mitxgraders/helpers/validatorfuncs.py | 4 +-- 2 files changed, 31 insertions(+), 9 deletions(-) diff --git a/mitxgraders/helpers/get_number_of_args.py b/mitxgraders/helpers/get_number_of_args.py index fe3bada0..e0f7321c 100644 --- a/mitxgraders/helpers/get_number_of_args.py +++ b/mitxgraders/helpers/get_number_of_args.py @@ -42,6 +42,24 @@ def get_number_of_args_py2(callable_obj): raises a DeprecationWarning in Python 3. - in Python 3, use the much simpler signature-based get_number_of_args_py3 funciton instead. + - Cannot handle class constructors + + Usage + ===== + See documetnation for get_number_of_args. + + Note, however, that this function cannot handle class constructors: + + >>> class Foo(object): + ... def __init__(self, x, y): + ... pass + >>> try: # doctest: +ELLIPSIS + ... get_number_of_args_py2(Foo) + ... except ValueError as error: + ... print(error) + Cannot detect number of arguments for + + """ if inspect.isbuiltin(callable_obj): @@ -57,11 +75,14 @@ def get_number_of_args_py2(callable_obj): func = callable_obj # see https://docs.python.org/2/library/inspect.html#inspect.inspect.getargspec # defaults might be None, or something weird for Mock functions - args, _, _, defaults = inspect.getargspec(func) + elif inspect.isclass(callable_obj): + # We don't need this anyway + raise ValueError("Cannot detect number of arguments for {}".format(callable_obj)) else: - # Callable object + # callable object instance func = callable_obj.__call__ - args, _, _, defaults = inspect.getargspec(func) + + args, _, _, defaults = inspect.getargspec(func) try: num_args = len(args) - len(defaults) @@ -124,16 +145,17 @@ def get_number_of_args(callable_obj): >>> get_number_of_args(foo.do_stuff) # bound, is automatically passed self as argument 3 - Works for bound and unbound callable objects + Works for callable objects instances: >>> class Bar: ... def __call__(self, x, y): ... return x + y - >>> get_number_of_args(Bar) # unbound, is NOT automatically passed self as argument - 3 >>> bar = Bar() - >>> get_number_of_args(bar) # bound, is automatically passed self as argument + >>> get_number_of_args(bar) # bound instance, is automatically passed self as argument 2 + Note about class constructors: In Python 2, get_number_of_args(Bar) will + raise an error; in Python 3, the number of arguments of __init__ is returned. + Works on built-in functions (assuming their docstring is correct) >>> import math >>> get_number_of_args(math.sin) diff --git a/mitxgraders/helpers/validatorfuncs.py b/mitxgraders/helpers/validatorfuncs.py index 6aeea1b0..96eca31d 100644 --- a/mitxgraders/helpers/validatorfuncs.py +++ b/mitxgraders/helpers/validatorfuncs.py @@ -193,10 +193,10 @@ def is_callable_with_args(num_args): >>> is_callable_with_args(1)(foo) == foo True >>> try: # doctest: +ELLIPSIS - ... is_callable_with_args(1)(Foo) + ... is_callable_with_args(2)(foo) ... except Invalid as error: ... print(error) - Expected function... to have 1 arguments, instead it has 2 + Expected function ... to have 2 arguments, instead it has 1 """ def _validate(func): # first, check that the function is callable From 1b5a4a2e5c3d9ab5b20c372fed05969854a2a635 Mon Sep 17 00:00:00 2001 From: Chris Chudzicki Date: Mon, 1 Jul 2019 22:41:40 -0400 Subject: [PATCH 12/14] fix typo --- conftest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/conftest.py b/conftest.py index 98548384..3bbc3925 100644 --- a/conftest.py +++ b/conftest.py @@ -6,7 +6,7 @@ # causes problems for doctests. # # Setting the printer to legacy 1.13 combined with the doctest directive - # NORMALIZE_WHITESPACE is fixes the issue. + # NORMALIZE_WHITESPACE fixes the issue. np.set_printoptions(legacy='1.13') body = "# Setting numpy to print in legacy mode" msg = "{header}\n{body}\n{footer}".format(header='#'*40, footer='#'*40, body=body) From 9e57fd1e3a58bb7e672d77ceef7b3cc5d7435689 Mon Sep 17 00:00:00 2001 From: Chris Chudzicki Date: Tue, 2 Jul 2019 16:08:50 -0400 Subject: [PATCH 13/14] fix typos --- mitxgraders/helpers/calc/math_array.py | 2 +- mitxgraders/helpers/get_number_of_args.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/mitxgraders/helpers/calc/math_array.py b/mitxgraders/helpers/calc/math_array.py index bd5e44a6..598520fa 100644 --- a/mitxgraders/helpers/calc/math_array.py +++ b/mitxgraders/helpers/calc/math_array.py @@ -16,7 +16,7 @@ def is_number_zero(value): """ Tests whether a value is the scalar number 0. - >>> is_number_zero(0), is_number_zero(0.0), is_number_zero(0) + >>> is_number_zero(0), is_number_zero(0.0), is_number_zero(0j) (True, True, True) >>> is_number_zero(np.matrix([0, 0, 0])) False diff --git a/mitxgraders/helpers/get_number_of_args.py b/mitxgraders/helpers/get_number_of_args.py index e0f7321c..d857cc47 100644 --- a/mitxgraders/helpers/get_number_of_args.py +++ b/mitxgraders/helpers/get_number_of_args.py @@ -46,7 +46,7 @@ def get_number_of_args_py2(callable_obj): Usage ===== - See documetnation for get_number_of_args. + See documentation for get_number_of_args. Note, however, that this function cannot handle class constructors: @@ -105,7 +105,7 @@ def get_number_of_args_py3(callable_obj): NOTES: - based on inspect.signature - - in Python 2, use getargspec-based get_number_of_args_py3 instead + - in Python 2, use getargspec-based get_number_of_args_py2 instead """ if hasattr(callable_obj, "nin"): # Matches RandomFunction or numpy ufunc From 8f68818d70a1a129c031c416b2907a7b12434748 Mon Sep 17 00:00:00 2001 From: Chris Chudzicki Date: Tue, 2 Jul 2019 16:08:57 -0400 Subject: [PATCH 14/14] change ugly conditional expression to if/else blocks --- tests/formulagrader/test_formulagrader.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/tests/formulagrader/test_formulagrader.py b/tests/formulagrader/test_formulagrader.py index 675e7d9c..d98524d8 100644 --- a/tests/formulagrader/test_formulagrader.py +++ b/tests/formulagrader/test_formulagrader.py @@ -446,17 +446,17 @@ def is_coterminal_and_large(comparer_params, student_input, utils): reduced = student_input % (360) return utils.within_tolerance(answer, reduced) and student_input > min_value - mocked = (mock.create_autospec(is_coterminal_and_large, - side_effect=is_coterminal_and_large) - if six.PY3 else - mock.Mock(side_effect=is_coterminal_and_large, - # The next two kwargs ensure that the Mock behaves nicely - # for inspect.getargspec in Python 2 - # but this does NOT work in Python 3, where create_autospec - # is needed - spec=is_coterminal_and_large, - func_code=is_coterminal_and_large.__code__) - ) + if six.PY3: + mocked = mock.create_autospec(is_coterminal_and_large, + side_effect=is_coterminal_and_large) + else: + # The last two kwargs ensure that the Mock behaves nicely + # for inspect.getargspec in Python 2 + # but this does NOT work in Python 3, where create_autospec + # is needed + mocked = mock.Mock(side_effect=is_coterminal_and_large, + spec=is_coterminal_and_large, + func_code=is_coterminal_and_large.__code__) grader = FormulaGrader(