# You should have received a copy of the GNU General Public License along with
# quizzer. If not, see <http://www.gnu.org/licenses/>.
+import cmd as _cmd
+import logging as _logging
+import os.path as _os_path
try:
import readline as _readline
except ImportError as _readline_import_error:
_readline = None
-from . import UserInterface
+try:
+ from pygments.console import colorize as _colorize
+except ImportError as e:
+ def _colorize(color_key=None, text=None):
+ return text
+ print(e)
+from .. import error as _error
+from .. import question as _question
+from . import UserInterface as _UserInterface
+from . import util as _util
-class CommandLineInterface (UserInterface):
- def run(self):
- while True:
- question = self.get_question()
- if not question:
- break
- print(question.format_prompt())
- if question.multiline:
+
+_LOG = _logging.getLogger(__name__)
+
+
+class QuestionCommandLine (_cmd.Cmd):
+ _help = [
+ 'Type help or ? to list commands.',
+ 'Non-commands will be interpreted as answers.',
+ 'Use a blank line to terminate multi-line answers.',
+ ]
+ intro = '\n'.join(['Welcome to the quizzer shell.'] + _help)
+ _prompt = 'quizzer? '
+
+ def __init__(self, ui):
+ super(QuestionCommandLine, self).__init__()
+ self.ui = ui
+ if self.ui.quiz.introduction:
+ self.intro = '\n\n'.join([self.intro, self.ui.quiz.introduction])
+ self._tempdir = None
+ self._children = []
+
+ def get_question(self):
+ self.question = self.ui.get_question(user=self.ui.user)
+ if self.question:
+ self._reset()
+ else:
+ return True # out of questions
+
+ def preloop(self):
+ self.get_question()
+
+ def postcmd(self, stop, line):
+ self._reap_children()
+ return stop
+
+ def _reset(self):
+ self.answers = []
+ if self._tempdir:
+ self._tempdir.cleanup() # occasionally redundant, but that's ok
+ self._tempdir = None
+ self._set_ps1()
+
+ def _set_ps1(self):
+ "Pose a question and prompt"
+ if self.question:
+ lines = [
+ '',
+ _colorize(
+ self.ui.colors['question'], self.question.format_prompt()),
+ ]
+ lines.extend(
+ _colorize(self.ui.colors['prompt'], line)
+ for line in self._extra_ps1_lines())
+ lines.append(_colorize(self.ui.colors['prompt'], self._prompt))
+ self.prompt = '\n'.join(lines)
+ else:
+ self.prompt = _colorize(self.ui.colors['prompt'], self._prompt)
+
+ def _set_ps2(self):
+ "Just prompt (without the question, e.g. for multi-line answers)"
+ self.prompt = _colorize(self.ui.colors['prompt'], self._prompt)
+
+ def _extra_ps1_lines(self):
+ for multimedia in self.question.multimedia:
+ for line in self._format_multimedia(multimedia):
+ yield line # for Python 3.3, use PEP 380's `yield from ...`
+ if (isinstance(self.question, _question.ChoiceQuestion) and
+ self.question.display_choices):
+ for line in self._format_choices(question=self.question):
+ yield line
+
+ def _format_multimedia(self, multimedia):
+ path = self.ui.quiz.multimedia_path(multimedia=multimedia)
+ content_type = multimedia['content-type']
+ try:
+ self._children.append(_util.mailcap_view(
+ path=path, content_type=content_type, background=True))
+ except NotImplementedError:
+ path = _os_path.abspath(path)
+ yield 'multimedia ({}): {}'.format(content_type, path)
+
+ def _reap_children(self):
+ reaped = []
+ for process in self._children:
+ _LOG.debug('poll child process {}'.format(process.pid))
+ if process.poll() is not None:
+ _LOG.debug('process {} returned {}'.format(
+ process.pid, process.returncode))
+ reaped.append(process)
+ for process in reaped:
+ self._children.remove(process)
+
+ def _format_choices(self, question):
+ for i,choice in enumerate(question.answer):
+ yield '{}) {}'.format(i, choice)
+ yield 'Answer with the index of your choice'
+ if question.accept_all:
+ conj = 'or'
+ if question.multiple_answers:
+ conj = 'and/or'
+ yield '{} fill in an alternative answer'.format(conj)
+ if question.multiple_answers:
+ self._separator = ','
+ yield ("Separate multiple answers with the '{}' character"
+ ).format(self._separator)
+
+ def _process_answer(self, answer):
+ "Back out any mappings suggested by _extra_ps1_lines()"
+ if (isinstance(self.question, _question.ChoiceQuestion) and
+ self.question.display_choices):
+ if self.question.multiple_answers:
answers = []
- while True:
+ for a in answer.split(self._separator):
+ try:
+ i = int(a)
+ answers.append(self.question.answer[i])
+ except (ValueError, IndexError):
+ answers.append(a)
+ return answers
+ else:
try:
- answer = input('? ')
- except EOFError:
- answer = 'quit'
- a = answer.strip().lower()
- if a in ['q', 'quit']:
- print()
- return
- if a in ['?', 'help']:
- print()
- print(question.format_prompt())
- print(question.help)
- continue
- if question.multiline:
- answers.append(answer)
- if not a:
- break
- else:
- break
- if question.multiline:
- answer = answers
- correct = self.process_answer(question=question, answer=answer)
- if correct:
- print('correct\n')
+ i = int(answer)
+ return self.question.answer[i]
+ except (ValueError, IndexError):
+ pass
+ return answer
+
+ def default(self, line):
+ self.answers.append(line)
+ if self.question.multiline:
+ self._set_ps2()
+ else:
+ return self._answer()
+
+ def emptyline(self):
+ return self._answer()
+
+ def _answer(self):
+ if self.question.multiline:
+ answer = self.answers
+ elif self.answers:
+ answer = self.answers[0]
+ else:
+ answer = ''
+ if answer == 'EOF':
+ return True # quit
+ if answer == '':
+ return
+ kwargs = {}
+ if self._tempdir:
+ kwargs['tempdir'] = self._tempdir
+ answer = self._process_answer(answer=answer)
+ correct,details = self.ui.process_answer(
+ question=self.question, answer=answer, **kwargs)
+ if correct:
+ print(_colorize(self.ui.colors['correct'], 'correct\n'))
+ else:
+ print(_colorize(self.ui.colors['incorrect'], 'incorrect'))
+ if details:
+ print(_colorize(
+ self.ui.colors['incorrect'], '{}\n'.format(details)))
else:
- print('incorrect\n')
+ print('')
+ return self.get_question()
+
+ def do_answer(self, arg):
+ """Explicitly add a line to your answer
+
+ This is useful if the line you'd like to add starts with a
+ quizzer-shell command. For example:
+
+ quizzer? answer help=5
+ """
+ return self.default(arg)
+
+ def do_shell(self, arg):
+ """Run a shell command in the question temporary directory
+
+ For example, you can spawn an interactive session with:
+
+ quizzer? !bash
+
+ If the question does not allow interactive sessions, this
+ action is a no-op.
+ """
+ if getattr(self.question, 'allow_interactive', False):
+ if not self._tempdir:
+ self._tempdir = self.question.setup_tempdir()
+ try:
+ self._tempdir.invoke(
+ interpreter='/bin/sh', text=arg, stdout=None, stderr=None,
+ universal_newlines=False,
+ env=self.question.get_environment())
+ except (KeyboardInterrupt, _error.CommandError) as e:
+ if isinstance(e, KeyboardInterrupt):
+ LOG.warning('KeyboardInterrupt')
+ else:
+ LOG.warning(e)
+ self._tempdir.cleanup()
+ self._tempdir = None
- def display_results(self):
- print('results:')
+ def do_quit(self, arg):
+ "Stop taking the quiz"
+ self._reset()
+ return True
+
+ def do_skip(self, arg):
+ "Skip the current question, and continue with the quiz"
+ self.ui.stack[self.ui.user].append(self.question)
+ return self.get_question()
+
+ def do_hint(self, arg):
+ "Show a hint for the current question"
+ self._reset()
+ print(self.question.format_help())
+
+ def do_copyright(self, arg):
+ "Print the quiz copyright notice"
+ if self.ui.quiz.copight:
+ print('\n'.join(self.ui.quiz.copyright))
+ else:
+ print(self.ui.quiz.copyright)
+
+ def do_help(self, arg):
+ 'List available commands with "help" or detailed help with "help cmd"'
+ if not arg:
+ print('\n'.join(self._help))
+ super(QuestionCommandLine, self).do_help(arg)
+
+
+class CommandLineInterface (_UserInterface):
+ colors = { # listed in pygments.console.light_colors
+ 'question': 'turquoise',
+ 'prompt': 'blue',
+ 'correct': 'green',
+ 'incorrect': 'red',
+ 'result': 'fuchsia',
+ }
+
+ def run(self):
+ self.user = None
+ if self.stack[self.user]:
+ cmd = QuestionCommandLine(ui=self)
+ cmd.cmdloop()
+ print()
+ self._display_results()
+
+ def _display_results(self):
+ print(_colorize(self.colors['result'], 'results:'))
+ answers = self.answers.get_answers(user=self.user)
for question in self.quiz:
- if question.id in self.answers:
- self.display_result(question=question)
+ if question.id in answers:
+ self._display_result(question=question)
print()
- self.display_totals()
+ self._display_totals()
- def display_result(self, question):
- answers = self.answers.get(question.id, [])
+ def _display_result(self, question):
+ answers = self.answers.get_answers(user=self.user).get(question.id, [])
print('question:')
- print(' {}'.format(question.format_prompt(newline='\n ')))
+ print(' {}'.format(
+ _colorize(
+ self.colors['question'],
+ question.format_prompt(newline='\n '))))
la = len(answers)
lc = len([a for a in answers if a['correct']])
- print('answers: {}/{} ({:.2f})'.format(lc, la, float(lc)/la))
+ if la:
+ print('answers: {}/{} ({:.2f})'.format(lc, la, float(lc)/la))
for answer in answers:
if answer['correct']:
correct = 'correct'
else:
correct = 'incorrect'
- print(' you answered: {}'.format(answer['answer']))
+ correct = _colorize(self.colors[correct], correct)
+ ans = answer['answer']
+ if question.multiline:
+ ans = '\n '.join(ans)
+ print(' you answered: {}'.format(ans))
print(' which was: {}'.format(correct))
- def display_totals(self):
- answered = self.answers.get_answered(questions=self.quiz)
+ def _display_totals(self):
+ answered = self.answers.get_answered(
+ questions=self.quiz, user=self.user)
correctly_answered = self.answers.get_correctly_answered(
- questions=self.quiz)
+ questions=self.quiz, user=self.user)
la = len(answered)
lc = len(correctly_answered)
print('answered {} of {} questions'.format(la, len(self.quiz)))
- print(('of the answered questions, {} ({:.2f}) were answered correctly'
- ).format(lc, float(lc)/la))
+ if la:
+ print(('of the answered questions, '
+ '{} ({:.2f}) were answered correctly'
+ ).format(lc, float(lc)/la))