Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/nbformat/v4/convert.py: 13%

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

155 statements  

1"""Code for converting notebooks to and from v3.""" 

2 

3# Copyright (c) IPython Development Team. 

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

5from __future__ import annotations 

6 

7import json 

8import re 

9 

10from traitlets.log import get_logger 

11 

12from nbformat import v3, validator 

13from nbformat.corpus.words import generate_corpus_id as random_cell_id 

14from nbformat.notebooknode import NotebookNode 

15 

16from .nbbase import nbformat, nbformat_minor 

17 

18 

19def _warn_if_invalid(nb, version): 

20 """Log validation errors, if there are any.""" 

21 from nbformat import ValidationError, validate 

22 

23 try: 

24 validate(nb, version=version) 

25 except ValidationError as e: 

26 get_logger().error("Notebook JSON is not valid v%i: %s", version, e) 

27 

28 

29def upgrade(nb, from_version=None, from_minor=None): 

30 """Convert a notebook to latest v4. 

31 

32 Parameters 

33 ---------- 

34 nb : NotebookNode 

35 The Python representation of the notebook to convert. 

36 from_version : int 

37 The original version of the notebook to convert. 

38 from_minor : int 

39 The original minor version of the notebook to convert (only relevant for v >= 3). 

40 """ 

41 if not from_version: 

42 from_version = nb["nbformat"] 

43 if not from_minor: 

44 if "nbformat_minor" not in nb: 

45 if from_version == 4: 

46 msg = "The v4 notebook does not include the nbformat minor, which is needed." 

47 raise validator.ValidationError(msg) 

48 from_minor = 0 

49 else: 

50 from_minor = nb["nbformat_minor"] 

51 

52 if from_version == 3: 

53 # Validate the notebook before conversion 

54 _warn_if_invalid(nb, from_version) 

55 

56 # Mark the original nbformat so consumers know it has been converted 

57 orig_nbformat = nb.pop("orig_nbformat", None) 

58 orig_nbformat_minor = nb.pop("orig_nbformat_minor", None) 

59 nb.metadata.orig_nbformat = orig_nbformat or 3 

60 nb.metadata.orig_nbformat_minor = orig_nbformat_minor or 0 

61 

62 # Mark the new format 

63 nb.nbformat = nbformat 

64 nb.nbformat_minor = nbformat_minor 

65 

66 # remove worksheet(s) 

67 nb["cells"] = cells = [] 

68 # In the unlikely event of multiple worksheets, 

69 # they will be flattened 

70 for ws in nb.pop("worksheets", []): 

71 # upgrade each cell 

72 for cell in ws["cells"]: 

73 cells.append(upgrade_cell(cell)) 

74 # upgrade metadata 

75 nb.metadata.pop("name", "") 

76 nb.metadata.pop("signature", "") 

77 # Validate the converted notebook before returning it 

78 _warn_if_invalid(nb, nbformat) 

79 return nb 

80 if from_version == 4: 

81 if from_minor == nbformat_minor: 

82 return nb 

83 

84 # other versions migration code e.g. 

85 # if from_minor < 3: 

86 # if from_minor < 4: 

87 

88 if from_minor < 5: 

89 for cell in nb.cells: 

90 cell.id = random_cell_id() 

91 

92 nb.metadata.orig_nbformat_minor = from_minor 

93 nb.nbformat_minor = nbformat_minor 

94 

95 return nb 

96 raise ValueError( 

97 "Cannot convert a notebook directly from v%s to v4. " 

98 "Try using the nbformat.convert module." % from_version 

99 ) 

100 

101 

102def upgrade_cell(cell): 

103 """upgrade a cell from v3 to v4 

104 

105 heading cell: 

106 - -> markdown heading 

107 code cell: 

108 - remove language metadata 

109 - cell.input -> cell.source 

110 - cell.prompt_number -> cell.execution_count 

111 - update outputs 

112 """ 

113 cell.setdefault("metadata", NotebookNode()) 

114 cell.id = random_cell_id() 

115 if cell.cell_type == "code": 

116 cell.pop("language", "") 

117 if "collapsed" in cell: 

118 cell.metadata["collapsed"] = cell.pop("collapsed") 

119 cell.source = cell.pop("input", "") 

120 cell.execution_count = cell.pop("prompt_number", None) 

121 cell.outputs = upgrade_outputs(cell.outputs) 

122 elif cell.cell_type == "heading": 

123 cell.cell_type = "markdown" 

124 level = cell.pop("level", 1) 

125 cell.source = "{hashes} {single_line}".format( 

126 hashes="#" * level, 

127 single_line=" ".join(cell.get("source", "").splitlines()), 

128 ) 

129 elif cell.cell_type == "html": 

130 # Technically, this exists. It will never happen in practice. 

131 cell.cell_type = "markdown" 

132 return cell 

133 

134 

135def downgrade_cell(cell): 

136 """downgrade a cell from v4 to v3 

137 

138 code cell: 

139 - set cell.language 

140 - cell.input <- cell.source 

141 - cell.prompt_number <- cell.execution_count 

142 - update outputs 

143 markdown cell: 

144 - single-line heading -> heading cell 

145 """ 

146 if cell.cell_type == "code": 

147 cell.language = "python" 

148 cell.input = cell.pop("source", "") 

149 cell.prompt_number = cell.pop("execution_count", None) 

150 cell.collapsed = cell.metadata.pop("collapsed", False) 

151 cell.outputs = downgrade_outputs(cell.outputs) 

152 elif cell.cell_type == "markdown": 

153 source = cell.get("source", "") 

154 if "\n" not in source and source.startswith("#"): 

155 match = re.match(r"(#+)\s*(.*)", source) 

156 assert match is not None 

157 prefix, text = match.groups() 

158 cell.cell_type = "heading" 

159 cell.source = text 

160 cell.level = len(prefix) 

161 cell.pop("id", None) 

162 cell.pop("attachments", None) 

163 return cell 

164 

165 

166_mime_map = { 

167 "text": "text/plain", 

168 "html": "text/html", 

169 "svg": "image/svg+xml", 

170 "png": "image/png", 

171 "jpeg": "image/jpeg", 

172 "latex": "text/latex", 

173 "json": "application/json", 

174 "javascript": "application/javascript", 

175} 

176 

177 

178def to_mime_key(d): 

179 """convert dict with v3 aliases to plain mime-type keys""" 

180 for alias, mime in _mime_map.items(): 

181 if alias in d: 

182 d[mime] = d.pop(alias) 

183 return d 

184 

185 

186def from_mime_key(d): 

187 """convert dict with mime-type keys to v3 aliases""" 

188 d2 = {} 

189 for alias, mime in _mime_map.items(): 

190 if mime in d: 

191 d2[alias] = d[mime] 

192 return d2 

193 

194 

195def upgrade_output(output): 

196 """upgrade a single code cell output from v3 to v4 

197 

198 - pyout -> execute_result 

199 - pyerr -> error 

200 - output.type -> output.data.mime/type 

201 - mime-type keys 

202 - stream.stream -> stream.name 

203 """ 

204 if output["output_type"] in {"pyout", "display_data"}: 

205 output.setdefault("metadata", NotebookNode()) 

206 if output["output_type"] == "pyout": 

207 output["output_type"] = "execute_result" 

208 output["execution_count"] = output.pop("prompt_number", None) 

209 

210 # move output data into data sub-dict 

211 data = {} 

212 for key in list(output): 

213 if key in {"output_type", "execution_count", "metadata"}: 

214 continue 

215 data[key] = output.pop(key) 

216 to_mime_key(data) 

217 output["data"] = data 

218 to_mime_key(output.metadata) 

219 if "application/json" in data: 

220 data["application/json"] = json.loads(data["application/json"]) 

221 # promote ascii bytes (from v2) to unicode 

222 for key in ("image/png", "image/jpeg"): 

223 if key in data and isinstance(data[key], bytes): 

224 data[key] = data[key].decode("ascii") 

225 elif output["output_type"] == "pyerr": 

226 output["output_type"] = "error" 

227 elif output["output_type"] == "stream": 

228 output["name"] = output.pop("stream", "stdout") 

229 return output 

230 

231 

232def downgrade_output(output): 

233 """downgrade a single code cell output to v3 from v4 

234 

235 - pyout <- execute_result 

236 - pyerr <- error 

237 - output.data.mime/type -> output.type 

238 - un-mime-type keys 

239 - stream.stream <- stream.name 

240 """ 

241 if output["output_type"] in {"execute_result", "display_data"}: 

242 if output["output_type"] == "execute_result": 

243 output["output_type"] = "pyout" 

244 output["prompt_number"] = output.pop("execution_count", None) 

245 

246 # promote data dict to top-level output namespace 

247 data = output.pop("data", {}) 

248 if "application/json" in data: 

249 data["application/json"] = json.dumps(data["application/json"]) 

250 data = from_mime_key(data) 

251 output.update(data) 

252 from_mime_key(output.get("metadata", {})) 

253 elif output["output_type"] == "error": 

254 output["output_type"] = "pyerr" 

255 elif output["output_type"] == "stream": 

256 output["stream"] = output.pop("name") 

257 return output 

258 

259 

260def upgrade_outputs(outputs): 

261 """upgrade outputs of a code cell from v3 to v4""" 

262 return [upgrade_output(op) for op in outputs] 

263 

264 

265def downgrade_outputs(outputs): 

266 """downgrade outputs of a code cell to v3 from v4""" 

267 return [downgrade_output(op) for op in outputs] 

268 

269 

270def downgrade(nb): 

271 """Convert a v4 notebook to v3. 

272 

273 Parameters 

274 ---------- 

275 nb : NotebookNode 

276 The Python representation of the notebook to convert. 

277 """ 

278 if nb.nbformat != nbformat: 

279 return nb 

280 

281 # Validate the notebook before conversion 

282 _warn_if_invalid(nb, nbformat) 

283 

284 nb.nbformat = v3.nbformat 

285 nb.nbformat_minor = v3.nbformat_minor 

286 cells = [downgrade_cell(cell) for cell in nb.pop("cells")] 

287 nb.worksheets = [v3.new_worksheet(cells=cells)] 

288 nb.metadata.setdefault("name", "") 

289 

290 # Validate the converted notebook before returning it 

291 _warn_if_invalid(nb, v3.nbformat) 

292 

293 nb.orig_nbformat = nb.metadata.pop("orig_nbformat", nbformat) 

294 nb.orig_nbformat_minor = nb.metadata.pop("orig_nbformat_minor", nbformat_minor) 

295 

296 return nb