Commit f1ad38ab169168b4cd13d950150e9b82abc5fa30
1 parent
5822a2ec
Exists in
master
and in
1 other branch
- update to sqlalchemy-1.4
- fix mypy errors and warnings - remove unused code
Showing
7 changed files
with
146 additions
and
308 deletions
Show diff stats
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