Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/nbconvert/exporters/templateexporter.py: 29%

316 statements  

« prev     ^ index     » next       coverage.py v7.2.7, created at 2023-07-01 06:54 +0000

1"""This module defines TemplateExporter, a highly configurable converter 

2that uses Jinja2 to export notebook files into different formats. 

3""" 

4 

5# Copyright (c) IPython Development Team. 

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

7 

8 

9import html 

10import json 

11import os 

12import typing as t 

13import uuid 

14import warnings 

15from pathlib import Path 

16 

17from jinja2 import ( 

18 BaseLoader, 

19 ChoiceLoader, 

20 DictLoader, 

21 Environment, 

22 FileSystemLoader, 

23 TemplateNotFound, 

24) 

25from jupyter_core.paths import jupyter_path 

26from nbformat import NotebookNode 

27from traitlets import Bool, Dict, HasTraits, List, Unicode, default, observe, validate 

28from traitlets.config import Config 

29from traitlets.utils.importstring import import_item 

30 

31from nbconvert import filters 

32 

33from .exporter import Exporter 

34 

35# Jinja2 extensions to load. 

36JINJA_EXTENSIONS = ["jinja2.ext.loopcontrols"] 

37 

38ROOT = os.path.dirname(__file__) 

39DEV_MODE = os.path.exists(os.path.join(ROOT, "../../.git")) 

40 

41 

42default_filters = { 

43 "indent": filters.indent, 

44 "markdown2html": filters.markdown2html, 

45 "markdown2asciidoc": filters.markdown2asciidoc, 

46 "ansi2html": filters.ansi2html, 

47 "filter_data_type": filters.DataTypeFilter, 

48 "get_lines": filters.get_lines, 

49 "highlight2html": filters.Highlight2HTML, 

50 "highlight2latex": filters.Highlight2Latex, 

51 "ipython2python": filters.ipython2python, 

52 "posix_path": filters.posix_path, 

53 "markdown2latex": filters.markdown2latex, 

54 "markdown2rst": filters.markdown2rst, 

55 "comment_lines": filters.comment_lines, 

56 "strip_ansi": filters.strip_ansi, 

57 "strip_dollars": filters.strip_dollars, 

58 "strip_files_prefix": filters.strip_files_prefix, 

59 "html2text": filters.html2text, 

60 "add_anchor": filters.add_anchor, 

61 "ansi2latex": filters.ansi2latex, 

62 "wrap_text": filters.wrap_text, 

63 "escape_latex": filters.escape_latex, 

64 "citation2latex": filters.citation2latex, 

65 "path2url": filters.path2url, 

66 "add_prompts": filters.add_prompts, 

67 "ascii_only": filters.ascii_only, 

68 "prevent_list_blocks": filters.prevent_list_blocks, 

69 "get_metadata": filters.get_metadata, 

70 "convert_pandoc": filters.convert_pandoc, 

71 "json_dumps": json.dumps, 

72 # For removing any HTML 

73 "escape_html": lambda s: html.escape(str(s)), 

74 "escape_html_keep_quotes": lambda s: html.escape(str(s), quote=False), 

75 # For sanitizing HTML for any XSS 

76 "clean_html": filters.clean_html, 

77 "strip_trailing_newline": filters.strip_trailing_newline, 

78 "text_base64": filters.text_base64, 

79} 

80 

81 

82# copy of https://github.com/jupyter/jupyter_server/blob/b62458a7f5ad6b5246d2f142258dedaa409de5d9/jupyter_server/config_manager.py#L19 

83def recursive_update(target, new): 

84 """Recursively update one dictionary using another. 

85 None values will delete their keys. 

86 """ 

87 for k, v in new.items(): 

88 if isinstance(v, dict): 

89 if k not in target: 

90 target[k] = {} 

91 recursive_update(target[k], v) 

92 if not target[k]: 

93 # Prune empty subdicts 

94 del target[k] 

95 

96 elif v is None: 

97 target.pop(k, None) 

98 

99 else: 

100 target[k] = v 

101 return target # return for convenience 

102 

103 

104# define function at the top level to avoid pickle errors 

105def deprecated(msg): 

106 """Emit a deprecation warning.""" 

107 warnings.warn(msg, DeprecationWarning, stacklevel=2) 

108 

109 

110class ExtensionTolerantLoader(BaseLoader): 

111 """A template loader which optionally adds a given extension when searching. 

112 

113 Constructor takes two arguments: *loader* is another Jinja loader instance 

114 to wrap. *extension* is the extension, which will be added to the template 

115 name if finding the template without it fails. This should include the dot, 

116 e.g. '.tpl'. 

117 """ 

118 

119 def __init__(self, loader, extension): 

120 """Initialize the loader.""" 

121 self.loader = loader 

122 self.extension = extension 

123 

124 def get_source(self, environment, template): 

125 """Get the source for a template.""" 

126 try: 

127 return self.loader.get_source(environment, template) 

128 except TemplateNotFound: 

129 if template.endswith(self.extension): 

130 raise TemplateNotFound(template) from None 

131 return self.loader.get_source(environment, template + self.extension) 

132 

133 def list_templates(self): 

134 """List available templates.""" 

135 return self.loader.list_templates() 

136 

137 

138class TemplateExporter(Exporter): 

139 """ 

140 Exports notebooks into other file formats. Uses Jinja 2 templating engine 

141 to output new formats. Inherit from this class if you are creating a new 

142 template type along with new filters/preprocessors. If the filters/ 

143 preprocessors provided by default suffice, there is no need to inherit from 

144 this class. Instead, override the template_file and file_extension 

145 traits via a config file. 

146 

147 Filters available by default for templates: 

148 

149 {filters} 

150 """ 

151 

152 # finish the docstring 

153 __doc__ = __doc__.format(filters="- " + "\n - ".join(sorted(default_filters.keys()))) # noqa 

154 

155 _template_cached = None 

156 

157 def _invalidate_template_cache(self, change=None): 

158 self._template_cached = None 

159 

160 @property 

161 def template(self): 

162 if self._template_cached is None: 

163 self._template_cached = self._load_template() 

164 return self._template_cached 

165 

166 _environment_cached = None 

167 

168 def _invalidate_environment_cache(self, change=None): 

169 self._environment_cached = None 

170 self._invalidate_template_cache() 

171 

172 @property 

173 def environment(self): 

174 if self._environment_cached is None: 

175 self._environment_cached = self._create_environment() 

176 return self._environment_cached 

177 

178 @property 

179 def default_config(self): 

180 c = Config( 

181 { 

182 "RegexRemovePreprocessor": {"enabled": True}, 

183 "TagRemovePreprocessor": {"enabled": True}, 

184 } 

185 ) 

186 if super().default_config: 

187 c2 = super().default_config.copy() 

188 c2.merge(c) 

189 c = c2 

190 return c 

191 

192 template_name = Unicode(help="Name of the template to use").tag( 

193 config=True, affects_template=True 

194 ) 

195 

196 template_file = Unicode(None, allow_none=True, help="Name of the template file to use").tag( 

197 config=True, affects_template=True 

198 ) 

199 

200 raw_template = Unicode("", help="raw template string").tag(affects_environment=True) 

201 

202 enable_async = Bool(False, help="Enable Jinja async template execution").tag( 

203 affects_environment=True 

204 ) 

205 

206 _last_template_file = "" 

207 _raw_template_key = "<memory>" 

208 

209 @validate("template_name") 

210 def _template_name_validate(self, change): 

211 template_name = change["value"] 

212 if template_name and template_name.endswith(".tpl"): 

213 warnings.warn( 

214 f"5.x style template name passed '{self.template_name}'. Use --template-name for the template directory with a index.<ext>.j2 file and/or --template-file to denote a different template.", 

215 DeprecationWarning, 

216 stacklevel=2, 

217 ) 

218 directory, self.template_file = os.path.split(self.template_name) 

219 if directory: 

220 directory, template_name = os.path.split(directory) 

221 if directory and os.path.isabs(directory): 

222 self.extra_template_basedirs = [directory] 

223 return template_name 

224 

225 @observe("template_file") 

226 def _template_file_changed(self, change): 

227 new = change["new"] 

228 if new == "default": 

229 self.template_file = self.default_template # type:ignore 

230 return 

231 # check if template_file is a file path 

232 # rather than a name already on template_path 

233 full_path = os.path.abspath(new) 

234 if os.path.isfile(full_path): 

235 directory, self.template_file = os.path.split(full_path) 

236 self.extra_template_paths = [directory, *self.extra_template_paths] 

237 # While not strictly an invalid template file name, the extension hints that there isn't a template directory involved 

238 if self.template_file.endswith(".tpl"): 

239 warnings.warn( 

240 f"5.x style template file passed '{new}'. Use --template-name for the template directory with a index.<ext>.j2 file and/or --template-file to denote a different template.", 

241 DeprecationWarning, 

242 stacklevel=2, 

243 ) 

244 

245 @default("template_file") 

246 def _template_file_default(self): 

247 if self.template_extension: 

248 return "index" + self.template_extension 

249 

250 @observe("raw_template") 

251 def _raw_template_changed(self, change): 

252 if not change["new"]: 

253 self.template_file = self._last_template_file 

254 self._invalidate_template_cache() 

255 

256 template_paths = List(["."]).tag(config=True, affects_environment=True) 

257 extra_template_basedirs = List().tag(config=True, affects_environment=True) 

258 extra_template_paths = List([]).tag(config=True, affects_environment=True) 

259 

260 @default("extra_template_basedirs") 

261 def _default_extra_template_basedirs(self): 

262 return [os.getcwd()] 

263 

264 # Extension that the template files use. 

265 template_extension = Unicode().tag(config=True, affects_environment=True) 

266 

267 template_data_paths = List( 

268 jupyter_path("nbconvert", "templates"), help="Path where templates can be installed too." 

269 ).tag(affects_environment=True) 

270 

271 # Extension that the template files use. 

272 template_extension = Unicode().tag(config=True, affects_environment=True) 

273 

274 @default("template_extension") 

275 def _template_extension_default(self): 

276 if self.file_extension: 

277 return self.file_extension + ".j2" 

278 else: 

279 return self.file_extension 

280 

281 exclude_input = Bool( 

282 False, help="This allows you to exclude code cell inputs from all templates if set to True." 

283 ).tag(config=True) 

284 

285 exclude_input_prompt = Bool( 

286 False, help="This allows you to exclude input prompts from all templates if set to True." 

287 ).tag(config=True) 

288 

289 exclude_output = Bool( 

290 False, 

291 help="This allows you to exclude code cell outputs from all templates if set to True.", 

292 ).tag(config=True) 

293 

294 exclude_output_prompt = Bool( 

295 False, help="This allows you to exclude output prompts from all templates if set to True." 

296 ).tag(config=True) 

297 

298 exclude_output_stdin = Bool( 

299 True, 

300 help="This allows you to exclude output of stdin stream from lab template if set to True.", 

301 ).tag(config=True) 

302 

303 exclude_code_cell = Bool( 

304 False, help="This allows you to exclude code cells from all templates if set to True." 

305 ).tag(config=True) 

306 

307 exclude_markdown = Bool( 

308 False, help="This allows you to exclude markdown cells from all templates if set to True." 

309 ).tag(config=True) 

310 

311 exclude_raw = Bool( 

312 False, help="This allows you to exclude raw cells from all templates if set to True." 

313 ).tag(config=True) 

314 

315 exclude_unknown = Bool( 

316 False, help="This allows you to exclude unknown cells from all templates if set to True." 

317 ).tag(config=True) 

318 

319 extra_loaders = List( 

320 help="Jinja loaders to find templates. Will be tried in order " 

321 "before the default FileSystem ones.", 

322 ).tag(affects_environment=True) 

323 

324 filters = Dict( 

325 help="""Dictionary of filters, by name and namespace, to add to the Jinja 

326 environment.""" 

327 ).tag(config=True, affects_environment=True) 

328 

329 raw_mimetypes = List( 

330 help="""formats of raw cells to be included in this Exporter's output.""" 

331 ).tag(config=True) 

332 

333 @default("raw_mimetypes") 

334 def _raw_mimetypes_default(self): 

335 return [self.output_mimetype, ""] 

336 

337 # TODO: passing config is wrong, but changing this revealed more complicated issues 

338 def __init__(self, config=None, **kw): 

339 """ 

340 Public constructor 

341 

342 Parameters 

343 ---------- 

344 config : config 

345 User configuration instance. 

346 extra_loaders : list[of Jinja Loaders] 

347 ordered list of Jinja loader to find templates. Will be tried in order 

348 before the default FileSystem ones. 

349 template_file : str (optional, kw arg) 

350 Template to use when exporting. 

351 """ 

352 super().__init__(config=config, **kw) 

353 

354 self.observe( 

355 self._invalidate_environment_cache, list(self.traits(affects_environment=True)) 

356 ) 

357 self.observe(self._invalidate_template_cache, list(self.traits(affects_template=True))) 

358 

359 def _load_template(self): 

360 """Load the Jinja template object from the template file 

361 

362 This is triggered by various trait changes that would change the template. 

363 """ 

364 

365 # this gives precedence to a raw_template if present 

366 with self.hold_trait_notifications(): 

367 if self.template_file != self._raw_template_key: 

368 self._last_template_file = self.template_file 

369 if self.raw_template: 

370 self.template_file = self._raw_template_key 

371 

372 if not self.template_file: 

373 msg = "No template_file specified!" 

374 raise ValueError(msg) 

375 

376 # First try to load the 

377 # template by name with extension added, then try loading the template 

378 # as if the name is explicitly specified. 

379 template_file = self.template_file 

380 self.log.debug("Attempting to load template %s", template_file) 

381 self.log.debug(" template_paths: %s", os.pathsep.join(self.template_paths)) 

382 return self.environment.get_template(template_file) 

383 

384 def from_filename( # type:ignore 

385 self, filename: str, resources: t.Optional[dict] = None, **kw: t.Any 

386 ) -> t.Tuple[str, dict]: 

387 """Convert a notebook from a filename.""" 

388 return super().from_filename(filename, resources, **kw) # type:ignore 

389 

390 def from_file( # type:ignore 

391 self, file_stream: t.Any, resources: t.Optional[dict] = None, **kw: t.Any 

392 ) -> t.Tuple[str, dict]: 

393 """Convert a notebook from a file.""" 

394 return super().from_file(file_stream, resources, **kw) # type:ignore 

395 

396 def from_notebook_node( # type:ignore 

397 self, nb: NotebookNode, resources: t.Optional[dict] = None, **kw: t.Any 

398 ) -> t.Tuple[str, dict]: 

399 """ 

400 Convert a notebook from a notebook node instance. 

401 

402 Parameters 

403 ---------- 

404 nb : :class:`~nbformat.NotebookNode` 

405 Notebook node 

406 resources : dict 

407 Additional resources that can be accessed read/write by 

408 preprocessors and filters. 

409 """ 

410 nb_copy, resources = super().from_notebook_node(nb, resources, **kw) 

411 resources.setdefault("raw_mimetypes", self.raw_mimetypes) 

412 resources["global_content_filter"] = { 

413 "include_code": not self.exclude_code_cell, 

414 "include_markdown": not self.exclude_markdown, 

415 "include_raw": not self.exclude_raw, 

416 "include_unknown": not self.exclude_unknown, 

417 "include_input": not self.exclude_input, 

418 "include_output": not self.exclude_output, 

419 "include_output_stdin": not self.exclude_output_stdin, 

420 "include_input_prompt": not self.exclude_input_prompt, 

421 "include_output_prompt": not self.exclude_output_prompt, 

422 "no_prompt": self.exclude_input_prompt and self.exclude_output_prompt, 

423 } 

424 

425 # Top level variables are passed to the template_exporter here. 

426 output = self.template.render(nb=nb_copy, resources=resources) 

427 output = output.lstrip("\r\n") 

428 return output, resources 

429 

430 def _register_filter(self, environ, name, jinja_filter): 

431 """ 

432 Register a filter. 

433 A filter is a function that accepts and acts on one string. 

434 The filters are accessible within the Jinja templating engine. 

435 

436 Parameters 

437 ---------- 

438 name : str 

439 name to give the filter in the Jinja engine 

440 filter : filter 

441 """ 

442 if jinja_filter is None: 

443 msg = "filter" 

444 raise TypeError(msg) 

445 isclass = isinstance(jinja_filter, type) 

446 constructed = not isclass 

447 

448 # Handle filter's registration based on it's type 

449 if constructed and isinstance(jinja_filter, (str,)): 

450 # filter is a string, import the namespace and recursively call 

451 # this register_filter method 

452 filter_cls = import_item(jinja_filter) 

453 return self._register_filter(environ, name, filter_cls) 

454 

455 if constructed and hasattr(jinja_filter, "__call__"): # noqa 

456 # filter is a function, no need to construct it. 

457 environ.filters[name] = jinja_filter 

458 return jinja_filter 

459 

460 elif isclass and issubclass(jinja_filter, HasTraits): 

461 # filter is configurable. Make sure to pass in new default for 

462 # the enabled flag if one was specified. 

463 filter_instance = jinja_filter(parent=self) 

464 self._register_filter(environ, name, filter_instance) 

465 

466 elif isclass: 

467 # filter is not configurable, construct it 

468 filter_instance = jinja_filter() 

469 self._register_filter(environ, name, filter_instance) 

470 

471 else: 

472 # filter is an instance of something without a __call__ 

473 # attribute. 

474 msg = "filter" 

475 raise TypeError(msg) 

476 

477 def register_filter(self, name, jinja_filter): 

478 """ 

479 Register a filter. 

480 A filter is a function that accepts and acts on one string. 

481 The filters are accessible within the Jinja templating engine. 

482 

483 Parameters 

484 ---------- 

485 name : str 

486 name to give the filter in the Jinja engine 

487 filter : filter 

488 """ 

489 return self._register_filter(self.environment, name, jinja_filter) 

490 

491 def default_filters(self): 

492 """Override in subclasses to provide extra filters. 

493 

494 This should return an iterable of 2-tuples: (name, class-or-function). 

495 You should call the method on the parent class and include the filters 

496 it provides. 

497 

498 If a name is repeated, the last filter provided wins. Filters from 

499 user-supplied config win over filters provided by classes. 

500 """ 

501 return default_filters.items() 

502 

503 def _create_environment(self): 

504 """ 

505 Create the Jinja templating environment. 

506 """ 

507 paths = self.template_paths 

508 self.log.debug("Template paths:\n\t%s", "\n\t".join(paths)) 

509 

510 loaders = [ 

511 *self.extra_loaders, 

512 ExtensionTolerantLoader(FileSystemLoader(paths), self.template_extension), 

513 DictLoader({self._raw_template_key: self.raw_template}), 

514 ] 

515 environment = Environment( # noqa 

516 loader=ChoiceLoader(loaders), 

517 extensions=JINJA_EXTENSIONS, 

518 enable_async=self.enable_async, 

519 ) 

520 

521 environment.globals["uuid4"] = uuid.uuid4 

522 

523 # Add default filters to the Jinja2 environment 

524 for key, value in self.default_filters(): 

525 self._register_filter(environment, key, value) 

526 

527 # Load user filters. Overwrite existing filters if need be. 

528 if self.filters: 

529 for key, user_filter in self.filters.items(): 

530 self._register_filter(environment, key, user_filter) 

531 

532 return environment 

533 

534 def _init_preprocessors(self): 

535 super()._init_preprocessors() 

536 conf = self._get_conf() 

537 preprocessors = conf.get("preprocessors", {}) 

538 # preprocessors is a dict for three reasons 

539 # * We rely on recursive_update, which can only merge dicts, lists will be overwritten 

540 # * We can use the key with numerical prefixing to guarantee ordering (/etc/*.d/XY-file style) 

541 # * We can disable preprocessors by overwriting the value with None 

542 for _, preprocessor in sorted(preprocessors.items(), key=lambda x: x[0]): 

543 if preprocessor is not None: 

544 kwargs = preprocessor.copy() 

545 preprocessor_cls = kwargs.pop("type") 

546 preprocessor_cls = import_item(preprocessor_cls) 

547 if preprocessor_cls.__name__ in self.config: 

548 kwargs.update(self.config[preprocessor_cls.__name__]) 

549 preprocessor = preprocessor_cls(**kwargs) # noqa 

550 self.register_preprocessor(preprocessor) 

551 

552 def _get_conf(self): 

553 conf: dict = {} # the configuration once all conf files are merged 

554 for path in map(Path, self.template_paths): 

555 conf_path = path / "conf.json" 

556 if conf_path.exists(): 

557 with conf_path.open() as f: 

558 conf = recursive_update(conf, json.load(f)) 

559 return conf 

560 

561 @default("template_paths") 

562 def _template_paths(self, prune=True, root_dirs=None): 

563 paths = [] 

564 root_dirs = self.get_prefix_root_dirs() 

565 template_names = self.get_template_names() 

566 for template_name in template_names: 

567 for base_dir in self.extra_template_basedirs: 

568 path = os.path.join(base_dir, template_name) 

569 if not prune or os.path.exists(path): 

570 paths.append(path) 

571 for root_dir in root_dirs: 

572 base_dir = os.path.join(root_dir, "nbconvert", "templates") 

573 path = os.path.join(base_dir, template_name) 

574 if not prune or os.path.exists(path): 

575 paths.append(path) 

576 

577 for root_dir in root_dirs: 

578 # we include root_dir for when we want to be very explicit, e.g. 

579 # {% extends 'nbconvert/templates/classic/base.html' %} 

580 paths.append(root_dir) 

581 # we include base_dir for when we want to be explicit, but less than root_dir, e.g. 

582 # {% extends 'classic/base.html' %} 

583 base_dir = os.path.join(root_dir, "nbconvert", "templates") 

584 paths.append(base_dir) 

585 

586 compatibility_dir = os.path.join(root_dir, "nbconvert", "templates", "compatibility") 

587 paths.append(compatibility_dir) 

588 

589 additional_paths = [] 

590 for path in self.template_data_paths: 

591 if not prune or os.path.exists(path): 

592 additional_paths.append(path) 

593 

594 return paths + self.extra_template_paths + additional_paths 

595 

596 @classmethod 

597 def get_compatibility_base_template_conf(cls, name): 

598 """Get the base template config.""" 

599 # Hard-coded base template confs to use for backwards compatibility for 5.x-only templates 

600 if name == "display_priority": 

601 return {"base_template": "base"} 

602 if name == "full": 

603 return {"base_template": "classic", "mimetypes": {"text/html": True}} 

604 

605 def get_template_names(self): # noqa 

606 """Finds a list of template names where each successive template name is the base template""" 

607 template_names = [] 

608 root_dirs = self.get_prefix_root_dirs() 

609 base_template = self.template_name 

610 merged_conf: dict = {} # the configuration once all conf files are merged 

611 while base_template is not None: 

612 template_names.append(base_template) 

613 conf: dict = {} 

614 found_at_least_one = False 

615 for base_dir in self.extra_template_basedirs: 

616 template_dir = os.path.join(base_dir, base_template) 

617 if os.path.exists(template_dir): 

618 found_at_least_one = True 

619 conf_file = os.path.join(template_dir, "conf.json") 

620 if os.path.exists(conf_file): 

621 with open(conf_file) as f: 

622 conf = recursive_update(json.load(f), conf) 

623 for root_dir in root_dirs: 

624 template_dir = os.path.join(root_dir, "nbconvert", "templates", base_template) 

625 if os.path.exists(template_dir): 

626 found_at_least_one = True 

627 conf_file = os.path.join(template_dir, "conf.json") 

628 if os.path.exists(conf_file): 

629 with open(conf_file) as f: 

630 conf = recursive_update(json.load(f), conf) 

631 if not found_at_least_one: 

632 # Check for backwards compatibility template names 

633 for root_dir in root_dirs: 

634 compatibility_file = base_template + ".tpl" 

635 compatibility_path = os.path.join( 

636 root_dir, "nbconvert", "templates", "compatibility", compatibility_file 

637 ) 

638 if os.path.exists(compatibility_path): 

639 found_at_least_one = True 

640 warnings.warn( 

641 f"5.x template name passed '{self.template_name}'. Use 'lab' or 'classic' for new template usage.", 

642 DeprecationWarning, 

643 stacklevel=2, 

644 ) 

645 self.template_file = compatibility_file 

646 conf = self.get_compatibility_base_template_conf(base_template) 

647 self.template_name = conf.get("base_template") 

648 break 

649 if not found_at_least_one: 

650 paths = "\n\t".join(root_dirs) 

651 msg = f"No template sub-directory with name {base_template!r} found in the following paths:\n\t{paths}" 

652 raise ValueError(msg) 

653 merged_conf = recursive_update(dict(conf), merged_conf) 

654 base_template = conf.get("base_template") 

655 conf = merged_conf 

656 mimetypes = [mimetype for mimetype, enabled in conf.get("mimetypes", {}).items() if enabled] 

657 if self.output_mimetype and self.output_mimetype not in mimetypes and mimetypes: 

658 supported_mimetypes = "\n\t".join(mimetypes) 

659 msg = f"Unsupported mimetype {self.output_mimetype!r} for template {self.template_name!r}, mimetypes supported are: \n\t{supported_mimetypes}" 

660 raise ValueError(msg) 

661 return template_names 

662 

663 def get_prefix_root_dirs(self): 

664 """Get the prefix root dirs.""" 

665 # We look at the usual jupyter locations, and for development purposes also 

666 # relative to the package directory (first entry, meaning with highest precedence) 

667 root_dirs = [] 

668 if DEV_MODE: 

669 root_dirs.append(os.path.abspath(os.path.join(ROOT, "..", "..", "share", "jupyter"))) 

670 root_dirs.extend(jupyter_path()) 

671 return root_dirs 

672 

673 def _init_resources(self, resources): 

674 resources = super()._init_resources(resources) 

675 resources["deprecated"] = deprecated 

676 return resources