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