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.
		
		
		
		
		
			
		
			
				
	
	
		
			484 lines
		
	
	
		
			16 KiB
		
	
	
	
		
			Python
		
	
			
		
		
	
	
			484 lines
		
	
	
		
			16 KiB
		
	
	
	
		
			Python
		
	
"""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)
 |