Commit a333dc72030ffe2603e289666d469e8cc5e10caa

Authored by Miguel Barão
1 parent 3259fc7c
Exists in master and in 1 other branch dev

Add type annotations mostly in questions.py

@@ -2,4 +2,5 @@ @@ -2,4 +2,5 @@
2 /aprendizations.egg-info/ 2 /aprendizations.egg-info/
3 /aprendizations/__pycache__/ 3 /aprendizations/__pycache__/
4 /demo/students.db 4 /demo/students.db
5 -/node_modules/  
6 \ No newline at end of file 5 \ No newline at end of file
  6 +/node_modules/
  7 +/.mypy_cache/
7 \ No newline at end of file 8 \ No newline at end of file
aprendizations/learnapp.py
@@ -6,6 +6,7 @@ from contextlib import contextmanager # `with` statement in db sessions @@ -6,6 +6,7 @@ from contextlib import contextmanager # `with` statement in db sessions
6 import asyncio 6 import asyncio
7 from datetime import datetime 7 from datetime import datetime
8 from random import random 8 from random import random
  9 +from typing import Dict
9 10
10 # third party libraries 11 # third party libraries
11 import bcrypt 12 import bcrypt
@@ -75,13 +76,12 @@ class LearnApp(object): @@ -75,13 +76,12 @@ class LearnApp(object):
75 errors = 0 76 errors = 0
76 for qref in self.factory: 77 for qref in self.factory:
77 logger.debug(f'Checking "{qref}"...') 78 logger.debug(f'Checking "{qref}"...')
78 - q = self.factory[qref].generate()  
79 try: 79 try:
80 q = self.factory[qref].generate() 80 q = self.factory[qref].generate()
81 except Exception: 81 except Exception:
82 logger.error(f'Failed to generate "{qref}".') 82 logger.error(f'Failed to generate "{qref}".')
83 errors += 1 83 errors += 1
84 - raise 84 + raise LearnException('Sanity checks')
85 continue 85 continue
86 86
87 if 'tests_right' in q: 87 if 'tests_right' in q:
@@ -89,8 +89,7 @@ class LearnApp(object): @@ -89,8 +89,7 @@ class LearnApp(object):
89 q['answer'] = t 89 q['answer'] = t
90 q.correct() 90 q.correct()
91 if q['grade'] < 1.0: 91 if q['grade'] < 1.0:
92 - logger.error(f'Failed to correct right answer in '  
93 - f'"{qref}".') 92 + logger.error(f'Failed right answer in "{qref}".')
94 errors += 1 93 errors += 1
95 continue # to next right test 94 continue # to next right test
96 95
@@ -99,14 +98,13 @@ class LearnApp(object): @@ -99,14 +98,13 @@ class LearnApp(object):
99 q['answer'] = t 98 q['answer'] = t
100 q.correct() 99 q.correct()
101 if q['grade'] >= 1.0: 100 if q['grade'] >= 1.0:
102 - logger.error(f'Failed to correct right answer in '  
103 - f'"{qref}".') 101 + logger.error(f'Failed wrong answer in "{qref}".')
104 errors += 1 102 errors += 1
105 continue # to next wrong test 103 continue # to next wrong test
106 104
107 if errors > 0: 105 if errors > 0:
108 logger.info(f'{errors:>6} errors found.') 106 logger.info(f'{errors:>6} errors found.')
109 - raise 107 + raise LearnException('Sanity checks')
110 else: 108 else:
111 logger.info('No errors found.') 109 logger.info('No errors found.')
112 110
@@ -315,8 +313,8 @@ class LearnApp(object): @@ -315,8 +313,8 @@ class LearnApp(object):
315 for tref, attr in topics.items(): 313 for tref, attr in topics.items():
316 for d in attr.get('deps', []): 314 for d in attr.get('deps', []):
317 if d not in g.nodes(): 315 if d not in g.nodes():
318 - logger.error(f'Topic "{tref}" depends on "{d}" but the '  
319 - f'latter does not exist') 316 + logger.error(f'Topic "{tref}" depends on "{d}" but it '
  317 + f'does not exist')
320 raise LearnException() 318 raise LearnException()
321 else: 319 else:
322 g.add_edge(d, tref) 320 g.add_edge(d, tref)
@@ -345,7 +343,7 @@ class LearnApp(object): @@ -345,7 +343,7 @@ class LearnApp(object):
345 # ------------------------------------------------------------------------ 343 # ------------------------------------------------------------------------
346 # Buils dictionary of question factories 344 # Buils dictionary of question factories
347 # ------------------------------------------------------------------------ 345 # ------------------------------------------------------------------------
348 - def make_factory(self): 346 + def make_factory(self) -> Dict[str, QFactory]:
349 logger.info('Building questions factory...') 347 logger.info('Building questions factory...')
350 factory = {} # {'qref': QFactory()} 348 factory = {} # {'qref': QFactory()}
351 g = self.deps 349 g = self.deps
@@ -382,39 +380,39 @@ class LearnApp(object): @@ -382,39 +380,39 @@ class LearnApp(object):
382 return factory 380 return factory
383 381
384 # ------------------------------------------------------------------------ 382 # ------------------------------------------------------------------------
385 - def get_login_counter(self, uid): 383 + def get_login_counter(self, uid: str) -> int:
386 return self.online[uid]['counter'] 384 return self.online[uid]['counter']
387 385
388 # ------------------------------------------------------------------------ 386 # ------------------------------------------------------------------------
389 - def get_student_name(self, uid): 387 + def get_student_name(self, uid: str) -> str:
390 return self.online[uid].get('name', '') 388 return self.online[uid].get('name', '')
391 389
392 # ------------------------------------------------------------------------ 390 # ------------------------------------------------------------------------
393 - def get_student_state(self, uid): 391 + def get_student_state(self, uid: str):
394 return self.online[uid]['state'].get_knowledge_state() 392 return self.online[uid]['state'].get_knowledge_state()
395 393
396 # ------------------------------------------------------------------------ 394 # ------------------------------------------------------------------------
397 - def get_student_progress(self, uid): 395 + def get_student_progress(self, uid: str):
398 return self.online[uid]['state'].get_topic_progress() 396 return self.online[uid]['state'].get_topic_progress()
399 397
400 # ------------------------------------------------------------------------ 398 # ------------------------------------------------------------------------
401 - def get_current_question(self, uid): 399 + def get_current_question(self, uid: str):
402 return self.online[uid]['state'].get_current_question() # dict 400 return self.online[uid]['state'].get_current_question() # dict
403 401
404 # ------------------------------------------------------------------------ 402 # ------------------------------------------------------------------------
405 - def get_student_question_type(self, uid): 403 + def get_student_question_type(self, uid: str) -> str:
406 return self.online[uid]['state'].get_current_question()['type'] 404 return self.online[uid]['state'].get_current_question()['type']
407 405
408 # ------------------------------------------------------------------------ 406 # ------------------------------------------------------------------------
409 - def get_student_topic(self, uid): 407 + def get_student_topic(self, uid: str) -> str:
410 return self.online[uid]['state'].get_current_topic() # str 408 return self.online[uid]['state'].get_current_topic() # str
411 409
412 # ------------------------------------------------------------------------ 410 # ------------------------------------------------------------------------
413 - def get_title(self): 411 + def get_title(self) -> str:
414 return self.deps.graph.get('title', '') # FIXME 412 return self.deps.graph.get('title', '') # FIXME
415 413
416 # ------------------------------------------------------------------------ 414 # ------------------------------------------------------------------------
417 - def get_topic_name(self, ref): 415 + def get_topic_name(self, ref: str) -> str:
418 return self.deps.node[ref]['name'] 416 return self.deps.node[ref]['name']
419 417
420 # ------------------------------------------------------------------------ 418 # ------------------------------------------------------------------------
aprendizations/questions.py
@@ -5,6 +5,7 @@ import re @@ -5,6 +5,7 @@ import re
5 from os import path 5 from os import path
6 import logging 6 import logging
7 import asyncio 7 import asyncio
  8 +from typing import Any, Dict, NewType
8 9
9 # this project 10 # this project
10 from .tools import run_script 11 from .tools import run_script
@@ -13,6 +14,9 @@ from .tools import run_script @@ -13,6 +14,9 @@ from .tools import run_script
13 logger = logging.getLogger(__name__) 14 logger = logging.getLogger(__name__)
14 15
15 16
  17 +QDict = NewType('QDict', Dict[str, Any])
  18 +
  19 +
16 class QuestionException(Exception): 20 class QuestionException(Exception):
17 pass 21 pass
18 22
@@ -27,18 +31,18 @@ class Question(dict): @@ -27,18 +31,18 @@ class Question(dict):
27 to a student. 31 to a student.
28 Instances can shuffle options, or automatically generate questions. 32 Instances can shuffle options, or automatically generate questions.
29 ''' 33 '''
30 - def __init__(self, q): 34 + def __init__(self, q: QDict) -> None:
31 super().__init__(q) 35 super().__init__(q)
32 36
33 # add required keys if missing 37 # add required keys if missing
34 - self.set_defaults({ 38 + self.set_defaults(QDict({
35 'title': '', 39 'title': '',
36 'answer': None, 40 'answer': None,
37 'comments': '', 41 'comments': '',
38 'solution': '', 42 'solution': '',
39 'files': {}, 43 'files': {},
40 'max_tries': 3, 44 'max_tries': 3,
41 - }) 45 + }))
42 46
43 def correct(self) -> None: 47 def correct(self) -> None:
44 self['comments'] = '' 48 self['comments'] = ''
@@ -48,7 +52,7 @@ class Question(dict): @@ -48,7 +52,7 @@ class Question(dict):
48 loop = asyncio.get_running_loop() 52 loop = asyncio.get_running_loop()
49 await loop.run_in_executor(None, self.correct) 53 await loop.run_in_executor(None, self.correct)
50 54
51 - def set_defaults(self, d) -> None: 55 + def set_defaults(self, d: QDict) -> None:
52 'Add k:v pairs from default dict d for nonexistent keys' 56 'Add k:v pairs from default dict d for nonexistent keys'
53 for k, v in d.items(): 57 for k, v in d.items():
54 self.setdefault(k, v) 58 self.setdefault(k, v)
@@ -69,18 +73,18 @@ class QuestionRadio(Question): @@ -69,18 +73,18 @@ class QuestionRadio(Question):
69 73
70 # ------------------------------------------------------------------------ 74 # ------------------------------------------------------------------------
71 # FIXME marking all options right breaks 75 # FIXME marking all options right breaks
72 - def __init__(self, q): 76 + def __init__(self, q: QDict) -> None:
73 super().__init__(q) 77 super().__init__(q)
74 78
75 n = len(self['options']) 79 n = len(self['options'])
76 80
77 - self.set_defaults({ 81 + self.set_defaults(QDict({
78 'text': '', 82 'text': '',
79 'correct': 0, 83 'correct': 0,
80 'shuffle': True, 84 'shuffle': True,
81 'discount': True, 85 'discount': True,
82 'max_tries': (n + 3) // 4 # 1 try for each 4 options 86 'max_tries': (n + 3) // 4 # 1 try for each 4 options
83 - }) 87 + }))
84 88
85 # convert int to list, e.g. correct: 2 --> correct: [0,0,1,0,0] 89 # convert int to list, e.g. correct: 2 --> correct: [0,0,1,0,0]
86 # correctness levels from 0.0 to 1.0 (no discount here!) 90 # correctness levels from 0.0 to 1.0 (no discount here!)
@@ -97,7 +101,7 @@ class QuestionRadio(Question): @@ -97,7 +101,7 @@ class QuestionRadio(Question):
97 right = [i for i in range(n) if self['correct'][i] >= 1] 101 right = [i for i in range(n) if self['correct'][i] >= 1]
98 wrong = [i for i in range(n) if self['correct'][i] < 1] 102 wrong = [i for i in range(n) if self['correct'][i] < 1]
99 103
100 - self.set_defaults({'choose': 1+len(wrong)}) 104 + self.set_defaults(QDict({'choose': 1+len(wrong)}))
101 105
102 # try to choose 1 correct option 106 # try to choose 1 correct option
103 if right: 107 if right:
@@ -147,20 +151,20 @@ class QuestionCheckbox(Question): @@ -147,20 +151,20 @@ class QuestionCheckbox(Question):
147 ''' 151 '''
148 152
149 # ------------------------------------------------------------------------ 153 # ------------------------------------------------------------------------
150 - def __init__(self, q): 154 + def __init__(self, q: QDict) -> None:
151 super().__init__(q) 155 super().__init__(q)
152 156
153 n = len(self['options']) 157 n = len(self['options'])
154 158
155 # set defaults if missing 159 # set defaults if missing
156 - self.set_defaults({ 160 + self.set_defaults(QDict({
157 'text': '', 161 'text': '',
158 'correct': [1.0] * n, # Using 0.0 breaks (right, wrong) options 162 'correct': [1.0] * n, # Using 0.0 breaks (right, wrong) options
159 'shuffle': True, 163 'shuffle': True,
160 'discount': True, 164 'discount': True,
161 'choose': n, # number of options 165 'choose': n, # number of options
162 'max_tries': max(1, min(n - 1, 3)) 166 'max_tries': max(1, min(n - 1, 3))
163 - }) 167 + }))
164 168
165 if len(self['correct']) != n: 169 if len(self['correct']) != n:
166 msg = f'Options and correct mismatch in "{self["ref"]}"' 170 msg = f'Options and correct mismatch in "{self["ref"]}"'
@@ -218,13 +222,13 @@ class QuestionText(Question): @@ -218,13 +222,13 @@ class QuestionText(Question):
218 ''' 222 '''
219 223
220 # ------------------------------------------------------------------------ 224 # ------------------------------------------------------------------------
221 - def __init__(self, q): 225 + def __init__(self, q: QDict) -> None:
222 super().__init__(q) 226 super().__init__(q)
223 227
224 - self.set_defaults({ 228 + self.set_defaults(QDict({
225 'text': '', 229 'text': '',
226 'correct': [], 230 'correct': [],
227 - }) 231 + }))
228 232
229 # make sure its always a list of possible correct answers 233 # make sure its always a list of possible correct answers
230 if not isinstance(self['correct'], list): 234 if not isinstance(self['correct'], list):
@@ -251,13 +255,13 @@ class QuestionTextRegex(Question): @@ -251,13 +255,13 @@ class QuestionTextRegex(Question):
251 ''' 255 '''
252 256
253 # ------------------------------------------------------------------------ 257 # ------------------------------------------------------------------------
254 - def __init__(self, q): 258 + def __init__(self, q: QDict) -> None:
255 super().__init__(q) 259 super().__init__(q)
256 260
257 - self.set_defaults({ 261 + self.set_defaults(QDict({
258 'text': '', 262 'text': '',
259 'correct': '$.^', # will always return false 263 'correct': '$.^', # will always return false
260 - }) 264 + }))
261 265
262 # ------------------------------------------------------------------------ 266 # ------------------------------------------------------------------------
263 def correct(self) -> None: 267 def correct(self) -> None:
@@ -282,13 +286,13 @@ class QuestionNumericInterval(Question): @@ -282,13 +286,13 @@ class QuestionNumericInterval(Question):
282 ''' 286 '''
283 287
284 # ------------------------------------------------------------------------ 288 # ------------------------------------------------------------------------
285 - def __init__(self, q): 289 + def __init__(self, q: QDict) -> None:
286 super().__init__(q) 290 super().__init__(q)
287 291
288 - self.set_defaults({ 292 + self.set_defaults(QDict({
289 'text': '', 293 'text': '',
290 'correct': [1.0, -1.0], # will always return false 294 'correct': [1.0, -1.0], # will always return false
291 - }) 295 + }))
292 296
293 # ------------------------------------------------------------------------ 297 # ------------------------------------------------------------------------
294 def correct(self) -> None: 298 def correct(self) -> None:
@@ -317,16 +321,16 @@ class QuestionTextArea(Question): @@ -317,16 +321,16 @@ class QuestionTextArea(Question):
317 ''' 321 '''
318 322
319 # ------------------------------------------------------------------------ 323 # ------------------------------------------------------------------------
320 - def __init__(self, q): 324 + def __init__(self, q: QDict) -> None:
321 super().__init__(q) 325 super().__init__(q)
322 326
323 - self.set_defaults({ 327 + self.set_defaults(QDict({
324 'text': '', 328 'text': '',
325 'lines': 8, 329 'lines': 8,
326 'timeout': 5, # seconds 330 'timeout': 5, # seconds
327 'correct': '', # trying to execute this will fail => grade 0.0 331 'correct': '', # trying to execute this will fail => grade 0.0
328 'args': [] 332 'args': []
329 - }) 333 + }))
330 334
331 self['correct'] = path.join(self['path'], self['correct']) # FIXME 335 self['correct'] = path.join(self['path'], self['correct']) # FIXME
332 336
@@ -360,11 +364,11 @@ class QuestionTextArea(Question): @@ -360,11 +364,11 @@ class QuestionTextArea(Question):
360 # =========================================================================== 364 # ===========================================================================
361 class QuestionInformation(Question): 365 class QuestionInformation(Question):
362 # ------------------------------------------------------------------------ 366 # ------------------------------------------------------------------------
363 - def __init__(self, q): 367 + def __init__(self, q: QDict) -> None:
364 super().__init__(q) 368 super().__init__(q)
365 - self.set_defaults({ 369 + self.set_defaults(QDict({
366 'text': '', 370 'text': '',
367 - }) 371 + }))
368 372
369 # ------------------------------------------------------------------------ 373 # ------------------------------------------------------------------------
370 # can return negative values for wrong answers 374 # can return negative values for wrong answers
@@ -414,14 +418,14 @@ class QFactory(object): @@ -414,14 +418,14 @@ class QFactory(object):
414 'success': QuestionInformation, 418 'success': QuestionInformation,
415 } 419 }
416 420
417 - def __init__(self, question_dict={}): 421 + def __init__(self, question_dict: QDict = QDict({})) -> None:
418 self.question = question_dict 422 self.question = question_dict
419 423
420 # ----------------------------------------------------------------------- 424 # -----------------------------------------------------------------------
421 # Given a ref returns an instance of a descendent of Question(), 425 # Given a ref returns an instance of a descendent of Question(),
422 # i.e. a question object (radio, checkbox, ...). 426 # i.e. a question object (radio, checkbox, ...).
423 # ----------------------------------------------------------------------- 427 # -----------------------------------------------------------------------
424 - def generate(self): 428 + def generate(self) -> Question:
425 logger.debug(f'Generating "{self.question["ref"]}"...') 429 logger.debug(f'Generating "{self.question["ref"]}"...')
426 # Shallow copy so that script generated questions will not replace 430 # Shallow copy so that script generated questions will not replace
427 # the original generators 431 # the original generators
@@ -433,14 +437,14 @@ class QFactory(object): @@ -433,14 +437,14 @@ class QFactory(object):
433 if q['type'] == 'generator': 437 if q['type'] == 'generator':
434 logger.debug(f' \\_ Running "{q["script"]}".') 438 logger.debug(f' \\_ Running "{q["script"]}".')
435 q.setdefault('args', []) 439 q.setdefault('args', [])
436 - q.setdefault('stdin', '') 440 + q.setdefault('stdin', '') # FIXME does not exist anymore?
437 script = path.join(q['path'], q['script']) 441 script = path.join(q['path'], q['script'])
438 out = run_script(script=script, args=q['args'], stdin=q['stdin']) 442 out = run_script(script=script, args=q['args'], stdin=q['stdin'])
439 q.update(out) 443 q.update(out)
440 444
441 # Finally we create an instance of Question() 445 # Finally we create an instance of Question()
442 try: 446 try:
443 - qinstance = self._types[q['type']](q) # instance matching class 447 + qinstance = self._types[q['type']](QDict(q)) # of matching class
444 except QuestionException as e: 448 except QuestionException as e:
445 logger.error(e) 449 logger.error(e)
446 raise e 450 raise e
aprendizations/serve.py
@@ -13,6 +13,7 @@ import signal @@ -13,6 +13,7 @@ import signal
13 import functools 13 import functools
14 import ssl 14 import ssl
15 import asyncio 15 import asyncio
  16 +# from typing import NoReturn
16 17
17 # third party libraries 18 # third party libraries
18 import tornado.ioloop 19 import tornado.ioloop
@@ -187,8 +188,6 @@ class RootHandler(BaseHandler): @@ -187,8 +188,6 @@ class RootHandler(BaseHandler):
187 # FIXME should not change state... 188 # FIXME should not change state...
188 # ---------------------------------------------------------------------------- 189 # ----------------------------------------------------------------------------
189 class TopicHandler(BaseHandler): 190 class TopicHandler(BaseHandler):
190 - SUPPORTED_METHODS = ['GET']  
191 -  
192 @tornado.web.authenticated 191 @tornado.web.authenticated
193 async def get(self, topic): 192 async def get(self, topic):
194 uid = self.current_user 193 uid = self.current_user
@@ -209,8 +208,6 @@ class TopicHandler(BaseHandler): @@ -209,8 +208,6 @@ class TopicHandler(BaseHandler):
209 # Serves files from the /public subdir of the topics. 208 # Serves files from the /public subdir of the topics.
210 # ---------------------------------------------------------------------------- 209 # ----------------------------------------------------------------------------
211 class FileHandler(BaseHandler): 210 class FileHandler(BaseHandler):
212 - SUPPORTED_METHODS = ['GET']  
213 -  
214 @tornado.web.authenticated 211 @tornado.web.authenticated
215 async def get(self, filename): 212 async def get(self, filename):
216 uid = self.current_user 213 uid = self.current_user
@@ -238,8 +235,6 @@ class FileHandler(BaseHandler): @@ -238,8 +235,6 @@ class FileHandler(BaseHandler):
238 # respond to AJAX to get a JSON question 235 # respond to AJAX to get a JSON question
239 # ---------------------------------------------------------------------------- 236 # ----------------------------------------------------------------------------
240 class QuestionHandler(BaseHandler): 237 class QuestionHandler(BaseHandler):
241 - SUPPORTED_METHODS = ['GET', 'POST']  
242 -  
243 templates = { 238 templates = {
244 'checkbox': 'question-checkbox.html', 239 'checkbox': 'question-checkbox.html',
245 'radio': 'question-radio.html', 240 'radio': 'question-radio.html',
aprendizations/tools.py
@@ -4,6 +4,7 @@ from os import path @@ -4,6 +4,7 @@ from os import path
4 import subprocess 4 import subprocess
5 import logging 5 import logging
6 import re 6 import re
  7 +from typing import Any, List
7 8
8 # third party libraries 9 # third party libraries
9 import yaml 10 import yaml
@@ -123,7 +124,7 @@ class HighlightRenderer(mistune.Renderer): @@ -123,7 +124,7 @@ class HighlightRenderer(mistune.Renderer):
123 markdown = MarkdownWithMath(HighlightRenderer(escape=True)) 124 markdown = MarkdownWithMath(HighlightRenderer(escape=True))
124 125
125 126
126 -def md_to_html(text, strip_p_tag=False, q=None): 127 +def md_to_html(text: str, strip_p_tag: bool = False) -> str:
127 md = markdown(text) 128 md = markdown(text)
128 if strip_p_tag and md.startswith('<p>') and md.endswith('</p>'): 129 if strip_p_tag and md.startswith('<p>') and md.endswith('</p>'):
129 return md[3:-5] 130 return md[3:-5]
@@ -134,7 +135,7 @@ def md_to_html(text, strip_p_tag=False, q=None): @@ -134,7 +135,7 @@ def md_to_html(text, strip_p_tag=False, q=None):
134 # --------------------------------------------------------------------------- 135 # ---------------------------------------------------------------------------
135 # load data from yaml file 136 # load data from yaml file
136 # --------------------------------------------------------------------------- 137 # ---------------------------------------------------------------------------
137 -def load_yaml(filename, default=None): 138 +def load_yaml(filename: str, default: Any = None) -> Any:
138 filename = path.expanduser(filename) 139 filename = path.expanduser(filename)
139 try: 140 try:
140 f = open(filename, 'r', encoding='utf-8') 141 f = open(filename, 'r', encoding='utf-8')
@@ -149,9 +150,12 @@ def load_yaml(filename, default=None): @@ -149,9 +150,12 @@ def load_yaml(filename, default=None):
149 try: 150 try:
150 default = yaml.safe_load(f) 151 default = yaml.safe_load(f)
151 except yaml.YAMLError as e: 152 except yaml.YAMLError as e:
152 - mark = e.problem_mark  
153 - logger.error(f'In file "{filename}" near line {mark.line}, '  
154 - f'column {mark.column+1}') 153 + if hasattr(e, 'problem_mark'):
  154 + mark = e.problem_mark
  155 + logger.error(f'File "{filename}" near line {mark.line}, '
  156 + f'column {mark.column+1}')
  157 + else:
  158 + logger.error(f'File "{filename}"')
155 finally: 159 finally:
156 return default 160 return default
157 161
@@ -161,7 +165,11 @@ def load_yaml(filename, default=None): @@ -161,7 +165,11 @@ def load_yaml(filename, default=None):
161 # The script is run in another process but this function blocks waiting 165 # The script is run in another process but this function blocks waiting
162 # for its termination. 166 # for its termination.
163 # --------------------------------------------------------------------------- 167 # ---------------------------------------------------------------------------
164 -def run_script(script, args=[], stdin='', timeout=5): 168 +def run_script(script: str,
  169 + args: List[str] = [],
  170 + stdin: str = '',
  171 + timeout: int = 5) -> Any:
  172 +
165 script = path.expanduser(script) 173 script = path.expanduser(script)
166 try: 174 try:
167 cmd = [script] + [str(a) for a in args] 175 cmd = [script] + [str(a) for a in args]
mypy.ini 0 → 100644
@@ -0,0 +1,18 @@ @@ -0,0 +1,18 @@
  1 +[mypy-sqlalchemy.*]
  2 +ignore_missing_imports = True
  3 +
  4 +[mypy-pygments.*]
  5 +ignore_missing_imports = True
  6 +
  7 +[mypy-networkx.*]
  8 +ignore_missing_imports = True
  9 +
  10 +[mypy-bcrypt.*]
  11 +ignore_missing_imports = True
  12 +
  13 +[mypy-mistune.*]
  14 +ignore_missing_imports = True
  15 +
  16 +[mypy-setuptools.*]
  17 +ignore_missing_imports = True
  18 +