Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions conftest.py
Original file line number Diff line number Diff line change
@@ -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 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
36 changes: 22 additions & 14 deletions docs/grading_math/matrix_grader/matrix_grader.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -200,20 +202,26 @@ Some sample error messages:
| `'A^2'` | No | Cannot raise a non-square matrix to powers. |
| `'B^2'` | Yes | n/a |

<!-- Test the messages -->
<!-- Test the messages (internal verification, not shown in docs) -->
<!-- ```pycon
>>> A + B
Traceback (most recent call last):
MathArrayShapeError: Cannot add/subtract a matrix of shape (rows: 3, cols: 2) with a matrix of shape (rows: 2, cols: 2).

>>> from mitxgraders.helpers.calc.exceptions import MathArrayShapeError
>>> try:
... A + B
... except MathArrayShapeError as error:
... print(error)
Cannot add/subtract a matrix of shape (rows: 3, cols: 2) with a matrix of shape (rows: 2, cols: 2).

>>> v*A
Traceback (most recent call last):
MathArrayShapeError: Cannot multiply a vector of length 2 with a matrix of shape (rows: 3, cols: 2).
>>> try:
... v*A
... except MathArrayShapeError as error:
... print(error)
Cannot multiply a vector of length 2 with a matrix of shape (rows: 3, cols: 2).

>>> A**2
Traceback (most recent call last):
MathArrayShapeError: Cannot raise a non-square matrix to powers.
>>> try:
... A**2
... except MathArrayShapeError as error:
... print(error)
Cannot raise a non-square matrix to powers.

``` -->

Expand Down
10 changes: 6 additions & 4 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

```

Expand Down
32 changes: 20 additions & 12 deletions mitxgraders/comparers/comparers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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(
Expand All @@ -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
Expand Down Expand Up @@ -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]):
Expand Down
109 changes: 58 additions & 51 deletions mitxgraders/formulagrader/formulagrader.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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)
Expand Down Expand Up @@ -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
Expand All @@ -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:
Expand Down Expand Up @@ -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({
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -702,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,
Expand Down
Loading