| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483 |
- """Utilities for installing Javascript extensions for the notebook"""
- # Copyright (c) Jupyter Development Team.
- # Distributed under the terms of the Modified BSD License.
- import importlib
- import json
- import os
- import os.path as osp
- import platform
- import shutil
- import subprocess
- import sys
- from pathlib import Path
- try:
- from importlib.metadata import PackageNotFoundError, version
- except ImportError:
- from importlib_metadata import PackageNotFoundError, version
- from os.path import basename, normpath
- from os.path import join as pjoin
- from jupyter_core.paths import ENV_JUPYTER_PATH, SYSTEM_JUPYTER_PATH, jupyter_data_dir
- from jupyter_core.utils import ensure_dir_exists
- from jupyter_server.extension.serverextension import ArgumentConflict
- from jupyterlab_server.config import get_federated_extensions
- try:
- from tomllib import load # Python 3.11+
- except ImportError:
- from tomli import load
- from .commands import _test_overlap
- DEPRECATED_ARGUMENT = object()
- HERE = osp.abspath(osp.dirname(__file__))
- # ------------------------------------------------------------------------------
- # Public API
- # ------------------------------------------------------------------------------
- def develop_labextension( # noqa
- path,
- symlink=True,
- overwrite=False,
- user=False,
- labextensions_dir=None,
- destination=None,
- logger=None,
- sys_prefix=False,
- ):
- """Install a prebuilt extension for JupyterLab
- Stages files and/or directories into the labextensions directory.
- By default, this compares modification time, and only stages files that need updating.
- If `overwrite` is specified, matching files are purged before proceeding.
- Parameters
- ----------
- path : path to file, directory, zip or tarball archive, or URL to install
- By default, the file will be installed with its base name, so '/path/to/foo'
- will install to 'labextensions/foo'. See the destination argument below to change this.
- Archives (zip or tarballs) will be extracted into the labextensions directory.
- user : bool [default: False]
- Whether to install to the user's labextensions directory.
- Otherwise do a system-wide install (e.g. /usr/local/share/jupyter/labextensions).
- overwrite : bool [default: False]
- If True, always install the files, regardless of what may already be installed.
- symlink : bool [default: True]
- If True, create a symlink in labextensions, rather than copying files.
- Windows support for symlinks requires a permission bit which only admin users
- have by default, so don't rely on it.
- labextensions_dir : str [optional]
- Specify absolute path of labextensions directory explicitly.
- destination : str [optional]
- name the labextension is installed to. For example, if destination is 'foo', then
- the source file will be installed to 'labextensions/foo', regardless of the source name.
- logger : Jupyter logger [optional]
- Logger instance to use
- """
- # the actual path to which we eventually installed
- full_dest = None
- labext = _get_labextension_dir(
- user=user, sys_prefix=sys_prefix, labextensions_dir=labextensions_dir
- )
- # make sure labextensions dir exists
- ensure_dir_exists(labext)
- if isinstance(path, (list, tuple)):
- msg = "path must be a string pointing to a single extension to install; call this function multiple times to install multiple extensions"
- raise TypeError(msg)
- if not destination:
- destination = basename(normpath(path))
- full_dest = normpath(pjoin(labext, destination))
- if overwrite and os.path.lexists(full_dest):
- if logger:
- logger.info(f"Removing: {full_dest}")
- if os.path.isdir(full_dest) and not os.path.islink(full_dest):
- shutil.rmtree(full_dest)
- else:
- os.remove(full_dest)
- # Make sure the parent directory exists
- os.makedirs(os.path.dirname(full_dest), exist_ok=True)
- if symlink:
- path = os.path.abspath(path)
- if not os.path.exists(full_dest):
- if logger:
- logger.info(f"Symlinking: {full_dest} -> {path}")
- try:
- os.symlink(path, full_dest)
- except OSError as e:
- if platform.platform().startswith("Windows"):
- msg = (
- "Symlinks can be activated on Windows 10 for Python version 3.8 or higher"
- " by activating the 'Developer Mode'. That may not be allowed by your administrators.\n"
- "See https://docs.microsoft.com/en-us/windows/apps/get-started/enable-your-device-for-development"
- )
- raise OSError(msg) from e
- raise
- elif not os.path.islink(full_dest):
- msg = f"{full_dest} exists and is not a symlink"
- raise ValueError(msg)
- elif os.path.isdir(path):
- path = pjoin(os.path.abspath(path), "") # end in path separator
- for parent, _, files in os.walk(path):
- dest_dir = pjoin(full_dest, parent[len(path) :])
- if not os.path.exists(dest_dir):
- if logger:
- logger.info(f"Making directory: {dest_dir}")
- os.makedirs(dest_dir)
- for file_name in files:
- src = pjoin(parent, file_name)
- dest_file = pjoin(dest_dir, file_name)
- _maybe_copy(src, dest_file, logger=logger)
- else:
- src = path
- _maybe_copy(src, full_dest, logger=logger)
- return full_dest
- def develop_labextension_py(
- module,
- user=False,
- sys_prefix=False,
- overwrite=True,
- symlink=True,
- labextensions_dir=None,
- logger=None,
- ):
- """Develop a labextension bundled in a Python package.
- Returns a list of installed/updated directories.
- See develop_labextension for parameter information."""
- m, labexts = _get_labextension_metadata(module)
- base_path = os.path.split(m.__file__)[0]
- full_dests = []
- for labext in labexts:
- src = os.path.join(base_path, labext["src"])
- dest = labext["dest"]
- if logger:
- logger.info(f"Installing {src} -> {dest}")
- if not os.path.exists(src):
- build_labextension(base_path, logger=logger)
- full_dest = develop_labextension(
- src,
- overwrite=overwrite,
- symlink=symlink,
- user=user,
- sys_prefix=sys_prefix,
- labextensions_dir=labextensions_dir,
- destination=dest,
- logger=logger,
- )
- full_dests.append(full_dest)
- return full_dests
- def build_labextension(
- path, logger=None, development=False, static_url=None, source_map=False, core_path=None
- ):
- """Build a labextension in the given path"""
- core_path = osp.join(HERE, "staging") if core_path is None else str(Path(core_path).resolve())
- ext_path = str(Path(path).resolve())
- if logger:
- logger.info(f"Building extension in {path}")
- builder = _ensure_builder(ext_path, core_path)
- arguments = ["node", builder, "--core-path", core_path, ext_path]
- if static_url is not None:
- arguments.extend(["--static-url", static_url])
- if development:
- arguments.append("--development")
- if source_map:
- arguments.append("--source-map")
- subprocess.check_call(arguments, cwd=ext_path) # noqa S603
- def watch_labextension(
- path, labextensions_path, logger=None, development=False, source_map=False, core_path=None
- ):
- """Watch a labextension in a given path"""
- core_path = osp.join(HERE, "staging") if core_path is None else str(Path(core_path).resolve())
- ext_path = str(Path(path).resolve())
- if logger:
- logger.info(f"Building extension in {path}")
- # Check to see if we need to create a symlink
- federated_extensions = get_federated_extensions(labextensions_path)
- with open(pjoin(ext_path, "package.json")) as fid:
- ext_data = json.load(fid)
- if ext_data["name"] not in federated_extensions:
- develop_labextension_py(ext_path, sys_prefix=True)
- else:
- full_dest = pjoin(federated_extensions[ext_data["name"]]["ext_dir"], ext_data["name"])
- output_dir = pjoin(ext_path, ext_data["jupyterlab"].get("outputDir", "static"))
- if not osp.islink(full_dest):
- shutil.rmtree(full_dest)
- os.symlink(output_dir, full_dest)
- builder = _ensure_builder(ext_path, core_path)
- arguments = ["node", builder, "--core-path", core_path, "--watch", ext_path]
- if development:
- arguments.append("--development")
- if source_map:
- arguments.append("--source-map")
- subprocess.check_call(arguments, cwd=ext_path) # noqa S603
- # ------------------------------------------------------------------------------
- # Private API
- # ------------------------------------------------------------------------------
- def _ensure_builder(ext_path, core_path):
- """Ensure that we can build the extension and return the builder script path"""
- # Test for compatible dependency on @jupyterlab/builder
- with open(osp.join(core_path, "package.json")) as fid:
- core_data = json.load(fid)
- with open(osp.join(ext_path, "package.json")) as fid:
- ext_data = json.load(fid)
- dep_version1 = core_data["devDependencies"]["@jupyterlab/builder"]
- dep_version2 = ext_data.get("devDependencies", {}).get("@jupyterlab/builder")
- dep_version2 = dep_version2 or ext_data.get("dependencies", {}).get("@jupyterlab/builder")
- if dep_version2 is None:
- msg = f"Extensions require a devDependency on @jupyterlab/builder@{dep_version1}"
- raise ValueError(msg)
- # if we have installed from disk (version is a path), assume we know what
- # we are doing and do not check versions.
- if "/" in dep_version2:
- with open(osp.join(ext_path, dep_version2, "package.json")) as fid:
- dep_version2 = json.load(fid).get("version")
- if not osp.exists(osp.join(ext_path, "node_modules")):
- subprocess.check_call(["jlpm"], cwd=ext_path) # noqa S603 S607
- # Find @jupyterlab/builder using node module resolution
- # We cannot use a script because the script path is a shell script on Windows
- target = ext_path
- while not osp.exists(osp.join(target, "node_modules", "@jupyterlab", "builder")):
- if osp.dirname(target) == target:
- msg = "Could not find @jupyterlab/builder"
- raise ValueError(msg)
- target = osp.dirname(target)
- overlap = _test_overlap(
- dep_version1, dep_version2, drop_prerelease1=True, drop_prerelease2=True
- )
- if not overlap:
- with open(
- osp.join(target, "node_modules", "@jupyterlab", "builder", "package.json")
- ) as fid:
- dep_version2 = json.load(fid).get("version")
- overlap = _test_overlap(
- dep_version1, dep_version2, drop_prerelease1=True, drop_prerelease2=True
- )
- if not overlap:
- msg = f"Extensions require a devDependency on @jupyterlab/builder@{dep_version1}, you have a dependency on {dep_version2}"
- raise ValueError(msg)
- return osp.join(
- target, "node_modules", "@jupyterlab", "builder", "lib", "build-labextension.js"
- )
- def _should_copy(src, dest, logger=None):
- """Should a file be copied, if it doesn't exist, or is newer?
- Returns whether the file needs to be updated.
- Parameters
- ----------
- src : string
- A path that should exist from which to copy a file
- src : string
- A path that might exist to which to copy a file
- logger : Jupyter logger [optional]
- Logger instance to use
- """
- if not os.path.exists(dest):
- return True
- if os.stat(src).st_mtime - os.stat(dest).st_mtime > 1e-6: # noqa
- # we add a fudge factor to work around a bug in python 2.x
- # that was fixed in python 3.x: https://bugs.python.org/issue12904
- if logger:
- logger.warning(f"Out of date: {dest}")
- return True
- if logger:
- logger.info(f"Up to date: {dest}")
- return False
- def _maybe_copy(src, dest, logger=None):
- """Copy a file if it needs updating.
- Parameters
- ----------
- src : string
- A path that should exist from which to copy a file
- src : string
- A path that might exist to which to copy a file
- logger : Jupyter logger [optional]
- Logger instance to use
- """
- if _should_copy(src, dest, logger=logger):
- if logger:
- logger.info(f"Copying: {src} -> {dest}")
- shutil.copy2(src, dest)
- def _get_labextension_dir(user=False, sys_prefix=False, prefix=None, labextensions_dir=None):
- """Return the labextension directory specified
- Parameters
- ----------
- user : bool [default: False]
- Get the user's .jupyter/labextensions directory
- sys_prefix : bool [default: False]
- Get sys.prefix, i.e. ~/.envs/my-env/share/jupyter/labextensions
- prefix : str [optional]
- Get custom prefix
- labextensions_dir : str [optional]
- Get what you put in
- """
- conflicting = [
- ("user", user),
- ("prefix", prefix),
- ("labextensions_dir", labextensions_dir),
- ("sys_prefix", sys_prefix),
- ]
- conflicting_set = [f"{n}={v!r}" for n, v in conflicting if v]
- if len(conflicting_set) > 1:
- msg = "cannot specify more than one of user, sys_prefix, prefix, or labextensions_dir, but got: {}".format(
- ", ".join(conflicting_set)
- )
- raise ArgumentConflict(msg)
- if user:
- labext = pjoin(jupyter_data_dir(), "labextensions")
- elif sys_prefix:
- labext = pjoin(ENV_JUPYTER_PATH[0], "labextensions")
- elif prefix:
- labext = pjoin(prefix, "share", "jupyter", "labextensions")
- elif labextensions_dir:
- labext = labextensions_dir
- else:
- labext = pjoin(SYSTEM_JUPYTER_PATH[0], "labextensions")
- return labext
- def _get_labextension_metadata(module): # noqa
- """Get the list of labextension paths associated with a Python module.
- Returns a tuple of (the module path, [{
- 'src': 'mockextension',
- 'dest': '_mockdestination'
- }])
- Parameters
- ----------
- module : str
- Importable Python module exposing the
- magic-named `_jupyter_labextension_paths` function
- """
- mod_path = osp.abspath(module)
- if not osp.exists(mod_path):
- msg = f"The path `{mod_path}` does not exist."
- raise FileNotFoundError(msg)
- errors = []
- # Check if the path is a valid labextension
- try:
- m = importlib.import_module(module)
- if hasattr(m, "_jupyter_labextension_paths"):
- return m, m._jupyter_labextension_paths()
- except Exception as exc:
- errors.append(exc)
- # Try to get the package name
- package = None
- # Try getting the package name from pyproject.toml
- if os.path.exists(os.path.join(mod_path, "pyproject.toml")):
- with open(os.path.join(mod_path, "pyproject.toml"), "rb") as fid:
- data = load(fid)
- package = data.get("project", {}).get("name")
- # Try getting the package name from setup.py
- if not package:
- try:
- package = (
- subprocess.check_output( # noqa S603
- [sys.executable, "setup.py", "--name"],
- cwd=mod_path,
- )
- .decode("utf8")
- .strip()
- )
- except subprocess.CalledProcessError:
- msg = (
- f"The Python package `{module}` is not a valid package, "
- "it is missing the `setup.py` file."
- )
- raise FileNotFoundError(msg) from None
- # Make sure the package is installed
- try:
- version(package)
- except PackageNotFoundError:
- subprocess.check_call([sys.executable, "-m", "pip", "install", "-e", mod_path]) # noqa S603
- sys.path.insert(0, mod_path)
- from setuptools import find_namespace_packages, find_packages
- package_candidates = [
- package.replace("-", "_"), # Module with the same name as package
- ]
- package_candidates.extend(find_packages(mod_path)) # Packages in the module path
- package_candidates.extend(
- find_namespace_packages(mod_path)
- ) # Namespace packages in the module path
- for package in package_candidates:
- try:
- m = importlib.import_module(package)
- if hasattr(m, "_jupyter_labextension_paths"):
- return m, m._jupyter_labextension_paths()
- except Exception as exc:
- errors.append(exc)
- msg = f"There is no labextension at {module}. Errors encountered: {errors}"
- raise ModuleNotFoundError(msg)
|