Skip to content

Commit

Permalink
Merge pull request #177 from DigiKlausur/refactor_e2xgrader_apps
Browse files Browse the repository at this point in the history
Refactor e2xgrader apps
  • Loading branch information
tmetzl authored Mar 7, 2024
2 parents be89d4a + 927d83c commit 31ccffb
Show file tree
Hide file tree
Showing 23 changed files with 870 additions and 183 deletions.
6 changes: 6 additions & 0 deletions docs/source/getting_started/modes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,12 @@ To deactivate all e2xgrader modes:
e2xgrader deactivate --sys-prefix
To show the current active mode:

.. code-block:: sh
e2xgrader show_mode
Teacher Mode
------------

Expand Down
15 changes: 15 additions & 0 deletions e2xgrader/apps/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
from .activatemodeapp import ActivateModeApp
from .baseapp import E2xGrader
from .deactivatemodeapp import DeactivateModeApp
from .e2xgraderapp import E2xGraderApp
from .showmodeapp import ShowModeApp
from .togglemodeapp import ToggleModeApp

__all__ = [
"E2xGrader",
"E2xGraderApp",
"ActivateModeApp",
"DeactivateModeApp",
"ShowModeApp",
"ToggleModeApp",
]
22 changes: 22 additions & 0 deletions e2xgrader/apps/activatemodeapp.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
from ..utils.mode import E2xGraderMode
from .togglemodeapp import ToggleModeApp


class ActivateModeApp(ToggleModeApp):
description = "Activate a specific mode (teacher, student, student_exam)"

def start(self) -> None:
super().start()
if len(self.extra_args) != 1:
self.fail("Exactly one mode has to be specified")
if self.extra_args[0] not in [
E2xGraderMode.TEACHER.value,
E2xGraderMode.STUDENT.value,
E2xGraderMode.STUDENT_EXAM.value,
]:
self.fail(
f"Mode {self.extra_args[0]} is not a valid mode that can be activated."
)
self.mode = self.extra_args[0]

self.activate_mode()
33 changes: 33 additions & 0 deletions e2xgrader/apps/baseapp.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
from textwrap import dedent

from jupyter_core.application import JupyterApp
from traitlets import Enum

from ..utils.mode import E2xGraderMode, infer_e2xgrader_mode


class E2xGrader(JupyterApp):

mode = Enum(
values=[mode.value for mode in E2xGraderMode],
default_value=E2xGraderMode.INACTIVE.value,
help=dedent(
"""
Which mode is activated, can be teacher, student, student_exam or deactivated.
Is set to invalid if the mode cannot be inferred.
"""
),
)

def fail(self, msg, *args):
self.log.error(msg, *args)
self.exit(1)

def initialize(self, argv=None):
try:
mode = infer_e2xgrader_mode()
self.mode = mode
except ValueError as e:
self.log.error(str(e))
self.mode = E2xGraderMode.INVALID.value
super().initialize(argv)
13 changes: 13 additions & 0 deletions e2xgrader/apps/deactivatemodeapp.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
from ..utils.mode import E2xGraderMode
from .togglemodeapp import ToggleModeApp


class DeactivateModeApp(ToggleModeApp):
description = "Deactivate all e2xgrader extensions"

def start(self) -> None:
super().start()
if len(self.extra_args) != 0:
self.fail("e2xgrader deactivate does not take any arguments.")
self.mode = E2xGraderMode.INACTIVE.value
self.activate_mode()
133 changes: 38 additions & 95 deletions e2xgrader/apps/e2xgraderapp.py
Original file line number Diff line number Diff line change
@@ -1,105 +1,48 @@
import sys
from argparse import ArgumentParser
from textwrap import dedent

from ..extensions import E2xExtensionManager
from .activatemodeapp import ActivateModeApp
from .baseapp import E2xGrader
from .deactivatemodeapp import DeactivateModeApp
from .showmodeapp import ShowModeApp


class Manager:
def __init__(self):
self.extension_manager = E2xExtensionManager()
parser = ArgumentParser(
description="E2X extension manager.",
usage=dedent(
"""
e2xgrader <command> [<args>]
Available sub commands are:
activate activate a specific mode (teacher, student, student-exam)
deactivate deactivate all extensions"""
),
)

parser.add_argument("command", help="Subcommand to run")
class E2xGraderApp(E2xGrader):

args = parser.parse_args(sys.argv[1:2])
if not hasattr(self, args.command):
print("Unrecognized command")
parser.print_help()
exit(1)
getattr(self, args.command)()

def activate(self):
parser = ArgumentParser(
description="Activate different modes",
usage=dedent(
subcommands = dict(
activate=(
ActivateModeApp,
dedent(
"""\
Activate a specific mode (teacher, student, student_exam)
"""
e2xgrader activate <mode> [--sys-prefix] [--user]
Available modes are:
teacher activate the grader and all teaching extensions
student activate the student extensions
student_exam activate the student extensions in exam mode"""
),
)
# prefixing the argument with -- means it's optional
parser.add_argument(
"mode",
help="Which mode to activate, can be teacher, student or student-exam",
)
parser.add_argument(
"--sys-prefix",
action="store_true",
help="If the extensions should be installed to sys.prefix",
)
parser.add_argument(
"--user",
action="store_true",
help="If the extensions should be installed to the user space",
)

args = parser.parse_args(sys.argv[2:])
if not hasattr(self.extension_manager, f"activate_{args.mode}"):
print("Unrecognized mode")
parser.print_help()
exit(1)
sys_prefix = False
user = False
if args.sys_prefix:
sys_prefix = True
if args.user:
user = True
getattr(self.extension_manager, f"activate_{args.mode}")(
sys_prefix=sys_prefix, user=user
)

def deactivate(self):
parser = ArgumentParser(
description="Deactivate extensions",
usage=dedent("python -m e2xgrader deactivate [--sys-prefix] [--user]"),
)
# prefixing the argument with -- means it's optional
parser.add_argument(
"--sys-prefix",
action="store_true",
help="If the extensions should be uninstalled from sys.prefix",
)
parser.add_argument(
"--user",
action="store_true",
help="If the extensions should be uninstalled from the user space",
)

args = parser.parse_args(sys.argv[2:])
).strip(),
),
deactivate=(
DeactivateModeApp,
dedent(
"""\
Deactivate all e2xgrader extensions
"""
).strip(),
),
show_mode=(
ShowModeApp,
dedent(
"""\
Show the currently active mode
"""
).strip(),
),
)

sys_prefix = False
user = False
if args.sys_prefix:
sys_prefix = True
if args.user:
user = True
self.extension_manager.deactivate(sys_prefix=sys_prefix, user=user)
def start(self) -> None:
super().start()
if self.subapp is None:
print(
"No subcommand given (run with --help for options). List of subcommands:\n"
)
self.print_subcommands()


def main():
Manager()
E2xGraderApp.launch_instance()
9 changes: 9 additions & 0 deletions e2xgrader/apps/showmodeapp.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
from .baseapp import E2xGrader


class ShowModeApp(E2xGrader):
description = "Show the currently active mode"

def start(self) -> None:
super().start()
print(f"Current mode: {self.mode}")
62 changes: 62 additions & 0 deletions e2xgrader/apps/togglemodeapp.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
from traitlets import Bool

from ..extensions import E2xExtensionManager
from ..utils.mode import E2xGraderMode, infer_e2xgrader_mode
from .baseapp import E2xGrader


class ToggleModeApp(E2xGrader):

sys_prefix = Bool(False, help="Install extensions to sys.prefix", config=True)

user = Bool(False, help="Install extensions to the user space", config=True)

flags = {
"sys-prefix": (
{"ToggleModeApp": {"sys_prefix": True}},
"Install extensions to sys.prefix",
),
"user": (
{"ToggleModeApp": {"user": True}},
"Install extensions to the user space",
),
}

def activate_mode(self):
"""
Activates the specified mode by activating the corresponding extensions
using the E2xExtensionManager.
If the mode is "None", it deactivates all e2xgrader extensions.
"""
extension_manager = E2xExtensionManager()
if self.mode == E2xGraderMode.INACTIVE.value:
print(
f"Deactivating e2xgrader extensions with sys_prefix={self.sys_prefix} "
f"and user={self.user}"
)
extension_manager.deactivate(sys_prefix=self.sys_prefix, user=self.user)
else:
print(
f"Activating mode {self.mode} with sys_prefix={self.sys_prefix} "
f"and user={self.user}"
)
getattr(extension_manager, f"activate_{self.mode}")(
sys_prefix=self.sys_prefix, user=self.user
)
self.log.info(f"Activated mode {self.mode}. ")
try:
mode = infer_e2xgrader_mode()
if mode != self.mode:
self.log.warning(
f"The activated mode {self.mode} does not match the infered mode {mode}. \n"
f"The mode {mode} may be activated on a higher level."
)
except ValueError as e:
self.log.error(str(e))
self.mode = E2xGraderMode.INVALID.value

def start(self) -> None:
super().start()
if self.sys_prefix and self.user:
self.fail("Cannot install in both sys-prefix and user space")
8 changes: 8 additions & 0 deletions e2xgrader/extensions/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,14 @@ def get_nbextension_utils() -> Optional[ModuleType]:
return None


def get_notebook_config_manager() -> Optional[ModuleType]:
if is_installed("jupyter_server"):
return import_module("jupyter_server.config_manager").BaseJSONConfigManager
if is_installed("notebook") and get_notebook_major_version() < 7:
return import_module("notebook.services.config.manager").BaseJSONConfigManager
return None


def discover_nbextensions(mode: str) -> List[Dict[str, str]]:
extensions = list()
for nbextension in _jupyter_nbextension_paths():
Expand Down
3 changes: 1 addition & 2 deletions e2xgrader/server_extensions/base/extension.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
from importlib import import_module

from e2xcore.utils.utils import get_nbgrader_config
from jinja2 import Environment, FileSystemLoader
from traitlets import Any, List, TraitError, validate
from traitlets.config import Application

from e2xgrader.utils import get_nbgrader_config


class BaseExtension(Application):
apps = List(
Expand Down
35 changes: 35 additions & 0 deletions e2xgrader/tests/apps/test_activatemodeapp.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import unittest
from unittest.mock import patch

from jupyter_core.application import NoStart

from e2xgrader.apps.activatemodeapp import ActivateModeApp
from e2xgrader.utils.mode import E2xGraderMode


class TestActivateModeApp(unittest.TestCase):

def setUp(self):
self.app = ActivateModeApp()
self.app.initialize([])

def test_fail_without_args(self):
with self.assertRaises(SystemExit):
self.app.initialize([])
self.app.start()

def test_fail_with_invalid_mode(self):
with self.assertRaises(SystemExit):
self.app.initialize(["invalid_mode"])
self.app.start()

@patch("e2xgrader.apps.togglemodeapp.ToggleModeApp.activate_mode")
def test_activate_mode(self, mock_activate_mode):
try:
self.app.initialize([E2xGraderMode.TEACHER.value])
self.app.start()
except NoStart:
pass
finally:
self.assertEqual(self.app.mode, E2xGraderMode.TEACHER.value)
mock_activate_mode.assert_called_once()
Loading

0 comments on commit 31ccffb

Please sign in to comment.