Commit 7aa80f63 by Sarina Canelake

Merge pull request #1554 from edx/sarina/fix-violations

Clean up pep8/pylint
parents 9b3bc84c d711d29d
......@@ -35,17 +35,17 @@ from calc import evaluator, UndefinedVariable
from . import correctmap
from datetime import datetime
from pytz import UTC
from .util import *
from .util import compare_with_tolerance, contextualize_text, convert_files_to_filenames, is_list_of_files, find_with_default
from lxml import etree
from lxml.html.soupparser import fromstring as fromstring_bs # uses Beautiful Soup!!! FIXME?
import capa.xqueue_interface as xqueue_interface
import safe_exec
import capa.safe_exec as safe_exec
log = logging.getLogger(__name__)
CorrectMap = correctmap.CorrectMap
CorrectMap = correctmap.CorrectMap # pylint: disable=C0103
CORRECTMAP_PY = None
......@@ -1181,7 +1181,7 @@ class CustomResponse(LoncapaResponse):
fn = self.code
answer_given = submission[0] if (len(idset) == 1) else submission
kwnames = self.xml.get("cfn_extra_args", "").split()
kwargs = {n:self.context.get(n) for n in kwnames}
kwargs = {n: self.context.get(n) for n in kwnames}
log.debug(" submission = %s" % submission)
try:
ret = fn(self.expect, answer_given, **kwargs)
......@@ -1304,7 +1304,7 @@ class CustomResponse(LoncapaResponse):
# Notify student with a student input error
_, _, traceback_obj = sys.exc_info()
raise ResponseError, err.message, traceback_obj
raise ResponseError(err.message, traceback_obj)
#-----------------------------------------------------------------------------
......@@ -1597,15 +1597,25 @@ class CodeResponse(LoncapaResponse):
class ExternalResponse(LoncapaResponse):
'''
"""
Grade the students input using an external server.
Typically used by coding problems.
'''
"""
response_tag = 'externalresponse'
allowed_inputfields = ['textline', 'textbox']
awdmap = {
'EXACT_ANS': 'correct', # TODO: handle other loncapa responses
'WRONG_FORMAT': 'incorrect',
}
def __init__(self, *args, **kwargs):
self.url = ''
self.tests = []
self.code = ''
super(ExternalResponse, self).__init__(*args, **kwargs)
def setup_response(self):
xml = self.xml
......@@ -1633,45 +1643,44 @@ class ExternalResponse(LoncapaResponse):
self.tests = xml.get('tests')
def do_external_request(self, cmd, extra_payload):
'''
"""
Perform HTTP request / post to external server.
cmd = remote command to perform (str)
extra_payload = dict of extra stuff to post.
Return XML tree of response (from response body)
'''
"""
xmlstr = etree.tostring(self.xml, pretty_print=True)
payload = {'xml': xmlstr,
'edX_cmd': cmd,
'edX_tests': self.tests,
'processor': self.code,
}
payload = {
'xml': xmlstr,
'edX_cmd': cmd,
'edX_tests': self.tests,
'processor': self.code,
}
payload.update(extra_payload)
try:
# call external server. TODO: synchronous call, can block for a
# long time
r = requests.post(self.url, data=payload)
req = requests.post(self.url, data=payload)
except Exception as err:
msg = 'Error %s - cannot connect to external server url=%s' % (
err, self.url)
msg = 'Error {0} - cannot connect to external server url={1}'.format(err, self.url)
log.error(msg)
raise Exception(msg)
if self.system.DEBUG:
log.info('response = %s' % r.text)
log.info('response = %s', req.text)
if (not r.text) or (not r.text.strip()):
if (not req.text) or (not req.text.strip()):
raise Exception(
'Error: no response from external server url=%s' % self.url)
try:
# response is XML; parse it
rxml = etree.fromstring(r.text)
rxml = etree.fromstring(req.text)
except Exception as err:
msg = 'Error %s - cannot parse response from external server r.text=%s' % (
err, r.text)
msg = 'Error {0} - cannot parse response from external server req.text={1}'.format(err, req.text)
log.error(msg)
raise Exception(msg)
......@@ -1682,9 +1691,13 @@ class ExternalResponse(LoncapaResponse):
cmap = CorrectMap()
try:
submission = [student_answers[k] for k in idset]
except Exception as err:
log.error('Error %s: cannot get student answer for %s; student_answers=%s' %
(err, self.answer_ids, student_answers))
except Exception as err: # pylint: disable=W0703
log.error(
'Error %s: cannot get student answer for %s; student_answers=%s',
err,
self.answer_ids,
student_answers
)
raise Exception(err)
self.context.update({'submission': submission})
......@@ -1693,8 +1706,8 @@ class ExternalResponse(LoncapaResponse):
try:
rxml = self.do_external_request('get_score', extra_payload)
except Exception as err:
log.error('Error %s' % err)
except Exception as err: # pylint: disable=W0703
log.error('Error %s', err)
if self.system.DEBUG:
cmap.set_dict(dict(zip(sorted(
self.answer_ids), ['incorrect'] * len(idset))))
......@@ -1703,13 +1716,11 @@ class ExternalResponse(LoncapaResponse):
'<span class="inline-error">%s</span>' % str(err).replace('<', '&lt;'))
return cmap
ad = rxml.find('awarddetail').text
admap = {'EXACT_ANS': 'correct', # TODO: handle other loncapa responses
'WRONG_FORMAT': 'incorrect',
}
awd = rxml.find('awarddetail').text
self.context['correct'] = ['correct']
if ad in admap:
self.context['correct'][0] = admap[ad]
if awd in self.awdmap:
self.context['correct'][0] = self.awdmap[awd]
# create CorrectMap
for key in idset:
......@@ -1721,14 +1732,14 @@ class ExternalResponse(LoncapaResponse):
return cmap
def get_answers(self):
'''
"""
Use external server to get expected answers
'''
"""
try:
rxml = self.do_external_request('get_answers', {})
exans = json.loads(rxml.find('expected').text)
except Exception as err:
log.error('Error %s' % err)
except Exception as err: # pylint: disable=W0703
log.error('Error %s', err)
if self.system.DEBUG:
msg = '<span class="inline-error">%s</span>' % str(
err).replace('<', '&lt;')
......@@ -1736,8 +1747,8 @@ class ExternalResponse(LoncapaResponse):
exans[0] = msg
if not (len(exans) == len(self.answer_ids)):
log.error('Expected %d answers from external server, only got %d!' %
(len(self.answer_ids), len(exans)))
log.error('Expected %s answers from external server, only got %s!',
len(self.answer_ids), len(exans))
raise Exception('Short response from external server')
return dict(zip(self.answer_ids, exans))
......@@ -1745,9 +1756,9 @@ class ExternalResponse(LoncapaResponse):
#-----------------------------------------------------------------------------
class FormulaResponse(LoncapaResponse):
'''
"""
Checking of symbolic math response using numerical sampling.
'''
"""
response_tag = 'formularesponse'
hint_tag = 'formulahint'
......@@ -1776,11 +1787,11 @@ class FormulaResponse(LoncapaResponse):
if tolerance_xml: # If it isn't an empty list...
self.tolerance = contextualize_text(tolerance_xml[0], context)
ts = xml.get('type')
if ts is None:
types = xml.get('type')
if types is None:
typeslist = []
else:
typeslist = ts.split(',')
typeslist = types.split(',')
if 'ci' in typeslist:
# Case insensitive
self.case_sensitive = False
......@@ -1812,30 +1823,33 @@ class FormulaResponse(LoncapaResponse):
answer,
case_sensitive=self.case_sensitive,
))
except UndefinedVariable as uv:
except UndefinedVariable as err:
log.debug(
'formularesponse: undefined variable in formula=%s' % answer)
'formularesponse: undefined variable in formula=%s',
cgi.escape(answer)
)
raise StudentInputError(
"Invalid input: " + uv.message + " not permitted in answer"
"Invalid input: " + err.message + " not permitted in answer"
)
except ValueError as ve:
if 'factorial' in ve.message:
except ValueError as err:
if 'factorial' in err.message:
# This is thrown when fact() or factorial() is used in a formularesponse answer
# that tests on negative and/or non-integer inputs
# ve.message will be: `factorial() only accepts integral values` or
# err.message will be: `factorial() only accepts integral values` or
# `factorial() not defined for negative values`
log.debug(
('formularesponse: factorial function used in response '
'that tests negative and/or non-integer inputs. '
'given={0}').format(given)
'Provided answer was: %s'),
cgi.escape(answer)
)
raise StudentInputError(
("factorial function not permitted in answer "
"for this problem. Provided answer was: "
"{0}").format(cgi.escape(given))
"{0}").format(cgi.escape(answer))
)
# If non-factorial related ValueError thrown, handle it the same as any other Exception
log.debug('formularesponse: error {0} in formula'.format(ve))
log.debug('formularesponse: error %s in formula', err)
raise StudentInputError("Invalid input: Could not parse '%s' as a formula" %
cgi.escape(answer))
except Exception as err:
......@@ -1857,7 +1871,7 @@ class FormulaResponse(LoncapaResponse):
ranges = dict(zip(variables, sranges))
out = []
for i in range(numsamples):
for _ in range(numsamples):
var_dict = {}
# ranges give numerical ranges for testing
for var in ranges:
......@@ -1902,15 +1916,17 @@ class FormulaResponse(LoncapaResponse):
except StudentInputError:
return False
def strip_dict(self, d):
''' Takes a dict. Returns an identical dict, with all non-word
def strip_dict(self, inp_d):
"""
Takes a dict. Returns an identical dict, with all non-word
keys and all non-numeric values stripped out. All values also
converted to float. Used so we can safely use Python contexts.
'''
d = dict([(k, numpy.complex(d[k])) for k in d if type(k) == str and
k.isalnum() and
isinstance(d[k], numbers.Number)])
return d
"""
inp_d = dict([(k, numpy.complex(inp_d[k]))
for k in inp_d if type(k) == str and
k.isalnum() and
isinstance(inp_d[k], numbers.Number)])
return inp_d
def check_hint_condition(self, hxml_set, student_answers):
given = student_answers[self.answer_id]
......@@ -1920,14 +1936,18 @@ class FormulaResponse(LoncapaResponse):
name = hxml.get('name')
correct_answer = contextualize_text(
hxml.get('answer'), self.context)
# pylint: disable=W0703
try:
correctness = self.check_formula(
correct_answer, given, samples)
correct_answer,
given,
samples
)
except Exception:
correctness = 'incorrect'
if correctness == 'correct':
hints_to_show.append(name)
log.debug('hints_to_show = %s' % hints_to_show)
log.debug('hints_to_show = %s', hints_to_show)
return hints_to_show
def get_answers(self):
......@@ -1937,10 +1957,16 @@ class FormulaResponse(LoncapaResponse):
class SchematicResponse(LoncapaResponse):
"""
Circuit schematic response type.
"""
response_tag = 'schematicresponse'
allowed_inputfields = ['schematic']
def __init__(self, *args, **kwargs):
self.code = ''
super(SchematicResponse, self).__init__(*args, **kwargs)
def setup_response(self):
xml = self.xml
answer = xml.xpath('//*[@id=$id]//answer', id=xml.get('id'))[0]
......@@ -2010,6 +2036,10 @@ class ImageResponse(LoncapaResponse):
response_tag = 'imageresponse'
allowed_inputfields = ['imageinput']
def __init__(self, *args, **kwargs):
self.ielements = []
super(ImageResponse, self).__init__(*args, **kwargs)
def setup_response(self):
self.ielements = self.inputfields
self.answer_ids = [ie.get('id') for ie in self.ielements]
......@@ -2018,40 +2048,39 @@ class ImageResponse(LoncapaResponse):
correct_map = CorrectMap()
expectedset = self.get_mapped_answers()
for aid in self.answer_ids: # loop through IDs of <imageinput>
# fields in our stanza
given = student_answers[
aid] # this should be a string of the form '[x,y]'
# Fields in our stanza
given = student_answers[aid] # This should be a string of the form '[x,y]'
correct_map.set(aid, 'incorrect')
if not given: # No answer to parse. Mark as incorrect and move on
continue
# parse given answer
m = re.match(r'\[([0-9]+),([0-9]+)]', given.strip().replace(' ', ''))
if not m:
# Parse given answer
acoords = re.match(r'\[([0-9]+),([0-9]+)]', given.strip().replace(' ', ''))
if not acoords:
raise Exception('[capamodule.capa.responsetypes.imageinput] '
'error grading %s (input=%s)' % (aid, given))
(gx, gy) = [int(x) for x in m.groups()]
'error grading {0} (input={1})'.format(aid, given))
(ans_x, ans_y) = [int(x) for x in acoords.groups()]
rectangles, regions = expectedset
if rectangles[aid]: # rectangles part - for backward compatibility
if rectangles[aid]: # Rectangles part - for backward compatibility
# Check whether given point lies in any of the solution
# rectangles
solution_rectangles = rectangles[aid].split(';')
for solution_rectangle in solution_rectangles:
# parse expected answer
# TODO: Compile regexp on file load
m = re.match(
sr_coords = re.match(
r'[\(\[]([0-9]+),([0-9]+)[\)\]]-[\(\[]([0-9]+),([0-9]+)[\)\]]',
solution_rectangle.strip().replace(' ', ''))
if not m:
if not sr_coords:
msg = 'Error in problem specification! cannot parse rectangle in %s' % (
etree.tostring(self.ielements[aid], pretty_print=True))
raise Exception(
'[capamodule.capa.responsetypes.imageinput] ' + msg)
(llx, lly, urx, ury) = [int(x) for x in m.groups()]
(llx, lly, urx, ury) = [int(x) for x in sr_coords.groups()]
# answer is correct if (x,y) is within the specified
# rectangle
if (llx <= gx <= urx) and (lly <= gy <= ury):
if (llx <= ans_x <= urx) and (lly <= ans_y <= ury):
correct_map.set(aid, 'correct')
break
if correct_map[aid]['correctness'] != 'correct' and regions[aid]:
......@@ -2065,13 +2094,13 @@ class ImageResponse(LoncapaResponse):
for region in parsed_region:
polygon = MultiPoint(region).convex_hull
if (polygon.type == 'Polygon' and
polygon.contains(Point(gx, gy))):
polygon.contains(Point(ans_x, ans_y))):
correct_map.set(aid, 'correct')
break
return correct_map
def get_mapped_answers(self):
'''
"""
Returns the internal representation of the answers
Input:
......@@ -2080,7 +2109,7 @@ class ImageResponse(LoncapaResponse):
tuple (dict, dict) -
rectangles (dict) - a map of inputs to the defined rectangle for that input
regions (dict) - a map of inputs to the defined region for that input
'''
"""
answers = (
dict([(ie.get('id'), ie.get(
'rectangle')) for ie in self.ielements]),
......@@ -2088,7 +2117,7 @@ class ImageResponse(LoncapaResponse):
return answers
def get_answers(self):
'''
"""
Returns the external representation of the answers
Input:
......@@ -2096,11 +2125,11 @@ class ImageResponse(LoncapaResponse):
Returns:
dict (str, (str, str)) - a map of inputs to a tuple of their rectange
and their regions
'''
"""
answers = {}
for ie in self.ielements:
ie_id = ie.get('id')
answers[ie_id] = (ie.get('rectangle'), ie.get('regions'))
for ielt in self.ielements:
ie_id = ielt.get('id')
answers[ie_id] = (ielt.get('rectangle'), ielt.get('regions'))
return answers
......@@ -2108,26 +2137,32 @@ class ImageResponse(LoncapaResponse):
class AnnotationResponse(LoncapaResponse):
'''
"""
Checking of annotation responses.
The response contains both a comment (student commentary) and an option (student tag).
Only the tag is currently graded. Answers may be incorrect, partially correct, or correct.
'''
"""
response_tag = 'annotationresponse'
allowed_inputfields = ['annotationinput']
max_inputfields = 1
default_scoring = {'incorrect': 0, 'partially-correct': 1, 'correct': 2}
def __init__(self, *args, **kwargs):
self.scoring_map = {}
self.answer_map = {}
super(AnnotationResponse, self).__init__(*args, **kwargs)
def setup_response(self):
xml = self.xml
self.scoring_map = self._get_scoring_map()
self.answer_map = self._get_answer_map()
self.maxpoints = self._get_max_points()
def get_score(self, student_answers):
''' Returns a CorrectMap for the student answer, which may include
partially correct answers.'''
"""
Returns a CorrectMap for the student answer, which may include
partially correct answers.
"""
student_answer = student_answers[self.answer_id]
student_option = self._get_submitted_option_id(student_answer)
......@@ -2146,23 +2181,26 @@ class AnnotationResponse(LoncapaResponse):
return self.answer_map
def _get_scoring_map(self):
''' Returns a dict of option->scoring for each input. '''
"""Returns a dict of option->scoring for each input."""
scoring = self.default_scoring
choices = dict([(choice, choice) for choice in scoring])
scoring_map = {}
for inputfield in self.inputfields:
option_scoring = dict([(option['id'], {
option_scoring = dict([(
option['id'],
{
'correctness': choices.get(option['choice']),
'points': scoring.get(option['choice'])
}) for option in self._find_options(inputfield)])
}
) for option in self._find_options(inputfield)])
scoring_map[inputfield.get('id')] = option_scoring
return scoring_map
def _get_answer_map(self):
''' Returns a dict of answers for each input.'''
"""Returns a dict of answers for each input."""
answer_map = {}
for inputfield in self.inputfields:
correct_option = self._find_option_with_choice(
......@@ -2173,13 +2211,13 @@ class AnnotationResponse(LoncapaResponse):
return answer_map
def _get_max_points(self):
''' Returns a dict of the max points for each input: input id -> maxpoints. '''
"""Returns a dict of the max points for each input: input id -> maxpoints."""
scoring = self.default_scoring
correct_points = scoring.get('correct')
return dict([(inputfield.get('id'), correct_points) for inputfield in self.inputfields])
def _find_options(self, inputfield):
''' Returns an array of dicts where each dict represents an option. '''
"""Returns an array of dicts where each dict represents an option. """
elements = inputfield.findall('./options/option')
return [{
'id': index,
......@@ -2188,22 +2226,22 @@ class AnnotationResponse(LoncapaResponse):
} for (index, option) in enumerate(elements)]
def _find_option_with_choice(self, inputfield, choice):
''' Returns the option with the given choice value, otherwise None. '''
"""Returns the option with the given choice value, otherwise None. """
for option in self._find_options(inputfield):
if option['choice'] == choice:
return option
def _unpack(self, json_value):
''' Unpacks a student response value submitted as JSON.'''
d = json.loads(json_value)
if type(d) != dict:
d = {}
"""Unpacks a student response value submitted as JSON."""
json_d = json.loads(json_value)
if type(json_d) != dict:
json_d = {}
comment_value = d.get('comment', '')
if not isinstance(d, basestring):
comment_value = json_d.get('comment', '')
if not isinstance(json_d, basestring):
comment_value = ''
options_value = d.get('options', [])
options_value = json_d.get('options', [])
if not isinstance(options_value, list):
options_value = []
......@@ -2213,7 +2251,7 @@ class AnnotationResponse(LoncapaResponse):
}
def _get_submitted_option_id(self, student_answer):
''' Return the single option that was selected, otherwise None.'''
"""Return the single option that was selected, otherwise None."""
submitted = self._unpack(student_answer)
option_ids = submitted['options_value']
if len(option_ids) == 1:
......@@ -2235,6 +2273,12 @@ class ChoiceTextResponse(LoncapaResponse):
'radiotextgroup'
]
def __init__(self, *args, **kwargs):
self.correct_inputs = {}
self.answer_values = {}
self.correct_choices = {}
super(ChoiceTextResponse, self).__init__(*args, **kwargs)
def setup_response(self):
"""
Sets up three dictionaries for use later:
......@@ -2250,10 +2294,8 @@ class ChoiceTextResponse(LoncapaResponse):
"""
context = self.context
self.correct_choices = {}
self.assign_choice_names()
self.correct_inputs = {}
self.answer_values = {self.answer_id: []}
self.assign_choice_names()
correct_xml = self.xml.xpath('//*[@id=$id]//choice[@correct="true"]',
id=self.xml.get('id'))
for node in correct_xml:
......@@ -2552,6 +2594,7 @@ class ChoiceTextResponse(LoncapaResponse):
# TEMPORARY: List of all response subclasses
# FIXME: To be replaced by auto-registration
# pylint: disable=E0604
__all__ = [CodeResponse,
NumericalResponse,
FormulaResponse,
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment