Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/jupyter_client/provisioning/factory.py: 32%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

72 statements  

1"""Kernel Provisioner Classes""" 

2 

3# Copyright (c) Jupyter Development Team. 

4# Distributed under the terms of the Modified BSD License. 

5import glob 

6 

7# See compatibility note on `group` keyword in https://docs.python.org/3/library/importlib.metadata.html#entry-points 

8from importlib.metadata import EntryPoint, entry_points 

9from os import getenv, path 

10from typing import Any 

11 

12from traitlets.config import SingletonConfigurable, Unicode, default 

13 

14from .provisioner_base import KernelProvisionerBase 

15 

16 

17class KernelProvisionerFactory(SingletonConfigurable): 

18 """ 

19 :class:`KernelProvisionerFactory` is responsible for creating provisioner instances. 

20 

21 A singleton instance, `KernelProvisionerFactory` is also used by the :class:`KernelSpecManager` 

22 to validate `kernel_provisioner` references found in kernel specifications to confirm their 

23 availability (in cases where the kernel specification references a kernel provisioner that has 

24 not been installed into the current Python environment). 

25 

26 It's ``default_provisioner_name`` attribute can be used to specify the default provisioner 

27 to use when a kernel_spec is found to not reference a provisioner. It's value defaults to 

28 `"local-provisioner"` which identifies the local provisioner implemented by 

29 :class:`LocalProvisioner`. 

30 """ 

31 

32 GROUP_NAME = "jupyter_client.kernel_provisioners" 

33 provisioners: dict[str, EntryPoint] = {} 

34 

35 default_provisioner_name_env = "JUPYTER_DEFAULT_PROVISIONER_NAME" 

36 default_provisioner_name = Unicode( 

37 config=True, 

38 help="""Indicates the name of the provisioner to use when no kernel_provisioner 

39 entry is present in the kernelspec.""", 

40 ) 

41 

42 @default("default_provisioner_name") 

43 def _default_provisioner_name_default(self) -> str: 

44 """The default provisioner name.""" 

45 return getenv(self.default_provisioner_name_env, "local-provisioner") 

46 

47 def __init__(self, **kwargs: Any) -> None: 

48 """Initialize a kernel provisioner factory.""" 

49 super().__init__(**kwargs) 

50 

51 for ep in KernelProvisionerFactory._get_all_provisioners(): 

52 self.provisioners[ep.name] = ep 

53 

54 def is_provisioner_available(self, kernel_spec: Any) -> bool: 

55 """ 

56 Reads the associated ``kernel_spec`` to determine the provisioner and returns whether it 

57 exists as an entry_point (True) or not (False). If the referenced provisioner is not 

58 in the current cache or cannot be loaded via entry_points, a warning message is issued 

59 indicating it is not available. 

60 """ 

61 is_available: bool = True 

62 provisioner_cfg = self._get_provisioner_config(kernel_spec) 

63 provisioner_name = str(provisioner_cfg.get("provisioner_name")) 

64 if not self._check_availability(provisioner_name): 

65 is_available = False 

66 self.log.warning( 

67 f"Kernel '{kernel_spec.display_name}' is referencing a kernel " 

68 f"provisioner ('{provisioner_name}') that is not available. " 

69 f"Ensure the appropriate package has been installed and retry." 

70 ) 

71 return is_available 

72 

73 def create_provisioner_instance( 

74 self, kernel_id: str, kernel_spec: Any, parent: Any 

75 ) -> KernelProvisionerBase: 

76 """ 

77 Reads the associated ``kernel_spec`` to see if it has a `kernel_provisioner` stanza. 

78 If one exists, it instantiates an instance. If a kernel provisioner is not 

79 specified in the kernel specification, a default provisioner stanza is fabricated 

80 and instantiated corresponding to the current value of ``default_provisioner_name`` trait. 

81 The instantiated instance is returned. 

82 

83 If the provisioner is found to not exist (not registered via entry_points), 

84 `ModuleNotFoundError` is raised. 

85 """ 

86 provisioner_cfg = self._get_provisioner_config(kernel_spec) 

87 provisioner_name = str(provisioner_cfg.get("provisioner_name")) 

88 if not self._check_availability(provisioner_name): 

89 msg = f"Kernel provisioner '{provisioner_name}' has not been registered." 

90 raise ModuleNotFoundError(msg) 

91 

92 self.log.debug( 

93 f"Instantiating kernel '{kernel_spec.display_name}' with " 

94 f"kernel provisioner: {provisioner_name}" 

95 ) 

96 provisioner_class = self.provisioners[provisioner_name].load() 

97 provisioner_config = provisioner_cfg.get("config") 

98 provisioner: KernelProvisionerBase = provisioner_class( 

99 kernel_id=kernel_id, kernel_spec=kernel_spec, parent=parent, **provisioner_config 

100 ) 

101 return provisioner 

102 

103 def _check_availability(self, provisioner_name: str) -> bool: 

104 """ 

105 Checks that the given provisioner is available. 

106 

107 If the given provisioner is not in the current set of loaded provisioners an attempt 

108 is made to fetch the named entry point and, if successful, loads it into the cache. 

109 

110 :param provisioner_name: 

111 :return: 

112 """ 

113 is_available = True 

114 if provisioner_name not in self.provisioners: 

115 try: 

116 ep = self._get_provisioner(provisioner_name) 

117 self.provisioners[provisioner_name] = ep # Update cache 

118 except Exception: 

119 is_available = False 

120 return is_available 

121 

122 def _get_provisioner_config(self, kernel_spec: Any) -> dict[str, Any]: 

123 """ 

124 Return the kernel_provisioner stanza from the kernel_spec. 

125 

126 Checks the kernel_spec's metadata dictionary for a kernel_provisioner entry. 

127 If found, it is returned, else one is created relative to the DEFAULT_PROVISIONER 

128 and returned. 

129 

130 Parameters 

131 ---------- 

132 kernel_spec : Any - this is a KernelSpec type but listed as Any to avoid circular import 

133 The kernel specification object from which the provisioner dictionary is derived. 

134 

135 Returns 

136 ------- 

137 dict 

138 The provisioner portion of the kernel_spec. If one does not exist, it will contain 

139 the default information. If no `config` sub-dictionary exists, an empty `config` 

140 dictionary will be added. 

141 """ 

142 env_provisioner = kernel_spec.metadata.get("kernel_provisioner", {}) 

143 if "provisioner_name" in env_provisioner: # If no provisioner_name, return default 

144 if ( 

145 "config" not in env_provisioner 

146 ): # if provisioner_name, but no config stanza, add one 

147 env_provisioner.update({"config": {}}) 

148 return env_provisioner # Return what we found (plus config stanza if necessary) 

149 return {"provisioner_name": self.default_provisioner_name, "config": {}} 

150 

151 def get_provisioner_entries(self) -> dict[str, str]: 

152 """ 

153 Returns a dictionary of provisioner entries. 

154 

155 The key is the provisioner name for its entry point. The value is the colon-separated 

156 string of the entry point's module name and object name. 

157 """ 

158 entries = {} 

159 for name, ep in self.provisioners.items(): 

160 entries[name] = ep.value 

161 return entries 

162 

163 @staticmethod 

164 def _get_all_provisioners() -> list[EntryPoint]: 

165 """Wrapper around entry_points (to fetch the set of provisioners) - primarily to facilitate testing.""" 

166 return entry_points(group=KernelProvisionerFactory.GROUP_NAME) 

167 

168 def _get_provisioner(self, name: str) -> EntryPoint: 

169 """Wrapper around entry_points (to fetch a single provisioner) - primarily to facilitate testing.""" 

170 eps = entry_points(group=KernelProvisionerFactory.GROUP_NAME, name=name) 

171 if eps: 

172 return eps[0] 

173 

174 # Check if the entrypoint name is 'local-provisioner'. Although this should never 

175 # happen, we have seen cases where the previous distribution of jupyter_client has 

176 # remained which doesn't include kernel-provisioner entrypoints (so 'local-provisioner' 

177 # is deemed not found even though its definition is in THIS package). In such cases, 

178 # the entrypoints package uses what it first finds - which is the older distribution 

179 # resulting in a violation of a supposed invariant condition. To address this scenario, 

180 # we will log a warning message indicating this situation, then build the entrypoint 

181 # instance ourselves - since we have that information. 

182 if name == "local-provisioner": 

183 distros = glob.glob(f"{path.dirname(path.dirname(__file__))}-*") 

184 self.log.warning( 

185 f"Kernel Provisioning: The 'local-provisioner' is not found. This is likely " 

186 f"due to the presence of multiple jupyter_client distributions and a previous " 

187 f"distribution is being used as the source for entrypoints - which does not " 

188 f"include 'local-provisioner'. That distribution should be removed such that " 

189 f"only the version-appropriate distribution remains (version >= 7). Until " 

190 f"then, a 'local-provisioner' entrypoint will be automatically constructed " 

191 f"and used.\nThe candidate distribution locations are: {distros}" 

192 ) 

193 return EntryPoint( 

194 "local-provisioner", "jupyter_client.provisioning", "LocalProvisioner" 

195 ) 

196 err_message = "Was unable to find a provisioner" 

197 raise RuntimeError(err_message)