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.
		
		
		
		
		
			
		
			
				
	
	
		
			201 lines
		
	
	
		
			9.4 KiB
		
	
	
	
		
			Python
		
	
			
		
		
	
	
			201 lines
		
	
	
		
			9.4 KiB
		
	
	
	
		
			Python
		
	
"""Kernel Provisioner Classes"""
 | 
						|
# Copyright (c) Jupyter Development Team.
 | 
						|
# Distributed under the terms of the Modified BSD License.
 | 
						|
import glob
 | 
						|
import sys
 | 
						|
from os import getenv, path
 | 
						|
from typing import Any, Dict, List
 | 
						|
 | 
						|
# See compatibility note on `group` keyword in https://docs.python.org/3/library/importlib.metadata.html#entry-points
 | 
						|
if sys.version_info < (3, 10):  # pragma: no cover
 | 
						|
    from importlib_metadata import EntryPoint, entry_points  # type:ignore[import-not-found]
 | 
						|
else:  # pragma: no cover
 | 
						|
    from importlib.metadata import EntryPoint, entry_points
 | 
						|
 | 
						|
from traitlets.config import SingletonConfigurable, Unicode, default
 | 
						|
 | 
						|
from .provisioner_base import KernelProvisionerBase
 | 
						|
 | 
						|
 | 
						|
class KernelProvisionerFactory(SingletonConfigurable):
 | 
						|
    """
 | 
						|
    :class:`KernelProvisionerFactory` is responsible for creating provisioner instances.
 | 
						|
 | 
						|
    A singleton instance, `KernelProvisionerFactory` is also used by the :class:`KernelSpecManager`
 | 
						|
    to validate `kernel_provisioner` references found in kernel specifications to confirm their
 | 
						|
    availability (in cases where the kernel specification references a kernel provisioner that has
 | 
						|
    not been installed into the current Python environment).
 | 
						|
 | 
						|
    It's ``default_provisioner_name`` attribute can be used to specify the default provisioner
 | 
						|
    to use when a kernel_spec is found to not reference a provisioner.  It's value defaults to
 | 
						|
    `"local-provisioner"` which identifies the local provisioner implemented by
 | 
						|
    :class:`LocalProvisioner`.
 | 
						|
    """
 | 
						|
 | 
						|
    GROUP_NAME = "jupyter_client.kernel_provisioners"
 | 
						|
    provisioners: Dict[str, EntryPoint] = {}
 | 
						|
 | 
						|
    default_provisioner_name_env = "JUPYTER_DEFAULT_PROVISIONER_NAME"
 | 
						|
    default_provisioner_name = Unicode(
 | 
						|
        config=True,
 | 
						|
        help="""Indicates the name of the provisioner to use when no kernel_provisioner
 | 
						|
                                       entry is present in the kernelspec.""",
 | 
						|
    )
 | 
						|
 | 
						|
    @default("default_provisioner_name")
 | 
						|
    def _default_provisioner_name_default(self) -> str:
 | 
						|
        """The default provisioner name."""
 | 
						|
        return getenv(self.default_provisioner_name_env, "local-provisioner")
 | 
						|
 | 
						|
    def __init__(self, **kwargs: Any) -> None:
 | 
						|
        """Initialize a kernel provisioner factory."""
 | 
						|
        super().__init__(**kwargs)
 | 
						|
 | 
						|
        for ep in KernelProvisionerFactory._get_all_provisioners():
 | 
						|
            self.provisioners[ep.name] = ep
 | 
						|
 | 
						|
    def is_provisioner_available(self, kernel_spec: Any) -> bool:
 | 
						|
        """
 | 
						|
        Reads the associated ``kernel_spec`` to determine the provisioner and returns whether it
 | 
						|
        exists as an entry_point (True) or not (False).  If the referenced provisioner is not
 | 
						|
        in the current cache or cannot be loaded via entry_points, a warning message is issued
 | 
						|
        indicating it is not available.
 | 
						|
        """
 | 
						|
        is_available: bool = True
 | 
						|
        provisioner_cfg = self._get_provisioner_config(kernel_spec)
 | 
						|
        provisioner_name = str(provisioner_cfg.get("provisioner_name"))
 | 
						|
        if not self._check_availability(provisioner_name):
 | 
						|
            is_available = False
 | 
						|
            self.log.warning(
 | 
						|
                f"Kernel '{kernel_spec.display_name}' is referencing a kernel "
 | 
						|
                f"provisioner ('{provisioner_name}') that is not available.  "
 | 
						|
                f"Ensure the appropriate package has been installed and retry."
 | 
						|
            )
 | 
						|
        return is_available
 | 
						|
 | 
						|
    def create_provisioner_instance(
 | 
						|
        self, kernel_id: str, kernel_spec: Any, parent: Any
 | 
						|
    ) -> KernelProvisionerBase:
 | 
						|
        """
 | 
						|
        Reads the associated ``kernel_spec`` to see if it has a `kernel_provisioner` stanza.
 | 
						|
        If one exists, it instantiates an instance.  If a kernel provisioner is not
 | 
						|
        specified in the kernel specification, a default provisioner stanza is fabricated
 | 
						|
        and instantiated corresponding to the current value of ``default_provisioner_name`` trait.
 | 
						|
        The instantiated instance is returned.
 | 
						|
 | 
						|
        If the provisioner is found to not exist (not registered via entry_points),
 | 
						|
        `ModuleNotFoundError` is raised.
 | 
						|
        """
 | 
						|
        provisioner_cfg = self._get_provisioner_config(kernel_spec)
 | 
						|
        provisioner_name = str(provisioner_cfg.get("provisioner_name"))
 | 
						|
        if not self._check_availability(provisioner_name):
 | 
						|
            msg = f"Kernel provisioner '{provisioner_name}' has not been registered."
 | 
						|
            raise ModuleNotFoundError(msg)
 | 
						|
 | 
						|
        self.log.debug(
 | 
						|
            f"Instantiating kernel '{kernel_spec.display_name}' with "
 | 
						|
            f"kernel provisioner: {provisioner_name}"
 | 
						|
        )
 | 
						|
        provisioner_class = self.provisioners[provisioner_name].load()
 | 
						|
        provisioner_config = provisioner_cfg.get("config")
 | 
						|
        provisioner: KernelProvisionerBase = provisioner_class(
 | 
						|
            kernel_id=kernel_id, kernel_spec=kernel_spec, parent=parent, **provisioner_config
 | 
						|
        )
 | 
						|
        return provisioner
 | 
						|
 | 
						|
    def _check_availability(self, provisioner_name: str) -> bool:
 | 
						|
        """
 | 
						|
        Checks that the given provisioner is available.
 | 
						|
 | 
						|
        If the given provisioner is not in the current set of loaded provisioners an attempt
 | 
						|
        is made to fetch the named entry point and, if successful, loads it into the cache.
 | 
						|
 | 
						|
        :param provisioner_name:
 | 
						|
        :return:
 | 
						|
        """
 | 
						|
        is_available = True
 | 
						|
        if provisioner_name not in self.provisioners:
 | 
						|
            try:
 | 
						|
                ep = self._get_provisioner(provisioner_name)
 | 
						|
                self.provisioners[provisioner_name] = ep  # Update cache
 | 
						|
            except Exception:
 | 
						|
                is_available = False
 | 
						|
        return is_available
 | 
						|
 | 
						|
    def _get_provisioner_config(self, kernel_spec: Any) -> Dict[str, Any]:
 | 
						|
        """
 | 
						|
        Return the kernel_provisioner stanza from the kernel_spec.
 | 
						|
 | 
						|
        Checks the kernel_spec's metadata dictionary for a kernel_provisioner entry.
 | 
						|
        If found, it is returned, else one is created relative to the DEFAULT_PROVISIONER
 | 
						|
        and returned.
 | 
						|
 | 
						|
        Parameters
 | 
						|
        ----------
 | 
						|
        kernel_spec : Any - this is a KernelSpec type but listed as Any to avoid circular import
 | 
						|
            The kernel specification object from which the provisioner dictionary is derived.
 | 
						|
 | 
						|
        Returns
 | 
						|
        -------
 | 
						|
        dict
 | 
						|
            The provisioner portion of the kernel_spec.  If one does not exist, it will contain
 | 
						|
            the default information.  If no `config` sub-dictionary exists, an empty `config`
 | 
						|
            dictionary will be added.
 | 
						|
        """
 | 
						|
        env_provisioner = kernel_spec.metadata.get("kernel_provisioner", {})
 | 
						|
        if "provisioner_name" in env_provisioner:  # If no provisioner_name, return default
 | 
						|
            if (
 | 
						|
                "config" not in env_provisioner
 | 
						|
            ):  # if provisioner_name, but no config stanza, add one
 | 
						|
                env_provisioner.update({"config": {}})
 | 
						|
            return env_provisioner  # Return what we found (plus config stanza if necessary)
 | 
						|
        return {"provisioner_name": self.default_provisioner_name, "config": {}}
 | 
						|
 | 
						|
    def get_provisioner_entries(self) -> Dict[str, str]:
 | 
						|
        """
 | 
						|
        Returns a dictionary of provisioner entries.
 | 
						|
 | 
						|
        The key is the provisioner name for its entry point.  The value is the colon-separated
 | 
						|
        string of the entry point's module name and object name.
 | 
						|
        """
 | 
						|
        entries = {}
 | 
						|
        for name, ep in self.provisioners.items():
 | 
						|
            entries[name] = ep.value
 | 
						|
        return entries
 | 
						|
 | 
						|
    @staticmethod
 | 
						|
    def _get_all_provisioners() -> List[EntryPoint]:
 | 
						|
        """Wrapper around entry_points (to fetch the set of provisioners) - primarily to facilitate testing."""
 | 
						|
        return entry_points(group=KernelProvisionerFactory.GROUP_NAME)
 | 
						|
 | 
						|
    def _get_provisioner(self, name: str) -> EntryPoint:
 | 
						|
        """Wrapper around entry_points (to fetch a single provisioner) - primarily to facilitate testing."""
 | 
						|
        eps = entry_points(group=KernelProvisionerFactory.GROUP_NAME, name=name)
 | 
						|
        if eps:
 | 
						|
            return eps[0]
 | 
						|
 | 
						|
        # Check if the entrypoint name is 'local-provisioner'.  Although this should never
 | 
						|
        # happen, we have seen cases where the previous distribution of jupyter_client has
 | 
						|
        # remained which doesn't include kernel-provisioner entrypoints (so 'local-provisioner'
 | 
						|
        # is deemed not found even though its definition is in THIS package).  In such cases,
 | 
						|
        # the entrypoints package uses what it first finds - which is the older distribution
 | 
						|
        # resulting in a violation of a supposed invariant condition.  To address this scenario,
 | 
						|
        # we will log a warning message indicating this situation, then build the entrypoint
 | 
						|
        # instance ourselves - since we have that information.
 | 
						|
        if name == "local-provisioner":
 | 
						|
            distros = glob.glob(f"{path.dirname(path.dirname(__file__))}-*")
 | 
						|
            self.log.warning(
 | 
						|
                f"Kernel Provisioning: The 'local-provisioner' is not found.  This is likely "
 | 
						|
                f"due to the presence of multiple jupyter_client distributions and a previous "
 | 
						|
                f"distribution is being used as the source for entrypoints - which does not "
 | 
						|
                f"include 'local-provisioner'.  That distribution should be removed such that "
 | 
						|
                f"only the version-appropriate distribution remains (version >= 7).  Until "
 | 
						|
                f"then, a 'local-provisioner' entrypoint will be automatically constructed "
 | 
						|
                f"and used.\nThe candidate distribution locations are: {distros}"
 | 
						|
            )
 | 
						|
            return EntryPoint(
 | 
						|
                "local-provisioner", "jupyter_client.provisioning", "LocalProvisioner"
 | 
						|
            )
 | 
						|
 | 
						|
        raise
 |