Commit f1ad38ab169168b4cd13d950150e9b82abc5fa30

Authored by Miguel Barao
1 parent 5822a2ec
Exists in master and in 1 other branch dev

- update to sqlalchemy-1.4

- fix mypy errors and warnings
- remove unused code
mypy.ini
1 1 [mypy]
2   -python_version = 3.7
  2 +python_version = 3.8
3 3 # warn_return_any = True
4 4 # warn_unused_configs = True
5 5  
6   -[mypy-sqlalchemy.*]
  6 +[mypy-setuptools.*]
7 7 ignore_missing_imports = True
8 8  
9   -[mypy-pygments.*]
  9 +[mypy-sqlalchemy.*]
10 10 ignore_missing_imports = True
11 11  
12   -[mypy-bcrypt.*]
  12 +[mypy-pygments.*]
13 13 ignore_missing_imports = True
14 14  
15 15 [mypy-mistune.*]
... ...
perguntations/app.py
... ... @@ -5,7 +5,7 @@ Main application module
5 5  
6 6 # python standard libraries
7 7 import asyncio
8   -from contextlib import contextmanager # `with` statement in db sessions
  8 +# from contextlib import contextmanager # `with` statement in db sessions
9 9 import csv
10 10 import io
11 11 import json
... ... @@ -14,8 +14,10 @@ from os import path
14 14  
15 15 # installed packages
16 16 import bcrypt
17   -from sqlalchemy import create_engine, exc
18   -from sqlalchemy.orm import sessionmaker
  17 +from sqlalchemy import create_engine, select, func
  18 +from sqlalchemy.orm import Session
  19 +from sqlalchemy.exc import NoResultFound
  20 +# from sqlalchemy.orm import sessionmaker
19 21  
20 22 # this project
21 23 from perguntations.models import Student, Test, Question
... ... @@ -24,6 +26,7 @@ from perguntations.testfactory import TestFactory, TestFactoryException
24 26 import perguntations.test
25 27 from perguntations.questions import question_from
26 28  
  29 +# setup logger for this module
27 30 logger = logging.getLogger(__name__)
28 31  
29 32  
... ... @@ -35,20 +38,20 @@ class AppException(Exception):
35 38 # ============================================================================
36 39 # helper functions
37 40 # ============================================================================
38   -async def check_password(try_pw, hashed_pw):
39   - '''check password in executor'''
40   - try_pw = try_pw.encode('utf-8')
41   - loop = asyncio.get_running_loop()
42   - hashed = await loop.run_in_executor(None, bcrypt.hashpw, try_pw, hashed_pw)
43   - return hashed_pw == hashed
  41 +# async def check_password(try_pw, hashed_pw):
  42 +# '''check password in executor'''
  43 +# try_pw = try_pw.encode('utf-8')
  44 +# loop = asyncio.get_running_loop()
  45 +# hashed = await loop.run_in_executor(None, bcrypt.hashpw, try_pw, hashed_pw)
  46 +# return hashed_pw == hashed
44 47  
45 48  
46   -async def hash_password(password):
47   - '''hash password in executor'''
48   - loop = asyncio.get_running_loop()
49   - return await loop.run_in_executor(None, bcrypt.hashpw,
50   - password.encode('utf-8'),
51   - bcrypt.gensalt())
  49 +# async def hash_password(password):
  50 +# '''hash password in executor'''
  51 +# loop = asyncio.get_running_loop()
  52 +# return await loop.run_in_executor(None, bcrypt.hashpw,
  53 +# password.encode('utf-8'),
  54 +# bcrypt.gensalt())
52 55  
53 56  
54 57 # ============================================================================
... ... @@ -67,23 +70,23 @@ class App():
67 70 self.testfactory - TestFactory
68 71 '''
69 72  
70   - # ------------------------------------------------------------------------
71   - @contextmanager
72   - def _db_session(self):
73   - '''
74   - helper to manage db sessions using the `with` statement, for example:
75   - with self._db_session() as s: s.query(...)
76   - '''
77   - session = self.Session()
78   - try:
79   - yield session
80   - session.commit()
81   - except exc.SQLAlchemyError:
82   - logger.error('DB rollback!!!')
83   - session.rollback()
84   - raise
85   - finally:
86   - session.close()
  73 + # # ------------------------------------------------------------------------
  74 + # @contextmanager
  75 + # def _db_session(self):
  76 + # '''
  77 + # helper to manage db sessions using the `with` statement, for example:
  78 + # with self._db_session() as s: s.query(...)
  79 + # '''
  80 + # session = self.Session()
  81 + # try:
  82 + # yield session
  83 + # session.commit()
  84 + # except exc.SQLAlchemyError:
  85 + # logger.error('DB rollback!!!')
  86 + # session.rollback()
  87 + # raise
  88 + # finally:
  89 + # session.close()
87 90  
88 91 # ------------------------------------------------------------------------
89 92 def __init__(self, conf):
... ... @@ -94,18 +97,7 @@ class App():
94 97 self.pregenerated_tests = [] # list of tests to give to students
95 98  
96 99 self._make_test_factory(conf)
97   -
98   - # connect to database and check registered students
99   - dbfile = self.testfactory['database']
100   - database = f'sqlite:///{path.expanduser(dbfile)}'
101   - engine = create_engine(database, echo=False)
102   - self.Session = sessionmaker(bind=engine)
103   - try:
104   - with self._db_session() as sess:
105   - num = sess.query(Student).filter(Student.id != '0').count()
106   - except Exception as exc:
107   - raise AppException(f'Database unusable {dbfile}.') from exc
108   - logger.info('Database "%s" has %s students.', dbfile, num)
  100 + self._db_setup()
109 101  
110 102 # command line option --allow-all
111 103 if conf['allow_all']:
... ... @@ -126,6 +118,31 @@ class App():
126 118 if conf['correct']:
127 119 self._correct_tests()
128 120  
  121 + def _db_setup(self) -> None:
  122 + logger.info('Setup database')
  123 +
  124 + # connect to database and check registered students
  125 + dbfile = path.expanduser(self.testfactory['database'])
  126 + if not path.exists(dbfile):
  127 + raise AppException('Database does not exist.')
  128 + self._engine = create_engine(f'sqlite:///{dbfile}', future=True)
  129 +
  130 + try:
  131 + query = select(func.count(Student.id)).where(Student.id != '0')
  132 + with Session(self._engine, future=True) as session:
  133 + num = session.execute(query).scalar()
  134 + except Exception as exc:
  135 + raise AppException(f'Database unusable {dbfile}.') from exc
  136 +
  137 + # try:
  138 + # with self._db_session() as sess:
  139 + # num = sess.query(Student).filter(Student.id != '0').count()
  140 + # except Exception as exc:
  141 + # raise AppException(f'Database unusable {dbfile}.') from exc
  142 + logger.info('Database "%s" has %s students.', dbfile, num)
  143 +
  144 +
  145 +
129 146 # ------------------------------------------------------------------------
130 147 def _correct_tests(self):
131 148 with self._db_session() as sess:
... ...
perguntations/initdb.py
... ... @@ -13,7 +13,9 @@ from concurrent.futures import ThreadPoolExecutor
13 13  
14 14 # installed packages
15 15 import bcrypt
16   -import sqlalchemy as sa
  16 +from sqlalchemy import create_engine, select
  17 +from sqlalchemy.orm import Session
  18 +from sqlalchemy.exc import IntegrityError
17 19  
18 20 # this project
19 21 from perguntations.models import Base, Student
... ... @@ -22,6 +24,7 @@ from perguntations.models import Base, Student
22 24 # ============================================================================
23 25 def parse_commandline_arguments():
24 26 '''Parse command line options'''
  27 +
25 28 parser = argparse.ArgumentParser(
26 29 formatter_class=argparse.ArgumentDefaultsHelpFormatter,
27 30 description='Insert new users into a database. Users can be imported '
... ... @@ -102,7 +105,7 @@ def get_students_from_csv(filename):
102 105  
103 106  
104 107 # ============================================================================
105   -def hashpw(student, password=None):
  108 +def hashpw(student, password=None) -> None:
106 109 '''replace password by hash for a single student'''
107 110 print('.', end='', flush=True)
108 111 if password is None:
... ... @@ -113,14 +116,14 @@ def hashpw(student, password=None):
113 116  
114 117  
115 118 # ============================================================================
116   -def insert_students_into_db(session, students):
  119 +def insert_students_into_db(session, students) -> None:
117 120 '''insert list of students into the database'''
118 121 try:
119 122 session.add_all([Student(id=s['uid'], name=s['name'], password=s['pw'])
120 123 for s in students])
121 124 session.commit()
122 125  
123   - except sa.exc.IntegrityError:
  126 + except IntegrityError:
124 127 print('!!! Integrity error. Users already in database. Aborted !!!\n')
125 128 session.rollback()
126 129  
... ... @@ -128,11 +131,12 @@ def insert_students_into_db(session, students):
128 131 # ============================================================================
129 132 def show_students_in_database(session, verbose=False):
130 133 '''get students from database'''
131   - users = session.query(Student).all()
  134 + users = session.execute(select(Student)).scalars().all()
  135 + # users = session.query(Student).all()
  136 + total = len(users)
132 137  
133   - total_users = len(users)
134 138 print('Registered users:')
135   - if total_users == 0:
  139 + if total == 0:
136 140 print(' -- none --')
137 141 else:
138 142 users.sort(key=lambda u: f'{u.id:>12}') # sort by number
... ... @@ -141,13 +145,13 @@ def show_students_in_database(session, verbose=False):
141 145 print(f'{user.id:>12} {user.name}')
142 146 else:
143 147 print(f'{users[0].id:>12} {users[0].name}')
144   - if total_users > 1:
  148 + if total > 1:
145 149 print(f'{users[1].id:>12} {users[1].name}')
146   - if total_users > 3:
  150 + if total > 3:
147 151 print(' | |')
148   - if total_users > 2:
  152 + if total > 2:
149 153 print(f'{users[-1].id:>12} {users[-1].name}')
150   - print(f'Total: {total_users}.')
  154 + print(f'Total: {total}.')
151 155  
152 156  
153 157 # ============================================================================
... ... @@ -157,39 +161,41 @@ def main():
157 161 args = parse_commandline_arguments()
158 162  
159 163 # --- database
160   - print(f'Using database: {args.db}')
161   - engine = sa.create_engine(f'sqlite:///{args.db}', echo=False)
  164 + print(f'Database: {args.db}')
  165 + engine = create_engine(f'sqlite:///{args.db}', echo=False, future=True)
162 166 Base.metadata.create_all(engine) # Criates schema if needed
163   - SessionMaker = sa.orm.sessionmaker(bind=engine)
164   - session = SessionMaker()
  167 + session = Session(engine, future=True)
165 168  
166 169 # --- make list of students to insert
167   - new_students = []
  170 + students = []
168 171  
169 172 if args.admin:
170 173 print('Adding user: 0, Admin.')
171   - new_students.append({'uid': '0', 'name': 'Admin'})
  174 + students.append({'uid': '0', 'name': 'Admin'})
172 175  
173 176 for csvfile in args.csvfile:
174 177 print('Adding users from:', csvfile)
175   - new_students.extend(get_students_from_csv(csvfile))
  178 + students.extend(get_students_from_csv(csvfile))
176 179  
177 180 if args.add:
178 181 for uid, name in args.add:
179 182 print(f'Adding user: {uid}, {name}.')
180   - new_students.append({'uid': uid, 'name': name})
  183 + students.append({'uid': uid, 'name': name})
181 184  
182 185 # --- insert new students
183   - if new_students:
  186 + if students:
184 187 print('Generating password hashes', end='')
185   - with ThreadPoolExecutor() as executor: # hashing in parallel
186   - executor.map(lambda s: hashpw(s, args.pw), new_students)
187   - print(f'\nInserting {len(new_students)}')
188   - insert_students_into_db(session, new_students)
  188 + with ThreadPoolExecutor() as executor: # hashing
  189 + executor.map(lambda s: hashpw(s, args.pw), students)
  190 + print(f'\nInserting {len(students)}')
  191 + insert_students_into_db(session, students)
189 192  
190 193 # --- update all students
191 194 if args.update_all:
192   - all_students = session.query(Student).filter(Student.id != '0').all()
  195 + all_students = session.execute(
  196 + select(Student).where(Student.id != '0')
  197 + ).all()
  198 +
193 199 print(f'Updating password of {len(all_students)} users', end='')
194 200 for student in all_students:
195 201 password = (args.pw or student.id).encode('utf-8')
... ... @@ -202,7 +208,7 @@ def main():
202 208 else:
203 209 for student_id in args.update:
204 210 print(f'Updating password of {student_id}')
205   - student = session.query(Student).get(student_id)
  211 + student = session.execute(select(Student.id))
206 212 password = (args.pw or student_id).encode('utf-8')
207 213 student.password = bcrypt.hashpw(password, bcrypt.gensalt())
208 214 session.commit()
... ...
perguntations/models.py
1 1 '''
  2 +perguntations/models.py
2 3 SQLAlchemy ORM
3   -
4   -The classes below correspond to database tables
5 4 '''
6 5  
7 6  
8 7 from sqlalchemy import Column, ForeignKey, Integer, Float, String
9   -from sqlalchemy.ext.declarative import declarative_base
10   -from sqlalchemy.orm import relationship
  8 +from sqlalchemy.orm import declarative_base, relationship
11 9  
12 10  
13 11 # ============================================================================
14 12 # Declare ORM
15   -Base = declarative_base()
  13 +# FIXME Any is a workaround for mypy static type checking (see https://github.com/python/mypy/issues/6372)
  14 +from typing import Any
  15 +Base: Any = declarative_base()
  16 +# Base = declarative_base()
16 17  
17 18  
18 19 # ----------------------------------------------------------------------------
... ... @@ -26,11 +27,11 @@ class Student(Base):
26 27 # ---
27 28 tests = relationship('Test', back_populates='student')
28 29  
29   - def __str__(self):
30   - return (f'Student:\n'
31   - f' id: "{self.id}"\n'
32   - f' name: "{self.name}"\n'
33   - f' password: "{self.password}"\n')
  30 + def __repr__(self):
  31 + return (f'Student('
  32 + f'id={self.id!r}, '
  33 + f'name={self.name!r}, '
  34 + f'password={self.password!r})')
34 35  
35 36  
36 37 # ----------------------------------------------------------------------------
... ... @@ -52,18 +53,18 @@ class Test(Base):
52 53 student = relationship('Student', back_populates='tests')
53 54 questions = relationship('Question', back_populates='test')
54 55  
55   - def __str__(self):
56   - return (f'Test:\n'
57   - f' id: {self.id}\n'
58   - f' ref: "{self.ref}"\n'
59   - f' title: "{self.title}"\n'
60   - f' grade: {self.grade}\n'
61   - f' state: "{self.state}"\n'
62   - f' comment: "{self.comment}"\n'
63   - f' starttime: "{self.starttime}"\n'
64   - f' finishtime: "{self.finishtime}"\n'
65   - f' filename: "{self.filename}"\n'
66   - f' student_id: "{self.student_id}"\n')
  56 + def __repr__(self):
  57 + return (f'Test('
  58 + f'id={self.id!r}, '
  59 + f'ref={self.ref!r}, '
  60 + f'title={self.title!r}, '
  61 + f'grade={self.grade!r}, '
  62 + f'state={self.state!r}, '
  63 + f'comment={self.comment!r}, '
  64 + f'starttime={self.starttime!r}, '
  65 + f'finishtime={self.finishtime!r}, '
  66 + f'filename={self.filename!r}, '
  67 + f'student_id={self.student_id!r})')
67 68  
68 69  
69 70 # ---------------------------------------------------------------------------
... ... @@ -82,13 +83,13 @@ class Question(Base):
82 83 # ---
83 84 test = relationship('Test', back_populates='questions')
84 85  
85   - def __str__(self):
86   - return (f'Question:\n'
87   - f' id: {self.id}\n'
88   - f' number: {self.number}\n'
89   - f' ref: "{self.ref}"\n'
90   - f' grade: {self.grade}\n'
91   - f' comment: "{self.comment}"\n'
92   - f' starttime: "{self.starttime}"\n'
93   - f' finishtime: "{self.finishtime}"\n'
94   - f' test_id: "{self.test_id}"\n')
  86 + def __repr__(self):
  87 + return (f'Question('
  88 + f'id={self.id!r}, '
  89 + f'number={self.number!r}, '
  90 + f'ref={self.ref!r}, '
  91 + f'grade={self.grade!r}, '
  92 + f'comment={self.comment!r}, '
  93 + f'starttime={self.starttime!r}, '
  94 + f'finishtime={self.finishtime!r}, '
  95 + f'test_id={self.test_id!r})')
... ...
perguntations/questions.py
1 1 '''
2   -Classes the implement several types of questions.
  2 +File: perguntations/questions.py
  3 +Description: Classes the implement several types of questions.
3 4 '''
4 5  
5 6  
... ... @@ -13,24 +14,15 @@ import re
13 14 from typing import Any, Dict, NewType
14 15 import uuid
15 16  
16   -
17   -# from urllib.error import HTTPError
18   -# import json
19   -# import http.client
20   -
21   -
22 17 # this project
23 18 from perguntations.tools import run_script, run_script_async
24 19  
25 20 # setup logger for this module
26 21 logger = logging.getLogger(__name__)
27 22  
28   -
29 23 QDict = NewType('QDict', Dict[str, Any])
30 24  
31 25  
32   -
33   -
34 26 class QuestionException(Exception):
35 27 '''Exceptions raised in this module'''
36 28  
... ... @@ -45,8 +37,6 @@ class Question(dict):
45 37 for each student.
46 38 Instances can shuffle options or automatically generate questions.
47 39 '''
48   - # def __init__(self, q: QDict) -> None:
49   - # super().__init__(q)
50 40  
51 41 def gen(self) -> None:
52 42 '''
... ... @@ -96,9 +86,6 @@ class QuestionRadio(Question):
96 86 '''
97 87  
98 88 # ------------------------------------------------------------------------
99   - # def __init__(self, q: QDict) -> None:
100   - # super().__init__(q)
101   -
102 89 def gen(self) -> None:
103 90 '''
104 91 Sets defaults, performs checks and generates the actual question
... ... @@ -231,9 +218,6 @@ class QuestionCheckbox(Question):
231 218 '''
232 219  
233 220 # ------------------------------------------------------------------------
234   - # def __init__(self, q: QDict) -> None:
235   - # super().__init__(q)
236   -
237 221 def gen(self) -> None:
238 222 super().gen()
239 223  
... ... @@ -288,19 +272,6 @@ class QuestionCheckbox(Question):
288 272 f'Please fix "{self["ref"]}" in "{self["path"]}"')
289 273 logger.error(msg)
290 274 raise QuestionException(msg)
291   - # msg0 = ('+--------------- BEHAVIOR CHANGE NOTICE ---------------+')
292   - # msg1 = ('| Correct values in checkbox questions must be in the |')
293   - # msg2 = ('| interval [0.0, 1.0]. I will convert to the new |')
294   - # msg3 = ('| behavior, for now, but you should fix it. |')
295   - # msg4 = ('+------------------------------------------------------+')
296   - # logger.warning(msg0)
297   - # logger.warning(msg1)
298   - # logger.warning(msg2)
299   - # logger.warning(msg3)
300   - # logger.warning(msg4)
301   - # logger.warning('please fix "%s"', self["ref"])
302   - # # normalize to [0,1]
303   - # self['correct'] = [(x+1)/2 for x in self['correct']]
304 275  
305 276 # if an option is a list of (right, wrong), pick one
306 277 options = []
... ... @@ -356,9 +327,6 @@ class QuestionText(Question):
356 327 '''
357 328  
358 329 # ------------------------------------------------------------------------
359   - # def __init__(self, q: QDict) -> None:
360   - # super().__init__(q)
361   -
362 330 def gen(self) -> None:
363 331 super().gen()
364 332 self.set_defaults(QDict({
... ... @@ -389,6 +357,7 @@ class QuestionText(Question):
389 357 def transform(self, ans):
390 358 '''apply optional filters to the answer'''
391 359  
  360 + # apply transformations in sequence
392 361 for transform in self['transform']:
393 362 if transform == 'remove_space': # removes all spaces
394 363 ans = ans.replace(' ', '')
... ... @@ -410,7 +379,7 @@ class QuestionText(Question):
410 379 super().correct()
411 380  
412 381 if self['answer'] is not None:
413   - answer = self.transform(self['answer']) # apply transformations
  382 + answer = self.transform(self['answer'])
414 383 self['grade'] = 1.0 if answer in self['correct'] else 0.0
415 384  
416 385  
... ... @@ -427,9 +396,6 @@ class QuestionTextRegex(Question):
427 396 '''
428 397  
429 398 # ------------------------------------------------------------------------
430   - # def __init__(self, q: QDict) -> None:
431   - # super().__init__(q)
432   -
433 399 def gen(self) -> None:
434 400 super().gen()
435 401  
... ... @@ -442,14 +408,6 @@ class QuestionTextRegex(Question):
442 408 if not isinstance(self['correct'], list):
443 409 self['correct'] = [self['correct']]
444 410  
445   - # converts patterns to compiled versions
446   - # try:
447   - # self['correct'] = [re.compile(a) for a in self['correct']]
448   - # except Exception as exc:
449   - # msg = f'Failed to compile regex in "{self["ref"]}"'
450   - # logger.error(msg)
451   - # raise QuestionException(msg) from exc
452   -
453 411 # ------------------------------------------------------------------------
454 412 def correct(self) -> None:
455 413 super().correct()
... ... @@ -464,15 +422,6 @@ class QuestionTextRegex(Question):
464 422 regex, self['answer'])
465 423 self['grade'] = 0.0
466 424  
467   - # try:
468   - # if regex.match(self['answer']):
469   - # self['grade'] = 1.0
470   - # return
471   - # except TypeError:
472   - # logger.error('While matching regex %s with answer "%s".',
473   - # regex.pattern, self["answer"])
474   -
475   -
476 425 # ============================================================================
477 426 class QuestionNumericInterval(Question):
478 427 '''An instance of QuestionTextNumeric will always have the keys:
... ... @@ -484,9 +433,6 @@ class QuestionNumericInterval(Question):
484 433 '''
485 434  
486 435 # ------------------------------------------------------------------------
487   - # def __init__(self, q: QDict) -> None:
488   - # super().__init__(q)
489   -
490 436 def gen(self) -> None:
491 437 super().gen()
492 438  
... ... @@ -548,9 +494,6 @@ class QuestionTextArea(Question):
548 494 '''
549 495  
550 496 # ------------------------------------------------------------------------
551   - # def __init__(self, q: QDict) -> None:
552   - # super().__init__(q)
553   -
554 497 def gen(self) -> None:
555 498 super().gen()
556 499  
... ... @@ -625,129 +568,6 @@ class QuestionTextArea(Question):
625 568  
626 569  
627 570 # ============================================================================
628   -# class QuestionCode(Question):
629   -# '''
630   -# Submits answer to a JOBE server to compile and run against the test cases.
631   -# '''
632   -
633   -# _outcomes = {
634   -# 0: 'JOBE outcome: Successful run',
635   -# 11: 'JOBE outcome: Compile error',
636   -# 12: 'JOBE outcome: Runtime error',
637   -# 13: 'JOBE outcome: Time limit exceeded',
638   -# 15: 'JOBE outcome: Successful run',
639   -# 17: 'JOBE outcome: Memory limit exceeded',
640   -# 19: 'JOBE outcome: Illegal system call',
641   -# 20: 'JOBE outcome: Internal error, please report',
642   -# 21: 'JOBE outcome: Server overload',
643   -# }
644   -
645   -# # ------------------------------------------------------------------------
646   -# def __init__(self, q: QDict) -> None:
647   -# super().__init__(q)
648   -
649   -# self.set_defaults(QDict({
650   -# 'text': '',
651   -# 'timeout': 5, # seconds
652   -# 'server': '127.0.0.1', # JOBE server
653   -# 'language': 'c',
654   -# 'correct': [{'stdin': '', 'stdout': '', 'stderr': '', 'args': ''}],
655   -# }))
656   -
657   - # ------------------------------------------------------------------------
658   - # def correct(self) -> None:
659   - # super().correct()
660   -
661   - # if self['answer'] is None:
662   - # return
663   -
664   - # # submit answer to JOBE server
665   - # resource = '/jobe/index.php/restapi/runs/'
666   - # headers = {"Content-type": "application/json; charset=utf-8",
667   - # "Accept": "application/json"}
668   -
669   - # for expected in self['correct']:
670   - # data_json = json.dumps({
671   - # 'run_spec' : {
672   - # 'language_id': self['language'],
673   - # 'sourcecode': self['answer'],
674   - # 'input': expected.get('stdin', ''),
675   - # },
676   - # })
677   -
678   - # try:
679   - # connect = http.client.HTTPConnection(self['server'])
680   - # connect.request(
681   - # method='POST',
682   - # url=resource,
683   - # body=data_json,
684   - # headers=headers
685   - # )
686   - # response = connect.getresponse()
687   - # logger.debug('JOBE response status %d', response.status)
688   - # if response.status != 204:
689   - # content = response.read().decode('utf8')
690   - # if content:
691   - # result = json.loads(content)
692   - # connect.close()
693   -
694   - # except (HTTPError, ValueError):
695   - # logger.error('HTTPError while connecting to JOBE server')
696   -
697   - # try:
698   - # outcome = result['outcome']
699   - # except (NameError, TypeError, KeyError):
700   - # logger.error('Bad result returned from JOBE server: %s', result)
701   - # return
702   - # logger.debug(self._outcomes[outcome])
703   -
704   -
705   -
706   - # if result['cmpinfo']: # compiler errors and warnings
707   - # self['comments'] = f'Erros de compilação:\n{result["cmpinfo"]}'
708   - # self['grade'] = 0.0
709   - # return
710   -
711   - # if result['stdout'] != expected.get('stdout', ''):
712   - # self['comments'] = 'O output gerado é diferente do esperado.' # FIXME mostrar porque?
713   - # self['grade'] = 0.0
714   - # return
715   -
716   - # self['comments'] = 'Ok!'
717   - # self['grade'] = 1.0
718   -
719   -
720   - # # ------------------------------------------------------------------------
721   - # async def correct_async(self) -> None:
722   - # self.correct() # FIXME there is no async correction!!!
723   -
724   -
725   - # out = run_script(
726   - # script=self['correct'],
727   - # args=self['args'],
728   - # stdin=self['answer'],
729   - # timeout=self['timeout']
730   - # )
731   -
732   - # if out is None:
733   - # logger.warning('No grade after running "%s".', self["correct"])
734   - # self['comments'] = 'O programa de correcção abortou...'
735   - # self['grade'] = 0.0
736   - # elif isinstance(out, dict):
737   - # self['comments'] = out.get('comments', '')
738   - # try:
739   - # self['grade'] = float(out['grade'])
740   - # except ValueError:
741   - # logger.error('Output error in "%s".', self["correct"])
742   - # except KeyError:
743   - # logger.error('No grade in "%s".', self["correct"])
744   - # else:
745   - # try:
746   - # self['grade'] = float(out)
747   - # except (TypeError, ValueError):
748   - # logger.error('Invalid grade in "%s".', self["correct"])
749   -
750   -# ============================================================================
751 571 class QuestionInformation(Question):
752 572 '''
753 573 Not really a question, just an information panel.
... ... @@ -755,9 +575,6 @@ class QuestionInformation(Question):
755 575 '''
756 576  
757 577 # ------------------------------------------------------------------------
758   - # def __init__(self, q: QDict) -> None:
759   - # super().__init__(q)
760   -
761 578 def gen(self) -> None:
762 579 super().gen()
763 580 self.set_defaults(QDict({
... ... @@ -770,7 +587,6 @@ class QuestionInformation(Question):
770 587 self['grade'] = 1.0 # always "correct" but points should be zero!
771 588  
772 589  
773   -
774 590 # ============================================================================
775 591 def question_from(qdict: QDict) -> Question:
776 592 '''
... ... @@ -783,7 +599,6 @@ def question_from(qdict: QDict) -> Question:
783 599 'text-regex': QuestionTextRegex,
784 600 'numeric-interval': QuestionNumericInterval,
785 601 'textarea': QuestionTextArea,
786   - # 'code': QuestionCode,
787 602 # -- informative panels --
788 603 'information': QuestionInformation,
789 604 'success': QuestionInformation,
... ... @@ -856,7 +671,7 @@ class QFactory():
856 671 logger.debug('generating %s...', self.qdict["ref"])
857 672 # Shallow copy so that script generated questions will not replace
858 673 # the original generators
859   - qdict = self.qdict.copy()
  674 + qdict = QDict(self.qdict.copy())
860 675 qdict['qid'] = str(uuid.uuid4()) # unique for each question
861 676  
862 677 # If question is of generator type, an external program will be run
... ...
perguntations/testfactory.py
... ... @@ -10,7 +10,7 @@ import re
10 10 from typing import Any, Dict
11 11  
12 12 # this project
13   -from perguntations.questions import QFactory, QuestionException
  13 +from perguntations.questions import QFactory, QuestionException, QDict
14 14 from perguntations.test import Test
15 15 from perguntations.tools import load_yaml
16 16  
... ... @@ -99,14 +99,14 @@ class TestFactory(dict):
99 99 if question['ref'] in qrefs:
100 100 question.update(zip(('path', 'filename', 'index'),
101 101 path.split(fullpath) + (i,)))
102   - if question['type'] == 'code' and 'server' not in question:
103   - try:
104   - question['server'] = self['jobe_server']
105   - except KeyError as exc:
106   - msg = f'Missing JOBE server in "{question["ref"]}"'
107   - raise TestFactoryException(msg) from exc
108   -
109   - self['question_factory'][question['ref']] = QFactory(question)
  102 + # if question['type'] == 'code' and 'server' not in question:
  103 + # try:
  104 + # question['server'] = self['jobe_server']
  105 + # except KeyError as exc:
  106 + # msg = f'Missing JOBE server in "{question["ref"]}"'
  107 + # raise TestFactoryException(msg) from exc
  108 +
  109 + self['question_factory'][question['ref']] = QFactory(QDict(question))
110 110  
111 111 qmissing = qrefs.difference(set(self['question_factory'].keys()))
112 112 if qmissing:
... ...
perguntations/tools.py
1 1 '''
2   -This module contains helper functions to:
3   -- load yaml files and report errors
4   -- run external programs (sync and async)
  2 +File: perguntations/tools.py
  3 +Description: Helper functions to load yaml files and run external programs.
5 4 '''
6 5  
7 6  
... ...