ui.cli: Import UserInterface with an underscore
[quizzer.git] / quizzer / ui / cli.py
1 # Copyright (C) 2013 W. Trevor King <wking@tremily.us>
2 #
3 # This file is part of quizzer.
4 #
5 # quizzer is free software: you can redistribute it and/or modify it under the
6 # terms of the GNU General Public License as published by the Free Software
7 # Foundation, either version 3 of the License, or (at your option) any later
8 # version.
9 #
10 # quizzer is distributed in the hope that it will be useful, but WITHOUT ANY
11 # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
12 # A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU General Public License along with
15 # quizzer.  If not, see <http://www.gnu.org/licenses/>.
16
17 import cmd as _cmd
18 try:
19     import readline as _readline
20 except ImportError as _readline_import_error:
21     _readline = None
22
23 try:
24     from pygments.console import colorize as _colorize
25 except ImportError as e:
26     def _colorize(color_key=None, text=None):
27         return text
28     print(e)
29
30 from .. import error as _error
31 from . import UserInterface as _UserInterface
32
33
34 class QuestionCommandLine (_cmd.Cmd):
35     _help = [
36         'Type help or ? to list commands.',
37         'Non-commands will be interpreted as answers.',
38         'Use a blank line to terminate multi-line answers.',
39         ]
40     intro = '\n'.join(['Welcome to the quizzer shell.'] + _help)
41     _prompt = 'quizzer? '
42
43     def __init__(self, ui):
44         super(QuestionCommandLine, self).__init__()
45         self.ui = ui
46         if self.ui.quiz.introduction:
47             self.intro = '\n\n'.join([self.intro, self.ui.quiz.introduction])
48         self._tempdir = None
49
50     def get_question(self):
51         self.question = self.ui.get_question()
52         if self.question:
53             self._reset()
54         else:
55             return True  # out of questions
56
57     def preloop(self):
58         self.get_question()
59
60     def _reset(self):
61         self.answers = []
62         if self._tempdir:
63             self._tempdir.cleanup()  # occasionally redundant, but that's ok
64         self._tempdir = None
65         self._set_ps1()
66
67     def _set_ps1(self):
68         "Pose a question and prompt"
69         if self.question:
70             self.prompt = '\n{}\n{}'.format(
71                 _colorize(
72                     self.ui.colors['question'], self.question.format_prompt()),
73                 _colorize(self.ui.colors['prompt'], self._prompt))
74         else:
75             self.prompt = _colorize(self.ui.colors['prompt'], self._prompt)
76
77     def _set_ps2(self):
78         "Just prompt (without the question, e.g. for multi-line answers)"
79         self.prompt = _colorize(self.ui.colors['prompt'], self._prompt)
80
81     def default(self, line):
82         self.answers.append(line)
83         if self.question.multiline:
84             self._set_ps2()
85         else:
86             return self._answer()
87
88     def emptyline(self):
89         return self._answer()
90
91     def _answer(self):
92         if self.question.multiline:
93             answer = self.answers
94         elif self.answers:
95             answer = self.answers[0]
96         else:
97             answer = ''
98         kwargs = {}
99         if self._tempdir:
100             kwargs['tempdir'] = self._tempdir
101         correct = self.ui.process_answer(
102             question=self.question, answer=answer, **kwargs)
103         if correct:
104             print(_colorize(self.ui.colors['correct'], 'correct\n'))
105         else:
106             print(_colorize(self.ui.colors['incorrect'], 'incorrect\n'))
107         return self.get_question()
108
109     def do_answer(self, arg):
110         """Explicitly add a line to your answer
111
112         This is useful if the line you'd like to add starts with a
113         quizzer-shell command.  For example:
114
115           quizzer? answer help=5
116         """
117         return self.default(arg)
118
119     def do_shell(self, arg):
120         """Run a shell command in the question temporary directory
121
122         For example, you can spawn an interactive session with:
123
124           quizzer? !bash
125
126         If the question does not allow interactive sessions, this
127         action is a no-op.
128         """
129         if getattr(self.question, 'allow_interactive', False):
130             if not self._tempdir:
131                 self._tempdir = self.question.setup_tempdir()
132             try:
133                 self._tempdir.invoke(
134                     interpreter='/bin/sh', text=arg, stdout=None, stderr=None,
135                     universal_newlines=False,
136                     env=self.question.get_environment())
137             except (KeyboardInterrupt, _error.CommandError) as e:
138                 if isinstance(e, KeyboardInterrupt):
139                     LOG.warning('KeyboardInterrupt')
140                 else:
141                     LOG.warning(e)
142                 self._tempdir.cleanup()
143                 self._tempdir = None
144
145     def do_quit(self, arg):
146         "Stop taking the quiz"
147         self._reset()
148         return True
149
150     def do_skip(self, arg):
151         "Skip the current question, and continue with the quiz"
152         self.ui.stack.append(self.question)
153         return self.get_question()
154
155     def do_hint(self, arg):
156         "Show a hint for the current question"
157         self._reset()
158         print(self.question.format_help())
159
160     def do_copyright(self, arg):
161         "Print the quiz copyright notice"
162         if self.ui.quiz.copyright:
163             print('\n'.join(self.ui.quiz.copyright))
164         else:
165             print(self.ui.quiz.copyright)
166
167     def do_help(self, arg):
168         'List available commands with "help" or detailed help with "help cmd"'
169         if not arg:
170             print('\n'.join(self._help))
171         super(QuestionCommandLine, self).do_help(arg)
172
173
174 class CommandLineInterface (_UserInterface):
175     colors = {  # listed in pygments.console.light_colors
176         'question': 'turquoise',
177         'prompt': 'blue',
178         'correct': 'green',
179         'incorrect': 'red',
180         'result': 'fuchsia',
181         }
182
183     def run(self):
184         if self.stack:
185             cmd = QuestionCommandLine(ui=self)
186             cmd.cmdloop()
187             print()
188         self._display_results()
189
190     def _display_results(self):
191         print(_colorize(self.colors['result'], 'results:'))
192         for question in self.quiz:
193             if question.id in self.answers:
194                 self._display_result(question=question)
195                 print()
196         self._display_totals()
197
198     def _display_result(self, question):
199         answers = self.answers.get(question.id, [])
200         print('question:')
201         print('  {}'.format(
202             _colorize(
203                 self.colors['question'],
204                 question.format_prompt(newline='\n  '))))
205         la = len(answers)
206         lc = len([a for a in answers if a['correct']])
207         if la:
208             print('answers: {}/{} ({:.2f})'.format(lc, la, float(lc)/la))
209         for answer in answers:
210             if answer['correct']:
211                 correct = 'correct'
212             else:
213                 correct = 'incorrect'
214             correct = _colorize(self.colors[correct], correct)
215             ans = answer['answer']
216             if question.multiline:
217                 ans = '\n                '.join(ans)
218             print('  you answered: {}'.format(ans))
219             print('     which was: {}'.format(correct))
220
221     def _display_totals(self):
222         answered = self.answers.get_answered(questions=self.quiz)
223         correctly_answered = self.answers.get_correctly_answered(
224             questions=self.quiz)
225         la = len(answered)
226         lc = len(correctly_answered)
227         print('answered {} of {} questions'.format(la, len(self.quiz)))
228         if la:
229             print(('of the answered questions, '
230                    '{} ({:.2f}) were answered correctly'
231                    ).format(lc, float(lc)/la))