Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Py23 unicode #2

Open
wants to merge 19 commits into
base: py23
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
c92ca24
add text_string voluptuous validator/coercer
ChristopherChudzicki Jun 27, 2019
92838b6
use unicode_literals in baseclasses.py and fix failing tests
ChristopherChudzicki Jun 27, 2019
189ea26
use unicode literals, fix most tests
ChristopherChudzicki Jun 27, 2019
66c43c6
replace explicit str calls in formulagrader and fix tests
ChristopherChudzicki Jun 27, 2019
30e082c
validate/coerce that student_input is unicode or [unicode]
ChristopherChudzicki Jun 27, 2019
aa0b4ec
replace explicit str calls with text_type, fix failing tests
ChristopherChudzicki Jun 27, 2019
cc478ab
ignore unicode literal prefix in doctests
ChristopherChudzicki Jun 28, 2019
d41cc46
remove more explicit str
ChristopherChudzicki Jun 28, 2019
5d29d9b
replace (str) with (text_string)
ChristopherChudzicki Jun 28, 2019
5b35fc3
when reading expect from argument, leave as unicode
ChristopherChudzicki Jun 28, 2019
44b25d2
add unicode_literal future import and fix tests
ChristopherChudzicki Jun 28, 2019
ec46708
add unicode_literal future import & fix tests
ChristopherChudzicki Jun 28, 2019
5d9f0c3
add unicode_literals
ChristopherChudzicki Jun 29, 2019
dff1a38
remove some explicit str()
ChristopherChudzicki Jun 29, 2019
e43a363
address most review comments
ChristopherChudzicki Jun 30, 2019
f334255
refactor ensure_text_inputs
ChristopherChudzicki Jun 30, 2019
d698263
stop checking that student_input is a list in ListGrader.check
ChristopherChudzicki Jun 30, 2019
2c89901
refactor ensure_text_inputs jolyon's way
ChristopherChudzicki Jun 30, 2019
adc375f
increase test coverage
ChristopherChudzicki Jun 30, 2019
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
45 changes: 27 additions & 18 deletions docs/grading_math/matrix_grader/matrix_grader.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,16 +21,19 @@ A typical use of MatrixGrader might look like

The next few lines call the grader as a check function. The inputs `'4*A*B^2*v'` and `'4*A*B*B*v'` are correct:
```pycon
>>> grader1(None, '4*A*B^2*v')
{'grade_decimal': 1, 'msg': '', 'ok': True}
>>> grader1(None, '4*A*B*B*v')
{'grade_decimal': 1, 'msg': '', 'ok': True}
>>> result = grader1(None, '4*A*B^2*v')
>>> result == {'grade_decimal': 1, 'msg': '', 'ok': True}
True
>>> result = grader1(None, '4*A*B*B*v')
>>> result == {'grade_decimal': 1, 'msg': '', 'ok': True}
True

```
while the input `'4*B*A*B*v'` is incorrect because the matrix-sampled variables are non-commutative:
```pycon
>>> grader1(None, '4*B*A*B*v')
{'msg': '', 'grade_decimal': 0, 'ok': False}
>>> result = grader1(None, '4*B*A*B*v')
>>> result == {'msg': '', 'grade_decimal': 0, 'ok': False}
True

```

Expand Down Expand Up @@ -234,8 +237,8 @@ For example, student enters `'[[1, 2],[3] ]'`, a matrix missing an entry in seco
>>> try:
... grader(None, student_input) # grade the input like edX would
... except StudentFacingError as error:
... str(error) # students see this error message
"Unable to parse vector/matrix. If you're trying to enter a matrix, this is most likely caused by an unequal number of elements in each row."
... print(error) # students see this error message
Unable to parse vector/matrix. If you're trying to enter a matrix, this is most likely caused by an unequal number of elements in each row.

```

Expand All @@ -253,8 +256,8 @@ If a student submits an answer that will raise shape-mismatch errors then an err
>>> try:
... grader(None, student_input) # grade the input like edX would
... except StudentFacingError as error:
... str(error) # students see this error message
'Cannot add/subtract a vector of length 3 with a vector of length 2.'
... print(error) # students see this error message
Cannot add/subtract a vector of length 3 with a vector of length 2.

```

Expand All @@ -269,22 +272,23 @@ If the author's answer is a 3-component vector, and the student submits a differ
... answers='[1, 2, 3]',
... )
>>> student_input = '[1, 2, -3]' # wrong answer
>>> grader(None, student_input) # grade the input like edX would
{'msg': '', 'grade_decimal': 0, 'ok': False}
>>> result = grader(None, student_input) # grade the input like edX would
>>> result == {'msg': '', 'grade_decimal': 0, 'ok': False}
True

>>> student_input = '[1, 2, 3, 4]' # too many components
>>> try:
... grader(None, student_input) # grade the input like edX would
... except StudentFacingError as error:
... str(error) # students see this error message
'Expected answer to be a vector, but input is a vector of incorrect shape'
... print(error) # students see this error message
Expected answer to be a vector, but input is a vector of incorrect shape

>>> student_input = '0' # scalar; should be a vector
>>> try:
... grader(None, student_input) # grade the input like edX would
... except StudentFacingError as error:
... str(error) # students see this error message
'Expected answer to be a vector, but input is a scalar'
... print(error) # students see this error message
Expected answer to be a vector, but input is a scalar

```

Expand All @@ -308,8 +312,13 @@ we can use:
... }
... )
>>> student_input = '0' # wrong shape
>>> grader(None, student_input) # grade the input like edX would
{'grade_decimal': 0, 'msg': 'Expected answer to be a vector of length 3, but input is a scalar', 'ok': False}
>>> result = grader(None, student_input) # grades the input like edX would
>>> result == {
... 'grade_decimal': 0,
... 'msg': 'Expected answer to be a vector of length 3, but input is a scalar',
... 'ok': False
... }
True

```

Expand Down
7 changes: 3 additions & 4 deletions docs/grading_math/sampling.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,8 +90,7 @@ Sample real matrices of a specific shape and norm. (`RealMatrices` uses the Frob
>>> # Sample 3 by 2 real matrices with norm between 5 and 10
>>> sampler = RealMatrices(shape=[3, 2], norm=[5, 10])
>>> # the default is shape=[2, 2] and norm=[1, 5]
>>> default_sampler = RealMatrices()
>>> default_sampler == RealMatrices(shape=[2, 2], norm=[1, 5])
>>> RealMatrices() == RealMatrices({'norm': [1, 5], 'shape': (2, 2)})
True

```
Expand All @@ -106,8 +105,8 @@ Sample square matrices of a given dimension consisting of the identity matrix mu
>>> # Sample 3x3 matrices consisting of a random number between 1 and 3 multiplying the identity
>>> sampler = IdentityMatrixMultiples(dimension=3, sampler=[1, 3])
>>> # The default is dimension=2 and sampler=[1, 5]
>>> IdentityMatrixMultiples()
IdentityMatrixMultiples({'dimension': 2, 'sampler': RealInterval({'start': 1, 'stop': 5})})
>>> IdentityMatrixMultiples() == IdentityMatrixMultiples(dimension=2, sampler=[1, 5])
True

```

Expand Down
2 changes: 1 addition & 1 deletion docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ A few error messages serve only as warnings. For example, if you attempt to conf
>>> 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.
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.

```

Expand Down
72 changes: 57 additions & 15 deletions mitxgraders/baseclasses.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
* AbstractGrader
* ItemGrader
"""
from __future__ import print_function, division, absolute_import
from __future__ import print_function, division, absolute_import, unicode_literals

import numbers
import abc
Expand All @@ -16,6 +16,7 @@
from voluptuous.humanize import validate_with_humanized_errors as voluptuous_validate
from mitxgraders.version import __version__
from mitxgraders.exceptions import ConfigError, MITxError, StudentFacingError
from mitxgraders.helpers.validatorfuncs import text_string

class ObjectWithSchema(object):
"""Represents an author-facing object whose configuration needs validation."""
Expand Down Expand Up @@ -63,7 +64,6 @@ def __eq__(self, other):
"""
return self.__class__ == other.__class__ and self.config == other.config


class AbstractGrader(ObjectWithSchema):
"""
Abstract grader class. All graders must build on this class.
Expand Down Expand Up @@ -132,6 +132,8 @@ def __call__(self, expect, student_input):
not work when an ItemGrader is embedded inside a ListGrader. See
ItemGrader.__call__ for the implementation.
"""
student_input = self.ensure_text_inputs(student_input)

# Initialize the debug log
# The debug log always exists and is written to, so that it can be accessed
# programmatically. It is only output with the grading when config["debug"] is True
Expand All @@ -143,9 +145,9 @@ def __call__(self, expect, student_input):
self.log("MITx Grading Library Version " + __version__)
# Add the student inputs to the debug log
if isinstance(student_input, list):
self.log("Student Responses:\n" + "\n".join(map(str, student_input)))
self.log("Student Responses:\n" + "\n".join(map(six.text_type, student_input)))
else:
self.log("Student Response:\n" + str(student_input))
self.log("Student Response:\n" + six.text_type(student_input))

# Compute the result of the check
try:
Expand All @@ -156,7 +158,7 @@ def __call__(self, expect, student_input):
elif isinstance(error, MITxError):
# we want to re-raise the error with a modified message but the
# same class type, hence calling __class__
raise error.__class__(str(error).replace('\n', '<br/>'))
raise error.__class__(six.text_type(error).replace('\n', '<br/>'))
else:
# Otherwise, give a generic error message
if isinstance(student_input, list):
Expand Down Expand Up @@ -205,6 +207,46 @@ def log_output(self):
content = "\n".join(self.debuglog)
return "<pre>{content}</pre>".format(content=content)

@staticmethod
def ensure_text_inputs(student_input, allow_lists=True, allow_single=True):
"""
Ensures that student_input is a text string or a list of text strings,
depending on arguments. Called by ItemGrader and ListGrader with
appropriate arguments. Defaults are set to be friendly to user-defined
grading classes.
"""
# Try to perform validation
try:
if allow_lists and isinstance(student_input, list):
return Schema([text_string])(student_input)
elif allow_single and not isinstance(student_input, list):
return Schema(text_string)(student_input)
except MultipleInvalid as error:
if allow_lists:
pos = error.path[0] if error.path else None

# The given student_input is invalid, so raise the appropriate error message
if allow_lists and allow_single:
msg = ("The student_input passed to a grader should be:\n"
" - a text string for problems with a single input box\n"
" - a list of text strings for problems with multiple input boxes\n"
"Received student_input of {}").format(type(student_input))
elif allow_lists and not isinstance(student_input, list):
msg = ("Expected student_input to be a list of text strings, but "
"received {}"
).format(type(student_input))
elif allow_lists:
msg = ("Expected a list of text strings for student_input, but "
"item at position {pos} has {thetype}"
).format(pos=pos, thetype=type(student_input[pos]))
elif allow_single:
msg = ("Expected string for student_input, received {}"
).format(type(student_input))
else:
raise ValueError('At least one of (allow_lists, allow_single) must be True.')

raise ConfigError(msg)

class ItemGrader(AbstractGrader):
"""
Abstract base class that represents a grader that grades a single input.
Expand Down Expand Up @@ -252,7 +294,7 @@ def schema_config(self):
schema = super(ItemGrader, self).schema_config
return schema.extend({
Required('answers', default=tuple()): self.schema_answers,
Required('wrong_msg', default=""): str
Required('wrong_msg', default=""): text_string
})

def schema_answers(self, answer_tuple):
Expand Down Expand Up @@ -306,7 +348,7 @@ def schema_answer(self):
return Schema({
Required('expect'): self.validate_expect,
Required('grade_decimal', default=1): All(numbers.Number, Range(0, 1)),
Required('msg', default=''): str,
Required('msg', default=''): text_string,
Required('ok', default='computed'): Any('computed', True, False, 'partial')
})

Expand All @@ -317,7 +359,7 @@ def validate_expect(expect):

Usually this is a just a string.
"""
return Schema(str)(expect)
return Schema(text_string)(expect)

@staticmethod
def grade_decimal_to_ok(grade):
Expand Down Expand Up @@ -367,7 +409,7 @@ def standardize_cfn_return(value):
"""
if value == True:
return {'ok': True, 'msg': '', 'grade_decimal': 1.0}
elif isinstance(value, str) and value.lower() == 'partial':
elif isinstance(value, six.text_type) and value.lower() == 'partial':
return {'ok': 'partial', 'msg': '', 'grade_decimal': 0.5}
elif value == False:
return {'ok': False, 'msg': '', 'grade_decimal': 0}
Expand All @@ -388,6 +430,7 @@ def check(self, answers, student_input, **kwargs):
**kwargs: Anything else that has been passed in. For example, sibling
graders when a grader is used as a subgrader in a ListGrader.
"""

# If no answers provided, use the internal configuration
answers = self.config['answers'] if answers is None else answers

Expand All @@ -402,11 +445,6 @@ def check(self, answers, student_input, **kwargs):
"Expected at least one answer in answers")
raise ConfigError(msg)

# Make sure the input is in the expected format
if not isinstance(student_input, six.string_types):
msg = "Expected string for student_input, received {}"
raise ConfigError(msg.format(type(student_input)))

# Compute the results for each answer
results = [self.check_response(answer, student_input, **kwargs) for answer in answers]

Expand Down Expand Up @@ -442,6 +480,10 @@ def __call__(self, expect, student_input):
grader configuration.
"""
if not self.config['answers'] and expect is not None:
self.config['answers'] = self.schema_answers(expect.encode('utf-8'))
self.config['answers'] = self.schema_answers(expect)

return super(ItemGrader, self).__call__(expect, student_input)

@staticmethod
def ensure_text_inputs(student_input):
return super(ItemGrader, ItemGrader).ensure_text_inputs(student_input, allow_lists=False)
1 change: 1 addition & 0 deletions mitxgraders/comparers/baseclasses.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
Note: Any callable object with the correct signature can be used as a comparer
function. We use classes for comparer functions that have configuration options.
"""
from __future__ import print_function, division, absolute_import, unicode_literals
import abc
from mitxgraders.baseclasses import ObjectWithSchema

Expand Down
13 changes: 8 additions & 5 deletions mitxgraders/comparers/comparers.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@
====================
See ./baseclasses.py and ./linear_comparer.py for examples.
"""
from __future__ import print_function, division, absolute_import
from __future__ import print_function, division, absolute_import, unicode_literals

from numbers import Number
import numpy as np
Expand Down Expand Up @@ -223,10 +223,13 @@ def vector_span_comparer(comparer_params_eval, student_eval, utils):

Student input should be nonzero:
>>> result = grader(None, '[0, 0, 0]')
>>> result['ok']
False
>>> result['msg']
'Input should be a nonzero vector.'
>>> expected = {
... 'ok': False,
... 'grade_decimal': 0.0,
... 'msg': 'Input should be a nonzero vector.'
... }
>>> result == expected
True

Input shape is validated:
>>> grader(None, '5')
Expand Down
10 changes: 6 additions & 4 deletions mitxgraders/comparers/linear_comparer.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
from __future__ import print_function, division, absolute_import, unicode_literals
from numbers import Number
import numpy as np
from voluptuous import Schema, Required, Any, Range
from mitxgraders.comparers.baseclasses import CorrelatedComparer
from mitxgraders.helpers.calc.mathfuncs import is_nearly_zero
from mitxgraders.helpers.validatorfuncs import text_string

def get_linear_fit_error(x, y):
"""
Expand Down Expand Up @@ -126,13 +128,13 @@ class LinearComparer(CorrelatedComparer):
Required('proportional', default=0.5): Any(None, Range(0, 1)),
Required('offset', default=None): Any(None, Range(0, 1)),
Required('linear', default=None): Any(None, Range(0, 1)),
Required('equals_msg', default=''): str,
Required('equals_msg', default=''): text_string,
Required('proportional_msg', default=(
'The submitted answer differs from an expected answer by a '
'constant factor.'
)): str,
Required('offset_msg', default=''): str,
Required('linear_msg', default=''): str,
)): text_string,
Required('offset_msg', default=''): text_string,
Required('linear_msg', default=''): text_string,
})

all_modes = ('equals', 'proportional', 'offset', 'linear')
Expand Down
2 changes: 1 addition & 1 deletion mitxgraders/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
exceptions.py
Contains generic grader-related exceptions
"""
from __future__ import print_function, division, absolute_import
from __future__ import print_function, division, absolute_import, unicode_literals


class MITxError(Exception):
Expand Down
Loading