Created
June 10, 2011 12:07
-
-
Save biern/1018713 to your computer and use it in GitHub Desktop.
Simple script parsing questions from a plain text file and making a quiz from them.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python | |
# -*- coding: utf-8 -*- | |
import os | |
import re | |
import random | |
import sys | |
import subprocess | |
from optparse import OptionParser | |
PATTERNS = dict( | |
question=re.compile(r'^(?P<key>\d+)\.\s*(?P<content>.*?)(?=^\d+\.)', | |
re.MULTILINE | re.DOTALL), | |
question_text=re.compile(r'(?P<text>.*?)' | |
# Aż do odpowiedzi: | |
'^\s*(>+)?\s*(?P<key>[a-m]*)(\)|\.)', | |
re.MULTILINE | re.DOTALL), | |
answer=re.compile(r'^\s*(>+)?\s*(?P<key>[a-m]*)(\)|\.) ?' | |
r'(?P<text>.*)', | |
re.MULTILINE | re.IGNORECASE), | |
answer_correct=re.compile(r'.*>+.*'), | |
) | |
SORT_KEY = int | |
ANSWER_FORMAT = [ | |
' {key}) {text}', | |
'>>> {key}) {text}', | |
] | |
MEDIA_HANDLER = 'display' | |
def parse_options(): | |
parser = OptionParser(usage="usage: %prog [questions_file] arg1 arg2") | |
parser.add_option("-p", "--patterns", | |
dest="patterns_file", default=False, | |
help="Use patterns from file instead of defaults") | |
parser.add_option("-i", "--iPython shell", | |
action="store_true", dest="ipython", default=False, | |
help="Load questions and enter shell") | |
parser.add_option("-s", "--shuffle", | |
action="store_true", dest="shuffle", default=False, | |
help="Shuffle answer order") | |
parser.add_option("-q", "--quiz", | |
action="store_true", dest="quiz", default=False, | |
help="Interactive quiz mode") | |
parser.add_option("-k", "--keys", | |
dest="keys", default='', | |
help="Questions to include in quiz mode" | |
"(ex: 1,2,12 or 2,3:10)") | |
parser.add_option("-o", "--stdout", | |
action="store_true", dest="stdout", default=False, | |
help="Write parsed questions to stdout") | |
if(len(sys.argv) == 1): | |
parser.print_help() | |
exit() | |
options, positional = parser.parse_args() | |
return options | |
class Answer(object): | |
def __init__(self, key, text, correct): | |
self.key = key | |
self.text = text | |
self.correct = correct | |
def __str__(self): | |
return self.to_string() | |
def to_string(self, show_correct=True, color=True): | |
highlight = show_correct and color and self.correct | |
return "{color}{content}{endcolor}".format( | |
content=ANSWER_FORMAT[show_correct and self.correct].format( | |
key=self.key, text=self.text), | |
color="\033[0;32m" * highlight, | |
endcolor="\033[1;m" * highlight) | |
class Question(object): | |
def __init__(self, key, text, answers, media=None): | |
self.key = key | |
self.text = text | |
self.answers = answers | |
self.media = media or [] | |
def to_string(self, full=False, raw=False, show_correct=True): | |
media_string = '' | |
# Skipping default media | |
media = [m for m in self.media \ | |
if not m.startswith('media/' + self.key + '.')] | |
if media and full: | |
media_string = '#MEDIA:' + ','.join(media) + '\n' | |
return "{key}. {text}\n{media}{answers}".format( | |
key=self.key, | |
text=self.text, | |
media=media_string, | |
answers='\n'.join(map(lambda a: | |
a.to_string(show_correct, | |
color=not raw), | |
self.answers))) | |
def __str__(self): | |
return self.to_string() | |
def shuffle_answers(self): | |
shuffled = [] | |
keys = [a.key for a in self.answers] | |
random.shuffle(self.answers) | |
for i, k in enumerate(keys): | |
a = self.answers[i] | |
a.key = k | |
shuffled.append(a) | |
self.answers = shuffled | |
def get_correct_answers(self): | |
return [a.key for a in self.answers if a.correct] | |
def set_answers(self, keys): | |
for a in self.answers: | |
if a.key in keys: | |
a.correct = True | |
else: | |
a.correct = False | |
@classmethod | |
def make_questions(cls, text, | |
question_pattern, # groups: key, content, | |
# [text, answers] | |
question_text_pattern, | |
answer_pattern, # groups: key, text | |
answer_correct_pattern): | |
def groupgetter(match, name): | |
try: | |
return match.group(name) | |
except IndexError: | |
return False | |
questions = {} | |
for match in re.finditer(question_pattern, text): | |
q_content = match.group('content') | |
q_key = match.group('key') | |
q_text = groupgetter(match, 'text') or \ | |
re.search(question_text_pattern, q_content).group('text') | |
q_media = re.search(r'^#MEDIA:(.*)$', q_content, re.MULTILINE) | |
if q_media: | |
q_media = [m.strip() for m in q_media.group(1).split(',')] | |
q_text = re.sub(r'^#MEDIA:(.*)$', '', q_text, 0, re.MULTILINE) | |
q_answers = [] | |
for match in re.finditer(answer_pattern, | |
groupgetter(match, 'answers') or \ | |
q_content): | |
q_answers.append(Answer( | |
match.group('key').strip(), | |
match.group('text').strip(), | |
bool(re.match(answer_correct_pattern, | |
match.group(0))) | |
)) | |
questions[q_key] = cls(q_key.strip(), q_text.strip(), q_answers, | |
media=q_media) | |
return questions | |
def quiz(questions): | |
# TODO: Make a class | |
wrong = [] | |
correct_answer = True | |
def bad_answer(key, ans): | |
print("Zła odpowiedź!\n Prawidłowe to: " + \ | |
', '.join(ans)) | |
wrong.append(key) | |
keys = questions.keys() | |
random.shuffle(keys) | |
for count, key in enumerate(keys, 1): | |
correct_answer = True | |
question = questions[key] | |
correct = question.get_correct_answers() | |
procs = [] | |
for m in question.media: | |
if os.path.exists(m): | |
procs.append(subprocess.Popen([MEDIA_HANDLER, m], | |
stderr=subprocess.STDOUT, | |
stdout=subprocess.PIPE)) | |
else: | |
print("Missing media: " + m) | |
print(question.to_string(show_correct=False)) | |
m_ans = raw_input('Podaj odpowiedzi: ') | |
print('') | |
print(question.to_string()) | |
print('') | |
# Sprawdzanie odpowiedzi | |
for l in m_ans: | |
if l not in correct: | |
correct_answer = bad_answer(key, correct) | |
break | |
else: | |
for l in correct: | |
if l not in m_ans: | |
correct_answer = bad_answer(key, correct) | |
break | |
if correct_answer: | |
print("Dobrze!") | |
for p in procs: | |
p.terminate() | |
print("Poprawne odpowiedzi: {0}/{1} ({2:.2%})\n".\ | |
format(count - len(wrong), count, | |
(count - len(wrong)) / float(count))) | |
remaining = len(keys) - count | |
if not remaining: | |
break | |
print("Pozostało {} pytań".format(remaining)) | |
if raw_input('Dalej? (T/n) ') == 'n': | |
break | |
print('') | |
print("Twój wynik: {0}/{1} ({2:.2%}) \n".format( | |
count - len(wrong), count, (count - len(wrong)) / float(count) )) | |
if wrong: | |
print("Miałeś problemy z pytaniami:\n{0}".format(','.join(wrong))) | |
if __name__ == '__main__': | |
options = parse_options() | |
questions_file = open(sys.argv[1]) | |
if options.patterns_file: | |
# Ugly but simple :-) | |
exec(open(options.patterns_file)) | |
patterns = dict( | |
((k + '_pattern', PATTERNS[k]) for k in PATTERNS.keys()) | |
) | |
questions = Question.make_questions(questions_file.read(), **patterns) | |
if options.keys: | |
keys = options.keys.split(',') | |
new_keys = [] | |
for k in keys: | |
if k.find(':') >= 0: | |
new_keys.extend(map(str, range(*map(int, k.split(":"))))) | |
else: | |
new_keys.append(k) | |
keys = new_keys | |
questions = dict([ | |
(k, questions[k]) for k in questions if k in keys | |
]) | |
# Media autodiscovery | |
if os.path.isdir('media'): | |
for m in os.listdir('media'): | |
key = m.split('.')[0] | |
path = 'media/' + m | |
if key not in questions.keys(): | |
continue | |
try: | |
if path not in questions[key].media: | |
questions[key].media.append(path) | |
except KeyError: | |
print("Skipping file: {path} (no such question)".format( | |
path=path)) | |
if options.shuffle: | |
for q in questions.values(): | |
q.shuffle_answers() | |
if options.ipython: | |
from IPython.Shell import IPShellEmbed | |
ipshell = IPShellEmbed(argv=[]) | |
ipshell() | |
if options.stdout: | |
for key in sorted(questions, key=SORT_KEY): | |
print(questions[key].to_string(full=True, raw=True)) | |
print('') | |
elif options.quiz: | |
quiz(questions) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment