Skip to content

Commit

Permalink
[common/tasks_problems] Problem types auto-discovery (#866)
Browse files Browse the repository at this point in the history
Replace hardcoded problem types by auto-discovery functions
  • Loading branch information
nrybowski committed Nov 30, 2022
1 parent 427d4d7 commit 103dc9e
Show file tree
Hide file tree
Showing 7 changed files with 104 additions and 28 deletions.
10 changes: 2 additions & 8 deletions inginious-autotest
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ from inginious.client.client_sync import ClientSync
from inginious.frontend.arch_helper import start_asyncio_and_zmq, create_arch
from yaml import load
from inginious.common.filesystems.local import LocalFSProvider
from inginious.frontend.task_problems import *
from inginious.frontend.task_problems import get_default_displayable_problem_types


def import_class(name):
Expand Down Expand Up @@ -266,13 +266,7 @@ if __name__ == "__main__":
task_dispenser.get_id(): task_dispenser for task_dispenser in [TableOfContents]
}

problem_types = {
problem_type.get_type(): problem_type for problem_type in [DisplayableCodeProblem,
DisplayableCodeSingleLineProblem,
DisplayableFileProblem,
DisplayableMultipleChoiceProblem,
DisplayableMatchProblem]
}
problem_types = get_default_displayable_problem_types()

if args.tdisp:
for tdisp_loc in args.tdisp:
Expand Down
6 changes: 2 additions & 4 deletions inginious/common/babel.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
""" Babel extractors for INGInious files """

from inginious.common import custom_yaml
from inginious.common.tasks_problems import CodeProblem, CodeSingleLineProblem, MultipleChoiceProblem, MatchProblem, FileProblem
from inginious.common.tasks_problems import get_default_problem_types

def import_class(name):
m = name.split('.')
Expand Down Expand Up @@ -38,9 +38,7 @@ def get_strings(content, fields):


def extract_yaml(fileobj, keywords, comment_tags, options):
task_problem_types = {"code": CodeProblem, "code_single_line": CodeSingleLineProblem,
"file": FileProblem, "multiple_choice": MultipleChoiceProblem,
"match": MatchProblem}
task_problem_types = get_default_problem_types()

problems = options["problems"].split() if "problems" in options else []
for problem in problems:
Expand Down
49 changes: 49 additions & 0 deletions inginious/common/tasks_problems.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,66 @@
# more information about the licensing of this file.

""" Tasks' problems """
import importlib
import gettext
import inspect
import sys
import re
from abc import ABCMeta, abstractmethod

from inginious.common.base import id_checker


def _get_problem_types(name: str, base_class) -> dict:
""" Generic function to get a mapping of Problem names and their associated class by
inspecting a given module.
:param name: The name of the module to explore.
:param base_class: The parent Problem class.
:return: The mapping of problem name and problem class.
"""
try:
""" Get the module by name """
myself = sys.modules[name]
except KeyError:
try:
""" If the module is not loaded, we try to load it """
myself = importlib.import_module(name)
except ModuleNotFoundError:
""" There is nothing much to do """
return None

""" Search for child classes of `base_class` """
members = [member for (_, member) in inspect.getmembers(myself, inspect.isclass)
if base_class in inspect.getmro(member) and member != base_class]

""" Return the mapping """
return {member.get_type(): member for member in members}

def get_problem_types(name: str) -> dict:
""" Get the mapping of Problem types available by inspecting a given module.
:param name: The name of the module to inspect.
:return: The mapping of problem name and problem class.
"""
raw = _get_problem_types(name, Problem)
return {pbl_name: pbl_cls for pbl_name, pbl_cls in raw.items() if pbl_name is not None}

def get_default_problem_types() -> dict:
""" Get the mapping of default Problem types available by inspecting the current module.
:return: The mapping of problem name and problem class.
"""
return get_problem_types(__name__)


class Problem(object, metaclass=ABCMeta):
"""Basic problem """

@classmethod
def get_problem_type(cls):
return (cls.get_type(), cls)

@classmethod
@abstractmethod
def get_type(cls):
Expand Down
11 changes: 3 additions & 8 deletions inginious/frontend/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import flask
import pymongo
import oauthlib
import gettext

from gridfs import GridFS
from binascii import hexlify
Expand All @@ -32,7 +33,7 @@
from inginious.common.entrypoints import filesystem_from_config_dict
from inginious.common.filesystems.local import LocalFSProvider
from inginious.frontend.lti_outcome_manager import LTIOutcomeManager
from inginious.frontend.task_problems import *
from inginious.frontend.task_problems import get_default_displayable_problem_types
from inginious.frontend.task_dispensers.toc import TableOfContents
from inginious.frontend.task_dispensers.combinatory_test import CombinatoryTest
from inginious.frontend.flask.mapping import init_flask_mapping, init_flask_maintenance_mapping
Expand Down Expand Up @@ -214,13 +215,7 @@ def get_app(config):
task_dispenser.get_id(): task_dispenser for task_dispenser in [TableOfContents, CombinatoryTest]
}

default_problem_types = {
problem_type.get_type(): problem_type for problem_type in [DisplayableCodeProblem,
DisplayableCodeSingleLineProblem,
DisplayableFileProblem,
DisplayableMultipleChoiceProblem,
DisplayableMatchProblem]
}
default_problem_types = get_default_displayable_problem_types()

course_factory, task_factory = create_factories(fs_provider, default_task_dispensers, default_problem_types, plugin_manager, database)

Expand Down
20 changes: 17 additions & 3 deletions inginious/frontend/plugin_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,12 @@
# more information about the licensing of this file.

""" Plugin Manager """
import bisect
import logging
import importlib
import logging
import bisect

from inginious.frontend.task_problems import get_displayable_problem_types
from inginious.common.tasks_problems import get_problem_types


class PluginManagerNotLoadedException(Exception):
Expand Down Expand Up @@ -71,7 +74,18 @@ def load(self, client, flask_app, course_factory, task_factory, database, user_m
self._submission_manager = submission_manager
self._loaded = True
for entry in config:
module = importlib.import_module(entry["plugin_module"])
module_name = entry["plugin_module"]
module = importlib.import_module(module_name)

""" Load Problem sub-classes """
pbl_types = get_problem_types(module_name)
self._task_factory.set_problem_types(pbl_types)

""" Load DisplayableProblem sub-classes """
displayable_pbl_types = get_displayable_problem_types(module_name)
self._task_factory.set_problem_types(displayable_pbl_types)

""" Initialize the module """
module.init(self, course_factory, client, entry)

def add_page(self, pattern, classname_or_viewfunc):
Expand Down
9 changes: 8 additions & 1 deletion inginious/frontend/task_factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,18 @@ def __init__(self, filesystem: FileSystemProvider, plugin_manager, task_problem_
self._task_problem_types = task_problem_types
self.add_custom_task_file_manager(TaskYAMLFileReader())

def set_problem_types(self, problem_types):
""" Set the problem types for the current TaskFactory.
:param problem_types: A mapping of problem types and their associated name.
"""
self._task_problem_types.update(problem_types)

def add_problem_type(self, problem_type):
"""
:param problem_type: Problem class
"""
self._task_problem_types.update({problem_type.get_type(): problem_type})
pass

def get_task(self, course, taskid):
"""
Expand Down
27 changes: 23 additions & 4 deletions inginious/frontend/task_problems.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,38 @@
# This file is part of INGInious. See the LICENSE and the COPYRIGHTS files for
# more information about the licensing of this file.

""" Displyable problems """
""" Displayable problems """

import gettext
import json
from abc import ABCMeta, abstractmethod
from random import Random
import gettext
import json

from inginious.common.tasks_problems import Problem, CodeProblem, CodeSingleLineProblem, \
MatchProblem, MultipleChoiceProblem, FileProblem
MatchProblem, MultipleChoiceProblem, FileProblem, _get_problem_types


from inginious.frontend.parsable_text import ParsableText


def get_displayable_problem_types(name: str) -> dict:
""" Get the mapping of DisplayableProblem types available by inspecting a given module.
:param name: The name of the module to inspect.
:return: The mapping of problem name and problem class.
"""
raw = _get_problem_types(name, DisplayableProblem)
return {pbl_name: pbl_cls for pbl_name, pbl_cls in raw.items() if pbl_name is not None}

def get_default_displayable_problem_types() -> dict:
""" Get the mapping of default DisplayableProblem types available by inspecting the current
module.
:return: The mapping of problem name and problem class.
"""
return get_displayable_problem_types(__name__)


class DisplayableProblem(Problem, metaclass=ABCMeta):
"""Basic problem """

Expand Down

0 comments on commit 103dc9e

Please sign in to comment.