You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
418 lines
14 KiB
Python
418 lines
14 KiB
Python
"""Utilities for installing extensions"""
|
|
|
|
# Copyright (c) Jupyter Development Team.
|
|
# Distributed under the terms of the Modified BSD License.
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
import os
|
|
import sys
|
|
import typing as t
|
|
|
|
from jupyter_core.application import JupyterApp
|
|
from jupyter_core.paths import ENV_CONFIG_PATH, SYSTEM_CONFIG_PATH, jupyter_config_dir
|
|
from tornado.log import LogFormatter
|
|
from traitlets import Bool
|
|
|
|
from jupyter_server._version import __version__
|
|
from jupyter_server.extension.config import ExtensionConfigManager
|
|
from jupyter_server.extension.manager import ExtensionManager, ExtensionPackage
|
|
|
|
|
|
def _get_config_dir(user: bool = False, sys_prefix: bool = False) -> str:
|
|
"""Get the location of config files for the current context
|
|
|
|
Returns the string to the environment
|
|
|
|
Parameters
|
|
----------
|
|
user : bool [default: False]
|
|
Get the user's .jupyter config directory
|
|
sys_prefix : bool [default: False]
|
|
Get sys.prefix, i.e. ~/.envs/my-env/etc/jupyter
|
|
"""
|
|
if user and sys_prefix:
|
|
sys_prefix = False
|
|
if user:
|
|
extdir = jupyter_config_dir()
|
|
elif sys_prefix:
|
|
extdir = ENV_CONFIG_PATH[0]
|
|
else:
|
|
extdir = SYSTEM_CONFIG_PATH[0]
|
|
return extdir
|
|
|
|
|
|
def _get_extmanager_for_context(
|
|
write_dir: str = "jupyter_server_config.d", user: bool = False, sys_prefix: bool = False
|
|
) -> tuple[str, ExtensionManager]:
|
|
"""Get an extension manager pointing at the current context
|
|
|
|
Returns the path to the current context and an ExtensionManager object.
|
|
|
|
Parameters
|
|
----------
|
|
write_dir : str [default: 'jupyter_server_config.d']
|
|
Name of config directory to write extension config.
|
|
user : bool [default: False]
|
|
Get the user's .jupyter config directory
|
|
sys_prefix : bool [default: False]
|
|
Get sys.prefix, i.e. ~/.envs/my-env/etc/jupyter
|
|
"""
|
|
config_dir = _get_config_dir(user=user, sys_prefix=sys_prefix)
|
|
config_manager = ExtensionConfigManager(
|
|
read_config_path=[config_dir],
|
|
write_config_dir=os.path.join(config_dir, write_dir),
|
|
)
|
|
extension_manager = ExtensionManager(
|
|
config_manager=config_manager,
|
|
)
|
|
return config_dir, extension_manager
|
|
|
|
|
|
class ArgumentConflict(ValueError):
|
|
pass
|
|
|
|
|
|
_base_flags: dict[str, t.Any] = {}
|
|
_base_flags.update(JupyterApp.flags)
|
|
_base_flags.pop("y", None)
|
|
_base_flags.pop("generate-config", None)
|
|
_base_flags.update(
|
|
{
|
|
"user": (
|
|
{
|
|
"BaseExtensionApp": {
|
|
"user": True,
|
|
}
|
|
},
|
|
"Apply the operation only for the given user",
|
|
),
|
|
"system": (
|
|
{
|
|
"BaseExtensionApp": {
|
|
"user": False,
|
|
"sys_prefix": False,
|
|
}
|
|
},
|
|
"Apply the operation system-wide",
|
|
),
|
|
"sys-prefix": (
|
|
{
|
|
"BaseExtensionApp": {
|
|
"sys_prefix": True,
|
|
}
|
|
},
|
|
"Use sys.prefix as the prefix for installing extensions (for environments, packaging)",
|
|
),
|
|
"py": (
|
|
{
|
|
"BaseExtensionApp": {
|
|
"python": True,
|
|
}
|
|
},
|
|
"Install from a Python package",
|
|
),
|
|
}
|
|
)
|
|
_base_flags["python"] = _base_flags["py"]
|
|
|
|
_base_aliases: dict[str, t.Any] = {}
|
|
_base_aliases.update(JupyterApp.aliases)
|
|
|
|
|
|
class BaseExtensionApp(JupyterApp):
|
|
"""Base extension installer app"""
|
|
|
|
_log_formatter_cls = LogFormatter # type:ignore[assignment]
|
|
flags = _base_flags
|
|
aliases = _base_aliases
|
|
version = __version__
|
|
|
|
user = Bool(False, config=True, help="Whether to do a user install")
|
|
sys_prefix = Bool(True, config=True, help="Use the sys.prefix as the prefix")
|
|
python = Bool(False, config=True, help="Install from a Python package")
|
|
|
|
def _log_format_default(self) -> str:
|
|
"""A default format for messages"""
|
|
return "%(message)s"
|
|
|
|
@property
|
|
def config_dir(self) -> str: # type:ignore[override]
|
|
return _get_config_dir(user=self.user, sys_prefix=self.sys_prefix)
|
|
|
|
|
|
# Constants for pretty print extension listing function.
|
|
# Window doesn't support coloring in the commandline
|
|
GREEN_ENABLED = "\033[32menabled\033[0m" if os.name != "nt" else "enabled"
|
|
RED_DISABLED = "\033[31mdisabled\033[0m" if os.name != "nt" else "disabled"
|
|
GREEN_OK = "\033[32mOK\033[0m" if os.name != "nt" else "ok"
|
|
RED_X = "\033[31m X\033[0m" if os.name != "nt" else " X"
|
|
|
|
# ------------------------------------------------------------------------------
|
|
# Public API
|
|
# ------------------------------------------------------------------------------
|
|
|
|
|
|
def toggle_server_extension_python(
|
|
import_name: str,
|
|
enabled: bool | None = None,
|
|
parent: t.Any = None,
|
|
user: bool = False,
|
|
sys_prefix: bool = True,
|
|
) -> None:
|
|
"""Toggle the boolean setting for a given server extension
|
|
in a Jupyter config file.
|
|
"""
|
|
sys_prefix = False if user else sys_prefix
|
|
config_dir = _get_config_dir(user=user, sys_prefix=sys_prefix)
|
|
manager = ExtensionConfigManager(
|
|
read_config_path=[config_dir],
|
|
write_config_dir=os.path.join(config_dir, "jupyter_server_config.d"),
|
|
)
|
|
if enabled:
|
|
manager.enable(import_name)
|
|
else:
|
|
manager.disable(import_name)
|
|
|
|
|
|
# ----------------------------------------------------------------------
|
|
# Applications
|
|
# ----------------------------------------------------------------------
|
|
|
|
flags = {}
|
|
flags.update(BaseExtensionApp.flags)
|
|
flags.pop("y", None)
|
|
flags.pop("generate-config", None)
|
|
flags.update(
|
|
{
|
|
"user": (
|
|
{
|
|
"ToggleServerExtensionApp": {
|
|
"user": True,
|
|
}
|
|
},
|
|
"Perform the operation for the current user",
|
|
),
|
|
"system": (
|
|
{
|
|
"ToggleServerExtensionApp": {
|
|
"user": False,
|
|
"sys_prefix": False,
|
|
}
|
|
},
|
|
"Perform the operation system-wide",
|
|
),
|
|
"sys-prefix": (
|
|
{
|
|
"ToggleServerExtensionApp": {
|
|
"sys_prefix": True,
|
|
}
|
|
},
|
|
"Use sys.prefix as the prefix for installing server extensions",
|
|
),
|
|
"py": (
|
|
{
|
|
"ToggleServerExtensionApp": {
|
|
"python": True,
|
|
}
|
|
},
|
|
"Install from a Python package",
|
|
),
|
|
}
|
|
)
|
|
flags["python"] = flags["py"]
|
|
|
|
|
|
_desc = "Enable/disable a server extension using frontend configuration files."
|
|
|
|
|
|
class ToggleServerExtensionApp(BaseExtensionApp):
|
|
"""A base class for enabling/disabling extensions"""
|
|
|
|
name = "jupyter server extension enable/disable"
|
|
description = _desc
|
|
|
|
flags = flags
|
|
|
|
_toggle_value = Bool()
|
|
_toggle_pre_message = ""
|
|
_toggle_post_message = ""
|
|
|
|
def toggle_server_extension(self, import_name: str) -> None:
|
|
"""Change the status of a named server extension.
|
|
|
|
Uses the value of `self._toggle_value`.
|
|
|
|
Parameters
|
|
---------
|
|
|
|
import_name : str
|
|
Importable Python module (dotted-notation) exposing the magic-named
|
|
`load_jupyter_server_extension` function
|
|
"""
|
|
# Create an extension manager for this instance.
|
|
config_dir, extension_manager = _get_extmanager_for_context(
|
|
user=self.user, sys_prefix=self.sys_prefix
|
|
)
|
|
try:
|
|
self.log.info(f"{self._toggle_pre_message.capitalize()}: {import_name}")
|
|
self.log.info(f"- Writing config: {config_dir}")
|
|
# Validate the server extension.
|
|
self.log.info(f" - Validating {import_name}...")
|
|
config = extension_manager.config_manager
|
|
enabled = False
|
|
if config:
|
|
jpserver_extensions = config.get_jpserver_extensions()
|
|
if import_name not in jpserver_extensions:
|
|
msg = (
|
|
f"The module '{import_name}' could not be found. Are you "
|
|
"sure the extension is installed?"
|
|
)
|
|
raise ValueError(msg)
|
|
enabled = jpserver_extensions[import_name]
|
|
|
|
# Interface with the Extension Package and validate.
|
|
extpkg = ExtensionPackage(name=import_name, enabled=enabled)
|
|
if not extpkg.validate():
|
|
msg = "validation failed"
|
|
raise ValueError(msg)
|
|
version = extpkg.version
|
|
self.log.info(f" {import_name} {version} {GREEN_OK}")
|
|
|
|
# Toggle extension config.
|
|
config = extension_manager.config_manager
|
|
if config:
|
|
if self._toggle_value is True:
|
|
config.enable(import_name)
|
|
else:
|
|
config.disable(import_name)
|
|
|
|
# If successful, let's log.
|
|
self.log.info(f" - Extension successfully {self._toggle_post_message}.")
|
|
except Exception as err:
|
|
self.log.error(f" {RED_X} Validation failed: {err}")
|
|
|
|
def start(self) -> None:
|
|
"""Perform the App's actions as configured"""
|
|
if not self.extra_args:
|
|
sys.exit("Please specify a server extension/package to enable or disable")
|
|
for arg in self.extra_args:
|
|
self.toggle_server_extension(arg)
|
|
|
|
|
|
class EnableServerExtensionApp(ToggleServerExtensionApp):
|
|
"""An App that enables (and validates) Server Extensions"""
|
|
|
|
name = "jupyter server extension enable"
|
|
description = """
|
|
Enable a server extension in configuration.
|
|
|
|
Usage
|
|
jupyter server extension enable [--system|--sys-prefix]
|
|
"""
|
|
_toggle_value = True # type:ignore[assignment]
|
|
_toggle_pre_message = "enabling"
|
|
_toggle_post_message = "enabled"
|
|
|
|
|
|
class DisableServerExtensionApp(ToggleServerExtensionApp):
|
|
"""An App that disables Server Extensions"""
|
|
|
|
name = "jupyter server extension disable"
|
|
description = """
|
|
Disable a server extension in configuration.
|
|
|
|
Usage
|
|
jupyter server extension disable [--system|--sys-prefix]
|
|
"""
|
|
_toggle_value = False # type:ignore[assignment]
|
|
_toggle_pre_message = "disabling"
|
|
_toggle_post_message = "disabled"
|
|
|
|
|
|
class ListServerExtensionsApp(BaseExtensionApp):
|
|
"""An App that lists (and validates) Server Extensions"""
|
|
|
|
name = "jupyter server extension list"
|
|
version = __version__
|
|
description = "List all server extensions known by the configuration system"
|
|
|
|
def list_server_extensions(self) -> None:
|
|
"""List all enabled and disabled server extensions, by config path
|
|
|
|
Enabled extensions are validated, potentially generating warnings.
|
|
"""
|
|
configurations = (
|
|
{"user": True, "sys_prefix": False},
|
|
{"user": False, "sys_prefix": True},
|
|
{"user": False, "sys_prefix": False},
|
|
)
|
|
|
|
for option in configurations:
|
|
config_dir = _get_config_dir(**option)
|
|
print(f"Config dir: {config_dir}")
|
|
write_dir = "jupyter_server_config.d"
|
|
config_manager = ExtensionConfigManager(
|
|
read_config_path=[config_dir],
|
|
write_config_dir=os.path.join(config_dir, write_dir),
|
|
)
|
|
jpserver_extensions = config_manager.get_jpserver_extensions()
|
|
for name, enabled in jpserver_extensions.items():
|
|
# Attempt to get extension metadata
|
|
print(f" {name} {GREEN_ENABLED if enabled else RED_DISABLED}")
|
|
try:
|
|
print(f" - Validating {name}...")
|
|
extension = ExtensionPackage(name=name, enabled=enabled)
|
|
if not extension.validate():
|
|
msg = "validation failed"
|
|
raise ValueError(msg)
|
|
version = extension.version
|
|
print(f" {name} {version} {GREEN_OK}")
|
|
except Exception as err:
|
|
self.log.debug("", exc_info=True)
|
|
print(f" {RED_X} {err}")
|
|
# Add a blank line between paths.
|
|
self.log.info("")
|
|
|
|
def start(self) -> None:
|
|
"""Perform the App's actions as configured"""
|
|
self.list_server_extensions()
|
|
|
|
|
|
_examples = """
|
|
jupyter server extension list # list all configured server extensions
|
|
jupyter server extension enable --py <packagename> # enable all server extensions in a Python package
|
|
jupyter server extension disable --py <packagename> # disable all server extensions in a Python package
|
|
"""
|
|
|
|
|
|
class ServerExtensionApp(BaseExtensionApp):
|
|
"""Root level server extension app"""
|
|
|
|
name = "jupyter server extension"
|
|
version = __version__
|
|
description: str = "Work with Jupyter server extensions"
|
|
examples = _examples
|
|
|
|
subcommands: dict[str, t.Any] = {
|
|
"enable": (EnableServerExtensionApp, "Enable a server extension"),
|
|
"disable": (DisableServerExtensionApp, "Disable a server extension"),
|
|
"list": (ListServerExtensionsApp, "List server extensions"),
|
|
}
|
|
|
|
def start(self) -> None:
|
|
"""Perform the App's actions as configured"""
|
|
super().start()
|
|
|
|
# The above should have called a subcommand and raised NoStart; if we
|
|
# get here, it didn't, so we should self.log.info a message.
|
|
subcmds = ", ".join(sorted(self.subcommands))
|
|
sys.exit("Please supply at least one subcommand: %s" % subcmds)
|
|
|
|
|
|
main = ServerExtensionApp.launch_instance
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|