factory.py
7.08 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
# We start with an empty QuestionFactory() that will be populated with
# question generators that we can load from YAML files.
# To generate an instance of a question we use the method generate(ref) where
# the argument is the reference of the question we wish to produce.
#
# Example:
#
# # read everything from question files
# factory = QuestionFactory()
# factory.load_files(['file1.yaml', 'file1.yaml'], '/path/to')
#
# question = factory.generate('some_ref')
#
# # experiment answering one question and correct it
# question['answer'] = 42 # insert answer
# grade = question.correct() # correct answer
# An instance of an actual question is an object that inherits from Question()
#
# Question - base class inherited by other classes
# QuestionInformation - not a question, just a box with content
# QuestionRadio - single choice from a list of options
# QuestionCheckbox - multiple choice, equivalent to multiple true/false
# QuestionText - line of text compared to a list of acceptable answers
# QuestionTextRegex - line of text matched against a regular expression
# QuestionTextArea - corrected by an external program
# QuestionNumericInterval - line of text parsed as a float
# python standard library
from os import path
import logging
# this project
from perguntations.tools import load_yaml, run_script
from perguntations.questions import (QuestionRadio, QuestionCheckbox,
QuestionText, QuestionTextRegex,
QuestionNumericInterval, QuestionTextArea,
QuestionInformation)
# setup logger for this module
logger = logging.getLogger(__name__)
# ============================================================================
class QuestionFactoryException(Exception):
pass
# ============================================================================
# This class contains a pool of questions generators from which particular
# Question() instances are generated using QuestionsFactory.generate(ref).
# ============================================================================
class QuestionFactory(dict):
_types = {
'radio': QuestionRadio,
'checkbox': QuestionCheckbox,
'text': QuestionText,
'text-regex': QuestionTextRegex,
'numeric-interval': QuestionNumericInterval,
'textarea': QuestionTextArea,
# -- informative panels --
'information': QuestionInformation,
'success': QuestionInformation,
'warning': QuestionInformation,
'alert': QuestionInformation
}
# ------------------------------------------------------------------------
def __init__(self):
super().__init__()
# ------------------------------------------------------------------------
# Add single question provided in a dictionary.
# After this, each question will have at least 'ref' and 'type' keys.
# ------------------------------------------------------------------------
def add_question(self, question):
q = question
# if missing defaults to ref='/path/file.yaml:3'
q.setdefault('ref', f'{q["filename"]}:{q["index"]}')
if q['ref'] in self:
logger.error(f'Duplicate reference "{q["ref"]}".')
q.setdefault('type', 'information')
self[q['ref']] = q
logger.debug(f'Added question "{q["ref"]}" to the pool.')
# ------------------------------------------------------------------------
# load single YAML questions file
# ------------------------------------------------------------------------
def load_file(self, pathfile, questions_dir=''):
# questions_dir is a base directory
# pathfile is a path of a file under the questions_dir
# For example, if
# pathfile = 'math/questions.yaml'
# questions_dir = '/home/john/questions'
# then the complete path is
# fullpath = '/home/john/questions/math/questions.yaml'
fullpath = path.normpath(path.join(questions_dir, pathfile))
(dirname, filename) = path.split(fullpath)
questions = load_yaml(fullpath, default=[])
for i, q in enumerate(questions):
try:
q.update({
'filename': filename,
'path': dirname,
'index': i # position in the file, 0 based
})
except AttributeError:
logger.error(f'Question {pathfile}:{i} not a dictionary!')
else:
self.add_question(q)
logger.info(f'{len(questions):>4} from "{pathfile}"')
# ------------------------------------------------------------------------
# load multiple YAML question files
# ------------------------------------------------------------------------
def load_files(self, files, questions_dir):
logger.info(f'Loading questions from files in "{questions_dir}":')
for filename in files:
self.load_file(filename, questions_dir)
# ------------------------------------------------------------------------
# Given a ref returns an instance of a descendent of Question(),
# i.e. a question object (radio, checkbox, ...).
# ------------------------------------------------------------------------
def generate(self, ref):
# Shallow copy so that script generated questions will not replace
# the original generators
try:
q = self[ref].copy()
except KeyError: # FIXME exception type?
logger.error(f'Can\'t find question "{ref}".')
raise QuestionFactoryException()
# If question is of generator type, an external program will be run
# which will print a valid question in yaml format to stdout. This
# output is then converted to a dictionary and `q` becomes that dict.
if q['type'] == 'generator':
logger.debug(f'Generating "{ref}" from {q["script"]}')
q.setdefault('args', []) # optional arguments
q.setdefault('stdin', '') # FIXME necessary?
script = path.join(q['path'], q['script'])
out = run_script(script=script, args=q['args'], stdin=q['stdin'])
try:
q.update(out)
except Exception:
q.update({
'type': 'alert',
'title': 'Erro interno',
'text': 'Ocorreu um erro a gerar esta pergunta.'
})
# The generator was replaced by a question but not yet instantiated
# Finally we create an instance of Question()
try:
qinstance = self._types[q['type']](q) # instance of correct class
except KeyError:
logger.error(f'Unknown type {q["type"]} in {q["filename"]}:{ref}.')
raise
except Exception:
logger.error(f'Failed to create question {q["filename"]}:{ref}.')
raise
else:
logger.debug(f'Generated question "{ref}".')
return qinstance