Skip to content

Instantly share code, notes, and snippets.

@biern
Created June 10, 2011 12:07
Show Gist options
  • Save biern/1018713 to your computer and use it in GitHub Desktop.
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.
#!/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