Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor e2xgrader apps #177

Merged
merged 36 commits into from
Mar 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
628c27e
Add E2xGrader baseapp
tmetzl Feb 26, 2024
cb7a17d
Add app to toggle e2xgrader modes
tmetzl Feb 26, 2024
0fb7ae4
Add app to activate e2xgrader modes
tmetzl Feb 26, 2024
07573dc
Add app to deactivate e2xgrader extensions
tmetzl Feb 26, 2024
29e755d
Add app to show the current e2xgrader mode
tmetzl Feb 26, 2024
317ef0f
Add e2xgrader app
tmetzl Feb 26, 2024
10b0fa6
Add __init__.py
tmetzl Feb 26, 2024
72ebadd
Add test for get_jupyter_config_path function
tmetzl Feb 26, 2024
79cf4e5
Update package-lock.json
tmetzl Feb 26, 2024
faba50c
Add test for baseapp
tmetzl Feb 26, 2024
c706890
Add test for e2xgraderapp
tmetzl Feb 26, 2024
a5902bc
Add test for togglemodeapp
tmetzl Feb 27, 2024
ff11d1a
Add test for deactivatemodeapp
tmetzl Feb 27, 2024
8ed48e7
Add test for activatemodeapp
tmetzl Feb 27, 2024
12a7840
Add test for showmodeapp
tmetzl Feb 27, 2024
5c7b306
Use get_nbgrader_config from e2xcore
tmetzl Feb 27, 2024
ee64313
Remove get_nbgrader_config function
tmetzl Feb 27, 2024
e18c602
Add function get_e2xgrader_mode
tmetzl Feb 27, 2024
eb6d549
Add test for get_e2xgrader_mode
tmetzl Feb 27, 2024
95410c9
Add function to get BaseJSONConfigManager
tmetzl Feb 29, 2024
869f60d
Infer the mode from extensions instead of from config
tmetzl Feb 29, 2024
a243550
Add tests for new mode utils
tmetzl Feb 29, 2024
7f77019
Extend tests for extension utils
tmetzl Feb 29, 2024
8f2cf8b
Use infer_e2xgrader_mode instead of config file in baseapp
tmetzl Feb 29, 2024
6a3ed6e
Do not write config_file in togglemodeapp
tmetzl Feb 29, 2024
323e257
Adapt tests for baseapp
tmetzl Feb 29, 2024
63bd6d5
Remove test for deleted function get_jupyter_config_path
tmetzl Feb 29, 2024
28a8ffa
Adapt test for togglemodeapp
tmetzl Feb 29, 2024
fc01a70
Use get_notebook_config_manager in test_e2xmanager
tmetzl Feb 29, 2024
0f79c8a
Add docs for e2xgrader show_mode
tmetzl Feb 29, 2024
3cc7365
Move sys-prefix and user to togglemodeapp
tmetzl Mar 1, 2024
cd716b8
Add enum for e2xgrader modes
tmetzl Mar 1, 2024
6f45ea6
Adapt tests for mode
tmetzl Mar 1, 2024
11af8fe
Use e2xgrader mode enum in baseapp
tmetzl Mar 1, 2024
76c98f9
Use e2xgrader mode enum in apps
tmetzl Mar 1, 2024
927d83c
Adapt tests for apps
tmetzl Mar 1, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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__ = [

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how are these called from where? (maybe update docs?)

"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()

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this method called by some convention or a leftover?

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 = {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

isn't that inferred now?

"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
Loading