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
1 [mypy] 1 [mypy]
2 -python_version = 3.7 2 +python_version = 3.8
3 # warn_return_any = True 3 # warn_return_any = True
4 # warn_unused_configs = True 4 # warn_unused_configs = True
5 5
6 -[mypy-sqlalchemy.*] 6 +[mypy-setuptools.*]
7 ignore_missing_imports = True 7 ignore_missing_imports = True
8 8
9 -[mypy-pygments.*] 9 +[mypy-sqlalchemy.*]
10 ignore_missing_imports = True 10 ignore_missing_imports = True
11 11
12 -[mypy-bcrypt.*] 12 +[mypy-pygments.*]
13 ignore_missing_imports = True 13 ignore_missing_imports = True
14 14
15 [mypy-mistune.*] 15 [mypy-mistune.*]
perguntations/app.py
@@ -5,7 +5,7 @@ Main application module @@ -5,7 +5,7 @@ Main application module
5 5
6 # python standard libraries 6 # python standard libraries
7 import asyncio 7 import asyncio
8 -from contextlib import contextmanager # `with` statement in db sessions 8 +# from contextlib import contextmanager # `with` statement in db sessions
9 import csv 9 import csv
10 import io 10 import io
11 import json 11 import json
@@ -14,8 +14,10 @@ from os import path @@ -14,8 +14,10 @@ from os import path
14 14
15 # installed packages 15 # installed packages
16 import bcrypt 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 # this project 22 # this project
21 from perguntations.models import Student, Test, Question 23 from perguntations.models import Student, Test, Question
@@ -24,6 +26,7 @@ from perguntations.testfactory import TestFactory, TestFactoryException @@ -24,6 +26,7 @@ from perguntations.testfactory import TestFactory, TestFactoryException
24 import perguntations.test 26 import perguntations.test
25 from perguntations.questions import question_from 27 from perguntations.questions import question_from
26 28
  29 +# setup logger for this module
27 logger = logging.getLogger(__name__) 30 logger = logging.getLogger(__name__)
28 31
29 32
@@ -35,20 +38,20 @@ class AppException(Exception): @@ -35,20 +38,20 @@ class AppException(Exception):
35 # ============================================================================ 38 # ============================================================================
36 # helper functions 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,23 +70,23 @@ class App():
67 self.testfactory - TestFactory 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 def __init__(self, conf): 92 def __init__(self, conf):
@@ -94,18 +97,7 @@ class App(): @@ -94,18 +97,7 @@ class App():
94 self.pregenerated_tests = [] # list of tests to give to students 97 self.pregenerated_tests = [] # list of tests to give to students
95 98
96 self._make_test_factory(conf) 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 # command line option --allow-all 102 # command line option --allow-all
111 if conf['allow_all']: 103 if conf['allow_all']:
@@ -126,6 +118,31 @@ class App(): @@ -126,6 +118,31 @@ class App():
126 if conf['correct']: 118 if conf['correct']:
127 self._correct_tests() 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 def _correct_tests(self): 147 def _correct_tests(self):
131 with self._db_session() as sess: 148 with self._db_session() as sess:
perguntations/initdb.py
@@ -13,7 +13,9 @@ from concurrent.futures import ThreadPoolExecutor @@ -13,7 +13,9 @@ from concurrent.futures import ThreadPoolExecutor
13 13
14 # installed packages 14 # installed packages
15 import bcrypt 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 # this project 20 # this project
19 from perguntations.models import Base, Student 21 from perguntations.models import Base, Student
@@ -22,6 +24,7 @@ from perguntations.models import Base, Student @@ -22,6 +24,7 @@ from perguntations.models import Base, Student
22 # ============================================================================ 24 # ============================================================================
23 def parse_commandline_arguments(): 25 def parse_commandline_arguments():
24 '''Parse command line options''' 26 '''Parse command line options'''
  27 +
25 parser = argparse.ArgumentParser( 28 parser = argparse.ArgumentParser(
26 formatter_class=argparse.ArgumentDefaultsHelpFormatter, 29 formatter_class=argparse.ArgumentDefaultsHelpFormatter,
27 description='Insert new users into a database. Users can be imported ' 30 description='Insert new users into a database. Users can be imported '
@@ -102,7 +105,7 @@ def get_students_from_csv(filename): @@ -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 '''replace password by hash for a single student''' 109 '''replace password by hash for a single student'''
107 print('.', end='', flush=True) 110 print('.', end='', flush=True)
108 if password is None: 111 if password is None:
@@ -113,14 +116,14 @@ def hashpw(student, password=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 '''insert list of students into the database''' 120 '''insert list of students into the database'''
118 try: 121 try:
119 session.add_all([Student(id=s['uid'], name=s['name'], password=s['pw']) 122 session.add_all([Student(id=s['uid'], name=s['name'], password=s['pw'])
120 for s in students]) 123 for s in students])
121 session.commit() 124 session.commit()
122 125
123 - except sa.exc.IntegrityError: 126 + except IntegrityError:
124 print('!!! Integrity error. Users already in database. Aborted !!!\n') 127 print('!!! Integrity error. Users already in database. Aborted !!!\n')
125 session.rollback() 128 session.rollback()
126 129
@@ -128,11 +131,12 @@ def insert_students_into_db(session, students): @@ -128,11 +131,12 @@ def insert_students_into_db(session, students):
128 # ============================================================================ 131 # ============================================================================
129 def show_students_in_database(session, verbose=False): 132 def show_students_in_database(session, verbose=False):
130 '''get students from database''' 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 print('Registered users:') 138 print('Registered users:')
135 - if total_users == 0: 139 + if total == 0:
136 print(' -- none --') 140 print(' -- none --')
137 else: 141 else:
138 users.sort(key=lambda u: f'{u.id:>12}') # sort by number 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,13 +145,13 @@ def show_students_in_database(session, verbose=False):
141 print(f'{user.id:>12} {user.name}') 145 print(f'{user.id:>12} {user.name}')
142 else: 146 else:
143 print(f'{users[0].id:>12} {users[0].name}') 147 print(f'{users[0].id:>12} {users[0].name}')
144 - if total_users > 1: 148 + if total > 1:
145 print(f'{users[1].id:>12} {users[1].name}') 149 print(f'{users[1].id:>12} {users[1].name}')
146 - if total_users > 3: 150 + if total > 3:
147 print(' | |') 151 print(' | |')
148 - if total_users > 2: 152 + if total > 2:
149 print(f'{users[-1].id:>12} {users[-1].name}') 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,39 +161,41 @@ def main():
157 args = parse_commandline_arguments() 161 args = parse_commandline_arguments()
158 162
159 # --- database 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 Base.metadata.create_all(engine) # Criates schema if needed 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 # --- make list of students to insert 169 # --- make list of students to insert
167 - new_students = [] 170 + students = []
168 171
169 if args.admin: 172 if args.admin:
170 print('Adding user: 0, Admin.') 173 print('Adding user: 0, Admin.')
171 - new_students.append({'uid': '0', 'name': 'Admin'}) 174 + students.append({'uid': '0', 'name': 'Admin'})
172 175
173 for csvfile in args.csvfile: 176 for csvfile in args.csvfile:
174 print('Adding users from:', csvfile) 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 if args.add: 180 if args.add:
178 for uid, name in args.add: 181 for uid, name in args.add:
179 print(f'Adding user: {uid}, {name}.') 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 # --- insert new students 185 # --- insert new students
183 - if new_students: 186 + if students:
184 print('Generating password hashes', end='') 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 # --- update all students 193 # --- update all students
191 if args.update_all: 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 print(f'Updating password of {len(all_students)} users', end='') 199 print(f'Updating password of {len(all_students)} users', end='')
194 for student in all_students: 200 for student in all_students:
195 password = (args.pw or student.id).encode('utf-8') 201 password = (args.pw or student.id).encode('utf-8')
@@ -202,7 +208,7 @@ def main(): @@ -202,7 +208,7 @@ def main():
202 else: 208 else:
203 for student_id in args.update: 209 for student_id in args.update:
204 print(f'Updating password of {student_id}') 210 print(f'Updating password of {student_id}')
205 - student = session.query(Student).get(student_id) 211 + student = session.execute(select(Student.id))
206 password = (args.pw or student_id).encode('utf-8') 212 password = (args.pw or student_id).encode('utf-8')
207 student.password = bcrypt.hashpw(password, bcrypt.gensalt()) 213 student.password = bcrypt.hashpw(password, bcrypt.gensalt())
208 session.commit() 214 session.commit()
perguntations/models.py
1 ''' 1 '''
  2 +perguntations/models.py
2 SQLAlchemy ORM 3 SQLAlchemy ORM
3 -  
4 -The classes below correspond to database tables  
5 ''' 4 '''
6 5
7 6
8 from sqlalchemy import Column, ForeignKey, Integer, Float, String 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 # Declare ORM 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,11 +27,11 @@ class Student(Base):
26 # --- 27 # ---
27 tests = relationship('Test', back_populates='student') 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,18 +53,18 @@ class Test(Base):
52 student = relationship('Student', back_populates='tests') 53 student = relationship('Student', back_populates='tests')
53 questions = relationship('Question', back_populates='test') 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,13 +83,13 @@ class Question(Base):
82 # --- 83 # ---
83 test = relationship('Test', back_populates='questions') 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,24 +14,15 @@ import re
13 from typing import Any, Dict, NewType 14 from typing import Any, Dict, NewType
14 import uuid 15 import uuid
15 16
16 -  
17 -# from urllib.error import HTTPError  
18 -# import json  
19 -# import http.client  
20 -  
21 -  
22 # this project 17 # this project
23 from perguntations.tools import run_script, run_script_async 18 from perguntations.tools import run_script, run_script_async
24 19
25 # setup logger for this module 20 # setup logger for this module
26 logger = logging.getLogger(__name__) 21 logger = logging.getLogger(__name__)
27 22
28 -  
29 QDict = NewType('QDict', Dict[str, Any]) 23 QDict = NewType('QDict', Dict[str, Any])
30 24
31 25
32 -  
33 -  
34 class QuestionException(Exception): 26 class QuestionException(Exception):
35 '''Exceptions raised in this module''' 27 '''Exceptions raised in this module'''
36 28
@@ -45,8 +37,6 @@ class Question(dict): @@ -45,8 +37,6 @@ class Question(dict):
45 for each student. 37 for each student.
46 Instances can shuffle options or automatically generate questions. 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 def gen(self) -> None: 41 def gen(self) -> None:
52 ''' 42 '''
@@ -96,9 +86,6 @@ class QuestionRadio(Question): @@ -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 def gen(self) -> None: 89 def gen(self) -> None:
103 ''' 90 '''
104 Sets defaults, performs checks and generates the actual question 91 Sets defaults, performs checks and generates the actual question
@@ -231,9 +218,6 @@ class QuestionCheckbox(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 def gen(self) -> None: 221 def gen(self) -> None:
238 super().gen() 222 super().gen()
239 223
@@ -288,19 +272,6 @@ class QuestionCheckbox(Question): @@ -288,19 +272,6 @@ class QuestionCheckbox(Question):
288 f'Please fix "{self["ref"]}" in "{self["path"]}"') 272 f'Please fix "{self["ref"]}" in "{self["path"]}"')
289 logger.error(msg) 273 logger.error(msg)
290 raise QuestionException(msg) 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 # if an option is a list of (right, wrong), pick one 276 # if an option is a list of (right, wrong), pick one
306 options = [] 277 options = []
@@ -356,9 +327,6 @@ class QuestionText(Question): @@ -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 def gen(self) -> None: 330 def gen(self) -> None:
363 super().gen() 331 super().gen()
364 self.set_defaults(QDict({ 332 self.set_defaults(QDict({
@@ -389,6 +357,7 @@ class QuestionText(Question): @@ -389,6 +357,7 @@ class QuestionText(Question):
389 def transform(self, ans): 357 def transform(self, ans):
390 '''apply optional filters to the answer''' 358 '''apply optional filters to the answer'''
391 359
  360 + # apply transformations in sequence
392 for transform in self['transform']: 361 for transform in self['transform']:
393 if transform == 'remove_space': # removes all spaces 362 if transform == 'remove_space': # removes all spaces
394 ans = ans.replace(' ', '') 363 ans = ans.replace(' ', '')
@@ -410,7 +379,7 @@ class QuestionText(Question): @@ -410,7 +379,7 @@ class QuestionText(Question):
410 super().correct() 379 super().correct()
411 380
412 if self['answer'] is not None: 381 if self['answer'] is not None:
413 - answer = self.transform(self['answer']) # apply transformations 382 + answer = self.transform(self['answer'])
414 self['grade'] = 1.0 if answer in self['correct'] else 0.0 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,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 def gen(self) -> None: 399 def gen(self) -> None:
434 super().gen() 400 super().gen()
435 401
@@ -442,14 +408,6 @@ class QuestionTextRegex(Question): @@ -442,14 +408,6 @@ class QuestionTextRegex(Question):
442 if not isinstance(self['correct'], list): 408 if not isinstance(self['correct'], list):
443 self['correct'] = [self['correct']] 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 def correct(self) -> None: 412 def correct(self) -> None:
455 super().correct() 413 super().correct()
@@ -464,15 +422,6 @@ class QuestionTextRegex(Question): @@ -464,15 +422,6 @@ class QuestionTextRegex(Question):
464 regex, self['answer']) 422 regex, self['answer'])
465 self['grade'] = 0.0 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 class QuestionNumericInterval(Question): 426 class QuestionNumericInterval(Question):
478 '''An instance of QuestionTextNumeric will always have the keys: 427 '''An instance of QuestionTextNumeric will always have the keys:
@@ -484,9 +433,6 @@ class QuestionNumericInterval(Question): @@ -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 def gen(self) -> None: 436 def gen(self) -> None:
491 super().gen() 437 super().gen()
492 438
@@ -548,9 +494,6 @@ class QuestionTextArea(Question): @@ -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 def gen(self) -> None: 497 def gen(self) -> None:
555 super().gen() 498 super().gen()
556 499
@@ -625,129 +568,6 @@ class QuestionTextArea(Question): @@ -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 class QuestionInformation(Question): 571 class QuestionInformation(Question):
752 ''' 572 '''
753 Not really a question, just an information panel. 573 Not really a question, just an information panel.
@@ -755,9 +575,6 @@ class QuestionInformation(Question): @@ -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 def gen(self) -> None: 578 def gen(self) -> None:
762 super().gen() 579 super().gen()
763 self.set_defaults(QDict({ 580 self.set_defaults(QDict({
@@ -770,7 +587,6 @@ class QuestionInformation(Question): @@ -770,7 +587,6 @@ class QuestionInformation(Question):
770 self['grade'] = 1.0 # always "correct" but points should be zero! 587 self['grade'] = 1.0 # always "correct" but points should be zero!
771 588
772 589
773 -  
774 # ============================================================================ 590 # ============================================================================
775 def question_from(qdict: QDict) -> Question: 591 def question_from(qdict: QDict) -> Question:
776 ''' 592 '''
@@ -783,7 +599,6 @@ def question_from(qdict: QDict) -> Question: @@ -783,7 +599,6 @@ def question_from(qdict: QDict) -> Question:
783 'text-regex': QuestionTextRegex, 599 'text-regex': QuestionTextRegex,
784 'numeric-interval': QuestionNumericInterval, 600 'numeric-interval': QuestionNumericInterval,
785 'textarea': QuestionTextArea, 601 'textarea': QuestionTextArea,
786 - # 'code': QuestionCode,  
787 # -- informative panels -- 602 # -- informative panels --
788 'information': QuestionInformation, 603 'information': QuestionInformation,
789 'success': QuestionInformation, 604 'success': QuestionInformation,
@@ -856,7 +671,7 @@ class QFactory(): @@ -856,7 +671,7 @@ class QFactory():
856 logger.debug('generating %s...', self.qdict["ref"]) 671 logger.debug('generating %s...', self.qdict["ref"])
857 # Shallow copy so that script generated questions will not replace 672 # Shallow copy so that script generated questions will not replace
858 # the original generators 673 # the original generators
859 - qdict = self.qdict.copy() 674 + qdict = QDict(self.qdict.copy())
860 qdict['qid'] = str(uuid.uuid4()) # unique for each question 675 qdict['qid'] = str(uuid.uuid4()) # unique for each question
861 676
862 # If question is of generator type, an external program will be run 677 # If question is of generator type, an external program will be run
perguntations/testfactory.py
@@ -10,7 +10,7 @@ import re @@ -10,7 +10,7 @@ import re
10 from typing import Any, Dict 10 from typing import Any, Dict
11 11
12 # this project 12 # this project
13 -from perguntations.questions import QFactory, QuestionException 13 +from perguntations.questions import QFactory, QuestionException, QDict
14 from perguntations.test import Test 14 from perguntations.test import Test
15 from perguntations.tools import load_yaml 15 from perguntations.tools import load_yaml
16 16
@@ -99,14 +99,14 @@ class TestFactory(dict): @@ -99,14 +99,14 @@ class TestFactory(dict):
99 if question['ref'] in qrefs: 99 if question['ref'] in qrefs:
100 question.update(zip(('path', 'filename', 'index'), 100 question.update(zip(('path', 'filename', 'index'),
101 path.split(fullpath) + (i,))) 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 qmissing = qrefs.difference(set(self['question_factory'].keys())) 111 qmissing = qrefs.difference(set(self['question_factory'].keys()))
112 if qmissing: 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