Commit 1ab9d6baf0b1ee14b05f5b29fa1db308225b78fa

Authored by Miguel Barão
1 parent f47339cd
Exists in master and in 1 other branch dev

Async question generator.

BUGS.md
1 1  
2 2 # BUGS
3 3  
  4 +- quando termina topico devia apagar as perguntas todas (se falhar a gerar novo topico, aparecem perguntas do antigo)
4 5 - apos clicar no botao responder, inactivar o input (importante quando o tempo de correcção é grande)
5 6 - devia mostrar timout para o aluno saber a razao.
6 7 - permitir configuracao para escolher entre static files locais ou remotos
... ...
aprendizations/knowledge.py
... ... @@ -58,7 +58,7 @@ class StudentKnowledge(object):
58 58 'level': 0.0, # unlocked
59 59 'date': datetime.now()
60 60 }
61   - logger.debug(f'Unlocked "{topic}".')
  61 + logger.debug(f'[unlock_topics] Unlocked "{topic}".')
62 62 # else: # lock this topic if deps do not satisfy min_level
63 63 # del self.state[topic]
64 64  
... ... @@ -68,7 +68,7 @@ class StudentKnowledge(object):
68 68 # current_question: the current question to be presented
69 69 # ------------------------------------------------------------------------
70 70 async def start_topic(self, topic):
71   - logger.debug('StudentKnowledge.start_topic()')
  71 + logger.debug(f'[start_topic] topic "{topic}"')
72 72  
73 73 if self.current_topic == topic:
74 74 logger.info('Restarting current topic is not allowed.')
... ... @@ -76,7 +76,7 @@ class StudentKnowledge(object):
76 76  
77 77 # do not allow locked topics
78 78 if self.is_locked(topic):
79   - logger.debug(f'Topic {topic} is locked')
  79 + logger.debug(f'[start_topic] topic "{topic}" is locked')
80 80 return False
81 81  
82 82 # starting new topic
... ... @@ -90,23 +90,20 @@ class StudentKnowledge(object):
90 90 questions = random.sample(t['questions'], k=k)
91 91 else:
92 92 questions = t['questions'][:k]
93   - logger.debug(f'Questions: {", ".join(questions)}')
  93 + logger.debug(f'[start_topic] questions: {", ".join(questions)}')
94 94  
95   - # generate instances of questions
  95 + # synchronous
  96 + # self.questions = [self.factory[ref].generate()
  97 + # for ref in questions]
96 98  
97   - # synchronous:
98   - # self.questions = [self.factory[ref].generate() for ref in questions]
99   -
100   - # async:
101   - loop = asyncio.get_running_loop()
102   - generators = [loop.run_in_executor(None, self.factory[qref].generate)
103   - for qref in questions]
104   - self.questions = await asyncio.gather(*generators)
  99 + # asynchronous:
  100 + self.questions = [await self.factory[ref].generate_async()
  101 + for ref in questions]
105 102  
106 103 # get first question
107 104 self.next_question()
108 105  
109   - logger.debug(f'Generated {len(self.questions)} questions')
  106 + logger.debug(f'[start_topic] generated {len(self.questions)} questions')
110 107 return True
111 108  
112 109 # ------------------------------------------------------------------------
... ... @@ -115,7 +112,7 @@ class StudentKnowledge(object):
115 112 # The current topic is unchanged.
116 113 # ------------------------------------------------------------------------
117 114 def finish_topic(self):
118   - logger.debug(f'StudentKnowledge.finish_topic({self.current_topic})')
  115 + logger.debug(f'[finish_topic] current_topic {self.current_topic}')
119 116  
120 117 self.state[self.current_topic] = {
121 118 'date': datetime.now(),
... ... @@ -132,13 +129,13 @@ class StudentKnowledge(object):
132 129 # - if wrong, counts number of tries. If exceeded, moves on.
133 130 # ------------------------------------------------------------------------
134 131 async def check_answer(self, answer):
135   - logger.debug('StudentKnowledge.check_answer()')
  132 + logger.debug('[check_answer]')
136 133  
137 134 q = self.current_question
138 135 q['answer'] = answer
139 136 q['finish_time'] = datetime.now()
140 137 await q.correct_async()
141   - logger.debug(f'Grade {q["grade"]:.2} ({q["ref"]})')
  138 + logger.debug(f'[check_answer] Grade {q["grade"]:.2} in {q["ref"]}')
142 139  
143 140 if q['grade'] > 0.999:
144 141 self.correct_answers += 1
... ... @@ -154,7 +151,7 @@ class StudentKnowledge(object):
154 151 else:
155 152 action = 'wrong'
156 153 if self.current_question['append_wrong']:
157   - logger.debug("Append new instance of question at the end")
  154 + logger.debug('[check_answer] Wrong, append new instance')
158 155 self.questions.append(self.factory[q['ref']].generate())
159 156 self.next_question()
160 157  
... ... @@ -175,7 +172,7 @@ class StudentKnowledge(object):
175 172 default_maxtries = self.deps.nodes[self.current_topic]['max_tries']
176 173 maxtries = self.current_question.get('max_tries', default_maxtries)
177 174 self.current_question['tries'] = maxtries
178   - logger.debug(f'Next question is "{self.current_question["ref"]}"')
  175 + logger.debug(f'[next_question] "{self.current_question["ref"]}"')
179 176  
180 177 return self.current_question # question or None
181 178  
... ...
aprendizations/learnapp.py
... ... @@ -75,7 +75,7 @@ class LearnApp(object):
75 75  
76 76 errors = 0
77 77 for qref in self.factory:
78   - logger.debug(f'Checking "{qref}"...')
  78 + logger.debug(f'[sanity_check_questions] Checking "{qref}"...')
79 79 try:
80 80 q = self.factory[qref].generate()
81 81 except Exception:
... ... @@ -204,7 +204,7 @@ class LearnApp(object):
204 204 finishtime=str(q['finish_time']),
205 205 student_id=uid,
206 206 topic_id=topic))
207   - logger.debug(f'Saved "{q["ref"]}" into database')
  207 + logger.debug(f'[check_answer] Saved "{q["ref"]}" into database')
208 208  
209 209 if knowledge.topic_has_finished():
210 210 # finished topic, save into database
... ... @@ -218,7 +218,7 @@ class LearnApp(object):
218 218 .one_or_none()
219 219 if a is None:
220 220 # insert new studenttopic into database
221   - logger.debug('Database insert new studenttopic')
  221 + logger.debug('[check_answer] Database insert studenttopic')
222 222 t = s.query(Topic).get(topic)
223 223 u = s.query(Student).get(uid)
224 224 # association object
... ... @@ -227,13 +227,13 @@ class LearnApp(object):
227 227 u.topics.append(a)
228 228 else:
229 229 # update studenttopic in database
230   - logger.debug('Database update studenttopic')
  230 + logger.debug('[check_answer] Database update studenttopic')
231 231 a.level = level
232 232 a.date = date
233 233  
234 234 s.add(a)
235 235  
236   - logger.debug(f'Saved topic "{topic}" into database')
  236 + logger.debug(f'[check_answer] Saved topic "{topic}" into database')
237 237  
238 238 return q, action
239 239  
... ... @@ -244,8 +244,8 @@ class LearnApp(object):
244 244 student = self.online[uid]['state']
245 245 try:
246 246 await student.start_topic(topic)
247   - except Exception:
248   - logger.warning(f'User "{uid}" could not start topic "{topic}"')
  247 + except Exception as e:
  248 + logger.warning(f'User "{uid}" couldn\'t start "{topic}": {e}')
249 249 else:
250 250 logger.info(f'User "{uid}" started topic "{topic}"')
251 251  
... ...
aprendizations/questions.py
... ... @@ -429,7 +429,7 @@ class QuestionInformation(Question):
429 429 # # answer one question and correct it
430 430 # question['answer'] = 42 # set answer
431 431 # question.correct() # correct answer
432   -# print(question['grade']) # print grade
  432 +# grade = question['grade'] # get grade
433 433 # ===========================================================================
434 434 class QFactory(object):
435 435 # Depending on the type of question, a different question class will be
... ... @@ -456,7 +456,7 @@ class QFactory(object):
456 456 # i.e. a question object (radio, checkbox, ...).
457 457 # -----------------------------------------------------------------------
458 458 def generate(self) -> Question:
459   - logger.debug(f'Generating "{self.question["ref"]}"...')
  459 + logger.debug(f'[generate] "{self.question["ref"]}"...')
460 460 # Shallow copy so that script generated questions will not replace
461 461 # the original generators
462 462 q = self.question.copy()
... ... @@ -468,7 +468,7 @@ class QFactory(object):
468 468 if q['type'] == 'generator':
469 469 logger.debug(f' \\_ Running "{q["script"]}".')
470 470 q.setdefault('args', [])
471   - q.setdefault('stdin', '') # FIXME does not exist anymore?
  471 + q.setdefault('stdin', '') # FIXME is it really necessary?
472 472 script = path.join(q['path'], q['script'])
473 473 out = run_script(script=script, args=q['args'], stdin=q['stdin'])
474 474 q.update(out)
... ... @@ -484,3 +484,36 @@ class QFactory(object):
484 484 raise
485 485 else:
486 486 return qinstance
  487 +
  488 + # -----------------------------------------------------------------------
  489 + async def generate_async(self) -> Question:
  490 + logger.debug(f'[generate_async] "{self.question["ref"]}"...')
  491 + # Shallow copy so that script generated questions will not replace
  492 + # the original generators
  493 + q = self.question.copy()
  494 + q['qid'] = str(uuid.uuid4()) # unique for each generated question
  495 +
  496 + # If question is of generator type, an external program will be run
  497 + # which will print a valid question in yaml format to stdout. This
  498 + # output is then yaml parsed into a dictionary `q`.
  499 + if q['type'] == 'generator':
  500 + logger.debug(f' \\_ Running "{q["script"]}".')
  501 + q.setdefault('args', [])
  502 + q.setdefault('stdin', '') # FIXME is it really necessary?
  503 + script = path.join(q['path'], q['script'])
  504 + out = await run_script_async(script=script, args=q['args'],
  505 + stdin=q['stdin'])
  506 + q.update(out)
  507 +
  508 + # Finally we create an instance of Question()
  509 + try:
  510 + qinstance = self._types[q['type']](QDict(q)) # of matching class
  511 + except QuestionException as e:
  512 + logger.error(e)
  513 + raise e
  514 + except KeyError:
  515 + logger.error(f'Invalid type "{q["type"]}" in "{q["ref"]}"')
  516 + raise
  517 + else:
  518 + logger.debug(f'[generate_async] Done instance of {q["ref"]}')
  519 + return qinstance
... ...
aprendizations/serve.py
... ... @@ -254,7 +254,7 @@ class QuestionHandler(BaseHandler):
254 254 # --- get question to render
255 255 @tornado.web.authenticated
256 256 def get(self):
257   - logging.debug('QuestionHandler.get()')
  257 + logging.debug('[QuestionHandler.get]')
258 258 user = self.current_user
259 259 q = self.learn.get_current_question(user)
260 260  
... ... @@ -285,7 +285,7 @@ class QuestionHandler(BaseHandler):
285 285 # --- post answer, returns what to do next: shake, new_question, finished
286 286 @tornado.web.authenticated
287 287 async def post(self) -> None:
288   - logging.debug('QuestionHandler.post()')
  288 + logging.debug('[QuestionHandler.post]')
289 289 user = self.current_user
290 290 answer = self.get_body_arguments('answer') # list
291 291  
... ...
aprendizations/tools.py
... ... @@ -170,7 +170,7 @@ def load_yaml(filename: str, default: Any = None) -> Any:
170 170 def run_script(script: str,
171 171 args: List[str] = [],
172 172 stdin: str = '',
173   - timeout: int = 5) -> Any:
  173 + timeout: int = 2) -> Any:
174 174  
175 175 script = path.expanduser(script)
176 176 try:
... ... @@ -210,13 +210,13 @@ def run_script(script: str,
210 210 async def run_script_async(script: str,
211 211 args: List[str] = [],
212 212 stdin: str = '',
213   - timeout: int = 5) -> Any:
  213 + timeout: int = 2) -> Any:
214 214  
215 215 script = path.expanduser(script)
216   - cmd = [script] + [str(a) for a in args]
  216 + args = [str(a) for a in args]
217 217  
218   - p = await asyncio.create_subprocess_shell(
219   - *cmd,
  218 + p = await asyncio.create_subprocess_exec(
  219 + script, *args,
220 220 stdin=asyncio.subprocess.PIPE,
221 221 stdout=asyncio.subprocess.PIPE,
222 222 stderr=asyncio.subprocess.DEVNULL,
... ... @@ -235,7 +235,7 @@ async def run_script_async(script: str,
235 235 logger.error(f'Return code {p.returncode} running "{script}".')
236 236 else:
237 237 try:
238   - output = yaml.safe_load(stdout.decode('utf-8'))
  238 + output = yaml.safe_load(stdout.decode('utf-8', 'ignore'))
239 239 except Exception:
240 240 logger.error(f'Error parsing yaml output of "{script}"')
241 241 else:
... ...