question: Question.check() now returns (correct, details)
[quizzer.git] / quizzer / ui / cli.py
index 60ce8968df1b2b7f3217e7c826a4eadb346f43fe..60d649fdc6f2b332a35de1b4d0f8fb570446a60b 100644 (file)
@@ -27,7 +27,8 @@ except ImportError as e:
         return text
     print(e)
 
-from . import UserInterface
+from .. import error as _error
+from . import UserInterface as _UserInterface
 
 
 class QuestionCommandLine (_cmd.Cmd):
@@ -42,13 +43,25 @@ class QuestionCommandLine (_cmd.Cmd):
     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
 
-    def preloop(self):
+    def get_question(self):
         self.question = self.ui.get_question()
-        self._reset()
+        if self.question:
+            self._reset()
+        else:
+            return True  # out of questions
+
+    def preloop(self):
+        self.get_question()
 
     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):
@@ -82,15 +95,21 @@ class QuestionCommandLine (_cmd.Cmd):
             answer = self.answers[0]
         else:
             answer = ''
-        correct = self.ui.process_answer(question=self.question, answer=answer)
+        kwargs = {}
+        if self._tempdir:
+            kwargs['tempdir'] = self._tempdir
+        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\n'))
-        self.question = self.ui.get_question()
-        if not self.question:
-            return True  # out of questions
-        self._reset()
+            print(_colorize(self.ui.colors['incorrect'], 'incorrect'))
+            if details:
+                print(_colorize(
+                        self.ui.colors['incorrect'], '{}\n'.format(details)))
+            else:
+                print('')
+        return self.get_question()
 
     def do_answer(self, arg):
         """Explicitly add a line to your answer
@@ -102,6 +121,32 @@ class QuestionCommandLine (_cmd.Cmd):
         """
         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 do_quit(self, arg):
         "Stop taking the quiz"
         self._reset()
@@ -110,16 +155,20 @@ class QuestionCommandLine (_cmd.Cmd):
     def do_skip(self, arg):
         "Skip the current question, and continue with the quiz"
         self.ui.stack.append(self.question)
-        self.question = self.ui.get_question()
-        if not self.question:
-            return True  # out of questions
-        self._reset()
+        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.copyright:
+            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:
@@ -127,7 +176,7 @@ class QuestionCommandLine (_cmd.Cmd):
         super(QuestionCommandLine, self).do_help(arg)
 
 
-class CommandLineInterface (UserInterface):
+class CommandLineInterface (_UserInterface):
     colors = {  # listed in pygments.console.light_colors
         'question': 'turquoise',
         'prompt': 'blue',
@@ -137,21 +186,21 @@ class CommandLineInterface (UserInterface):
         }
 
     def run(self):
-        if not self.stack:
-            return
-        cmd = QuestionCommandLine(ui=self)
-        cmd.cmdloop()
-        print()
+        if self.stack:
+            cmd = QuestionCommandLine(ui=self)
+            cmd.cmdloop()
+            print()
+        self._display_results()
 
-    def display_results(self):
+    def _display_results(self):
         print(_colorize(self.colors['result'], 'results:'))
         for question in self.quiz:
             if question.id in self.answers:
-                self.display_result(question=question)
+                self._display_result(question=question)
                 print()
-        self.display_totals()
+        self._display_totals()
 
-    def display_result(self, question):
+    def _display_result(self, question):
         answers = self.answers.get(question.id, [])
         print('question:')
         print('  {}'.format(
@@ -160,7 +209,8 @@ class CommandLineInterface (UserInterface):
                 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'
@@ -173,12 +223,14 @@ class CommandLineInterface (UserInterface):
             print('  you answered: {}'.format(ans))
             print('     which was: {}'.format(correct))
 
-    def display_totals(self):
+    def _display_totals(self):
         answered = self.answers.get_answered(questions=self.quiz)
         correctly_answered = self.answers.get_correctly_answered(
             questions=self.quiz)
         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))